├── .yarnrc.yml
├── src
├── app
│ ├── favicon.ico
│ ├── [lang]
│ │ ├── components
│ │ │ ├── NavBar
│ │ │ │ ├── index.ts
│ │ │ │ ├── NodeSelector
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── NodeMenuItem.tsx
│ │ │ │ ├── LanguageSelector
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── LanguageMenuItem.tsx
│ │ │ │ │ ├── LanguageSelector.test.tsx
│ │ │ │ │ └── LanguageSelector.tsx
│ │ │ │ ├── Settings
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ConnectWalletConnected.test.tsx
│ │ │ │ │ └── SettingsDialog.tsx
│ │ │ │ ├── NavBar.tsx
│ │ │ │ └── NavBar.test.tsx
│ │ │ ├── PageTitleHeading
│ │ │ │ ├── index.ts
│ │ │ │ ├── PageTitleHeading.tsx
│ │ │ │ ├── TxnPresetBadge.tsx
│ │ │ │ └── PageTitleHeading.test.tsx
│ │ │ ├── wallet
│ │ │ │ └── index.ts
│ │ │ ├── JotaiProvider.tsx
│ │ │ ├── PageLoadingPlaceholder.tsx
│ │ │ ├── DialogLoadingPlaceholder.tsx
│ │ │ ├── form
│ │ │ │ ├── FieldErrorMessage.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── FieldTip.test.tsx
│ │ │ │ ├── RadioButtonGroupField.tsx
│ │ │ │ ├── FieldGroup.tsx
│ │ │ │ ├── FieldGroup.test.tsx
│ │ │ │ ├── FileField.tsx
│ │ │ │ └── FieldTip.tsx
│ │ │ ├── ToastProvider.tsx
│ │ │ ├── index.ts
│ │ │ ├── ToastViewport.tsx
│ │ │ ├── ThemeChanger.tsx
│ │ │ ├── BuilderSteps.tsx
│ │ │ ├── ToastNotification.tsx
│ │ │ └── Footer.tsx
│ │ ├── txn
│ │ │ ├── compose
│ │ │ │ ├── components
│ │ │ │ │ ├── fields
│ │ │ │ │ │ ├── PaymentFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── Amount.tsx
│ │ │ │ │ │ │ └── Receiver.tsx
│ │ │ │ │ │ ├── AssetFreezeFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── Freeze.tsx
│ │ │ │ │ │ │ └── TargetAddr.tsx
│ │ │ │ │ │ ├── AssetTransferFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── Amount.tsx
│ │ │ │ │ │ │ ├── Receiver.tsx
│ │ │ │ │ │ │ └── ClawbackTarget.tsx
│ │ │ │ │ │ ├── AppCallFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── OnComplete.tsx
│ │ │ │ │ │ │ ├── AppProperties.tsx
│ │ │ │ │ │ │ └── ExtraPages.tsx
│ │ │ │ │ │ ├── GeneralFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── LastValid.tsx
│ │ │ │ │ │ │ ├── TxnType.tsx
│ │ │ │ │ │ │ ├── ValidRounds.tsx
│ │ │ │ │ │ │ └── FirstValid.tsx
│ │ │ │ │ │ ├── LoadingPlaceholders
│ │ │ │ │ │ │ ├── FullWidthField.tsx
│ │ │ │ │ │ │ ├── LargeField.tsx
│ │ │ │ │ │ │ ├── SmallField.tsx
│ │ │ │ │ │ │ ├── ExtraSmallField.tsx
│ │ │ │ │ │ │ ├── LargeAreaField.tsx
│ │ │ │ │ │ │ ├── SwitchField.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── ArrayFieldGroup.tsx
│ │ │ │ │ │ ├── KeyRegFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── Nonparticipation.tsx
│ │ │ │ │ │ │ └── SelectionKey.tsx
│ │ │ │ │ │ └── AssetConfigFields
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── DefaultFrozen.tsx
│ │ │ │ │ │ │ ├── UnitName.tsx
│ │ │ │ │ │ │ ├── AssetName.tsx
│ │ │ │ │ │ │ └── URL.tsx
│ │ │ │ │ └── wallet
│ │ │ │ │ │ ├── WalletFieldWidget.tsx
│ │ │ │ │ │ └── WalletConnected.test.tsx
│ │ │ │ ├── page.test.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── page.test.tsx
│ │ │ ├── sign
│ │ │ │ ├── components
│ │ │ │ │ └── NextStepButton.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── send
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── [...catchAll]
│ │ │ └── page.ts
│ │ ├── not-found.tsx
│ │ ├── group
│ │ │ ├── page.test.tsx
│ │ │ ├── compose
│ │ │ │ ├── page.tsx
│ │ │ │ ├── page.test.tsx
│ │ │ │ └── components
│ │ │ │ │ └── GrpComposeList.tsx
│ │ │ └── page.tsx
│ │ ├── NotFoundBody.tsx
│ │ └── privacy-policy
│ │ │ └── page.test.tsx
│ ├── apple-icon.png
│ ├── lib
│ │ ├── testing
│ │ │ ├── test_compiled.teal
│ │ │ ├── test_signed.txn.msgpack
│ │ │ ├── test_unsigned.txn.msgpack
│ │ │ ├── textcoderPolyfill.js
│ │ │ ├── i18nextClientMock.ts
│ │ │ └── duck_poem.txt
│ │ ├── txn-data
│ │ │ ├── index.ts
│ │ │ ├── validation-rules.ts
│ │ │ └── data-utils.ts
│ │ ├── validation-set-locale.ts
│ │ ├── wallet-utils.ts
│ │ └── fonts.ts
│ ├── i18n
│ │ ├── locales
│ │ │ ├── en
│ │ │ │ ├── grp_presets.yml
│ │ │ │ ├── grp_compose.yml
│ │ │ │ ├── send_txn.yml
│ │ │ │ ├── home.yml
│ │ │ │ ├── common.yml
│ │ │ │ └── privacy_policy.yml
│ │ │ └── es
│ │ │ │ ├── grp_presets.yml
│ │ │ │ ├── grp_compose.yml
│ │ │ │ ├── send_txn.yml
│ │ │ │ ├── common.yml
│ │ │ │ └── home.yml
│ │ └── settings.ts
│ ├── robots.ts
│ ├── layout.tsx
│ └── icon.svg
└── e2e
│ ├── shared
│ ├── index.ts
│ └── NavBarComponent.ts
│ ├── pageModels
│ ├── index.ts
│ ├── SendTxnPage.ts
│ ├── SignTxnPage.ts
│ ├── TxnPresetsPage.ts
│ ├── NotFoundPage.ts
│ ├── ComposeTxnPage.ts
│ ├── GroupComposePage.ts
│ ├── PrivacyPolicyPage.ts
│ └── HomePage.ts
│ ├── send_txn.spec.ts
│ ├── group_compose.spec.ts
│ ├── privacy_policy.spec.ts
│ ├── not_found.spec.ts
│ └── txn_presets.spec.ts
├── public
├── assets
│ ├── icon-192.png
│ ├── icon-512.png
│ ├── mstile-150x150.png
│ ├── icon-192-maskable.png
│ ├── icon-512-maskable.png
│ └── silhouette-icon.svg
├── index.html
└── browserconfig.xml
├── postcss.config.js
├── .swcrc
├── .gitattributes
├── docs
└── README.md
├── compose.yml
├── lefthook.yml
├── .editorconfig
├── .env.local.example
├── .env.development
├── .env.production
├── tsconfig.json
├── .env.test
├── .gitignore
├── .dockerignore
├── jest.config.mjs
├── LICENSE.md
├── .github
├── workflows
│ ├── release_standalone.yml
│ └── release_container_image.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ └── bug_report.yml
├── PULL_REQUEST_TEMPLATE.md
└── SECURITY.md
├── eslint.config.mjs
├── next.config.ts
├── playwright.config.js
└── .env
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/index.ts:
--------------------------------------------------------------------------------
1 | import NavBar from './NavBar';
2 | export default NavBar;
3 |
--------------------------------------------------------------------------------
/src/app/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/src/app/apple-icon.png
--------------------------------------------------------------------------------
/public/assets/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/public/assets/icon-192.png
--------------------------------------------------------------------------------
/public/assets/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/public/assets/icon-512.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/NodeSelector/index.ts:
--------------------------------------------------------------------------------
1 | export { default as NodeSelector } from './NodeSelector';
2 |
--------------------------------------------------------------------------------
/public/assets/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/public/assets/mstile-150x150.png
--------------------------------------------------------------------------------
/public/assets/icon-192-maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/public/assets/icon-192-maskable.png
--------------------------------------------------------------------------------
/public/assets/icon-512-maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/public/assets/icon-512-maskable.png
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/LanguageSelector/index.ts:
--------------------------------------------------------------------------------
1 | export { default as LanguageSelector } from './LanguageSelector';
2 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsc": {
3 | "experimental": {
4 | "plugins": [["@swc-jotai/react-refresh", {}]]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/lib/testing/test_compiled.teal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/src/app/lib/testing/test_compiled.teal
--------------------------------------------------------------------------------
/src/app/[lang]/components/PageTitleHeading/index.ts:
--------------------------------------------------------------------------------
1 | import PageTitleHeading from './PageTitleHeading';
2 | export default PageTitleHeading;
3 |
--------------------------------------------------------------------------------
/src/app/lib/testing/test_signed.txn.msgpack:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/src/app/lib/testing/test_signed.txn.msgpack
--------------------------------------------------------------------------------
/src/app/lib/testing/test_unsigned.txn.msgpack:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/No-Cash-7970/txnDuck/HEAD/src/app/lib/testing/test_unsigned.txn.msgpack
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/Settings/index.ts:
--------------------------------------------------------------------------------
1 | import SettingsDialog from './SettingsDialog';
2 | const Settings = SettingsDialog;
3 | export default Settings;
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior
2 | * text eol=lf
3 |
4 | # Denote all files that are truly binary and should not be modified.
5 | *.png binary
6 | *.jpeg binary
7 | *.jpg binary
8 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Extra Documentation
2 |
3 | - [Installation Guide](./installation.md)
4 | - [Developers Documentation](./developers.md)
5 | - [Making Shareable Links](./shareable_links.md)
6 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/PaymentFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Receiver } from './Receiver';
2 | export { default as Amount } from './Amount';
3 | export { default as CloseTo } from './CloseTo';
4 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/grp_presets.yml:
--------------------------------------------------------------------------------
1 | title: Choose a Group Preset
2 | instruction: Compose a group faster by using one of the following presets.
3 | skip_btn: Or skip this and compose a group without using a preset
4 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetFreezeFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AssetId } from './AssetId';
2 | export { default as TargetAddr } from './TargetAddr';
3 | export { default as Freeze } from './Freeze';
4 |
--------------------------------------------------------------------------------
/src/e2e/shared/index.ts:
--------------------------------------------------------------------------------
1 | /** @file Utilities, tests, etc. shared across multiple test suites */
2 |
3 | export * from './LanguageSupport';
4 | export * from './NavBarComponent';
5 | export * as NodeTestResp from './AlgodMockResponses';
6 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/es/grp_presets.yml:
--------------------------------------------------------------------------------
1 | title: Eligir una preselección de grupo
2 | instruction: Componga un grupo más rápidamente utilizando uno de las siguientes preselecciones.
3 | skip_btn: O sáltese esto y componga un grupo sin utilizar una preselección
4 |
--------------------------------------------------------------------------------
/compose.yml:
--------------------------------------------------------------------------------
1 | name: txnduck
2 | services:
3 | dev:
4 | container_name: txnduck-dev
5 | build:
6 | context: .
7 | target: dev-setup
8 | dockerfile: ./Containerfile
9 | volumes:
10 | - .:/app
11 | ports:
12 | - "3000:3000"
13 |
--------------------------------------------------------------------------------
/src/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from 'next';
2 |
3 | /** Generates a robots.txt file */
4 | export default function robots(): MetadataRoute.Robots {
5 | return {
6 | rules: {
7 | userAgent: '*',
8 | allow: '/',
9 | },
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | follow: true
3 | commands:
4 | precommit-gulp:
5 | run: yarn gulp precommitHook
6 | exclude:
7 | - "*.md"
8 | commit-msg:
9 | follow: true
10 | commands:
11 | commitlint:
12 | run: yarn commitlint --edit
13 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/wallet/index.ts:
--------------------------------------------------------------------------------
1 | export {default as MagicAuthPrompt} from './MagicAuthPrompt';
2 | export * from './MagicAuthPrompt';
3 | export {default as WalletDialogContent} from './WalletDialogContent';
4 | export {default as WalletProvider} from './WalletProvider';
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | indent_size = 4
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #332d2d
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ComposeTxnPage';
2 | export * from './GroupComposePage';
3 | export * from './HomePage';
4 | export * from './NotFoundPage';
5 | export * from './PrivacyPolicyPage';
6 | export * from './SendTxnPage';
7 | export * from './SignTxnPage';
8 | export * from './TxnPresetsPage';
9 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetTransferFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Receiver } from './Receiver';
2 | export { default as AssetId } from './AssetId';
3 | export { default as Amount } from './Amount';
4 | export { default as ClawbackTarget } from './ClawbackTarget';
5 | export { default as CloseTo } from './CloseTo';
6 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AppCallFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as OnComplete } from './OnComplete';
2 | export { default as AppId } from './AppId';
3 | export { default as AppArgs } from './AppArgs';
4 | export { default as AppProperties } from './AppProperties';
5 | export { default as AppDependencies } from './AppDependencies';
6 |
--------------------------------------------------------------------------------
/src/app/lib/txn-data/index.ts:
--------------------------------------------------------------------------------
1 | export * as txnDataAtoms from './atoms';
2 | export * from './constants';
3 | export * from './data-utils';
4 | export * from './processor';
5 | export * from './stored';
6 | export * from './types';
7 | export * from './field-validation';
8 | export * from './form-validation';
9 | export * from './validation-rules';
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/JotaiProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Provider } from 'jotai';
4 |
5 | /** Wrapper for the Jotai provider to convert it to a client component so it is compatible with
6 | * Next.js server-side rendering (SSR)
7 | */
8 | export default function JotaiProvider({ children }: { children: React.ReactNode }) {
9 | return {children} ;
10 | };
11 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/GeneralFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as TxnType } from './TxnType';
2 | export { default as Sender } from './Sender';
3 | export { default as Fee } from './Fee';
4 | export { default as Note } from './Note';
5 | export { default as ValidRounds } from './ValidRounds';
6 | export { default as Lease } from './Lease';
7 | export { default as Rekey } from './Rekey';
8 |
--------------------------------------------------------------------------------
/.env.local.example:
--------------------------------------------------------------------------------
1 | # Example of a .env.local file, which overrides the defaults in the other .env
2 | # files. It is ignored by Git, so secrets should be stored in this file.
3 | # Environment variables not specified here inherit the values from the .env file.
4 |
5 | ##### App configuration #####
6 |
7 | BASE_URL=
8 | NEXT_PUBLIC_DEFAULT_NETWORK=testnet
9 | NEXT_PUBLIC_WC_PROJECT_ID=
10 | NEXT_PUBLIC_MAGIC_API_KEY=
11 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/FullWidthField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when a field with a "full" width is loading */
2 | export default function FullWidthField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/LargeField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when a field with a "large" width is loading */
2 | export default function LargeField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/SmallField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when a field with a "small" width is loading */
2 | export default function SmallField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/ExtraSmallField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when a field with an "extra small" width is loading */
2 | export default function ExtraSmallField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/LargeAreaField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when an area-type field with a "large" width is loading */
2 | export default function LargeAreaField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/SwitchField.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when a switch-type field is loading */
2 | export default function SwitchField({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/PageLoadingPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder that indicates the page content is loading */
2 | export default function PageLoadingPlaceholder() {
3 | return (
4 |
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/KeyRegFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as VoteKey } from './VoteKey';
2 | export { default as SelectionKey } from './SelectionKey';
3 | export { default as StateProofKey } from './StateProofKey';
4 | export { default as FirstVoteRound } from './FirstVoteRound';
5 | export { default as LastVoteRound } from './LastVoteRound';
6 | export { default as KeyDilution } from './KeyDilution';
7 | export { default as Nonparticipation } from './Nonparticipation';
8 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/DialogLoadingPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder that indicates the dialog content is loading */
2 | export default function DialogLoadingPlaceholder() {
3 | return (
4 |
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/app/globals.css';
2 | import type { Metadata } from 'next';
3 | import { getMetadataBase } from '@/app/lib/utils';
4 |
5 | /** Generate the base metadata for the site. Parts may be overwritten by child pages. */
6 | export async function generateMetadata(): Promise {
7 | return { metadataBase: new URL(getMetadataBase()) };
8 | }
9 |
10 | export default function RootLayout({ children }: { children: React.ReactNode }) {
11 | return children;
12 | };
13 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ArrayFieldGroup } from './ArrayFieldGroup';
2 | export { default as ExtraSmallField } from './ExtraSmallField';
3 | export { default as FullWidthField } from './FullWidthField';
4 | export { default as LargeAreaField } from './LargeAreaField';
5 | export { default as LargeField } from './LargeField';
6 | export { default as SmallField } from './SmallField';
7 | export { default as SwitchField } from './SwitchField';
8 |
--------------------------------------------------------------------------------
/src/app/[lang]/[...catchAll]/page.ts:
--------------------------------------------------------------------------------
1 | import { notFound } from 'next/navigation';
2 |
3 | /** For each supported language, make Next JS generate a static page for the language when building
4 | * the project.
5 | * @returns List of languages as parameters
6 | */
7 | export function generateStaticParams() {
8 | return [{catchAll: ['catch-all']}];
9 | }
10 |
11 | /** Catch-all route that directs to the 404 Not-Found page */
12 | export default function CatchAll() {
13 | return notFound();
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/grp_compose.yml:
--------------------------------------------------------------------------------
1 | title: Compose Group
2 | grp_list_no_txn: No transactions
3 | add_slot_btn: Add transaction slot
4 | review_sign_btn: Review & sign group
5 | edit_txn_btn: Edit
6 | edit_txn_btn_title: "Edit transaction #{{index}}"
7 | remove_slot_btn: Remove
8 | remove_slot_btn_title: "Remove transaction #{{index}}"
9 | no_txn_in_slot: "No transaction for slot #{{index}}"
10 | move_slot_up_btn: "Move slot #{{index}} up one slot"
11 | move_slot_down_btn: "Move slot #{{index}} down one slot"
12 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FieldErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | import { IconExclamationCircle } from "@tabler/icons-react";
2 | import { type TFunction } from "i18next";
3 |
4 | export default function FieldErrorMessage(
5 | { t, i18nkey, dict }:
6 | { t: TFunction, i18nkey: string, dict?: {[k: string]: any} }
7 | ) {
8 | return (
9 |
10 |
11 | {t(i18nkey, dict) as string}
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/LoadingPlaceholders/ArrayFieldGroup.tsx:
--------------------------------------------------------------------------------
1 | /** Placeholder for when an array field group is loading */
2 | export default function ArrayFieldGroup({ containerClass }: { containerClass?: string }) {
3 | return (
4 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/es/grp_compose.yml:
--------------------------------------------------------------------------------
1 | title: Componer el grupo
2 | grp_list_no_txn: No hay transacciones
3 | add_slot_btn: Añadir un espacio para la transacción
4 | review_sign_btn: Revisar y firmar el grupo
5 | edit_txn_btn: Editar
6 | edit_txn_btn_title: "Editar la transacción nº {{index}}"
7 | remove_slot_btn: Eliminar
8 | remove_slot_btn_title: "Eliminar la transacción nº {{index}}"
9 | no_txn_in_slot: "No hay transacción para el espacio nº {{index}}"
10 | move_slot_up_btn: "Mover el espacio nº {{index}} un espacio hasta arriba"
11 | move_slot_down_btn: "Mover el espacio nº {{index}} un espacio hasta abajo"
12 |
--------------------------------------------------------------------------------
/src/app/lib/testing/textcoderPolyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Sets up the polyfills for TextEncoder and TextDecoder because jest/js-dom does not include
3 | * them. This is for unit tests only.
4 | */
5 |
6 | /* Polyfill for TextEncoder, TextDecoder and the Uint8Array they use */
7 | import { TextEncoder, TextDecoder } from 'node:util';
8 | window.TextEncoder = TextEncoder;
9 | window.TextDecoder = TextDecoder;
10 | // NOTE: For some reason, the Uint8Array class that the polyfills use is different from the actual
11 | // Uint8Array class, so polyfilling Uint8array is necessary too
12 | window.Uint8Array = (new TextEncoder).encode().constructor;
13 |
--------------------------------------------------------------------------------
/src/app/lib/txn-data/validation-rules.ts:
--------------------------------------------------------------------------------
1 | /** @file Declaration and initialization of validation rules */
2 |
3 | import {
4 | number as YupNumber,
5 | string as YupString,
6 | mixed as YupMixed,
7 | } from 'yup';
8 | import { ADDRESS_LENGTH } from './constants';
9 | import '@/app/lib/validation-set-locale'; // Run setup for the locales for Yup (`Yup.setLocale()`)
10 |
11 | /** Validation schema for wallet address */
12 | export const addressSchema = YupString().trim().length(ADDRESS_LENGTH);
13 | /** Validation schemea for asset/application IDs */
14 | export const idSchema = YupNumber().min(1);
15 |
16 | export {YupMixed, YupNumber, YupString};
17 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | # Default environment variables for the "development" environment (`next dev`)
2 | # Environment variables not specified here inherit the values from the .env file.
3 |
4 | ##### App configuration #####
5 |
6 | BASE_URL=
7 | NEXT_PUBLIC_DEFAULT_NETWORK=testnet
8 | NEXT_PUBLIC_WC_PROJECT_ID=
9 | NEXT_PUBLIC_MAGIC_API_KEY=
10 |
11 | ##### Feature flags #####
12 |
13 | DISABLE_PWA=true
14 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET=true
15 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET_PERSIST=true
16 | NEXT_PUBLIC_FEAT_TXN_GROUP_EXP=true
17 |
18 | ##### Debug, warning & error message settings #####
19 |
20 | I18NEXT_DEBUG=true
21 | NEXT_PUBLIC_WALLET_DEBUG=true
22 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CheckboxField } from './CheckboxField';
2 | export { default as FileField } from './FileField';
3 | export { default as FieldErrorMessage } from './FieldErrorMessage';
4 | export { default as FieldGroup } from './FieldGroup';
5 | export { default as NumberField } from './NumberField';
6 | export { default as RadioButtonGroupField } from './RadioButtonGroupField';
7 | export { default as SelectField } from './SelectField';
8 | export { default as TextAreaField } from './TextAreaField';
9 | export { default as TextField } from './TextField';
10 | export { default as ToggleField } from './ToggleField';
11 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/ToastProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Provider } from '@radix-ui/react-toast';
4 |
5 | /** Wrapper for the Radix UI Toast Provider to convert it to a client component so it is compatible
6 | * with Next.js server-side rendering (SSR)
7 | */
8 | export default function ToastProvider({
9 | children,
10 | swipeDirection,
11 | label,
12 | }: {
13 | children: React.ReactNode,
14 | swipeDirection?: 'right' | 'left',
15 | label?: string,
16 | }) {
17 | return ( // The left swipe direction is usually for when the language is (right-to-left) RTL
18 |
19 | {children}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/app/[lang]/not-found.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect } from 'react';
4 | import NotFoundBody from './NotFoundBody';
5 |
6 | /** 404 Not Found page */
7 | export default function NotFound() {
8 | useEffect(() => {
9 | const theme = localStorage.getItem('theme');
10 | const htmlElem = document.querySelector('html');
11 |
12 | if (theme) {
13 | htmlElem!.dataset.theme = theme;
14 | } else {
15 | // Unset the `data-theme` attribute if the theme is to be automatic
16 | delete htmlElem!.dataset.theme;
17 | }
18 | });
19 | return (
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetConfigFields/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AssetId } from './AssetId';
2 | export { default as UnitName } from './UnitName';
3 | export { default as AssetName } from './AssetName';
4 | export { default as Total } from './Total';
5 | export { default as DecimalPlaces } from './DecimalPlaces';
6 | export { default as DefaultFrozen } from './DefaultFrozen';
7 | export { default as URL } from './URL';
8 | export { default as ManagerAddr } from './ManagerAddr';
9 | export { default as FreezeAddr } from './FreezeAddr';
10 | export { default as ClawbackAddr } from './ClawbackAddr';
11 | export { default as ReserveAddr } from './ReserveAddr';
12 | export { default as MetadataHash } from './MetadataHash';
13 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | # Default environment variables for the "production" environment (`next build`
2 | # and `next start`)
3 | # Environment variables not specified here inherit the values from the .env file.
4 |
5 | ##### App configuration #####
6 |
7 | BASE_URL=
8 | NEXT_PUBLIC_DEFAULT_NETWORK=mainnet
9 | NEXT_PUBLIC_WC_PROJECT_ID=
10 | NEXT_PUBLIC_MAGIC_API_KEY=
11 |
12 | ##### Debug, warning & error message settings #####
13 |
14 | # Ignore TypeScript errors when building
15 | IGNORE_TS_BUILD_ERRORS=true
16 | # If I18Next should be in debug mode
17 | I18NEXT_DEBUG=false
18 | # If the wallet connection library/libraries should be in debug logging mode
19 | NEXT_PUBLIC_WALLET_DEBUG=false
20 | # Suppress hydration warnings
21 | SUPPRESS_HYDRATION_WARNINGS=true
22 |
--------------------------------------------------------------------------------
/src/app/lib/testing/i18nextClientMock.ts:
--------------------------------------------------------------------------------
1 | /** This mock makes sure any components using the translate hook can use it without a warning being
2 | * shown.
3 | *
4 | * From https://react.i18next.com/misc/testing
5 | *
6 | * NOTE: In test files, run `jest.mock` for this module before importing any modules that will use
7 | * this mock module.
8 | */
9 | const i18nextClientMock = {
10 | useTranslation: () => {
11 | return {
12 | t: (str: string) => str,
13 | i18n: {
14 | changeLanguage: () => new Promise(() => {}),
15 | },
16 | };
17 | },
18 | Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
19 | initReactI18next: {
20 | type: '3rdParty',
21 | init: () => {},
22 | }
23 | };
24 |
25 | export default i18nextClientMock;
26 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/index.ts:
--------------------------------------------------------------------------------
1 | export {default as BuilderSteps} from './BuilderSteps';
2 | export {default as DialogLoadingPlaceholder} from './DialogLoadingPlaceholder';
3 | export {default as Footer} from './Footer';
4 | export {default as JotaiProvider} from './JotaiProvider';
5 | export {default as NavBar} from './NavBar';
6 | export {default as PageLoadingPlaceholder} from './PageLoadingPlaceholder';
7 | export {default as PageTitleHeading} from './PageTitleHeading';
8 | export {default as ThemeChanger} from './ThemeChanger';
9 | export {default as ToastNotification} from './ToastNotification';
10 | export {default as ToastProvider} from './ToastProvider';
11 | export {default as ToastViewport} from './ToastViewport';
12 | export {default as WalletProvider} from './wallet/WalletProvider';
13 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/ToastViewport.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Viewport } from '@radix-ui/react-toast';
4 |
5 | /** Wrapper for the Radix UI Toast Viewport to convert it to a client component so it is compatible
6 | * with Next.js server-side rendering (SSR)
7 | */
8 | export default function ToastViewport({
9 | toastPosition,
10 | label,
11 | }: {
12 | toastPosition?: 'right' | 'left',
13 | label?: string,
14 | }) {
15 | if (toastPosition === 'left') { // Usually for when the language is (right-to-left) RTL
16 | return (
17 |
18 | );
19 | } else { // Default
20 | return (
21 |
22 | );
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "react-jsx",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | ".next/dev/types/**/*.ts"
31 | ],
32 | "exclude": [
33 | "node_modules",
34 | "build",
35 | "src/e2e"
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/PageTitleHeading/PageTitleHeading.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { TxnPresetBadge } from "./TxnPresetBadge";
3 |
4 | type Props = {
5 | children?: React.ReactNode,
6 | /** Language (Not needed if `showTxnPreset` is `false`) */
7 | lng?: string,
8 | /** If a badge for the transaction preset should be shown. Only used by a few pages. */
9 | showTxnPreset?: boolean
10 | };
11 |
12 | /** Top heading for the title of a page */
13 | export default function PageTitleHeading({ children, lng = '', showTxnPreset = false }: Props) {
14 | return (
15 |
16 | {showTxnPreset &&
17 |
18 |
}
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/lib/validation-set-locale.ts:
--------------------------------------------------------------------------------
1 | import { setLocale } from 'yup';
2 | import { ValidationMessage } from './utils';
3 |
4 | // Run function to set how validation messages are formatted
5 | setLocale({
6 | // use constant translation keys for messages without values
7 | mixed: {
8 | required: (): ValidationMessage => ({key: 'form.error.required'}),
9 | },
10 | string: {
11 | length: ({length}): ValidationMessage => (
12 | {key: 'form.error.string.length', dict: {count: length}}
13 | ),
14 | max: ({max}): ValidationMessage => ({key: 'form.error.string.max', dict: {count: max}}),
15 | email: (): ValidationMessage => ({key: 'form.error.string.email'}),
16 | },
17 | number: {
18 | min: ({min}): ValidationMessage => ({key: 'form.error.number.min', dict: {min}}),
19 | max: ({max}): ValidationMessage => ({key: 'form.error.number.max', dict: {max}}),
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FieldTip.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import FieldTip from './FieldTip';
5 |
6 | // This solution to the "ResizeObserver is not defined" error caused by Radix UI Popover (only in
7 | // Jest) found at
8 | // https://greenonsoftware.com/articles/testing/testing-and-mocking-resize-observer-in-java-script/
9 | global.ResizeObserver = class MockedResizeObserver {
10 | observe = jest.fn();
11 | unobserve = jest.fn();
12 | disconnect = jest.fn();
13 | };
14 |
15 | describe('Form Components - FieldTip', () => {
16 |
17 | it('shows tooltip when button is clicked', async () => {
18 | render( );
19 | await userEvent.click(screen.getByRole('button'));
20 | expect(screen.getByText(/foo/)).toBeInTheDocument();
21 | });
22 |
23 | });
24 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/wallet/WalletFieldWidget.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from "i18next";
2 | import { useAtomValue } from "jotai";
3 | import { useEffect } from "react";
4 | import { WalletProvider } from "@/app/[lang]/components";
5 | import { isWalletConnectedAtom } from "@/app/lib/wallet-utils";
6 | import ConnectWallet from "./ConnectWallet";
7 |
8 | /** Component that handles the "connect wallet" button below certain form fields */
9 | export default function ConnectWalletFieldWidget({ t, setvalfn }:{
10 | t: TFunction,
11 | setvalfn: (v: any) => void,
12 | }) {
13 | const isWalletConnected = useAtomValue(isWalletConnectedAtom);
14 |
15 | // Trigger rerender when wallet connection status has changed
16 | useEffect(() => {}, [isWalletConnected]);
17 |
18 | return (
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | # Default environment variables for the "development" environment (`next dev`)
2 | # Environment variables not specified here inherit the values from the .env file.
3 |
4 | ##### App configuration #####
5 |
6 | # Base URL for the site. If blank or omitted, Next.js's default is used.
7 | BASE_URL=
8 | # Default node network
9 | # Possible values: mainnet, testnet, betanet, fnet, voimain, localnet, custom
10 | NEXT_PUBLIC_DEFAULT_NETWORK=testnet
11 | # WalletConnect project ID
12 | # If the project ID is not set, WalletConnect support will not be enabled.
13 | NEXT_PUBLIC_WC_PROJECT_ID=
14 | # Magic publishable API key
15 | # If the API key is not set, Magic support will not be enabled.
16 | NEXT_PUBLIC_MAGIC_API_KEY=
17 |
18 | ##### Feature flags #####
19 |
20 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET=true
21 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET_PERSIST=true
22 | NEXT_PUBLIC_FEAT_TXN_GROUP_EXP=true
23 |
24 | ##### Debug, warning & error message settings #####
25 |
26 | IGNORE_TS_BUILD_ERRORS=true
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/sdks
12 | !.yarn/versions
13 |
14 | # testing
15 | /coverage
16 |
17 | # next.js
18 | /.next/
19 | /out/
20 |
21 | # production
22 | /build
23 |
24 | # misc
25 | .DS_Store
26 | *.pem
27 | .vscode
28 |
29 | # debug
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 |
34 | # local env files
35 | .env*.local
36 |
37 | # vercel
38 | .vercel
39 |
40 | # typescript
41 | *.tsbuildinfo
42 | next-env.d.ts
43 | /test-results/
44 |
45 | # Playwright
46 | /playwright-report/
47 | /playwright/.cache/
48 |
49 | # compiled translation files
50 | src/app/i18n/locales/.dist/
51 |
52 | # Auto Generated PWA (Progressive Web App) files
53 | /public/sw.js
54 | /public/workbox-*.js
55 | /public/worker-*.js
56 | /public/sw.js.map
57 | /public/workbox-*.js.map
58 | /public/worker-*.js.map
59 | /public/**/*.webmanifest
60 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # See https://docs.docker.com/build/concepts/context/#dockerignore-files for more about ignoring files.
2 | # This should be very similar to the .gitignore file
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.*
8 | .yarn
9 | !.yarn/patches
10 | !.yarn/plugins
11 | !.yarn/releases
12 | !.yarn/sdks
13 | !.yarn/versions
14 |
15 | # testing
16 | /coverage
17 |
18 | # next.js
19 | /.next/
20 | /out/
21 |
22 | # production
23 | /build
24 |
25 | # misc
26 | .DS_Store
27 | *.pem
28 |
29 | # debug
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 | /test-results/
41 |
42 | # Playwright
43 | /playwright-report/
44 | /playwright/.cache/
45 |
46 | # compiled translation files
47 | src/app/i18n/locales/.dist/
48 |
49 | # Auto Generated PWA (Progressive Web App) files
50 | /public/sw.js
51 | /public/workbox-*.js
52 | /public/worker-*.js
53 | /public/sw.js.map
54 | /public/workbox-*.js.map
55 | /public/worker-*.js.map
56 | /public/**/*.webmanifest
57 |
--------------------------------------------------------------------------------
/src/e2e/send_txn.spec.ts:
--------------------------------------------------------------------------------
1 | import { test as base, expect } from '@playwright/test';
2 | import { LanguageSupport, NavBarComponent as NavBar } from './shared';
3 | import { SendTxnPage } from './pageModels/SendTxnPage';
4 |
5 | // Extend basic test by providing a "sendTxnPage" fixture.
6 | // Code adapted from https://playwright.dev/docs/pom
7 | const test = base.extend<{ sendTxnPage: SendTxnPage }>({
8 | sendTxnPage: async ({ page }, use) => {
9 | // Set up the fixture.
10 | const sendTxnPage = new SendTxnPage(page);
11 | await sendTxnPage.goto();
12 | // Use the fixture value in the test.
13 | await use(sendTxnPage);
14 | },
15 | });
16 |
17 | test.describe('Send Transaction Page', () => {
18 |
19 | test.describe('Language Support', () => {
20 | (new LanguageSupport({
21 | en: { body: /Send/, title: /Send/ },
22 | es: { body: /Enviar/, title: /Enviar/ },
23 | })).check(test, SendTxnPage.url);
24 | });
25 |
26 | test.describe('Nav Bar', () => {
27 | NavBar.check(test, SendTxnPage.getFullUrl());
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/jest.config.mjs:
--------------------------------------------------------------------------------
1 | import nextJest from 'next/jest.js';
2 |
3 | const createJestConfig = nextJest({
4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test
5 | // environment
6 | dir: './',
7 | });
8 |
9 | // Add any custom config to be passed to Jest
10 | /** @type {import('jest').Config} */
11 | const config = {
12 | roots: ['src'],
13 |
14 | /*
15 | * Add more setup options before each test is run
16 | */
17 |
18 | // Setup files to run before the test framework is installed in the environment
19 | setupFiles: ['/src/app/lib/testing/textcoderPolyfill.js'],
20 | // Setup files to run after the test framework has been installed in the environment.
21 | // setupFilesAfterEnv: ['/jest.setup.js'],
22 |
23 | testEnvironment: 'jest-environment-jsdom',
24 | testPathIgnorePatterns: ['/node_modules/', '/src/e2e'],
25 | resetMocks: true,
26 | };
27 |
28 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which
29 | // is async
30 | export default createJestConfig(config);
31 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/LanguageSelector/LanguageMenuItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePathname, useSearchParams } from 'next/navigation';
4 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
5 | import { supportedLangs } from '@/app/i18n/settings';
6 |
7 | type Props = {
8 | /** Language of the page */
9 | page?: string
10 | /** Language the link is for */
11 | link?: string
12 | };
13 |
14 | /** Item (language) in the language menu */
15 | export default function LanguageMenuItem({ page='', link='' }: Props) {
16 | const currentURLPath = usePathname();
17 | const currentURLParams = useSearchParams();
18 | return (
19 |
20 |
21 |
26 | {supportedLangs[link].listName}
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/e2e/group_compose.spec.ts:
--------------------------------------------------------------------------------
1 | import { test as base, expect } from '@playwright/test';
2 | import { LanguageSupport, NavBarComponent as NavBar } from './shared';
3 | import { GroupComposePage } from './pageModels';
4 |
5 | // Extend basic test by providing a "grpComposePage" fixture.
6 | // Code adapted from https://playwright.dev/docs/pom
7 | const test = base.extend<{ grpComposePage: GroupComposePage }>({
8 | grpComposePage: async ({ page }, use) => {
9 | // Set up the fixture.
10 | const grpComposePage = new GroupComposePage(page);
11 | await grpComposePage.goto();
12 | // Use the fixture value in the test.
13 | await use(grpComposePage);
14 | },
15 | });
16 |
17 | test.describe('Transaction Group Compose Page', () => {
18 |
19 | test.describe('Language Support', () => {
20 | (new LanguageSupport({
21 | en: { body: /Compose/, title: /Compose/ },
22 | es: { body: /Componer/, title: /Componer/ },
23 | })).check(test, GroupComposePage.url);
24 | });
25 |
26 | test.describe('Nav Bar', () => {
27 | NavBar.check(test, GroupComposePage.getFullUrl());
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import { useTranslation } from '@/app/i18n';
3 | import Settings from './Settings';
4 | import { NodeSelector } from './NodeSelector';
5 | import { LanguageSelector } from './LanguageSelector';
6 |
7 | type Props = {
8 | /** Language */
9 | lng?: string
10 | };
11 |
12 | /** Navigation bar that serves as a header for every page */
13 | export default function NavBar({ lng }: Props) {
14 | const { t } = use(useTranslation(lng || '', ['app', 'common']));
15 |
16 | return (
17 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2023 No-Cash-7970
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/e2e/privacy_policy.spec.ts:
--------------------------------------------------------------------------------
1 | import { test as base, expect } from '@playwright/test';
2 | import { LanguageSupport, NavBarComponent as NavBar } from './shared';
3 | import { PrivacyPolicyPage } from './pageModels/PrivacyPolicyPage';
4 |
5 | // Extend basic test by providing a "privacyPolicyPage" fixture.
6 | // Code adapted from https://playwright.dev/docs/pom
7 | const test = base.extend<{ privacyPolicyPage: PrivacyPolicyPage }>({
8 | privacyPolicyPage: async ({ page }, use) => {
9 | // Set up the fixture.
10 | const privacyPolicyPage = new PrivacyPolicyPage(page);
11 | await privacyPolicyPage.goto();
12 | // Use the fixture value in the test.
13 | await use(privacyPolicyPage);
14 | },
15 | });
16 |
17 | test.describe('Privacy Policy Page', () => {
18 |
19 | test.describe('Language Support', () => {
20 | (new LanguageSupport({
21 | en: { body: /Privacy/, title: /Privacy/ },
22 | es: { body: /privacidad/, title: /privacidad/ },
23 | })).check(test, PrivacyPolicyPage.url);
24 | });
25 |
26 | test.describe('Nav Bar', () => {
27 | NavBar.check(test, PrivacyPolicyPage.getFullUrl());
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/SendTxnPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class SendTxnPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/txn/send';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + SendTxnPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(SendTxnPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/SignTxnPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class SignTxnPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/txn/sign';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + SignTxnPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(SignTxnPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/TxnPresetsPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class TxnPresetsPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/txn';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + TxnPresetsPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(TxnPresetsPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/NotFoundPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class NotFoundPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/doesnt-exist';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + NotFoundPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(NotFoundPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/ComposeTxnPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class ComposeTxnPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/txn/compose';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + ComposeTxnPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(ComposeTxnPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/page.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 |
5 | // Mock react `use` function before modules that use it are imported
6 | jest.mock('react', () => ({
7 | ...jest.requireActual('react'),
8 | use: () => ({ t: (key: string) => key }),
9 | }));
10 |
11 | // Mock i18next before modules that use it are imported because it is used by a child component
12 | jest.mock('react-i18next', () => i18nextClientMock);
13 |
14 | // Mock navigation hooks
15 | jest.mock('next/navigation', () => ({
16 | useSearchParams: () => ({
17 | get: () => null
18 | }),
19 | }));
20 |
21 | // Mock the wallet provider
22 | jest.mock('../components/wallet/WalletProvider.tsx', () => 'div');
23 |
24 | import TxnPresetsPage from './page';
25 |
26 | describe('Transaction Presets Page', () => {
27 |
28 | it('has page title heading', () => {
29 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
30 | render( );
31 | expect(screen.getByRole('heading', { level: 1 })).not.toBeEmptyDOMElement();
32 | });
33 |
34 | });
35 |
--------------------------------------------------------------------------------
/src/app/[lang]/group/page.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 |
5 | // Mock react `use` function before modules that use it are imported
6 | jest.mock('react', () => ({
7 | ...jest.requireActual('react'),
8 | use: () => ({ t: (key: string) => key }),
9 | }));
10 |
11 | // Mock i18next before modules that use it are imported because it is used by a child component
12 | jest.mock('react-i18next', () => i18nextClientMock);
13 |
14 | // Mock navigation hooks
15 | jest.mock('next/navigation', () => ({
16 | useSearchParams: () => ({
17 | get: () => null
18 | }),
19 | }));
20 |
21 | // Mock the wallet provider
22 | jest.mock('../components/wallet/WalletProvider.tsx', () => 'div');
23 |
24 | import GroupPresetsPage from './page';
25 |
26 | describe('Transaction Presets Page', () => {
27 |
28 | it('has page title heading', () => {
29 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
30 | render( );
31 | expect(screen.getByRole('heading', { level: 1 })).not.toBeEmptyDOMElement();
32 | });
33 |
34 | });
35 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/GroupComposePage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class GroupComposePage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/group/compose';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + GroupComposePage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(GroupComposePage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetConfigFields/DefaultFrozen.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { ToggleField } from '@/app/[lang]/components/form';
4 | import { assetConfigFormControlAtom, tipBtnClass, tipContentClass } from '@/app/lib/txn-data';
5 |
6 | export default function DefaultFrozen({ t }: { t: TFunction }) {
7 | const form = useAtomValue(assetConfigFormControlAtom);
8 | // If creation transaction
9 | return (!form.values.caid &&
10 | {
26 | form.setTouched('apar_df', true);
27 | form.handleOnChange('apar_df')(e.target.checked);
28 | }}
29 | />
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/PrivacyPolicyPage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class PrivacyPolicyPage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '/privacy-policy';
8 | /** Main section of the page */
9 | readonly main: Locator;
10 |
11 | /**
12 | * @param page Page fixture from Playwright
13 | */
14 | constructor(page: PageFixture) {
15 | this.page = page;
16 | this.main = page.getByRole('main');
17 | }
18 |
19 | /** Get the URL with language prefix.
20 | * @param lang The language prefix. Must be an ISO 639-1 code
21 | * @returns The URL with the language prefix
22 | */
23 | static getFullUrl(lang = 'en') {
24 | return '/' + lang + PrivacyPolicyPage.url;
25 | }
26 |
27 | /** Go to the page
28 | * @param lang The language prefix of the page to go to.
29 | * @param query The query parameter string with the question mark at the beginning
30 | * (e.g. "?a=1&b=2")
31 | */
32 | async goto(lang = 'en', query = '') {
33 | return await this.page.goto(PrivacyPolicyPage.getFullUrl(lang) + query);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/sign/components/NextStepButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { useSearchParams } from 'next/navigation';
5 | import { IconArrowRight, IconArrowLeft } from '@tabler/icons-react';
6 | import { useAtomValue } from 'jotai';
7 | import { useTranslation } from '@/app/i18n/client';
8 | import { storedSignedTxnAtom } from '@/app/lib/txn-data';
9 |
10 | type Props = {
11 | /** Language */
12 | lng?: string
13 | };
14 |
15 | /** Button for going to the next step in the "sign transaction" page */
16 | export default function NextStepButton({ lng }: Props) {
17 | const { t } = useTranslation(lng || '', 'sign_txn');
18 | const storedSignedTxn = useAtomValue(storedSignedTxnAtom);
19 | const currentURLParams = useSearchParams();
20 | return (
21 |
26 | {t('send_txn_btn')}
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/release_standalone.yml:
--------------------------------------------------------------------------------
1 | name: Release - Standalone
2 | on:
3 | release:
4 | types: [released]
5 | workflow_dispatch: null
6 | permissions:
7 | contents: write
8 | jobs:
9 | build:
10 | timeout-minutes: 30
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v4
15 | - name: Install Node
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 22
19 | - name: Enable Corepack for Yarn
20 | run: corepack enable
21 | - name: Set up Node cache # Set up Node cache by installing again
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 22
25 | cache: yarn
26 | - name: Install dependencies
27 | run: yarn
28 | - name: Build standalone zip file
29 | run: yarn build:standalone:zip
30 | env:
31 | NEXT_PUBLIC_WC_PROJECT_ID: ${{vars.NEXT_PUBLIC_WC_PROJECT_ID}}
32 | NEXT_PUBLIC_MAGIC_API_KEY: ${{vars.NEXT_PUBLIC_MAGIC_API_KEY}}
33 | - name: Upload standalone to latest release
34 | uses: softprops/action-gh-release@v2
35 | with:
36 | fail_on_unmatched_files: true
37 | files: build/*.zip
38 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/PageTitleHeading/TxnPresetBadge.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTranslation } from "@/app/i18n/client";
4 | import { useSearchParams } from "next/navigation";
5 | import { nodeConfigAtom } from "@/app/lib/node-config";
6 | import { useAtomValue } from "jotai";
7 | import { importParamName } from "@/app/lib/utils";
8 | import { Preset } from "@/app/lib/txn-data";
9 |
10 | type Props = {
11 | /** Language */
12 | lng?: string
13 | };
14 |
15 | /** Badge that shows the transaction preset being used. Usually in the page title heading */
16 | export function TxnPresetBadge({ lng }: Props) {
17 | const { t } = useTranslation(lng || '', ['txn_presets', 'common']);
18 | const nodeConfig = useAtomValue(nodeConfigAtom);
19 | const currentURLParams = useSearchParams();
20 | const txnPreset = currentURLParams.get(Preset.ParamName);
21 | const isImporting = currentURLParams.get(importParamName) !== null;
22 | return (<>
23 | {!isImporting && !!txnPreset &&
24 |
25 | {t(
26 | [txnPreset + '.heading', 'invalid_preset'],
27 | {coinName: nodeConfig.coinName ?? t('algo_other')}
28 | )}
29 |
30 | }
31 | >);
32 | }
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: ✨ Feature Request
2 | description: Suggest an idea for this project
3 | labels: [enhancement]
4 | body:
5 | - type: textarea
6 | id: problem
7 | attributes:
8 | label: Is your feature request related to a problem? Please describe.
9 | description: A clear and concise description of what the problem is.
10 | placeholder: Ex. "I'm always frustrated when..."
11 | validations:
12 | required: true
13 | - type: textarea
14 | id: solution
15 | attributes:
16 | label: Describe the solution you'd like
17 | description: A clear and concise description of what you want to happen.
18 | validations:
19 | required: true
20 | - type: textarea
21 | id: alternatives
22 | attributes:
23 | label: Describe alternatives you've considered
24 | description: A clear and concise description of any alternative solutions or features you've considered.
25 | - type: textarea
26 | id: additional-context
27 | attributes:
28 | label: Additional context
29 | description: |
30 | Add any other context or screenshots about the feature request here.
31 |
32 | Tip: You can attach images by clicking this area to highlight it and then dragging files in.
33 |
--------------------------------------------------------------------------------
/src/app/[lang]/NotFoundBody.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTranslation } from '@/app/i18n/client';
4 | import Image from 'next/image';
5 |
6 | /** The core content of the Not-Found page */
7 | export default function NotFoundBody() {
8 | // Attempt to get the current language stored in localStorage because the Not-Found page cannot
9 | // use the "lang" path parameter. We also cannot get the value from the cookie because that would
10 | // ruin the ability of this app to be statically exported. This leaves us with getting the current
11 | // language from localStorage on the client side as our only option.
12 | const { t } = useTranslation('', 'app');
13 | return (<>
14 |
15 |
16 | {t('page_not_found.quack')}
17 |
18 |
19 |
20 |
21 |
22 |
23 | {t('page_not_found.title')}
24 |
25 | {t('page_not_found.details')}
26 | >);
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/Settings/ConnectWalletConnected.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import { type TFunction } from 'i18next';
5 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
6 | import { fooDisconnectFn, useWalletConnectedMock } from '@/app/lib/testing/useWalletMock';
7 |
8 | // Mock i18next before modules that use it are imported
9 | jest.mock('react-i18next', () => i18nextClientMock);
10 | // Mock use-wallet
11 | jest.mock('@txnlab/use-wallet-react', () => useWalletConnectedMock);
12 |
13 | import ConnectWallet from './ConnectWallet';
14 |
15 | describe('Wallet Connect (in Settings) (Connected wallet)', () => {
16 | const t = i18nextClientMock.useTranslation().t as TFunction;
17 |
18 | it('has "disconnect" button', () => {
19 | render( );
20 | expect(screen.getByRole('button')).toHaveTextContent('wallet.disconnect');
21 | expect(screen.getByText('wallet.is_connected')).toBeInTheDocument();
22 | });
23 |
24 | it('disconnects wallet when "disconnect" button is clicked', async () => {
25 | render( );
26 | await userEvent.click(screen.getByRole('button'));
27 | expect(fooDisconnectFn).toHaveBeenCalledTimes(1);
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetFreezeFields/Freeze.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { ToggleField } from '@/app/[lang]/components/form';
5 | import {
6 | Preset,
7 | assetFreezeFormControlAtom,
8 | tipBtnClass,
9 | tipContentClass,
10 | } from '@/app/lib/txn-data';
11 |
12 | export default function Freeze({ t }: { t: TFunction }) {
13 | const preset = useSearchParams().get(Preset.ParamName);
14 | const form = useAtomValue(assetFreezeFormControlAtom);
15 | // If creation transaction
16 | return (
17 | {
34 | form.setTouched('afrz', true);
35 | form.handleOnChange('afrz')(e.target.checked);
36 | }}
37 | />
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/lib/wallet-utils.ts:
--------------------------------------------------------------------------------
1 | /** @file Collection of constants and functions for managing wallet connections */
2 |
3 | import { WalletId } from '@txnlab/use-wallet-react';
4 | import { atom } from 'jotai';
5 | import { atomWithValidate } from 'jotai-form';
6 | import { string } from 'yup';
7 | import '@/app/lib/validation-set-locale'; // Run setup for the locales for Yup (`Yup.setLocale()`)
8 |
9 | /** The type of wallet for each supported wallet provider */
10 | export const walletTypes: {[id: string]: string} = {
11 | [WalletId.PERA]: 'mobile_web',
12 | [WalletId.DEFLY]: 'mobile',
13 | [WalletId.DEFLY_WEB]: 'browser_extension',
14 | [WalletId.EXODUS]: 'browser_extension',
15 | [WalletId.W3_WALLET]: 'mobile',
16 | // [WalletId.DAFFI]: 'mobile',
17 | [WalletId.LUTE]: 'web',
18 | [WalletId.KMD]: 'cli',
19 | [WalletId.KIBISIS]: 'browser_extension',
20 | [WalletId.WALLETCONNECT]: 'protocol',
21 | [WalletId.MAGIC]: 'waas',
22 | [WalletId.BIATEC]: 'web',
23 | [WalletId.MNEMONIC]: 'mnemonic',
24 | };
25 |
26 | /** Validation atom that contains the Magic email address */
27 | export const magicEmailAtom = atomWithValidate('', {
28 | validate: v => {
29 | string().email().required().validateSync(v);
30 | return v;
31 | }
32 | });
33 |
34 | /** Atom used for detecting and syncing changes in wallet connection status across components */
35 | export const isWalletConnectedAtom = atom(false);
36 |
--------------------------------------------------------------------------------
/src/app/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Noto_Sans,
3 | Noto_Sans_Display,
4 | Noto_Sans_Mono,
5 | // Noto_Color_Emoji,
6 | } from 'next/font/google';
7 |
8 | export const notoSans = Noto_Sans({
9 | style: ['normal', 'italic'],
10 | weight: ['400', '700'],
11 | variable: '--font-noto-sans',
12 | subsets: [
13 | // 'cyrillic',
14 | // 'cyrillic-ext',
15 | // 'devanagari',
16 | // 'greek',
17 | // 'greek-ext',
18 | 'latin',
19 | 'latin-ext',
20 | // 'vietnamese',
21 | ],
22 | display: 'swap',
23 | });
24 |
25 | export const notoSansDisplay = Noto_Sans_Display({
26 | variable: '--font-noto-sans-display',
27 | subsets: [
28 | // 'cyrillic',
29 | // 'cyrillic-ext',
30 | // 'greek',
31 | // 'greek-ext',
32 | 'latin',
33 | 'latin-ext',
34 | // 'vietnamese',
35 | ],
36 | display: 'swap',
37 | });
38 |
39 | export const notoSansMono = Noto_Sans_Mono({
40 | variable: '--font-noto-sans-mono',
41 | subsets: [
42 | // 'cyrillic',
43 | // 'cyrillic-ext',
44 | // 'greek',
45 | // 'greek-ext',
46 | 'latin',
47 | 'latin-ext',
48 | // 'vietnamese',
49 | ],
50 | display: 'swap',
51 | });
52 |
53 | // export const notoColorEmoji = Noto_Color_Emoji({
54 | // weight: ['400'],
55 | // variable: '--font-noto-color-emoji',
56 | // subsets: [ 'emoji' ],
57 | // display: 'swap', // Decreases perceived load time by reducing the "First Contentful Paint" time
58 | // });
59 |
--------------------------------------------------------------------------------
/src/e2e/not_found.spec.ts:
--------------------------------------------------------------------------------
1 | import { test as base, expect } from '@playwright/test';
2 | import { LanguageSupport, NavBarComponent as NavBar } from './shared';
3 | import { NotFoundPage } from './pageModels/NotFoundPage';
4 |
5 | // Extend basic test by providing a "notFoundPage" fixture.
6 | // Code adapted from https://playwright.dev/docs/pom
7 | const test = base.extend<{ notFoundPage: NotFoundPage }>({
8 | notFoundPage: async ({ page }, use) => {
9 | // Set up the fixture.
10 | const notFoundPage = new NotFoundPage(page);
11 | await notFoundPage.goto();
12 | // Use the fixture value in the test.
13 | await use(notFoundPage);
14 | },
15 | });
16 |
17 | test.describe('Not Found Page', () => {
18 |
19 | test.describe('Language Support', () => {
20 | (new LanguageSupport({
21 | // The new behavior of Next.js is to have no title. It is not certain if this is the intended
22 | // behavior or not, but it is fine for this web app.
23 | en: { body: /Page Not Found/, title: new RegExp('') },
24 | es: { body: /Página no encontrada/, title: new RegExp('') },
25 |
26 | // These old tests for the page title broke when upgrading to Next 15.2.4
27 | // en: { body: /Page Not Found/, title: /Transaction/ },
28 | // es: { body: /Página no encontrada/, title: /transacciones/ },
29 | })).check(test, NotFoundPage.url);
30 | });
31 |
32 | test.describe('Nav Bar', () => {
33 | NavBar.check(test, NotFoundPage.getFullUrl());
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/src/e2e/shared/NavBarComponent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | expect,
3 | type Page,
4 | type TestType,
5 | type PlaywrightTestArgs,
6 | type PlaywrightTestOptions,
7 | type PlaywrightWorkerArgs,
8 | type PlaywrightWorkerOptions
9 | } from '@playwright/test';
10 | import { HomePage } from '../pageModels/HomePage';
11 |
12 | export class NavBarComponent {
13 | /** Checks if the navigation bar has a link to the home page, which is usually the site name.
14 | * @param page Playwright Page fixture
15 | * @param lang Language prefix for the page
16 | */
17 | static async hasHomeLink(page: Page, lang = 'en'): Promise {
18 | await page.getByRole('navigation').getByRole('link').click();
19 | await expect(page).toHaveURL(HomePage.getFullUrl(lang));
20 | }
21 |
22 | /** Runs all the tests for the navigation bar in the page with the given page URL
23 | * @param test Playwright "test" object
24 | * @param pageFullUrl URL of the page WITH the language prefix
25 | * @param lang Language prefix for the page
26 | */
27 | static check(
28 | test: TestType<
29 | PlaywrightTestArgs & PlaywrightTestOptions,
30 | PlaywrightWorkerArgs & PlaywrightWorkerOptions
31 | >,
32 | pageFullUrl: string,
33 | lang = 'en',
34 | ): void {
35 | test.beforeEach(async ({ page }) => {
36 | await page.goto(pageFullUrl);
37 | });
38 |
39 | test('has link to home page', async({ page }) => {
40 | await page.goto(pageFullUrl);
41 | await this.hasHomeLink(page, lang);
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/i18n/settings.ts:
--------------------------------------------------------------------------------
1 | type LanguageData = {
2 | // The key must be an ISO 639-1 language code
3 | [lang: string]: {
4 | /** Full name used to display the currently selected language. */
5 | name: string,
6 | /** Name used in list of supported languages. Usually the same as the full name. */
7 | listName: string,
8 | }
9 | };
10 |
11 | /** Collection of supported languages */
12 | export const supportedLangs: LanguageData = {
13 | en: {
14 | name: 'English',
15 | listName: 'English (US)',
16 | },
17 | es: {
18 | name: 'Español',
19 | listName: 'Español',
20 | },
21 | // ADD DATA FOR NEW LANGUAGE HERE
22 | };
23 |
24 | /*
25 | * NOTE: This code was copied (with a few modifications) from
26 | * https://github.com/i18next/next-13-app-dir-i18next-example-ts/blob/main/app/i18n/settings.ts
27 | */
28 |
29 | export const fallbackLng: string = 'en';
30 | export const defaultNS: string = 'common';
31 |
32 | export type i18nOptions = {
33 | debug?: boolean,
34 | supportedLngs: string[],
35 | fallbackLng: string,
36 | lng: string,
37 | fallbackNS: string,
38 | defaultNS: string,
39 | ns: string | string[],
40 | };
41 |
42 | export function getOptions (lng = fallbackLng, ns: string | string[] = defaultNS): i18nOptions {
43 | return {
44 | debug: process.env.I18NEXT_DEBUG?.toLowerCase() === 'true',
45 | supportedLngs: Object.keys(supportedLangs),
46 | // preload: languages,
47 | fallbackLng,
48 | lng,
49 | fallbackNS: defaultNS,
50 | defaultNS,
51 | ns,
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/send/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, use } from 'react';
2 | import { type Metadata } from 'next';
3 | import { BuilderSteps, PageLoadingPlaceholder, PageTitleHeading } from '@/app/[lang]/components';
4 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
5 | import SendTxn from './components/SendTxn';
6 |
7 | export async function generateMetadata(
8 | props: { params: Promise<{ lang: string }> }
9 | ): Promise {
10 | const params = await props.params;
11 | // eslint-disable-next-line react-hooks/rules-of-hooks
12 | const { t } = await useTranslation(params.lang, ['send_txn', 'app']);
13 | const path = '/txn/send';
14 | return {
15 | title: t('page_title', {page: t('title'), site: t('site_name')}),
16 | alternates: {
17 | canonical: `/${params.lang}${path}`,
18 | languages: generateLangAltsMetadata(path)
19 | },
20 | };
21 | }
22 |
23 | /** Make Next JS generate at static version of this page */
24 | export function generateStaticParams() { return ['send']; }
25 |
26 | /** Send Transaction page */
27 | export default function SendTxnPage(props: { params: Promise<{ lang: string }> }) {
28 | const { lang } = use(props.params);
29 | const { t } = use(useTranslation(lang, ['send_txn', 'common']));
30 | return (
31 |
32 |
33 | {t('title')}
34 | }>
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Description
4 |
5 |
6 | ## Motivation and Context
7 |
8 |
9 |
10 | ## How Has This Been Tested?
11 |
12 |
13 |
14 |
15 | ## Screenshots (if appropriate):
16 |
17 | ## Types of changes
18 |
19 | - [ ] Bug fix (non-breaking change which fixes an issue)
20 | - [ ] New feature (non-breaking change which adds functionality)
21 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
22 |
23 | ## Checklist:
24 |
25 |
26 | - [ ] My code follows the code style of this project.
27 | - [ ] My change requires a change to the **documentation**.
28 | - [ ] I have updated the **documentation** accordingly.
29 | - [ ] My change requires a change to the **translations**.
30 | - [ ] I have updated the **translations** accordingly.
31 | - [ ] I have read the [Contributing Guidelines](CONTRIBUTING.md).
32 | - [ ] I have added tests to cover my changes.
33 | - [ ] All new and existing tests passed.
34 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/LanguageSelector/LanguageSelector.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
5 |
6 | // Mock i18next before modules that use it are imported
7 | jest.mock('react-i18next', () => i18nextClientMock);
8 |
9 | // Mock navigation hooks
10 | jest.mock('next/navigation', () => ({
11 | usePathname: () => '/current/url/of/page',
12 | useSearchParams: () => ({toString: () => 'q=yes'})
13 | }));
14 |
15 | // Mock I18n settings
16 | jest.mock('../../../../../app/i18n/settings', () => ({
17 | supportedLangs: {
18 | foo: {
19 | name: 'Foo Language',
20 | listName: 'Foo Language (Machine-translated)',
21 | },
22 | bar: {
23 | name: 'Bar Language',
24 | listName: 'Bar Language (42)',
25 | }
26 | }
27 | }));
28 |
29 | import LanguageSelector from './LanguageSelector';
30 |
31 | describe('Language Selector', () => {
32 |
33 | it('displays current language', () => {
34 | render( );
35 | expect(screen.getByRole('button')).toHaveTextContent(/Foo Language/);
36 | expect(screen.getByRole('button')).toHaveTextContent(/FOO/);
37 | });
38 |
39 | it('displays list of languages in menu', async () => {
40 | render( );
41 |
42 | await userEvent.click(screen.getByRole('button'));
43 |
44 | expect(screen.getByText('Foo Language (Machine-translated)')).toBeInTheDocument();
45 | expect(screen.getByText('Bar Language (42)')).toBeInTheDocument();
46 | });
47 |
48 | });
49 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/wallet/WalletConnected.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import userEvent from '@testing-library/user-event';
4 | import { TFunction } from 'i18next';
5 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
6 | import { fooDisconnectFn, useWalletConnectedMock } from '@/app/lib/testing/useWalletMock';
7 |
8 | // Mock i18next before modules that use it are imported
9 | jest.mock('react-i18next', () => i18nextClientMock);
10 | // Mock use-wallet
11 | jest.mock('@txnlab/use-wallet-react', () => useWalletConnectedMock);
12 |
13 | import ConnectWallet from './ConnectWallet';
14 |
15 | describe('Wallet Connect (in Settings) (Connected wallet)', () => {
16 | const t = i18nextClientMock.useTranslation().t as TFunction;
17 |
18 | it('has "disconnect" button', () => {
19 | render( {}} />);
20 | expect(screen.getByText('app:wallet.disconnect')).toHaveRole('button');
21 | });
22 |
23 | it('disconnects wallet when "disconnect" button is clicked', async () => {
24 | render( {}} />);
25 | await userEvent.click(screen.getByText('app:wallet.disconnect'));
26 | expect(fooDisconnectFn).toHaveBeenCalledTimes(1);
27 | });
28 |
29 | it('calls function to set field when "use connected account" button clicked', async () => {
30 | const setValFnMock = jest.fn();
31 | render( );
32 | await userEvent.click(screen.getByText('app:wallet.set_field_to_connected_addr'));
33 | expect(setValFnMock).toHaveBeenCalledTimes(1);
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/LanguageSelector/LanguageSelector.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
4 | import { supportedLangs } from '@/app/i18n/settings';
5 | import { IconLanguage } from '@tabler/icons-react';
6 | import LanguageMenuItem from './LanguageMenuItem';
7 |
8 | type Props = {
9 | /** Language */
10 | lng?: string
11 | };
12 |
13 | /** Language selection button and menu */
14 | export default function LanguageSelector({ lng }: Props) {
15 | return (
16 |
17 |
18 |
21 |
22 | {supportedLangs[lng ?? '']?.name ?? ''}
23 | {lng?.toUpperCase()}
24 |
25 |
26 |
27 |
28 |
34 | {Object.keys(supportedLangs).map((l: string) => (
35 |
36 | ))}
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/e2e/pageModels/HomePage.ts:
--------------------------------------------------------------------------------
1 | import { type Page as PageFixture, type Locator } from '@playwright/test';
2 |
3 | export class HomePage {
4 | /** Page fixture from Playwright */
5 | readonly page: PageFixture;
6 | /** URL without the language prefix */
7 | static readonly url = '';
8 | /** The "start" button link that directs the user to use the app. */
9 | readonly startBtn: Locator;
10 | /** The "compose transaction" button that directs user to compose a transaction */
11 | readonly composeTxnBtn: Locator;
12 | /** The "sign transaction" button that directs user to compose a transaction */
13 | readonly signTxnBtn: Locator;
14 | /** The "send transaction" button that directs user to compose a transaction */
15 | readonly sendTxnBtn: Locator;
16 |
17 | /**
18 | * @param page Page fixture from Playwright
19 | */
20 | constructor(page: PageFixture) {
21 | this.page = page;
22 | this.startBtn = page.getByTestId('startBtn');
23 | this.composeTxnBtn = page.getByTestId('composeTxnBtn');
24 | this.signTxnBtn = page.getByTestId('signTxnBtn');
25 | this.sendTxnBtn = page.getByTestId('sendTxnBtn');
26 | }
27 |
28 | /** Get the URL with language prefix.
29 | * @param lang The language prefix. Must be an ISO 639-1 code
30 | * @returns The URL with the language prefix
31 | */
32 | static getFullUrl(lang = 'en') {
33 | return '/' + lang + HomePage.url;
34 | }
35 |
36 | /** Go to the page
37 | * @param lang The language prefix of the page to go to.
38 | * @param query The query parameter string with the question mark at the beginning
39 | * (e.g. "?a=1&b=2")
40 | */
41 | async goto(lang = 'en', query = '') {
42 | return await this.page.goto(HomePage.getFullUrl(lang) + query);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/release_container_image.yml:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images#publishing-images-to-github-packages
2 | name: Release - Container image
3 | on:
4 | release:
5 | types: [released]
6 | workflow_dispatch: null
7 | env:
8 | REGISTRY: ghcr.io
9 | IMAGE_NAME: ${{ github.repository }}
10 | jobs:
11 | push_to_registries:
12 | name: Push container image to multiple registries
13 | runs-on: ubuntu-latest
14 | permissions:
15 | packages: write
16 | contents: read
17 | attestations: write
18 | id-token: write
19 | steps:
20 | - name: Check out the repo
21 | uses: actions/checkout@v4
22 | - name: Log in to the container registry
23 | uses: docker/login-action@v3
24 | with:
25 | registry: ${{ env.REGISTRY }}
26 | username: ${{ github.actor }}
27 | password: ${{ secrets.GITHUB_TOKEN }}
28 | - name: Extract metadata (tags, labels) for Docker
29 | id: meta
30 | uses: docker/metadata-action@v5
31 | with:
32 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
33 | - name: Build and push container image
34 | id: push
35 | uses: docker/build-push-action@v6
36 | with:
37 | context: .
38 | file: ./Containerfile
39 | push: true
40 | tags: ${{ steps.meta.outputs.tags }}
41 | labels: ${{ steps.meta.outputs.labels }}
42 | - name: Generate artifact attestation
43 | uses: actions/attest-build-provenance@v1
44 | with:
45 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
46 | subject-digest: ${{ steps.push.outputs.digest }}
47 | push-to-registry: true
48 |
--------------------------------------------------------------------------------
/src/app/lib/testing/duck_poem.txt:
--------------------------------------------------------------------------------
1 | The Duck That Lost His Quack
2 |
3 | A Duck woke up late one day last week,
4 | And all he could do was to squeak.
5 | He looked everywhere and listened to different things,
6 | Even heard sounds all around, from pings to zings.
7 |
8 | For example, he tried many gates, stairs, and barn doors,
9 | Then went and stepped on cracks in nearby creaky floors.
10 | He visited several witches, doctors and some were both,
11 | They prescribed everything from lemons to ginger troth.
12 |
13 | In his travels, he came across a quaint woodshop,
14 | Being so tired, he sat down with a solid plop.
15 | A carpenter saw that the Duck was so very sad,
16 | From behind the counter, he came to help the lad.
17 |
18 | After hearing of the tale of a missing sound,
19 | The carpenter leapt up with a double bound.
20 | He said, “From within is where it comes,
21 | Not outside, as most would sum.”
22 |
23 | “I have made many instruments for music,
24 | And what you need is something acoustic.”
25 | He brought out a short board with a nail,
26 | Then attached several metal strings to a pail.
27 |
28 | The carpenter said, “Play away and listen to the sounds in your head.”
29 | The Duck strummed everything from Enya to the Grateful Dead.
30 | After a fashion, the Duck was soon lost in the tunes,
31 | And started to dance and sing like a midnight Lune.
32 |
33 | Who knew that this Duck had a knack,
34 | And in the middle of it all started to quack.
35 | So you see, it’s not external to what you seek,
36 | In many cases, its internal and who you meet.
37 |
38 |
39 |
40 | Written by Michael Eastman, 8-25-2015,
41 |
42 | This, after listening to Bubbles the Mouse speak,
43 | And hearing a long story composed of squeaks.
44 |
45 | https://www.poetrysoup.com/poem/the_duck_that_lost_his_quack_704294
46 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug Report
2 | description: File a bug report
3 | labels: [bug]
4 | body:
5 | - type: textarea
6 | id: describe-bug
7 | attributes:
8 | label: Describe the bug
9 | description: A clear and concise description of what the bug is.
10 | validations:
11 | required: true
12 | - type: textarea
13 | id: reproduce-steps
14 | attributes:
15 | label: To reproduce
16 | description: Steps to reproduce the behavior.
17 | placeholder: |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 | - type: textarea
23 | id: expected-behavior
24 | attributes:
25 | label: Expected behavior
26 | description: A clear and concise description of what you expected to happen.
27 | - type: dropdown
28 | id: website
29 | attributes:
30 | label: Which website are you seeing the problem on?
31 | multiple: true
32 | options:
33 | - txnduck.vercel.app (Production)
34 | - txnduck-preview.vercel.app (Preview)
35 | - Local installation
36 | - Other
37 | - type: dropdown
38 | id: browsers
39 | attributes:
40 | label: What browsers are you seeing the problem on?
41 | multiple: true
42 | options:
43 | - Firefox
44 | - Chrome
45 | - Safari
46 | - Microsoft Edge
47 | - Other
48 | - type: textarea
49 | id: additional-context
50 | attributes:
51 | label: Additional context
52 | description: |
53 | Links? References? Anything that will give us more context about the issue you are encountering!
54 |
55 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
56 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/ThemeChanger.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useAtom } from "jotai";
4 | import { RESET } from "jotai/utils";
5 | import { RadioButtonGroupField } from "@/app/[lang]/components/form";
6 | import { useTranslation } from "@/app/i18n/client";
7 | import { themeAtom, Themes } from "@/app/lib/app-settings";
8 | import { applyTheme } from "@/app/lib/utils";
9 |
10 | type Props = {
11 | /** Language */
12 | lng?: string,
13 | /** Function for notifying the user. If undefined, the user is not notified. */
14 | notify?: () => void,
15 | /** Classes to add to the container */
16 | containerClass?: string,
17 | /** Classes to add to the label */
18 | labelClass?: string,
19 | /** Classes to add to the element for the text content of the label */
20 | labelTextClass?: string,
21 | };
22 |
23 | export default function ThemeChanger(props: Props) {
24 | const { t } = useTranslation(props.lng || '', ['app', 'common']);
25 | const [theme, setTheme] = useAtom(themeAtom);
26 | return (
27 | applyTheme(
41 | e.target.value as Themes,
42 | theme => setTheme(theme === '' ? RESET : theme),
43 | props.notify
44 | )}
45 | />
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetConfigFields/UnitName.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
4 | import {
5 | UNIT_NAME_MAX_LENGTH,
6 | assetConfigFormControlAtom,
7 | showFormErrorsAtom,
8 | tipBtnClass,
9 | tipContentClass,
10 | } from '@/app/lib/txn-data';
11 |
12 | export default function UnitName({ t }: { t: TFunction }) {
13 | const form = useAtomValue(assetConfigFormControlAtom);
14 | const showFormErrors = useAtomValue(showFormErrorsAtom);
15 | // If creation transaction
16 | return (!form.values.caid && <>
17 | form.handleOnChange('apar_un')(e.target.value)}
36 | onFocus={form.handleOnFocus('apar_un')}
37 | onBlur={form.handleOnBlur('apar_un')}
38 | />
39 | {(showFormErrors || form.touched.apar_un) && form.fieldErrors.apar_un &&
40 |
44 | }
45 | >);
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetConfigFields/AssetName.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
4 | import {
5 | ASSET_NAME_MAX_LENGTH,
6 | assetConfigFormControlAtom,
7 | showFormErrorsAtom,
8 | tipBtnClass,
9 | tipContentClass,
10 | } from '@/app/lib/txn-data';
11 |
12 | export default function AssetName({ t }: { t: TFunction }) {
13 | const form = useAtomValue(assetConfigFormControlAtom);
14 | const showFormErrors = useAtomValue(showFormErrorsAtom);
15 | // If creation transaction
16 | return (!form.values.caid && <>
17 | form.handleOnChange('apar_an')(e.target.value)}
36 | onFocus={form.handleOnFocus('apar_an')}
37 | onBlur={form.handleOnBlur('apar_an')}
38 | />
39 | {(showFormErrors || form.touched.apar_an) && form.fieldErrors.apar_an &&
40 |
44 | }
45 | >);
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/KeyRegFields/Nonparticipation.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { IconAlertTriangle } from '@tabler/icons-react';
5 | import { ToggleField } from '@/app/[lang]/components/form';
6 | import {
7 | Preset,
8 | keyRegFormControlAtom,
9 | tipBtnClass,
10 | tipContentClass,
11 | } from '@/app/lib/txn-data';
12 |
13 | export default function Nonparticipation({ t }: { t: TFunction }) {
14 | const form = useAtomValue(keyRegFormControlAtom);
15 | const preset = useSearchParams().get(Preset.ParamName);
16 | return (!(
17 | form.values.votekey || form.values.selkey || form.values.sprfkey
18 | || form.values.votefst || form.values.votelst || form.values.votekd
19 | ) && <>
20 | {
37 | form.setTouched('nonpart', true);
38 | form.handleOnChange('nonpart')(e.target.checked);
39 | }}
40 | />
41 | {!!form.values.nonpart
42 | ?
43 |
44 | {t('fields.nonpart.warning')}
45 |
46 | : undefined
47 | }
48 | >);
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetConfigFields/URL.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
4 | import {
5 | URL_MAX_LENGTH,
6 | assetConfigFormControlAtom,
7 | showFormErrorsAtom,
8 | tipBtnClass,
9 | tipContentClass,
10 | } from '@/app/lib/txn-data';
11 |
12 | export default function URL({ t }: { t: TFunction }) {
13 | const form = useAtomValue(assetConfigFormControlAtom);
14 | const showFormErrors = useAtomValue(showFormErrorsAtom);
15 | // If creation transaction
16 | return (!form.values.caid && <>
17 | form.handleOnChange('apar_au')(e.target.value)}
37 | onFocus={form.handleOnFocus('apar_au')}
38 | onBlur={form.handleOnBlur('apar_au')}
39 | inputMode='url'
40 | />
41 | {(showFormErrors || form.touched.apar_au) && form.fieldErrors.apar_au &&
42 |
46 | }
47 | >);
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/[lang]/group/compose/page.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import { type Metadata } from 'next';
3 | import {
4 | IconTrafficCone
5 | } from '@tabler/icons-react';
6 | import { BuilderSteps, PageTitleHeading } from '@/app/[lang]/components';
7 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
8 | import GrpComposeList from './components/GrpComposeList';
9 |
10 | export async function generateMetadata(
11 | props: { params: Promise<{ lang: string }> }
12 | ): Promise {
13 | const params = await props.params;
14 | // eslint-disable-next-line react-hooks/rules-of-hooks
15 | const { t } = await useTranslation(params.lang, ['grp_compose', 'app']);
16 | const path = '/group/compose';
17 | return {
18 | title: t('page_title', {page: t('title'), site: t('site_name')}),
19 | alternates: {
20 | canonical: `/${params.lang}${path}`,
21 | languages: generateLangAltsMetadata(path)
22 | },
23 | };
24 | }
25 |
26 | /** Make Next JS generate at static version of this page */
27 | export function generateStaticParams() { return ['group_compose']; }
28 |
29 | /** Choose Transaction Group Presets page */
30 | export default function GroupComposePage(props: { params: Promise<{ lang: string }> }) {
31 | const { lang } = use(props.params);
32 | const { t } = use(useTranslation(lang, ['grp_compose', 'app']));
33 | return (
34 |
35 |
36 |
37 |
{t('page_under_construction')}
38 |
39 |
40 | {t('title')}
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/NodeSelector/NodeMenuItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
4 | import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
5 | import { useSetAtom } from 'jotai';
6 | import * as NodeConfigLib from '@/app/lib/node-config';
7 |
8 | type Props = {
9 | /** The node configuration data to be applied and stored when the menu item is clicked */
10 | config: NodeConfigLib.NodeConfig
11 | /** Contents (icon & name) of the menu item */
12 | children: React.ReactNode
13 | };
14 |
15 | /** Item (network) in the node selector menu */
16 | export default function NodeMenuItem({ config, children }: Props) {
17 | const router = useRouter();
18 | const setNodeConfig = useSetAtom(NodeConfigLib.nodeConfigAtom);
19 | const pathName = usePathname();
20 | const currentURLParams = useSearchParams();
21 |
22 | /** Set the node configuration to the given configuration and apply the change
23 | * @param newConfig The new node configuration to apply
24 | */
25 | const updateNodeConfig = (newConfig: NodeConfigLib.NodeConfig) => {
26 | setNodeConfig(newConfig);
27 |
28 | // The new node configuration isn't used unless the wallet provider is reloaded, which happens
29 | // when the page is refreshed. Also, remove the network specified in the URL, if present.
30 | const newURLParams = new URLSearchParams(currentURLParams.toString());
31 |
32 | if (newURLParams.size) { // The current URL has query parameters
33 | newURLParams.delete(NodeConfigLib.networkURLParamName);
34 | router.push(`${pathName}?${newURLParams}`);
35 | }
36 | };
37 |
38 | return (
39 |
40 | updateNodeConfig(config)}>
41 | {children}
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/send_txn.yml:
--------------------------------------------------------------------------------
1 | title: Send Transaction
2 | txn_confirm_wait: Waiting for transaction to be confirmed…
3 | click_for_details: Click “{{details_name}}” below for more information.
4 | fail:
5 | heading: Transaction failed.
6 | rejected_msg: >-
7 | The node rejected the transaction. It is most likely that at least one of the transaction fields
8 | contains an error.
9 | http_400_msg: >-
10 | The transaction failed to be confirmed. It is most likely that at least one of the transaction
11 | fields contains an error.
12 | http_401_msg: The transaction could not be sent. The API token for the Algorand node is invalid.
13 | http_500_msg: The transaction could not be sent. The Algorand node is malfunctioning.
14 | http_503_msg: The transaction could not be sent. The Algorand node is not available at the moment.
15 | http_unknown_msg: The transaction could not be sent.
16 | unknown_msg: The transaction could not be confirmed.
17 | details: Error details
18 | warn:
19 | heading: Transaction may have failed.
20 | not_confirmed_msg: >-
21 | The transaction was not confirmed after {{count, number}} round(s). However, if the
22 | transaction’s last valid round is still in the future, the transaction may
23 | still be confirmed later.
24 | success:
25 | heading: Transaction confirmed!
26 | msg: "Transaction ID: {{txn_id}}"
27 | details: More information
28 | done_btn: Done!
29 | wait_longer_btn: Wait longer
30 | retry_btn: Retry
31 | quit_btn: Quit
32 | compose_txn_btn: Edit transaction fields
33 | sign_txn_btn: Sign transaction again
34 | make_new_txn_btn: Make another transaction
35 | import_txn:
36 | label: Import signed transaction file
37 | overwrite_warning: >-
38 | You have a saved incomplete transaction. If you import a transaction from a file, the saved
39 | transaction will get overwritten.
40 | cancel: Cancel import
41 |
--------------------------------------------------------------------------------
/src/app/lib/txn-data/data-utils.ts:
--------------------------------------------------------------------------------
1 | /** @file Useful utilities for managing transaction data */
2 |
3 | import { Algodv2 } from "algosdk";
4 | import { NodeConfig } from "@/app/lib/node-config";
5 | import { DEFAULT_NODE_CONFIG } from "@/app/lib/node-config";
6 | import { RetrievedAssetInfo } from "./types";
7 |
8 | /** Get information about the asset with the given ID
9 | * @param assetId ID of the asset of which to get the information
10 | * @param nodeConfig Configuration for the node used query the chain to get the asset information
11 | * @param callback Function executed when information retrieval has succeeded or failed. If the
12 | * information retrieval succeeded, the asset information is passed into the
13 | * callback as an argument. If the information retrieval failed, `undefined` is
14 | * passed into the callback as an argument.
15 | */
16 | export const getAssetInfo = async (
17 | assetId?: number,
18 | nodeConfig: NodeConfig = DEFAULT_NODE_CONFIG,
19 | callback: (info?: RetrievedAssetInfo) => void = ()=>{},
20 | ) => {
21 | if (assetId) {
22 | try {
23 | const algod = new Algodv2(
24 | nodeConfig.nodeToken ?? '',
25 | nodeConfig.nodeServer,
26 | nodeConfig.nodePort,
27 | nodeConfig.nodeHeaders
28 | );
29 | const assetInfo = await algod.getAssetByID(assetId).do();
30 | callback({
31 | id: assetInfo.index.toString(),
32 | name: assetInfo.params.name,
33 | unitName: assetInfo.params.unitName,
34 | total: assetInfo.params.total.toString(),
35 | decimals: assetInfo.params.decimals,
36 | manager: assetInfo.params.manager,
37 | freeze: assetInfo.params.freeze,
38 | clawback: assetInfo.params.clawback,
39 | reserve: assetInfo.params.reserve,
40 | });
41 | } catch (error) {
42 | console.error(error);
43 | callback(undefined);
44 | }
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/PaymentFields/Amount.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, NumberField } from '@/app/[lang]/components/form';
4 | import {
5 | paymentFormControlAtom,
6 | showFormErrorsAtom,
7 | tipBtnClass,
8 | tipContentClass
9 | } from '@/app/lib/txn-data';
10 | import { nodeConfigAtom } from '@/app/lib/node-config';
11 |
12 | export default function Amount({ t }: { t: TFunction }) {
13 | const form = useAtomValue(paymentFormControlAtom);
14 | const showFormErrors = useAtomValue(showFormErrorsAtom);
15 | const nodeConfig = useAtomValue(nodeConfigAtom);
16 | return (<>
17 |
39 | form.handleOnChange('amt')(e.target.value === '' ? undefined : parseFloat(e.target.value))
40 | }
41 | onFocus={form.handleOnFocus('amt')}
42 | onBlur={form.handleOnBlur('amt')}
43 | />
44 | {(showFormErrors || form.touched.amt) && form.fieldErrors.amt &&
45 |
49 | }
50 | >);
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/sign/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, use } from 'react';
2 | import { type Metadata } from 'next';
3 | import {
4 | BuilderSteps,
5 | PageLoadingPlaceholder,
6 | PageTitleHeading,
7 | WalletProvider
8 | } from '@/app/[lang]/components';
9 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
10 | import TxnDataTable from './components/TxnDataTable';
11 | import SignTxn from './components/SignTxn';
12 | import TxnImport from './components/TxnImport';
13 |
14 | export async function generateMetadata(
15 | props: { params: Promise<{ lang: string }> }
16 | ): Promise {
17 | const params = await props.params;
18 | // eslint-disable-next-line react-hooks/rules-of-hooks
19 | const { t } = await useTranslation(params.lang, ['sign_txn', 'app']);
20 | const path = '/txn/sign';
21 | return {
22 | title: t('page_title', {page: t('title'), site: t('site_name')}),
23 | alternates: {
24 | canonical: `/${params.lang}${path}`,
25 | languages: generateLangAltsMetadata(path)
26 | },
27 | };
28 | }
29 |
30 | /** Make Next JS generate at static version of this page */
31 | export function generateStaticParams() { return ['sign']; }
32 |
33 | /** Sign Transaction page */
34 | export default function SignTxnPage(props: { params: Promise<{ lang: string }> }) {
35 | const { lang } = use(props.params);
36 | const { t } = use(useTranslation(lang, ['sign_txn', 'app']));
37 | return (
38 |
39 |
40 | {t('title')}
41 |
42 | }>
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/BuilderSteps.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import Link from 'next/link';
3 | import { useTranslation } from '@/app/i18n';
4 |
5 | type Props = {
6 | /** Language */
7 | lng?: string,
8 | /** Name of the current step */
9 | current?: 'compose' | 'sign' | 'send' | 'done',
10 | /** Color of current and completed steps */
11 | color?: 'primary' | 'secondary' | 'accent',
12 | };
13 |
14 | /** Roadmap display for showing the steps of building a transaction */
15 | export default function BuilderSteps({ lng, current, color = 'primary'}: Props) {
16 | const { t } = use(useTranslation(lng || '', 'app'));
17 |
18 | return (
19 |
20 |
25 | {current === 'compose'
26 | ? t('builder_steps.compose')
27 | :
28 | {t('builder_steps.compose')}
29 |
30 | }
31 |
32 |
36 | {current === 'sign'
37 | ? t('builder_steps.sign')
38 | :
39 | {t('builder_steps.sign')}
40 |
41 | }
42 |
43 |
44 | {current === 'send'
45 | ? t('builder_steps.send')
46 | :
47 | {t('builder_steps.send')}
48 |
49 | }
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/home.yml:
--------------------------------------------------------------------------------
1 | # Home page
2 |
3 | hero: # The attention-grabber that is first thing the user sees
4 | main_paragraph: Build almost any kind of Algorand transaction.
5 | start_button: Build a new transaction!
6 | what_is_this:
7 | heading: What is this?
8 | paragraph: >-
9 | A free and open source tool for easily building Algorand transactions.
10 | how_it_works:
11 | heading: How does it work?
12 | compose:
13 | heading: 1. Compose the transaction.
14 | paragraph: >-
15 | Begin creating a new transaction by entering the transaction information. Use one of the
16 | transaction presets to make this step faster and easier.
17 | button: Compose a new transaction
18 | sign:
19 | heading: 2. Sign the transaction.
20 | paragraph: >-
21 | Every transaction must be signed before it is sent. Connect your wallet and sign the
22 | transaction. If you saved a transaction as a file, you can import it to sign it.
23 | button: Import & sign a transaction
24 | send:
25 | heading: 3. Send the transaction.
26 | paragraph: >-
27 | Send the signed transaction. A successful transaction is usually confirmed and final in less
28 | than 4 seconds. If you saved a signed transaction as a file, you can import it to send it.
29 | button: Import & send a signed transaction
30 | uses:
31 | heading: What can I use this tool for?
32 | simple_things:
33 | heading: Simple things
34 | list:
35 | 0: Send Algos
36 | 1: Opt-in to a token or <1>ASA1>
37 | 2: Transfer an <1>NFT1>
38 | 3: And more
39 | complex_things:
40 | heading: Advanced or complex things
41 | list:
42 | 0: Mark an account as “online” to start participating consensus
43 | 1: Update an application (smart contract)
44 | 2: Revoke (Claw back) an asset
45 | 3: And more
46 | dangerous_things:
47 | heading: Dangerous things
48 | list:
49 | 0: Rekey an account
50 | 1: Close an account
51 | 2: And more
52 |
--------------------------------------------------------------------------------
/src/app/icon.svg:
--------------------------------------------------------------------------------
1 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/NavBar.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 | // This must be imported after the mock classes are imported
5 | import { JotaiProvider } from '@/app/[lang]/components';
6 |
7 | // Mock react `use` function before modules that use it are imported
8 | jest.mock('react', () => ({
9 | ...jest.requireActual('react'),
10 | use: () => ({ t: (key: string) => key }),
11 | }));
12 |
13 | // Mock useRouter because it is used by child components
14 | jest.mock('next/navigation', () => ({
15 | useRouter: () => ({ push: jest.fn() }),
16 | usePathname: () => '/current/url/of/page',
17 | useSearchParams: () => ({
18 | toString: () => 'q=yes',
19 | get: () => null,
20 | }),
21 | }));
22 |
23 | // Mock i18next because it is used by child components
24 | jest.mock('react-i18next', () => i18nextClientMock);
25 |
26 | // Mock the wallet provider
27 | jest.mock('../wallet/WalletProvider.tsx', () => 'div');
28 |
29 | import NavBar from './NavBar';
30 |
31 | describe('Nav Bar Component', () => {
32 |
33 | it('has site name', async () => {
34 | render( );
35 | expect(await screen.findByText('site_name_pt1')).toBeInTheDocument();
36 | expect(screen.getByText('site_name_pt2')).toBeInTheDocument();
37 | });
38 |
39 | it('has node selector button', async () => {
40 | render( );
41 | expect(await screen.findByTitle('node_selector.choose_node')).toBeInTheDocument();
42 | });
43 |
44 | it('has language selector button', async () => {
45 | render( );
46 | expect(await screen.findByTestId('lang-btn')).toBeInTheDocument();
47 | });
48 |
49 | it('has settings button', async () => {
50 | render( );
51 | expect(await screen.findByTitle('settings.heading')).toBeInTheDocument();
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/PageTitleHeading/PageTitleHeading.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 |
5 | // Mock i18next before modules that use it are imported
6 | jest.mock('react-i18next', () => i18nextClientMock);
7 |
8 | // Mock navigation hooks
9 | const mockGetSearchParam = jest.fn();
10 | jest.mock('next/navigation', () => ({
11 | useSearchParams: () => ({
12 | get: mockGetSearchParam //(paramName: string) => (paramName === 'preset' ? 'foo' : null)
13 | })
14 | }));
15 |
16 | import PageTitleHeading from './PageTitleHeading';
17 |
18 | describe('PageTitleHeading Component', () => {
19 | beforeEach(() => {
20 | mockGetSearchParam.mockImplementation((param: string) => {
21 | if (param === 'preset') return 'foo';
22 | if (param === 'import') return null;
23 | });
24 | });
25 |
26 | it('has heading', () => {
27 | render(Hello! );
28 | expect(screen.getByRole('heading')).toHaveTextContent('Hello!');
29 | });
30 |
31 | it('has badge with preset name if `showTxnPreset` is true', async() => {
32 | render( );
33 | expect(await screen.findByText(/foo\.heading/)).toBeInTheDocument();
34 | });
35 |
36 | it('does not have badge with preset name if `showTxnPreset` is false', () => {
37 | render( );
38 | expect(screen.queryByText(/foo\.heading/)).not.toBeInTheDocument();
39 | });
40 |
41 | it('does not have badge if the "import" parameter is present in the URL', () => {
42 | mockGetSearchParam.mockImplementation((param: string) => {
43 | if (param === 'preset') return 'foo';
44 | if (param === 'import') return '';
45 | });
46 | render( );
47 | expect(screen.queryByText(/foo\.heading/)).not.toBeInTheDocument();
48 | });
49 |
50 | });
51 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/common.yml:
--------------------------------------------------------------------------------
1 | # Common words and phrases used throughout the app
2 |
3 | home: Home # Home page name
4 | algo_std_asset: Algorand Standard Asset
5 | nonfungible_token: Non-Fungible Token
6 | close: Close # For a button to close dialog/modal
7 | none: None # Often used when displaying that some field was not given or was empty
8 | empty: Empty # Often used when displaying that some field was given but contains an "empty" value
9 | number_value: "{{value, number}}" # When displaying a number without any other text
10 | asset_amount: "{{amount, number}} {{asset}}" # For displaying amount of an asset
11 | loading: Loading…
12 | cancel: Cancel # Often used for cancel buttons
13 |
14 | # Unit name of the token for Algorand
15 | # More information about specifying singular and plural versions of a word:
16 | # https://www.i18next.com/translation-function/plurals#singular-plural
17 | algo_one: Algo # 1 Algo
18 | algo_other: Algos # Multiple Algos
19 |
20 | unit_one: unit # 1 unit
21 | unit_other: units # Multiple units
22 |
23 | # These labels are used to help screen readers
24 | # More information: https://www.radix-ui.com/primitives/docs/components/toast#api-reference
25 | toast: # Toast = an in-app notification
26 | provider_label: Notification # Should be singular
27 | viewport_label: Notifications ({hotkey}) # Should be plural, `{hotkey}` should not be translated
28 |
29 | form: # For forms
30 | required: Required # When a field is required
31 | error:
32 | required: This is required.
33 | string:
34 | length: This must be {{count, number}} characters long.
35 | length_bytes: This must be {{count, number}} bytes long.
36 | max: This must be at most {{count, number}} characters long.
37 | max_bytes: This must be at most {{count, number}} bytes long.
38 | email: This must be a valid email address.
39 | number:
40 | invalid: This is an invalid number.
41 | min: This must be at least {{min, number}}.
42 | max: This must be at most {{max, number}}.
43 | email_placeholder: someone@example.com
44 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/page.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 | import { useWalletUnconnectedMock } from '@/app/lib/testing/useWalletMock';
5 |
6 | // Mock react `use` function before modules that use it are imported
7 | jest.mock('react', () => ({
8 | ...jest.requireActual('react'),
9 | use: () => ({ t: (key: string) => key }),
10 | }));
11 |
12 | // Mock i18next before modules that use it are imported because it is used by a child component
13 | jest.mock('react-i18next', () => i18nextClientMock);
14 |
15 | // Mock navigation hooks because they are used by a child components
16 | jest.mock('next/navigation', () => ({
17 | useRouter: () => ({ push: jest.fn() }),
18 | useSearchParams: () => ({get: () => 'foo'})
19 | }));
20 |
21 |
22 | // Mock use-wallet before modules that use it are imported
23 | jest.mock('@txnlab/use-wallet-react', () => useWalletUnconnectedMock);
24 | // Mock the wallet provider
25 | jest.mock('../../components/wallet/WalletProvider.tsx', () => 'div');
26 |
27 | import ComposeTxnPage from './page';
28 |
29 | describe('Compose Transaction Page', () => {
30 |
31 | it('has builder steps', async () => {
32 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
33 | render( );
34 | expect(await screen.findByText(/builder_steps\.compose/)).toBeInTheDocument();
35 | });
36 |
37 | it('has page title heading', async () => {
38 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
39 | render( );
40 | expect(await screen.findByRole('heading', { level: 1 })).not.toBeEmptyDOMElement();
41 | });
42 |
43 | it('has form', async () => {
44 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
45 | render( );
46 | expect(await screen.findByRole('form')).toBeInTheDocument();
47 | });
48 |
49 | });
50 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/es/send_txn.yml:
--------------------------------------------------------------------------------
1 | title: Enviar la transacción
2 | txn_confirm_wait: A la espera de que se confirme la transacción…
3 | click_for_details: Haga clic en «{{details_name}}» para obtener más información.
4 | fail:
5 | heading: La transacción ha fallado.
6 | rejected_msg: >-
7 | El nodo ha rechazado la transacción. Lo más probable es que al menos uno de los datos de la
8 | transacción contenga un error.
9 | http_400_msg: >-
10 | No se ha podido confirmar la transacción. Lo más probable es que al menos uno de los datos de la
11 | transacción contenga un error.
12 | http_401_msg: >-
13 | No se ha podido enviar la transacción. El token de API para el nodo Algorand no es válido.
14 | http_500_msg: No se ha podido enviar la transacción. El nodo Algorand no funciona correctamente.
15 | http_503_msg: >-
16 | No se ha podido enviar la transacción. El nodo Algorand no está disponible en este momento.
17 | http_unknown_msg: No se ha podido enviar la transacción.
18 | unknown_msg: La transacción no ha podido confirmarse.
19 | details: Detalles del error
20 | warn:
21 | heading: La transacción puede haber fallado.
22 | not_confirmed_msg: >-
23 | La transacción no se confirmó después de {{count}} ronda(s). Sin embargo, si la última
24 | ronda válida de la transacción aún está en el futuro, la transacción aún puede
25 | confirmarse más tarde.
26 | success:
27 | heading: ¡Transacción confirmada!
28 | msg: "ID de la transacción: {{txn_id}}"
29 | details: Más información
30 | done_btn: ¡Hecho!
31 | wait_longer_btn: Esperar más
32 | retry_btn: Reintentar
33 | quit_btn: Abandonar
34 | compose_txn_btn: Editar los datos de la transacción
35 | sign_txn_btn: Firmar la transacción de nuevo
36 | make_new_txn_btn: Hacer otra transacción
37 | import_txn:
38 | label: Importar archivo de transacción firmado
39 | overwrite_warning: >-
40 | Usted tiene una transacción incompleta guardada. Si importa una transacción desde un archivo, la
41 | transacción guardada se sobrescribirá.
42 | cancel: Cancelar importación
43 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/es/common.yml:
--------------------------------------------------------------------------------
1 | # Common words and phrases used throughout the app
2 |
3 | home: Inicio # Home page name
4 | algo_std_asset: Algorand Standard Asset (El activo estándar de Algorand)
5 | nonfungible_token: Non-Fungible Token (La ficha no fungible)
6 | close: Cerrar # For a button to close dialog/modal
7 | none: Nada # Often used when displaying that some field was not given or was empty
8 | empty: Vacío # Often used when displaying that some field was given but contains an "empty" value
9 | number_value: "{{value, number}}" # When displaying a number without any other text
10 | asset_amount: "{{amount, number}} {{asset}}" # For displaying amount of an asset
11 | loading: Cargando…
12 | cancel: Cancelar # Often used for cancel buttons
13 |
14 | # Unit name of the token for Algorand
15 | # More information about specifying singular and plural versions of a word:
16 | # https://www.i18next.com/translation-function/plurals#singular-plural
17 | algo_one: Algo # 1 Algo
18 | algo_other: Algos # Multiple Algos
19 |
20 | unit_one: unidad # 1 unit
21 | unit_other: unidades # Multiple units
22 |
23 | # These labels are used to help screen readers
24 | # More information: https://www.radix-ui.com/primitives/docs/components/toast#api-reference
25 | toast: # Toast = an in-app notification
26 | provider_label: Notificación # Should be singular
27 | viewport_label: Notificaciones ({hotkey}) # Should be plural, `{hotkey}` should not be translated
28 |
29 | form: # For forms
30 | required: Obligatorio # When a field is required
31 | error:
32 | required: Esto es obligatorio.
33 | string:
34 | length: Debe tener {{count, number}} caracteres.
35 | length_bytes: Debe tener {{count, number}} bytes.
36 | max: Debe tener como máximo {{count, number}} caracteres.
37 | max_bytes: Debe tener como máximo {{count, number}} bytes.
38 | email: Debe ser una dirección de correo electrónico válida.
39 | number:
40 | invalid: Esto es un número inválido.
41 | min: Debe ser al menos {{min, number}}.
42 | max: Debe ser como máximo {{max, number}}.
43 | email_placeholder: alguien@ejemplo.com
44 |
--------------------------------------------------------------------------------
/src/app/[lang]/privacy-policy/page.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 |
5 | // Mock react `use` function before modules that use it are imported
6 | jest.mock('react', () => ({
7 | ...jest.requireActual('react'),
8 | use: () => ({ t: (key: string) => key }),
9 | }));
10 |
11 | // Mock i18next before modules that use it are imported because it is used by a child component
12 | jest.mock('react-i18next', () => i18nextClientMock);
13 |
14 | // Mock the wallet provider
15 | jest.mock('../components/wallet/WalletProvider.tsx', () => 'div');
16 |
17 | import PrivacyPolicyPage from './page';
18 |
19 | describe('Privacy Policy Page', () => {
20 |
21 | it('has page title heading', () => {
22 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
23 | render( );
24 | expect(screen.getByRole('heading', { level: 1 })).not.toBeEmptyDOMElement();
25 | });
26 |
27 | it('shows information about Magic when it is enabled', () => {
28 | process.env.NEXT_PUBLIC_MAGIC_API_KEY = 'Some API Key';
29 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
30 | render( );
31 |
32 | expect(screen.getByText('personal_info.details_magic')).toBeInTheDocument();
33 | expect(screen.queryByText('personal_info.details')).not.toBeInTheDocument();
34 | expect(screen.getByText('magic_auth.heading')).toBeInTheDocument();
35 | });
36 |
37 | it('does not show information about Magic when it is disabled', () => {
38 | process.env.NEXT_PUBLIC_MAGIC_API_KEY = '';
39 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
40 | render( );
41 |
42 | expect(screen.getByText('personal_info.details')).toBeInTheDocument();
43 | expect(screen.queryByText('personal_info.details_magic')).not.toBeInTheDocument();
44 | expect(screen.queryByText('magic_auth.heading')).not.toBeInTheDocument();
45 | });
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetFreezeFields/TargetAddr.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSearchParams } from 'next/navigation';
3 | import { type TFunction } from 'i18next';
4 | import { useAtomValue, useSetAtom } from 'jotai';
5 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
6 | import {
7 | ADDRESS_LENGTH,
8 | Preset,
9 | assetFreezeFormControlAtom,
10 | presetAtom,
11 | showFormErrorsAtom,
12 | tipBtnClass,
13 | tipContentClass,
14 | } from '@/app/lib/txn-data';
15 |
16 | export default function TargetAddr({ t }: { t: TFunction }) {
17 | const form = useAtomValue(assetFreezeFormControlAtom);
18 | const preset = useSearchParams().get(Preset.ParamName);
19 | const setPresetAtom = useSetAtom(presetAtom);
20 | const showFormErrors = useAtomValue(showFormErrorsAtom);
21 |
22 | // eslint-disable-next-line react-hooks/exhaustive-deps
23 | useEffect(() => setPresetAtom(preset), [preset]);
24 |
25 | return (<>
26 | form.handleOnChange('fadd')(e.target.value)}
47 | onFocus={form.handleOnFocus('fadd')}
48 | onBlur={form.handleOnBlur('fadd')}
49 | />
50 | {(showFormErrors || form.touched.fadd) && form.fieldErrors.fadd &&
51 |
55 | }
56 | >);
57 | }
58 |
--------------------------------------------------------------------------------
/public/assets/silhouette-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/es/home.yml:
--------------------------------------------------------------------------------
1 | # Home page
2 |
3 | hero: # The attention-grabber that is first thing the user sees
4 | main_paragraph: Cree casi cualquier tipo de transacción de Algorand.
5 | start_button: ¡Cree una nueva transacción!
6 | what_is_this:
7 | heading: ¿Qué es esto?
8 | paragraph: >-
9 | Una herramienta gratuita y de código abierto para crear fácilmente transacciones Algorand.
10 | how_it_works:
11 | heading: ¿Cómo funciona?
12 | compose:
13 | heading: 1. Componga la transacción.
14 | paragraph: >-
15 | Comience a crear una nueva transacción introduciendo la información de la transacción.
16 | Utilice una de las preselecciones de transacciones para que este paso sea más rápido y
17 | sencillo.
18 | button: Componer una nueva transacción
19 | sign:
20 | heading: 2. Firme la transacción.
21 | paragraph: >-
22 | Cada transacción debe ser firmada antes de ser enviada. Conecte su billetera y firme la
23 | transacción. Si usted guardó una transacción como un archivo, puede importarla para
24 | firmarla.
25 | button: Importar y firmar una transacción
26 | send:
27 | heading: 3. Envíe la transacción.
28 | paragraph: >-
29 | Envíe la transacción firmada. Una transacción exitosa se confirma y finaliza normalmente en
30 | menos de 4 segundos. Si usted guardó una transacción firmada como archivo, puede importarla
31 | para enviarla.
32 | button: Importar y enviar una transacción firmada
33 | uses:
34 | heading: ¿Para qué puedo utilizar esta herramienta?
35 | simple_things:
36 | heading: Cosas sencillas
37 | list:
38 | 0: Enviar Algos
39 | 1: Optar a un token o <1>ASA1>
40 | 2: Transferir un <1>NFT1>
41 | 3: Y más
42 | complex_things:
43 | heading: Cosas avanzadas o complejas
44 | list:
45 | 0: Marcar una cuenta como «en línea» to start participating consensus
46 | 1: Actualizar una aplicación (smart contract)
47 | 2: Recuperar (Claw back) un activo
48 | 3: Y más
49 | dangerous_things:
50 | heading: Cosas peligrosas
51 | list:
52 | 0: Recodificar una cuenta
53 | 1: Cerrar una cuenta
54 | 2: Y más
55 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/GeneralFields/LastValid.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, NumberField } from '@/app/[lang]/components/form';
4 | import {
5 | generalFormControlAtom,
6 | showFormErrorsAtom,
7 | tipContentClass,
8 | tipBtnClass,
9 | lvConditionalRequireAtom,
10 | } from '@/app/lib/txn-data';
11 |
12 | export default function LastValid({ t }: { t: TFunction }) {
13 | const form = useAtomValue(generalFormControlAtom);
14 | const lvCondReqGroup = useAtomValue(lvConditionalRequireAtom);
15 | const showFormErrors = useAtomValue(showFormErrorsAtom);
16 | return (<>
17 |
39 | form.handleOnChange('lv')(e.target.value === '' ? undefined : parseInt(e.target.value))
40 | }
41 | onFocus={form.handleOnFocus('lv')}
42 | onBlur={form.handleOnBlur('lv')}
43 | />
44 | {(showFormErrors || form.touched.lv) && form.fieldErrors.lv &&
45 |
49 | }
50 | {(showFormErrors || form.touched.lv) && !lvCondReqGroup.isValid
51 | && lvCondReqGroup.error &&
52 |
56 | }
57 | >);
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/RadioButtonGroupField.tsx:
--------------------------------------------------------------------------------
1 | import FieldTip from './FieldTip';
2 | import type { RadioButtonGroupFieldProps } from './types';
3 |
4 | /** Radio button group form field */
5 | export default function SelectField({
6 | required = false,
7 | optionClass = '',
8 | label = '',
9 | labelClass = '',
10 | labelTextClass = '',
11 | containerId = undefined,
12 | containerClass = '',
13 | requiredText = '',
14 | helpMsg = '',
15 | options = [],
16 | name ='',
17 | defaultValue = undefined,
18 | disabled = false,
19 | value = undefined,
20 | onChange = undefined,
21 | onFocus = undefined,
22 | onBlur = undefined,
23 | tip = undefined,
24 | }: RadioButtonGroupFieldProps) {
25 | return (
26 |
27 |
28 |
29 | {label}
30 | {required && * }
31 | {tip && }
32 |
33 |
34 |
35 | {
36 | options.map((option) => {
37 | return (
38 |
54 | );
55 | })
56 | }
57 |
58 | {helpMsg &&
59 |
60 | {helpMsg}
61 |
62 | }
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, use } from 'react';
2 | import { type Metadata } from 'next';
3 | import Link from 'next/link';
4 | import { IconArrowBigRightLinesFilled, IconArrowBigLeftLinesFilled } from '@tabler/icons-react';
5 | import { PageLoadingPlaceholder, PageTitleHeading } from '@/app/[lang]/components';
6 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
7 | import TxnPresetsList from './TxnPresetsList';
8 |
9 | export async function generateMetadata(
10 | props: { params: Promise<{ lang: string }> }
11 | ): Promise {
12 | const params = await props.params;
13 | // eslint-disable-next-line react-hooks/rules-of-hooks
14 | const { t } = await useTranslation(params.lang, ['txn_presets', 'app']);
15 | const path = '/txn';
16 | return {
17 | title: t('page_title', {page: t('title'), site: t('site_name')}),
18 | alternates: {
19 | canonical: `/${params.lang}${path}`,
20 | languages: generateLangAltsMetadata(path)
21 | },
22 | };
23 | }
24 |
25 | /** Make Next JS generate at static version of this page */
26 | export function generateStaticParams() { return ['txn']; }
27 |
28 | /** Choose Transaction Presets page */
29 | export default function TxnPresetsPage(props: { params: Promise<{ lang: string }> }) {
30 | const { lang } = use(props.params);
31 | const { t } = use(useTranslation(lang, 'txn_presets'));
32 | return (
33 |
34 | {t('title')}
35 | {t('instruction')}
36 |
37 |
43 |
44 |
45 | {t('skip_btn')}
46 |
47 |
48 | }>
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/PaymentFields/Receiver.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
5 | import {
6 | ADDRESS_LENGTH,
7 | paymentFormControlAtom,
8 | showFormErrorsAtom,
9 | tipBtnClass,
10 | tipContentClass
11 | } from '@/app/lib/txn-data';
12 | import ConnectWalletFieldWidget from '../../wallet/WalletFieldWidget';
13 |
14 | export default function Receiver({ t }: { t: TFunction }) {
15 | const form = useAtomValue(paymentFormControlAtom);
16 | const showFormErrors = useAtomValue(showFormErrorsAtom);
17 | const searchParams = useSearchParams();
18 | return (<>
19 | form.handleOnChange('rcv')(e.target.value)}
40 | onFocus={form.handleOnFocus('rcv')}
41 | onBlur={form.handleOnBlur('rcv')}
42 | />
43 | {(showFormErrors || form.touched.rcv) && form.fieldErrors.rcv &&
44 |
48 | }
49 | {/* Show wallet widget when either
50 | * (1) the `rcv` query parameter is NOT set
51 | * (2) or the `rcv` query parameter is set AND the field has been touched
52 | */}
53 | {(!searchParams.get('rcv') || form.touched.rcv) &&
54 |
55 | }
56 | >);
57 | }
58 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/ToastNotification.tsx:
--------------------------------------------------------------------------------
1 | import { dir } from 'i18next';
2 | import * as Toast from '@radix-ui/react-toast';
3 | import { IconSettingsCheck, IconX } from '@tabler/icons-react';
4 | import { useTranslation } from '@/app/i18n/client';
5 |
6 | interface Props {
7 | /** Language */
8 | lng?: string;
9 | /** Message the toast notification should contain */
10 | message?: string;
11 | /** The controlled open state of the toast notification. Must be used in conjunction with
12 | * `onOpenChange`.
13 | */
14 | open?: boolean;
15 | /** Event handler called when the open state of the dialog changes. */
16 | onOpenChange?(open: boolean): void
17 | };
18 |
19 | /** Toast (notification) for when a setting or multiple settings are updated and saved */
20 | export default function ToastNotification({ lng, message, open, onOpenChange }: Props) {
21 | const { t } = useTranslation(lng || '', ['app', 'common']);
22 | const langDir = dir(lng);
23 | return (
24 |
42 |
43 | {message}
44 |
45 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/[lang]/group/page.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import { type Metadata } from 'next';
3 | import Link from 'next/link';
4 | import {
5 | IconArrowBigRightLinesFilled,
6 | IconArrowBigLeftLinesFilled,
7 | IconTrafficCone
8 | } from '@tabler/icons-react';
9 | import { PageTitleHeading } from '@/app/[lang]/components';
10 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
11 |
12 | export async function generateMetadata(
13 | props: { params: Promise<{ lang: string }> }
14 | ): Promise {
15 | const params = await props.params;
16 | // eslint-disable-next-line react-hooks/rules-of-hooks
17 | const { t } = await useTranslation(params.lang, ['grp_presets', 'app']);
18 | const path = '/group';
19 | return {
20 | title: t('page_title', {page: t('title'), site: t('site_name')}),
21 | alternates: {
22 | canonical: `/${params.lang}${path}`,
23 | languages: generateLangAltsMetadata(path)
24 | },
25 | };
26 | }
27 |
28 | /** Make Next JS generate at static version of this page */
29 | export function generateStaticParams() { return ['group']; }
30 |
31 | /** Choose Transaction Group Presets page */
32 | export default function GroupPresetsPage(props: { params: Promise<{ lang: string }> }) {
33 | const { lang } = use(props.params);
34 | const { t } = use(useTranslation(lang, ['grp_presets', 'app']));
35 | return (
36 |
37 |
38 |
39 |
{t('page_under_construction')}
40 |
41 | {t('title')}
42 | {t('instruction')}
43 |
44 |
50 |
51 |
52 | {t('skip_btn')}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 | import testingLibrary from "eslint-plugin-testing-library";
5 | import stylistic from "@stylistic/eslint-plugin";
6 | import { defineConfig, globalIgnores } from "eslint/config";
7 | import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
8 | import nextTypescript from "eslint-config-next/typescript";
9 | import { includeIgnoreFile } from "@eslint/compat";
10 | import yamlparser from "yaml-eslint-parser";
11 |
12 | const __filename = fileURLToPath(import.meta.url);
13 | const __dirname = dirname(__filename);
14 |
15 | const compat = new FlatCompat({
16 | baseDirectory: __dirname,
17 | });
18 | const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
19 |
20 | const eslintConfig = defineConfig([
21 | includeIgnoreFile(gitignorePath, "Imported .gitignore patterns"),
22 | globalIgnores(["public/**/*"], "Ignore public directory"),
23 | ...nextCoreWebVitals,
24 | ...nextTypescript,
25 | {
26 | plugins: { '@stylistic': stylistic },
27 | rules: {
28 | '@stylistic/semi': 'error',
29 | '@stylistic/max-len': ["warn", { code: 100 }],
30 | '@typescript-eslint/no-explicit-any': 'off'
31 | },
32 | }, {
33 | files: [
34 | "**/__tests__/**/*.[jt]s?(x)",
35 | "**/?(*.)+(test).[jt]s?(x)",
36 | ],
37 | ...testingLibrary.configs['flat/react'],
38 | rules: {
39 | "testing-library/no-await-sync-events": ["error", { eventModules: ["fire-event"] }],
40 | "testing-library/await-async-events": ["error", { eventModule: "userEvent" }],
41 | }
42 | }, {
43 | files: ["**/?(*.)+(spec).[jt]s"],
44 | rules: {
45 | "@typescript-eslint/no-unused-vars": ["off", {
46 | argsIgnorePattern: "(Page)$"
47 | }],
48 | "react-hooks/rules-of-hooks": "off",
49 | }
50 | }, {
51 | files: ["**/*.yaml", "**/*.yml"],
52 | extends: [...compat.extends("plugin:yml/standard")],
53 | languageOptions: {
54 | parser: yamlparser,
55 | },
56 | rules: {
57 | '@stylistic/max-len': 'off'
58 | },
59 | }, {
60 | ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"]
61 | }
62 | ]);
63 |
64 | export default eslintConfig;
65 |
--------------------------------------------------------------------------------
/src/app/[lang]/group/compose/page.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import i18nextClientMock from '@/app/lib/testing/i18nextClientMock';
4 | import { useWalletUnconnectedMock } from '@/app/lib/testing/useWalletMock';
5 |
6 | // Mock react `use` function before modules that use it are imported
7 | jest.mock('react', () => ({
8 | ...jest.requireActual('react'),
9 | use: () => ({ t: (key: string) => key }),
10 | }));
11 |
12 | // Mock i18next before modules that use it are imported because it is used by a child component
13 | jest.mock('react-i18next', () => i18nextClientMock);
14 |
15 | // Mock navigation hooks because they are used by a child components
16 | jest.mock('next/navigation', () => ({
17 | useRouter: () => ({ push: jest.fn() }),
18 | useSearchParams: () => ({get: () => 'foo'})
19 | }));
20 |
21 | // Mock use-wallet before modules that use it are imported
22 | jest.mock('@txnlab/use-wallet-react', () => useWalletUnconnectedMock);
23 | // Mock the wallet provider
24 | jest.mock('../../components/wallet/WalletProvider.tsx', () => 'div');
25 |
26 | import GroupComposePage from './page';
27 |
28 | // Mock navigation hooks because they are used by a child components
29 | const paramsMock = {get: jest.fn()};
30 | jest.mock('next/navigation', () => ({
31 | useSearchParams: () => paramsMock,
32 | }));
33 |
34 | describe('Group Transactions Compose Page', () => {
35 |
36 | it('has builder steps', async () => {
37 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
38 | render( );
39 | expect(await screen.findByText(/builder_steps\.compose/)).toBeInTheDocument();
40 | });
41 |
42 | it('has page title heading', async () => {
43 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
44 | render( );
45 | expect(await screen.findByRole('heading', { level: 1 })).not.toBeEmptyDOMElement();
46 | });
47 |
48 | it('has transaction group list', async () => {
49 | const pageParam = new Promise(resolve => { resolve({lang: ''}); });
50 | render( );
51 | expect(await screen.findByText(/grp_list_no_txn/)).toBeInTheDocument();
52 | });
53 |
54 | });
55 |
--------------------------------------------------------------------------------
/src/app/[lang]/group/compose/components/GrpComposeList.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTranslation } from "@/app/i18n/client";
4 | import { MAX_GRP_TXNS, storedTxnGrpKeysAtom } from "@/app/lib/txn-data";
5 | import { useAtom } from "jotai";
6 | import GrpComposeListSlot from "./GrpComposeListSlot";
7 | import { IconPlus } from "@tabler/icons-react";
8 | import Link from "next/link";
9 | import { useState } from "react";
10 |
11 | type Props = {
12 | /** Language */
13 | lng?: string
14 | };
15 |
16 | /** A list that contains the group transactions */
17 | export default function GrpComposeList({ lng }: Props) {
18 | const { t } = useTranslation(lng || '', ['grp_compose']);
19 | const [grpList, setGrpList] = useAtom(storedTxnGrpKeysAtom);
20 | const [emptySlotExists, setEmptySlotExists] = useState(false);
21 |
22 | /** Adds slot to the transaction group list */
23 | function addTxnSlot() {
24 | if (grpList.length < MAX_GRP_TXNS) {
25 | setGrpList([...grpList, '']);
26 | }
27 | }
28 |
29 | return <>
30 |
31 | {grpList.length
32 | ? grpList.map((storageKey, i) => {
33 | if (!emptySlotExists && grpList[i] === '') setEmptySlotExists(true);
34 | return ;
35 | })
36 | : {t('grp_list_no_txn')}
37 | }
38 |
39 | = MAX_GRP_TXNS}
41 | onClick={addTxnSlot}
42 | >
43 |
44 | {t('add_slot_btn')}
45 |
46 | {/* */}
47 | {/* TODO: Calculate pooled transaction fee and give error when min fee isn't covered */}
48 | {/*
*/}
49 | {/* Current total fee: {0} Algos */}
50 | {/*
*/}
51 | {/*
*/}
52 | {/* Minimum total fee: {(grpList.length * MIN_TX_FEE) / 1_000_000} Algos */}
53 | {/*
*/}
54 | {/*
*/}
55 |
59 | {t('review_sign_btn')}
60 |
61 | >;
62 | }
63 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security
2 |
3 | We take security seriously. If you believe you have found a security
4 | vulnerability, please report it to us as described below.
5 |
6 | ## Reporting Security Issues
7 |
8 | **:warning: Please do not report security vulnerabilities through public GitHub
9 | issues.**
10 |
11 | Instead, please report them by sending an email to
12 |
13 | txnDuck+security@proton.me .
14 | If possible, encrypt your message with our PGP key:
15 |
16 | ```text
17 | -----BEGIN PGP PUBLIC KEY BLOCK-----
18 |
19 | xjMEZLz4wxYJKwYBBAHaRw8BAQdA88cCrkXE+qu7TE6ZiCK0Sqr4Dg8gJmUf
20 | dxEUfFVKoBzNJXR4bkR1Y2tAcHJvdG9uLm1lIDx0eG5EdWNrQHByb3Rvbi5t
21 | ZT7CjAQQFgoAPgWCZLz4wwQLCQcICZCtnW0m0MIOwgMVCAoEFgACAQIZAQKb
22 | AwIeARYhBBpnHNtL4ywJaWVHWK2dbSbQwg7CAAC0swEA+Ajb6exAVZmwWDwc
23 | XNHoPRx14IuErkhMRv6lM0Im/EoA/2gcecvvum3Od/6uSA6xi2+MVgrNeJfn
24 | /l/4BgOqZWsIzjgEZLz4wxIKKwYBBAGXVQEFAQEHQGcbKVt4U2IreRmgWFM1
25 | xdS/WbbgQKykFN/FfKgmPp5vAwEIB8J4BBgWCAAqBYJkvPjDCZCtnW0m0MIO
26 | wgKbDBYhBBpnHNtL4ywJaWVHWK2dbSbQwg7CAAB3aAD/RgbsNKmivIMZIrqj
27 | +K2p9pd9gEVHpVFwbNJjX0t372YBAJ2NQ6ld5/dPR3KJUEIwihgjh6AbVBkV
28 | SO+h6QpTmbIP
29 | =aHey
30 | -----END PGP PUBLIC KEY BLOCK-----
31 | ```
32 |
33 | Please include the requested information listed below (as much as you can
34 | provide) to help us better understand the nature and scope of the possible
35 | issue:
36 |
37 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting,
38 | etc.)
39 | * Full paths of source file(s) related to the manifestation of the issue
40 | * The location of the affected source code (tag/branch/commit or direct URL)
41 | * Any special configuration required to reproduce the issue
42 | * Step-by-step instructions to reproduce the issue
43 | * Proof-of-concept or exploit code (if possible)
44 | * Impact of the issue, including how an attacker might exploit the issue
45 |
46 | This information will help us triage your report more quickly.
47 |
48 | ## Preferred Languages
49 |
50 | We prefer all communications to be in English.
51 |
52 | ## Attribution
53 |
54 | This security policy is based on [Microsoft's security policy for its
55 | repositories on GitHub](https://github.com/microsoft/repo-templates/blob/main/shared/SECURITY.md).
56 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetTransferFields/Amount.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
4 | import {
5 | aamtConditionalMaxAtom,
6 | assetTransferFormControlAtom,
7 | showFormErrorsAtom,
8 | tipBtnClass,
9 | tipContentClass,
10 | txnDataAtoms,
11 | } from '@/app/lib/txn-data';
12 |
13 | export default function Amount({ t }: { t: TFunction }) {
14 | const form = useAtomValue(assetTransferFormControlAtom);
15 | const aamtCondMax = useAtomValue(aamtConditionalMaxAtom);
16 | const showFormErrors = useAtomValue(showFormErrorsAtom);
17 | const retrievedAssetInfo = useAtomValue(txnDataAtoms.retrievedAssetInfo);
18 | return (<>
19 | form.handleOnChange('aamt')(e.target.value)}
41 | onFocus={form.handleOnFocus('aamt')}
42 | onBlur={form.handleOnBlur('aamt')}
43 | inputMode='numeric'
44 | />
45 | {(showFormErrors || form.touched.aamt) && form.fieldErrors.aamt &&
46 |
50 | }
51 | {(showFormErrors || form.touched.aamt) && !form.fieldErrors.aamt
52 | && !aamtCondMax.isValid && aamtCondMax.error &&
53 |
57 | }
58 | >);
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetTransferFields/Receiver.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
5 | import {
6 | ADDRESS_LENGTH,
7 | assetTransferFormControlAtom,
8 | showFormErrorsAtom,
9 | tipBtnClass,
10 | tipContentClass,
11 | } from '@/app/lib/txn-data';
12 | import ConnectWalletFieldWidget from '../../wallet/WalletFieldWidget';
13 |
14 | export default function Receiver({ t }: { t: TFunction }) {
15 | const form = useAtomValue(assetTransferFormControlAtom);
16 | const showFormErrors = useAtomValue(showFormErrorsAtom);
17 | const searchParams = useSearchParams();
18 | return (<>
19 | form.handleOnChange('arcv')(e.target.value)}
40 | onFocus={form.handleOnFocus('arcv')}
41 | onBlur={form.handleOnBlur('arcv')}
42 | />
43 | {(showFormErrors || form.touched.arcv) && form.fieldErrors.arcv &&
44 |
48 | }
49 | {/* Show wallet widget when either
50 | * (1) the `arcv` query parameter is NOT set,
51 | * (2) the `xaid` query parameter is NOT set,
52 | * (3) or the `arcv` query parameter is set AND the field has been touched
53 | */}
54 | {(!searchParams.get('arcv') || !searchParams.get('xaid') || form.touched.arcv) &&
55 |
56 | }
57 | >);
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/page.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import { type Metadata } from 'next';
3 | import dynamic from 'next/dynamic';
4 | import { BuilderSteps, PageTitleHeading, WalletProvider } from '@/app/[lang]/components';
5 | import { generateLangAltsMetadata, useTranslation } from '@/app/i18n';
6 | import {
7 | ExtraSmallField,
8 | FullWidthField,
9 | LargeAreaField,
10 | LargeField,
11 | SwitchField
12 | } from './components/fields/LoadingPlaceholders';
13 |
14 | const ComposeForm = dynamic(() => import('./components/ComposeForm'), {
15 | loading: () =>
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | });
27 |
28 | export async function generateMetadata(
29 | props: { params: Promise<{ lang: string }> }
30 | ): Promise {
31 | const params = await props.params;
32 | // eslint-disable-next-line react-hooks/rules-of-hooks
33 | const { t } = await useTranslation(params.lang, ['compose_txn', 'app']);
34 | const path = '/txn/compose';
35 | return {
36 | title: t('page_title', {page: t('title'), site: t('site_name')}),
37 | alternates: {
38 | canonical: `/${params.lang}${path}`,
39 | languages: generateLangAltsMetadata(path)
40 | },
41 | };
42 | }
43 |
44 | /** Make Next JS generate at static version of this page */
45 | export function generateStaticParams() { return ['compose']; }
46 |
47 | /** Compose Transaction page */
48 | export default function ComposeTxnPage(props: { params: Promise<{ lang: string }> }) {
49 | const { lang } = use(props.params);
50 | const { t } = use(useTranslation(lang, ['compose_txn', 'common']));
51 | return (
52 |
53 |
54 | {t('title')}
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FieldGroup.tsx:
--------------------------------------------------------------------------------
1 | import FieldTip from './FieldTip';
2 | import type { FieldGroupProps } from './types';
3 |
4 | /** Grouping for form fields with an optional heading. */
5 | export default function FieldGroup({
6 | children,
7 | heading = '',
8 | headingLevel = 2,
9 | headingClass = '',
10 | headingId = '',
11 | containerId = undefined,
12 | containerClass = '',
13 | disabled = false,
14 | tip = undefined,
15 | }: FieldGroupProps) {
16 | return (
17 |
18 | {/* Putting a heading inside a legend is acceptable: "By adding a heading screen reader users
19 | * can navigate to the heading and the legend text is also included as part of navigable
20 | * document structure."
21 | * Source: https://www.tpgi.com/fieldsets-legends-and-screen-readers-again/
22 | */}
23 | {heading &&
24 | {headingLevel === 1 &&
25 |
26 | {heading}
27 | {tip && }
28 |
29 | }
30 | {headingLevel === 2 &&
31 |
32 | {heading}
33 | {tip && }
34 |
35 | }
36 | {headingLevel === 3 &&
37 |
38 | {heading}
39 | {tip && }
40 |
41 | }
42 | {headingLevel === 4 &&
43 |
44 | {heading}
45 | {tip && }
46 |
47 | }
48 | {headingLevel === 5 &&
49 |
50 | {heading}
51 | {tip && }
52 |
53 | }
54 | {headingLevel === 6 &&
55 |
56 | {heading}
57 | {tip && }
58 |
59 | }
60 | }
61 |
62 | {children}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AppCallFields/OnComplete.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { OnApplicationComplete } from 'algosdk';
5 | import { FieldErrorMessage, SelectField } from '@/app/[lang]/components/form';
6 | import {
7 | Preset,
8 | applFormControlAtom,
9 | showFormErrorsAtom,
10 | tipBtnClass,
11 | tipContentClass
12 | } from '@/app/lib/txn-data';
13 |
14 | export default function OnComplete({ t }: { t: TFunction }) {
15 | const form = useAtomValue(applFormControlAtom);
16 | const preset = useSearchParams().get(Preset.ParamName);
17 | const showFormErrors = useAtomValue(showFormErrorsAtom);
18 | return (<>
19 | form.handleOnChange('apan')(e.target.value)}
46 | onFocus={form.handleOnFocus('apan')}
47 | onBlur={form.handleOnBlur('apan')}
48 | />
49 | {form.touched.apan && form.fieldErrors.apan &&
50 |
54 | }
55 | >);
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/GeneralFields/TxnType.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { FieldErrorMessage, SelectField } from '@/app/[lang]/components/form';
5 | import {
6 | Preset,
7 | generalFormControlAtom,
8 | showFormErrorsAtom,
9 | tipContentClass,
10 | tipBtnClass,
11 | } from '@/app/lib/txn-data';
12 | import { TransactionType } from 'algosdk';
13 |
14 | export default function TxnType({ t }: { t: TFunction }) {
15 | const form = useAtomValue(generalFormControlAtom);
16 | const preset = useSearchParams().get(Preset.ParamName);
17 | const showFormErrors = useAtomValue(showFormErrorsAtom);
18 | return (<>
19 | form.handleOnChange('txnType')(e.target.value)}
47 | onFocus={form.handleOnFocus('txnType')}
48 | onBlur={form.handleOnBlur('txnType')}
49 | />
50 | {(showFormErrors || form.touched.txnType) && form.fieldErrors.txnType &&
51 |
55 | }
56 | >);
57 | }
58 |
--------------------------------------------------------------------------------
/src/e2e/txn_presets.spec.ts:
--------------------------------------------------------------------------------
1 | import { test as base, expect } from '@playwright/test';
2 | import { LanguageSupport, NavBarComponent as NavBar } from './shared';
3 | import { TxnPresetsPage } from './pageModels/TxnPresetsPage';
4 |
5 | // Extend basic test by providing a "txnPresetsPage" fixture.
6 | // Code adapted from https://playwright.dev/docs/pom
7 | const test = base.extend<{ txnPresetsPage: TxnPresetsPage }>({
8 | txnPresetsPage: async ({ page }, use) => {
9 | // Set up the fixture.
10 | const txnPresetsPage = new TxnPresetsPage(page);
11 | await txnPresetsPage.goto();
12 | // Use the fixture value in the test.
13 | await use(txnPresetsPage);
14 | },
15 | });
16 |
17 | test.describe('Transaction Presets Page', () => {
18 |
19 | test.describe('Language Support', () => {
20 | (new LanguageSupport({
21 | en: { body: /preset/, title: /Transaction/ },
22 | es: { body: /preselección/, title: /transacción/ },
23 | })).check(test, TxnPresetsPage.url);
24 | });
25 |
26 | test.describe('Nav Bar', () => {
27 | NavBar.check(test, TxnPresetsPage.getFullUrl());
28 | });
29 |
30 | test.describe('With URL Parameters', () => {
31 |
32 | test('overrides the preset specified in URL in links to presets', async ({ page }) => {
33 | await (new TxnPresetsPage(page)).goto('en', '?preset=foo');
34 | // Only check one of the links instead of all 20+ links
35 | await expect(page.getByRole('link', { name: 'Transfer Algos' }))
36 | .toHaveAttribute('href', '/en/txn/compose?preset=transfer');
37 | });
38 |
39 | test('overrides the preset specified with other URL parameters in URL in links to presets',
40 | async ({ page }) => {
41 | await (new TxnPresetsPage(page)).goto('en', '?preset=foo&a=b');
42 | // Only check one of the links instead of all 20+ links
43 | await expect(page.getByRole('link', { name: 'Transfer Algos' }))
44 | .toHaveAttribute('href', '/en/txn/compose?a=b&preset=transfer');
45 | });
46 |
47 | test('includes current URL parameters (without preset) specified in links to presets',
48 | async ({ page }) => {
49 | await (new TxnPresetsPage(page)).goto('en', '?a=b&c=d');
50 | // Only check one of the links instead of all 20+ links
51 | await expect(page.getByRole('link', { name: 'Transfer Algos' }))
52 | .toHaveAttribute('href', '/en/txn/compose?a=b&c=d&preset=transfer');
53 | });
54 |
55 | });
56 |
57 | });
58 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FieldGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render, screen } from '@testing-library/react';
3 | import FieldGroup from './FieldGroup';
4 |
5 | describe('Form Components - FieldGroup', () => {
6 | it('has children', () => {
7 | render(foo );
8 | expect(screen.getByText(/foo/)).toBeInTheDocument();
9 | });
10 |
11 | it('has heading', () => {
12 | render( );
13 | expect(screen.getByRole('heading')).toHaveTextContent(/Foo Group/);
14 | });
15 |
16 | it('has heading with level specified in `headingLevel` property', () => {
17 | render( );
18 | expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(/Foo Group/);
19 | });
20 |
21 | it('has heading with class specified in `headingClass` property', () => {
22 | render( );
23 | expect(screen.getByRole('heading')).toHaveClass('foo-class');
24 | });
25 |
26 | it('has heading with `id` specified in `headingId` property', () => {
27 | render( );
28 | expect(screen.getByRole('heading')).toHaveAttribute('id', 'foo-id');
29 | });
30 |
31 | it('has container with ID specified in `containerId` property', () => {
32 | render( );
33 | expect(screen.getByRole('group')).toHaveAttribute('id', 'foo');
34 | });
35 |
36 | it('has container with class(es) specified in `containerClass` property', () => {
37 | render( );
38 | expect(screen.getByRole('group')).toHaveClass('foo');
39 | });
40 |
41 | it('disables the input if `disabled` is true', () => {
42 | render( );
43 | expect(screen.getByRole('group')).toBeDisabled();
44 | });
45 |
46 | it('enables the input if `disabled` is false', () => {
47 | render( );
48 | expect(screen.getByRole('group')).not.toBeDisabled();
49 | });
50 |
51 | it('has field tip button in heading when `tip` and a heading are specified', () => {
52 | render( );
53 | expect(screen.getByTitle('Foo tip')).toBeInTheDocument();
54 | });
55 |
56 | });
57 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from 'next';
2 | import withPWAInit from "@ducanh2912/next-pwa";
3 |
4 | const nextConfig: NextConfig = {
5 | poweredByHeader: false,
6 | serverExternalPackages: ['thread-stream'],
7 | turbopack: {},
8 | webpack: (config, { isServer }) => {
9 | // Add use-wallet dependency modules that cause "not found" errors. Also see
10 | // https://github.com/WalletConnect/walletconnect-monorepo/issues/1908#issuecomment-1487801131
11 | config.externals.push('pino-pretty', 'lokijs', 'encoding');
12 | config.resolve.fallback = { fs: false };
13 |
14 | /**
15 | * Provide fallbacks for optional wallet dependencies.
16 | * This allows the app to build and run without these packages installed, enabling users to
17 | * include only the wallet packages they need. Each package is set to 'false', which means
18 | * Webpack will provide an empty module if the package is not found, preventing build errors for
19 | * unused wallets.
20 | */
21 | if (!isServer) {
22 | config.resolve.fallback = {
23 | ...config.resolve.fallback,
24 | '@agoralabs-sh/avm-web-provider': false,
25 | '@blockshake/defly-connect': false,
26 | '@magic-ext/algorand': false,
27 | '@perawallet/connect': false,
28 | '@perawallet/connect-beta': false,
29 | '@walletconnect/modal': false,
30 | '@walletconnect/sign-client': false,
31 | 'lute-connect': false,
32 | 'magic-sdk': false,
33 | '@algorandfoundation/liquid-auth-use-wallet-client': false,
34 | };
35 | }
36 |
37 | return config;
38 | },
39 | typescript: {
40 | // !! WARN !!
41 | // Dangerously allow production builds to successfully complete even if your project has type
42 | // errors.
43 | // !! WARN !!
44 | ignoreBuildErrors: process.env.IGNORE_TS_BUILD_ERRORS?.toLowerCase() === 'true',
45 | },
46 | };
47 |
48 | if (process.env.STATIC_BUILD?.toLowerCase() === 'true') {
49 | nextConfig.output = 'export';
50 | }
51 |
52 | if (process.env.STANDALONE_BUILD?.toLowerCase() === 'true') {
53 | nextConfig.output = 'standalone';
54 | }
55 |
56 | // Create configuration for next-pwa plugin
57 | const withPWA = withPWAInit({
58 | dest: 'public',
59 | disable: process.env.DISABLE_PWA === 'true',
60 | // register: true,
61 | // scope: '/app',
62 | // sw: 'service-worker.js',
63 | //...
64 | });
65 |
66 | // Export the combined Next.js and PWA configuration
67 | module.exports = withPWA(nextConfig);
68 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/GeneralFields/ValidRounds.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSearchParams } from 'next/navigation';
3 | import { type TFunction } from 'i18next';
4 | import { useAtomValue, useSetAtom } from 'jotai';
5 | import { FieldGroup, ToggleField } from '@/app/[lang]/components/form';
6 | import {
7 | generalFormControlAtom, getStoredTxnDataAtom, tipBtnClass, tipContentClass, txnDataAtoms,
8 | } from '@/app/lib/txn-data';
9 | import { defaultUseSugRounds as defaultUseSugRoundsAtom } from '@/app/lib/app-settings';
10 | import FirstValid from './FirstValid';
11 | import LastValid from './LastValid';
12 |
13 | export default function ValidRounds({ t }: { t: TFunction }) {
14 | const form = useAtomValue(generalFormControlAtom);
15 | return (
16 |
17 |
18 | {!form.values.useSugRounds && <>
19 |
20 |
21 | >}
22 |
23 | );
24 | }
25 |
26 | export function UseSugRoundsInput({ t }: { t: TFunction }) {
27 | const form = useAtomValue(generalFormControlAtom);
28 |
29 | const currentURLParams = useSearchParams();
30 | const storedTxnDataAtom = getStoredTxnDataAtom(currentURLParams);
31 | const storedTxnData = useAtomValue(storedTxnDataAtom);
32 |
33 | const defaultUseSugRounds = useAtomValue(defaultUseSugRoundsAtom);
34 | const setUseSugRounds = useSetAtom(txnDataAtoms.useSugRounds);
35 |
36 | useEffect(() => {
37 | if (storedTxnData?.useSugRounds === undefined && !form.touched.useSugRounds) {
38 | setUseSugRounds(defaultUseSugRounds);
39 | }
40 | // eslint-disable-next-line react-hooks/exhaustive-deps
41 | },[defaultUseSugRounds, storedTxnData]);
42 |
43 | return (
44 | {
60 | form.setTouched('useSugRounds', true);
61 | form.handleOnChange('useSugRounds')(e.target.checked);
62 | }}
63 | />
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FileField.tsx:
--------------------------------------------------------------------------------
1 | import FieldTip from './FieldTip';
2 | import type { FileFieldProps } from './types';
3 |
4 | /** File form field. Includes a `` element and an ` ` element */
5 | export default function FileField({
6 | required = false,
7 | id = '',
8 | inputClass = '',
9 | label = '',
10 | labelClass = '',
11 | labelTextClass = '',
12 | inputInsideLabel = false,
13 | containerId = undefined,
14 | containerClass = '',
15 | requiredText = '',
16 | helpMsg = '',
17 | name ='',
18 | disabled = false,
19 | onChange = undefined,
20 | onFocus = undefined,
21 | onBlur = undefined,
22 | tip = undefined,
23 | accept = undefined,
24 | capture = undefined,
25 | multiple = false,
26 | inputRef = undefined,
27 | }: FileFieldProps) {
28 | return (
29 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/GeneralFields/FirstValid.tsx:
--------------------------------------------------------------------------------
1 | import { type TFunction } from 'i18next';
2 | import { useAtomValue } from 'jotai';
3 | import { FieldErrorMessage, NumberField } from '@/app/[lang]/components/form';
4 | import {
5 | generalFormControlAtom,
6 | fvLvFormControlAtom,
7 | showFormErrorsAtom,
8 | tipContentClass,
9 | tipBtnClass,
10 | fvConditionalRequireAtom,
11 | } from '@/app/lib/txn-data';
12 |
13 | export default function FirstValid({ t }: { t: TFunction }) {
14 | const form = useAtomValue(generalFormControlAtom);
15 | const fvLvGroup = useAtomValue(fvLvFormControlAtom);
16 | const fvCondReqGroup = useAtomValue(fvConditionalRequireAtom);
17 | const showFormErrors = useAtomValue(showFormErrorsAtom);
18 | return (<>
19 |
45 | form.handleOnChange('fv')(e.target.value === '' ? undefined : parseInt(e.target.value))
46 | }
47 | onFocus={form.handleOnFocus('fv')}
48 | onBlur={form.handleOnBlur('fv')}
49 | />
50 | {(showFormErrors || form.touched.fv) && form.fieldErrors.fv &&
51 |
55 | }
56 | {!fvLvGroup.isValid && fvLvGroup.error &&
57 |
61 | }
62 | {(showFormErrors || form.touched.fv) && !fvCondReqGroup.isValid && fvCondReqGroup.error &&
63 |
67 | }
68 | >);
69 | }
70 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | // Use process.env.PORT by default and fallback to port 3000
4 | const PORT = process.env.PORT || 3000;
5 |
6 | /**
7 | * See https://playwright.dev/docs/test-configuration.
8 | */
9 | export default defineConfig({
10 | testDir: './src/e2e',
11 | /* Run tests in files in parallel */
12 | fullyParallel: true,
13 | /* Fail the build on CI if you accidentally left test.only in the source code. */
14 | forbidOnly: !!process.env.CI,
15 | /* Retry on CI only */
16 | retries: process.env.CI ? 2 : 0,
17 | /* Opt out of parallel tests on CI. */
18 | workers: process.env.CI ? 1 : undefined,
19 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
20 | reporter: 'list',
21 | /* Timeout for a test */
22 | timeout: 60 * 1000, // 1 minute
23 | /* Timeout for an `expect` assertion */
24 | expect: { timeout: 10000 },
25 | /* Shared settings for all the projects below.
26 | * See https://playwright.dev/docs/api/class-testoptions.
27 | */
28 | use: {
29 | /* Base URL to use in actions like `await page.goto('/')`. */
30 | baseURL: `http://localhost:${PORT}`,
31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32 | trace: 'on-first-retry',
33 | },
34 |
35 | /* Configure projects for major browsers */
36 | projects: [
37 | {
38 | name: 'chromium',
39 | use: { ...devices['Desktop Chrome'] },
40 | },
41 | {
42 | name: 'firefox',
43 | use: { ...devices['Desktop Firefox'] },
44 | },
45 | {
46 | name: 'webkit',
47 | use: { ...devices['Desktop Safari'] },
48 | },
49 | /* Test against mobile viewports. */
50 | // {
51 | // name: 'Mobile Chrome',
52 | // use: { ...devices['Pixel 5'] },
53 | // },
54 | // {
55 | // name: 'Mobile Safari',
56 | // use: { ...devices['iPhone 12'] },
57 | // },
58 |
59 | /* Test against branded browsers. */
60 | // {
61 | // name: 'Microsoft Edge',
62 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
63 | // },
64 | // {
65 | // name: 'Google Chrome',
66 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
67 | // },
68 | ],
69 |
70 | /* Run your local dev server before starting the tests */
71 | webServer: {
72 | command: 'yarn prod',
73 | url: `http://localhost:${PORT}`,
74 | reuseExistingServer: !process.env.CI,
75 | timeout: 5 * 60 * 1000, // 5 minutes
76 | env: {
77 | NODE_ENV: 'test'
78 | },
79 | stdout: 'pipe',
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { use } from 'react';
2 | import { IconBrandGithubFilled, IconLockSquare } from '@tabler/icons-react';
3 | import Link from 'next/link';
4 | import { Trans } from 'react-i18next/TransWithoutContext';
5 | import { useTranslation } from '@/app/i18n';
6 |
7 | type Props = {
8 | /** Language */
9 | lng?: string
10 | };
11 |
12 | /** Footer for every page */
13 | export default function Footer({ lng }: Props) {
14 | const { t } = use(useTranslation(lng || '', 'app'));
15 | return (
16 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/NavBar/Settings/SettingsDialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTranslation } from '@/app/i18n/client';
4 | import * as Dialog from '@radix-ui/react-dialog';
5 | import { IconSettings, IconX } from '@tabler/icons-react';
6 | import dynamic from 'next/dynamic';
7 | import { DialogLoadingPlaceholder } from '@/app/[lang]/components';
8 |
9 | const SettingsModalBox = dynamic(() => import('./SettingsForm'), {
10 | ssr: false,
11 | loading: () => ,
12 | });
13 |
14 | type Props = {
15 | /** Language */
16 | lng?: string,
17 | /** The open state of the dialog when it is initially rendered */
18 | open?: boolean,
19 | };
20 |
21 | /** Dialog that allows the user to change app settings */
22 | export default function SettingsDialog({ lng, open = false }: Props) {
23 | const { t } = useTranslation(lng || '', ['app', 'common']);
24 |
25 | return (<>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | e.preventDefault()}
38 | onInteractOutside={(e) => e.preventDefault()}
39 | >
40 |
41 |
{t('settings.heading')}
42 | {/* Max height = height of modal (100vh - 5em)
43 | - modal title height (2em)
44 | - modal title bottom margin (1.5em)
45 | - modal box top padding (1.5em)
46 | - modal box bottom padding (1.5em)
47 | */}
48 |
51 |
52 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | >);
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AssetTransferFields/ClawbackTarget.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSearchParams } from 'next/navigation';
3 | import { type TFunction } from 'i18next';
4 | import { useAtomValue, useSetAtom } from 'jotai';
5 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
6 | import {
7 | ADDRESS_LENGTH,
8 | Preset,
9 | asndConditionalRequireAtom,
10 | assetTransferFormControlAtom,
11 | presetAtom,
12 | showFormErrorsAtom,
13 | tipBtnClass,
14 | tipContentClass,
15 | } from '@/app/lib/txn-data';
16 |
17 | export default function ClawbackTarget({ t }: { t: TFunction }) {
18 | const form = useAtomValue(assetTransferFormControlAtom);
19 | const preset = useSearchParams().get(Preset.ParamName);
20 | const setPresetAtom = useSetAtom(presetAtom);
21 | const asndCondReqGroup = useAtomValue(asndConditionalRequireAtom);
22 | const showFormErrors = useAtomValue(showFormErrorsAtom);
23 |
24 | // eslint-disable-next-line react-hooks/exhaustive-deps
25 | useEffect(() => setPresetAtom(preset), [preset]);
26 |
27 | return (<>
28 | form.handleOnChange('asnd')(e.target.value)}
51 | onFocus={form.handleOnFocus('asnd')}
52 | onBlur={form.handleOnBlur('asnd')}
53 | />
54 | {(showFormErrors || form.touched.asnd) && form.fieldErrors.asnd &&
55 |
59 | }
60 | {(showFormErrors || form.touched.asnd) && !asndCondReqGroup.isValid && asndCondReqGroup.error &&
61 |
65 | }
66 | >);
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/[lang]/components/form/FieldTip.tsx:
--------------------------------------------------------------------------------
1 | import * as Popover from '@radix-ui/react-popover';
2 | import {
3 | IconAlertSquare,
4 | IconAlertTriangle,
5 | IconHelpCircle,
6 | IconInfoCircle
7 | } from '@tabler/icons-react';
8 | import { useState } from 'react';
9 |
10 | export type Props = {
11 | /** Icon of trigger button */
12 | btnIcon?: 'info'|'help'|'warning'|'error',
13 | /** Size of the icon in the button */
14 | btnIconSize?: number,
15 | /** Classes to add to the trigger button */
16 | btnClass?: string,
17 | /** Value of the `title` attribute of the trigger button.
18 | * Recommended for making the button more accessible
19 | */
20 | btnTitle?: string,
21 | /** Content of the tooltip. Usually a string of text. */
22 | content?: any,
23 | /** Classes to add to the tooltip */
24 | contentClass?: string,
25 | };
26 |
27 | /** Small button within a form field that show a tooltip when clicked */
28 | export default function FieldTip({tipProps}: {tipProps: Props}) {
29 | const [open, setOpen] = useState(false);
30 | return (
31 |
32 |
33 | { e.preventDefault(); setOpen(true); }}
41 | onBlur={() => setOpen(false)}
42 | >
43 | {(!tipProps?.btnIcon || tipProps?.btnIcon === 'info') &&
44 |
45 | }
46 | {tipProps?.btnIcon === 'help' &&
47 |
48 | }
49 | {tipProps?.btnIcon === 'warning' &&
50 |
51 | }
52 | {tipProps?.btnIcon === 'error' &&
53 |
54 | }
55 |
56 |
57 |
58 | e.preventDefault()}
62 | onCloseAutoFocus={(e) => e.preventDefault()}
63 | className={tipProps?.contentClass}
64 | >
65 | {tipProps?.content}
66 |
67 |
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AppCallFields/AppProperties.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'next/navigation';
2 | import { type TFunction } from 'i18next';
3 | import { useAtomValue } from 'jotai';
4 | import { OnApplicationComplete } from 'algosdk';
5 | import { FieldGroup } from '@/app/[lang]/components/form';
6 | import { Preset, applFormControlAtom } from '@/app/lib/txn-data';
7 | import ApprovalProg from './ApprovalProg';
8 | import ClearStateProg from './ClearStateProg';
9 | import dynamic from 'next/dynamic';
10 | import { ExtraSmallField } from '../LoadingPlaceholders';
11 |
12 | const GlobalInts = dynamic(() => import('./GlobalInts'),
13 | { ssr: false, loading: () => },
14 | );
15 | const GlobalByteSlices = dynamic(() => import('./GlobalByteSlices'),
16 | { ssr: false, loading: () => },
17 | );
18 | const LocalInts = dynamic(() => import('./LocalInts'),
19 | { ssr: false, loading: () => },
20 | );
21 | const LocalByteSlices = dynamic(() => import('./LocalByteSlices'),
22 | { ssr: false, loading: () => },
23 | );
24 | const ExtraPages = dynamic(() => import('./ExtraPages'),
25 | { ssr: false, loading: () => },
26 | );
27 |
28 | /** Application properties section */
29 | export default function AppProperties({ t }: { t: TFunction }) {
30 | const form = useAtomValue(applFormControlAtom);
31 | const preset = useSearchParams().get(Preset.ParamName);
32 | return (
33 | ( // Creating application
34 | ((!preset && form.values.apan === OnApplicationComplete.NoOpOC && !form.values.apid)
35 | || preset === Preset.AppDeploy)
36 | // updating application
37 | || form.values.apan === OnApplicationComplete.UpdateApplicationOC
38 | ) &&
39 |
40 |
41 |
42 | {// Creating app
43 | form.values.apan === OnApplicationComplete.NoOpOC && <>
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | >}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/AppCallFields/ExtraPages.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSearchParams } from 'next/navigation';
3 | import { type TFunction } from 'i18next';
4 | import { useAtomValue, useSetAtom } from 'jotai';
5 | import { FieldErrorMessage, NumberField } from '@/app/[lang]/components/form';
6 | import {
7 | MAX_APP_EXTRA_PAGES,
8 | Preset,
9 | apepConditionalRequireAtom,
10 | applFormControlAtom,
11 | showFormErrorsAtom,
12 | presetAtom,
13 | tipBtnClass,
14 | tipContentClass
15 | } from '@/app/lib/txn-data';
16 |
17 | /** Number of Application Extra Pages field */
18 | export default function ExtraPages({ t }: { t: TFunction }) {
19 | const form = useAtomValue(applFormControlAtom);
20 | const preset = useSearchParams().get(Preset.ParamName);
21 | const setPresetAtom = useSetAtom(presetAtom);
22 | const apepCondReqGroup = useAtomValue(apepConditionalRequireAtom);
23 | const showFormErrors = useAtomValue(showFormErrorsAtom);
24 |
25 | // eslint-disable-next-line react-hooks/exhaustive-deps
26 | useEffect(() => setPresetAtom(preset), [preset]);
27 |
28 | return (<>
29 |
53 | form.handleOnChange('apep')(
54 | e.target.value === '' ? undefined : parseInt(e.target.value)
55 | )
56 | }
57 | onFocus={form.handleOnFocus('apep')}
58 | onBlur={form.handleOnBlur('apep')}
59 | />
60 | {(showFormErrors || form.touched.apep) && form.fieldErrors.apep &&
61 |
65 | }
66 | {(showFormErrors || form.touched.apep) &&
67 | !apepCondReqGroup.isValid && apepCondReqGroup.error &&
68 |
72 | }
73 | >);
74 | }
75 |
--------------------------------------------------------------------------------
/src/app/i18n/locales/en/privacy_policy.yml:
--------------------------------------------------------------------------------
1 | title: Privacy Policy
2 | personal_info:
3 | heading: Collection of personal information
4 | details: >-
5 | TxnDuck does not collect or store any personal information (name, email address, etc.) or any
6 | diagnostic data. It does not log any usage information. It does not use any third-party
7 | analytics or advertising platforms.
8 | details_magic: >- # Same as `details`, but with information about Magic exception
9 | Except for the personal information collected when using
10 | Magic , txnDuck does not collect or store any personal information (name, email
11 | address, etc.) or any diagnostic data. It does not log any usage information. It does not use
12 | any third-party analytics or advertising platforms.
13 | app_state_data:
14 | heading: Storage of application state data
15 | details: >-
16 | All of txnDuck’s application state data (temporary transaction data, user preferences, selected
17 | language, etc.) is stored only your device. TxnDuck will never
18 | transmit any of the application state data to anywhere outside your device without your
19 | permission.
20 | wallet_security:
21 | heading: Wallet account and private keys
22 | details: >-
23 | TxnDuck will never directly collect or store any of your private keys or passphrases. Instead,
24 | txnDuck uses a wallet service, such as WalletConnect or Pera Connect ,
25 | to connect to your wallet account without accessing your private key(s). This means txnDuck
26 | cannot and will not access your funds or your assets without your explicit permission, which you
27 | grant through a wallet application, such as Pera and Defly .
28 | magic_auth:
29 | heading: Personal information collected when using Magic
30 | details_1: >-
31 | When using Magic to connect to your wallet, the following personal information is
32 | collected:
33 | details_2: >-
34 |
35 |
36 | Email address
37 |
38 |
39 | Browser and operating system
40 |
41 |
42 | IP address
43 |
44 |
45 | Date and time you attempted to log in using Magic, even if the attempt was unsuccessful
46 |
47 |
48 | details_3: >-
49 | This personal information is kept by Magic and accessible to the txnDuck developer for
50 | up to 30 days through a developer "Dashboard" provided by Magic. This
51 | collection of personal information is controlled by Magic, not the developer. Refer to
52 | Magic’s privacy policy for more information.
53 | notice: Only applies when using Magic as the wallet connection method.
54 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # Default environment variables for all environments
2 |
3 | ##### Build configuration #####
4 |
5 | # Build source into static site
6 | # More information: https://nextjs.org/docs/app/building-your-application/deploying/static-exports
7 | STATIC_BUILD=false
8 | # Build source into a standalone package
9 | # More information: https://nextjs.org/docs/app/api-reference/next-config-js/output#automatically-copying-traced-files
10 | STANDALONE_BUILD=false
11 |
12 | # If Next.js's telemetry should be disabled. More information: https://nextjs.org/telemetry
13 | NEXT_TELEMETRY_DISABLED=1
14 |
15 | ##### App configuration #####
16 |
17 | # Base URL for the site. If blank or omitted, Next.js's default is used.
18 | BASE_URL=
19 | # Default node network
20 | # Possible values: mainnet, testnet, betanet, fnet, voimain, localnet, custom
21 | NEXT_PUBLIC_DEFAULT_NETWORK=mainnet
22 | # WalletConnect project ID
23 | # If the project ID is not set, WalletConnect support will not be enabled.
24 | NEXT_PUBLIC_WC_PROJECT_ID=
25 | # Magic publishable API key
26 | # If the API key is not set, Magic support will not be enabled.
27 | NEXT_PUBLIC_MAGIC_API_KEY=
28 |
29 | # KMD configuration
30 | NEXT_PUBLIC_KMD_TOKEN=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
31 | NEXT_PUBLIC_KMD_BASE_SERVER=http://127.0.0.1
32 | NEXT_PUBLIC_KMD_PORT=4002
33 | NEXT_PUBLIC_KMD_WALLET=unencrypted-default-wallet
34 |
35 |
36 | ##### Feature flags #####
37 |
38 | # If the PWA (Progressive Web App) plugin should be disabled. This will silence the constant
39 | # warnings about how "GenerateSW has been called multiple times..." when in running the dev server.
40 | # However, with the plugin disabled, the web app will not be a PWA when it is built.
41 | DISABLE_PWA=false
42 |
43 | # Language switcher
44 | NEXT_PUBLIC_FEAT_LANG_SWITCHER=true
45 |
46 | # Mnemonic wallet - Entering a mnemonic instead of using a wallet
47 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET=false
48 | # Persist mnemonic wallet to local storage. If not "true", the user will be asked repeatedly for the
49 | # mnemonic every time they need to connect to a wallet or sign a transaction. If "true", the user is
50 | # asked for the mnemonic once because the mnemonic is dangerously stored in local storage in plain
51 | # text. This is ignored if NEXT_PUBLIC_FEAT_MNEMONIC_WALLET is not "true".
52 | NEXT_PUBLIC_FEAT_MNEMONIC_WALLET_PERSIST=false
53 |
54 | # Enable the Transaction Group experimental feature
55 | NEXT_PUBLIC_FEAT_TXN_GROUP_EXP=false
56 |
57 | ##### Debug, warning & error message settings #####
58 |
59 | # Ignore TypeScript errors when building
60 | IGNORE_TS_BUILD_ERRORS=false
61 | # If I18Next should be in debug mode
62 | I18NEXT_DEBUG=false
63 | # If the wallet connection library/libraries should be in debug logging mode
64 | NEXT_PUBLIC_WALLET_DEBUG=false
65 | # Suppress hydration warnings
66 | SUPPRESS_HYDRATION_WARNINGS=false
67 |
--------------------------------------------------------------------------------
/src/app/[lang]/txn/compose/components/fields/KeyRegFields/SelectionKey.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSearchParams } from 'next/navigation';
3 | import { type TFunction } from 'i18next';
4 | import { useAtomValue, useSetAtom } from 'jotai';
5 | import { FieldErrorMessage, TextField } from '@/app/[lang]/components/form';
6 | import {
7 | Preset,
8 | keyRegFormControlAtom,
9 | presetAtom,
10 | selkeyConditionalRequireAtom,
11 | showFormErrorsAtom,
12 | tipBtnClass,
13 | tipContentClass,
14 | } from '@/app/lib/txn-data';
15 |
16 | export default function SelectionKey({ t }: { t: TFunction }) {
17 | const form = useAtomValue(keyRegFormControlAtom);
18 | const preset = useSearchParams().get(Preset.ParamName);
19 | const setPresetAtom = useSetAtom(presetAtom);
20 | const selkeyCondReqGroup = useAtomValue(selkeyConditionalRequireAtom);
21 | const showFormErrors = useAtomValue(showFormErrorsAtom);
22 |
23 | // eslint-disable-next-line react-hooks/exhaustive-deps
24 | useEffect(() => setPresetAtom(preset), [preset]);
25 |
26 | return (!form.values.nonpart && <>
27 | form.handleOnChange('selkey')(e.target.value)}
52 | onFocus={form.handleOnFocus('selkey')}
53 | onBlur={form.handleOnBlur('selkey')}
54 | />
55 | {(showFormErrors || form.touched.selkey) && form.fieldErrors.selkey &&
56 |
60 | }
61 | {(showFormErrors || form.touched.selkey) && !selkeyCondReqGroup.isValid
62 | && selkeyCondReqGroup.error &&
63 |
67 | }
68 | >);
69 | }
70 |
--------------------------------------------------------------------------------