From a25343ef58f82389e1ee34d6e81304cf0bcba9c8 Mon Sep 17 00:00:00 2001 From: Ejirosoft Date: Thu, 25 Jun 2026 13:39:09 +0000 Subject: [PATCH 1/4] feat: add Storybook stories for ConfirmModal, ProgressIndicator, and InsufficientBalanceWarning Adds *.stories.tsx coverage for the remaining UI primitives so every component in src/components/UI/ has a default plus meaningful variants: - ConfirmModal: default, no-description, destructive, and closed states - ProgressIndicator: pending, in-progress, completed, and error states - InsufficientBalanceWarning: testnet, mainnet, and large-shortfall states (wrapped in mocked Wallet/Toast context providers) Also ignores the Storybook build output (storybook-static/). The build-storybook step already runs in .github/workflows/storybook.yml and passes locally via `npm run build-storybook`. --- frontend/.gitignore | 3 ++ .../components/UI/ConfirmModal.stories.tsx | 43 ++++++++++++++++ .../UI/InsufficientBalanceWarning.stories.tsx | 49 ++++++++++++++++++ .../UI/ProgressIndicator.stories.tsx | 51 +++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 frontend/src/components/UI/ConfirmModal.stories.tsx create mode 100644 frontend/src/components/UI/InsufficientBalanceWarning.stories.tsx create mode 100644 frontend/src/components/UI/ProgressIndicator.stories.tsx diff --git a/frontend/.gitignore b/frontend/.gitignore index 68e2ea05..84bb21be 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -2,6 +2,9 @@ dist/ dist-ssr/ +# Storybook build output +storybook-static/ + # Vitest coverage coverage/ diff --git a/frontend/src/components/UI/ConfirmModal.stories.tsx b/frontend/src/components/UI/ConfirmModal.stories.tsx new file mode 100644 index 00000000..2c43485d --- /dev/null +++ b/frontend/src/components/UI/ConfirmModal.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { fn } from '@storybook/test' +import { ConfirmModal } from './ConfirmModal' + +const meta: Meta = { + title: 'UI/ConfirmModal', + component: ConfirmModal, + tags: ['autodocs'], + args: { + isOpen: true, + title: 'Confirm Transaction', + details: [ + { label: 'Amount', value: '100 XLM' }, + { label: 'Recipient', value: 'GABCD…WXYZ' }, + { label: 'Network Fee', value: '0.00001 XLM' }, + ], + onConfirm: fn(), + onCancel: fn(), + }, + parameters: { layout: 'fullscreen' }, +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + description: 'Please review the details before confirming.', + }, +} + +export const WithoutDescription: Story = {} + +export const DestructiveAction: Story = { + args: { + title: 'Delete Token', + description: 'This action cannot be undone.', + confirmLabel: 'Delete', + details: [{ label: 'Token', value: 'MTK — My Token' }], + }, +} + +export const Closed: Story = { args: { isOpen: false } } diff --git a/frontend/src/components/UI/InsufficientBalanceWarning.stories.tsx b/frontend/src/components/UI/InsufficientBalanceWarning.stories.tsx new file mode 100644 index 00000000..d38f2350 --- /dev/null +++ b/frontend/src/components/UI/InsufficientBalanceWarning.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { fn } from '@storybook/test' +import { InsufficientBalanceWarning } from './InsufficientBalanceWarning' +import { WalletContext } from '../../context/WalletContext' +import { ToastContext } from '../../context/ToastContext' + +const mockWalletValue = { + wallet: { address: 'GABCDEFGHIJKLMNOPQRSTUVWXYZ234567', isConnected: true, balance: '5.0000000' }, + isConnecting: false, + error: null, + isInstalled: true, + connect: fn(), + disconnect: fn(), + refreshBalance: fn(), +} + +const mockToastValue = { + toasts: [], + addToast: fn(), + removeToast: fn(), +} + +const meta: Meta = { + title: 'UI/InsufficientBalanceWarning', + component: InsufficientBalanceWarning, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + + + ), + ], + args: { + shortfall: '0.5000000', + isTestnet: true, + }, +} +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +export const Mainnet: Story = { args: { isTestnet: false } } + +export const LargeShortfall: Story = { args: { shortfall: '125.0000000' } } diff --git a/frontend/src/components/UI/ProgressIndicator.stories.tsx b/frontend/src/components/UI/ProgressIndicator.stories.tsx new file mode 100644 index 00000000..53a6de9a --- /dev/null +++ b/frontend/src/components/UI/ProgressIndicator.stories.tsx @@ -0,0 +1,51 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { ProgressIndicator } from './ProgressIndicator' + +const meta: Meta = { + title: 'UI/ProgressIndicator', + component: ProgressIndicator, + tags: ['autodocs'], +} +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + steps: [ + { label: 'Validate token details', status: 'pending' }, + { label: 'Submit transaction', status: 'pending' }, + { label: 'Confirm on network', status: 'pending' }, + ], + }, +} + +export const InProgress: Story = { + args: { + steps: [ + { label: 'Validate token details', status: 'completed' }, + { label: 'Submit transaction', status: 'in-progress' }, + { label: 'Confirm on network', status: 'pending' }, + ], + }, +} + +export const Completed: Story = { + args: { + steps: [ + { label: 'Validate token details', status: 'completed' }, + { label: 'Submit transaction', status: 'completed' }, + { label: 'Confirm on network', status: 'completed' }, + ], + }, +} + +export const WithError: Story = { + args: { + steps: [ + { label: 'Validate token details', status: 'completed' }, + { label: 'Submit transaction', status: 'error' }, + { label: 'Confirm on network', status: 'pending' }, + ], + }, +} From 512b11e8cad65979e4fbbdac073ddb8dc5ba1fa8 Mon Sep 17 00:00:00 2001 From: Ejirosoft Date: Thu, 25 Jun 2026 13:47:45 +0000 Subject: [PATCH 2/4] fix: add esbuild as a direct devDependency for Storybook CI build The Storybook CI job installs with `npm install --legacy-peer-deps`, which skips peer dependencies. esbuild was only present as a peer dep, so esbuild-register (used to evaluate .storybook/main.ts) failed with "Cannot find module 'esbuild'" in CI. Declaring esbuild directly ensures it is hoisted to top-level node_modules during the workflow build. --- frontend/package-lock.json | 30 ++---------------------------- frontend/package.json | 1 + 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3b0fd9ed..3eff148a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,7 @@ "@vitest/coverage-v8": "^4.1.3", "@vitest/ui": "^4.1.3", "autoprefixer": "^10.4.16", + "esbuild": "^0.28.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsx-a11y": "^6.10.2", @@ -2247,7 +2248,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -2265,7 +2265,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2283,7 +2282,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2301,7 +2299,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2319,7 +2316,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2337,7 +2333,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2355,7 +2350,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2373,7 +2367,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2391,7 +2384,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2409,7 +2401,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2427,7 +2418,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2445,7 +2435,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2463,7 +2452,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2481,7 +2469,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2499,7 +2486,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2517,7 +2503,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2535,7 +2520,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2553,7 +2537,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2571,7 +2554,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2589,7 +2571,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2607,7 +2588,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2625,7 +2605,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -2643,7 +2622,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -2661,7 +2639,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2679,7 +2656,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2697,7 +2673,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -7720,7 +7695,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12780,7 +12754,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/frontend/package.json b/frontend/package.json index 2bc47e33..8cc24d79 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -61,6 +61,7 @@ "@vitest/coverage-v8": "^4.1.3", "@vitest/ui": "^4.1.3", "autoprefixer": "^10.4.16", + "esbuild": "^0.28.0", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsx-a11y": "^6.10.2", From 0469882e67c5ae3a99ab123d3ff23cc600c681bd Mon Sep 17 00:00:00 2001 From: Ejirosoft Date: Thu, 25 Jun 2026 14:38:57 +0000 Subject: [PATCH 3/4] style: apply prettier formatting to frontend src Fixes the Frontend 'Check formatting' CI job (prettier --check). Repo-wide formatting debt unrelated to the Storybook stories but required for this branch's CI to pass. Co-Authored-By: Claude Opus 4.8 --- frontend/src/App.tsx | 425 ++++++++++-------- frontend/src/components/AddressDisplay.tsx | 5 +- frontend/src/components/AdminPanel.tsx | 8 +- frontend/src/components/BurnForm.tsx | 55 ++- frontend/src/components/CopyButton.tsx | 30 +- frontend/src/components/CreateToken.tsx | 14 +- frontend/src/components/DropZone/DropZone.tsx | 21 +- .../src/components/DropZone/validateFile.ts | 4 +- frontend/src/components/ErrorBoundary.tsx | 15 +- frontend/src/components/ExplorerLink.tsx | 3 +- .../components/ExplorerLink/ExplorerLink.tsx | 8 +- frontend/src/components/FAQ.tsx | 51 ++- .../src/components/MetadataUploadForm.tsx | 20 +- frontend/src/components/MintForm.tsx | 52 ++- .../src/components/MisconfigurationScreen.tsx | 13 +- frontend/src/components/NavBar.tsx | 44 +- frontend/src/components/NetworkBadge.tsx | 10 +- frontend/src/components/ShareButton.tsx | 63 ++- frontend/src/components/TokenCreateForm.tsx | 40 +- frontend/src/components/TokenDashboard.tsx | 38 +- frontend/src/components/TokenDetail.tsx | 45 +- frontend/src/components/TokenForm.tsx | 15 +- frontend/src/components/TokenHistory.tsx | 2 +- .../src/components/TransactionHistory.tsx | 113 +++-- frontend/src/components/UI/ConfirmModal.tsx | 16 +- frontend/src/components/UI/Input.stories.tsx | 4 +- frontend/src/components/UI/Input.tsx | 5 +- frontend/src/components/UI/Modal.tsx | 11 +- frontend/src/components/UI/index.ts | 8 +- .../__tests__/AdminPanel.integration.test.tsx | 10 +- frontend/src/context/DarkModeContext.tsx | 9 +- frontend/src/context/ThemeContext.tsx | 14 +- frontend/src/context/WalletContext.tsx | 31 +- frontend/src/hooks/useLocalStorage.ts | 1 - frontend/src/hooks/useShareToken.ts | 7 +- frontend/src/hooks/useTokenDashboard.ts | 4 +- frontend/src/hooks/useTransactionHistory.ts | 195 ++++---- frontend/src/hooks/useWallet.test.tsx | 24 +- frontend/src/lib/monitoring/errorBoundary.tsx | 3 +- frontend/src/main.tsx | 4 +- .../src/test/DropZone/useDropZone.test.ts | 14 +- .../src/test/explorer/ExplorerLink.test.tsx | 4 +- frontend/src/test/formatting.test.ts | 2 +- frontend/src/test/monitoring/alerts.test.ts | 5 +- frontend/src/test/monitoring/sentry.test.ts | 4 +- frontend/src/test/retry.test.ts | 1 - frontend/src/utils/retry.ts | 3 +- frontend/src/utils/tokenFilters.test.ts | 19 +- frontend/src/utils/tokenFilters.ts | 9 +- frontend/src/utils/truncateAddress.ts | 6 +- frontend/src/utils/validation.test.ts | 16 +- 51 files changed, 929 insertions(+), 594 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 781e469f..e6e8ffe1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,18 +42,28 @@ interface RouteErrorFallbackProps { resetErrorBoundary?: () => void } -const RouteErrorFallback: React.FC = ({ routeName, resetErrorBoundary }) => ( +const RouteErrorFallback: React.FC = ({ + routeName, + resetErrorBoundary, +}) => (
- +
-

{routeName} encountered an error

+

+ {routeName} encountered an error +

- Something went wrong while loading this page. You can try again without affecting the rest of the app. + Something went wrong while loading this page. You can try again without affecting the rest + of the app.

) -const RouteBoundary: React.FC<{ routeName: string; children: React.ReactNode }> = ({ routeName, children }) => ( - }> - {children} - +const RouteBoundary: React.FC<{ routeName: string; children: React.ReactNode }> = ({ + routeName, + children, +}) => ( + }>{children} ) function AppContent() { @@ -97,46 +108,67 @@ function AppContent() { {t('app.skipToMain')} -
-
+
+
-

{t('app.title')}

-

{t('app.subtitle')}

+

+ {t('app.title')} +

+

+ {t('app.subtitle')} +

-
- -
- -
-
- -
- - {wallet.isConnected && ( +
+
- +
- )} - -
+
+ +
+ + {wallet.isConnected && ( +
+ +
+ )} + +
{/* Mobile-only info row */} @@ -146,7 +178,9 @@ function AppContent() { {truncateAddress(wallet.address)} - {wallet.balance && {formatXLM(wallet.balance)}} + {wallet.balance && ( + {formatXLM(wallet.balance)} + )}
)}
@@ -163,167 +197,176 @@ function AppContent() { {showOnboarding && null /* OnboardingModal placeholder */} {!isFactoryConfigured() && ( -
+
⚠️ Factory contract not configured. Please set{' '} - VITE_FACTORY_CONTRACT_ID{' '} - in your .env file. + + VITE_FACTORY_CONTRACT_ID + {' '} + in your{' '} + .env{' '} + file.
)}
- {error && ( -
-

{t('errors.title')}

-

{error}

-
- )} + {error && ( +
+

{t('errors.title')}

+

{error}

+
+ )} -
- - - +
+ + + + + } + /> + + + - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - + + } + /> + + + - } - /> - - -
-

Set Token Metadata

- -
-
- - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - - - - - - } - /> - } /> -
-
+ + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + } + /> + + +
+

+ Set Token Metadata +

+ +
+
+ + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + } /> +
+
- -
- - ) - } + +
+ + ) +} function App() { return ( diff --git a/frontend/src/components/AddressDisplay.tsx b/frontend/src/components/AddressDisplay.tsx index 4230ff15..ac6e7187 100644 --- a/frontend/src/components/AddressDisplay.tsx +++ b/frontend/src/components/AddressDisplay.tsx @@ -21,10 +21,7 @@ export const AddressDisplay = ({ {truncateAddress(address)} - {showCopy && ( - - )} - + {showCopy && } {showExplorer && ( { }, [state]) // Authorization guard — only the factory admin may see this panel - const isAdmin = - !!wallet.address && !!state?.admin && wallet.address === state.admin + const isAdmin = !!wallet.address && !!state?.admin && wallet.address === state.admin if (stateLoading) { return ( @@ -80,10 +79,7 @@ export const AdminPanel: React.FC = () => { if (!isAdmin) { return ( -
+
Access denied. Only the factory admin can view this page.
) diff --git a/frontend/src/components/BurnForm.tsx b/frontend/src/components/BurnForm.tsx index 87d3446e..e0f9b9cb 100644 --- a/frontend/src/components/BurnForm.tsx +++ b/frontend/src/components/BurnForm.tsx @@ -47,7 +47,9 @@ export const BurnForm: React.FC = ({ useEffect(() => { mountedRef.current = true - return () => { mountedRef.current = false } + return () => { + mountedRef.current = false + } }, []) const { @@ -72,10 +74,11 @@ export const BurnForm: React.FC = ({ const selectedToken = myTokens.find((t) => t.address === tokenSelect) const debouncedAddress = useDebounce(resolvedTokenAddress, 300) - const { balance, isLoading: balanceLoading, refresh: refreshBalance } = useTokenBalance( - debouncedAddress, - wallet.address ?? '', - ) + const { + balance, + isLoading: balanceLoading, + refresh: refreshBalance, + } = useTokenBalance(debouncedAddress, wallet.address ?? '') // Validate amount against balance using BigInt (token amounts can be large) const amountExceedsBalance = @@ -83,7 +86,11 @@ export const BurnForm: React.FC = ({ !!balance && balance !== '0' && (() => { - try { return BigInt(amount) > BigInt(balance) } catch { return false } + try { + return BigInt(amount) > BigInt(balance) + } catch { + return false + } })() const burnBuilder = useCallback( @@ -93,10 +100,16 @@ export const BurnForm: React.FC = ({ const { execute: executeBurn, status: txStatus } = useTransaction(burnBuilder) const isSubmitting = - txStatus === 'simulating' || txStatus === 'signing' || txStatus === 'submitting' || txStatus === 'polling' + txStatus === 'simulating' || + txStatus === 'signing' || + txStatus === 'submitting' || + txStatus === 'polling' const onValid = () => { - if (!wallet.isConnected) { addToast('Connect your wallet first', 'error'); return } + if (!wallet.isConnected) { + addToast('Connect your wallet first', 'error') + return + } requireTos(() => setPending(true)) } @@ -125,19 +138,23 @@ export const BurnForm: React.FC = ({ return ( <>
- {/* Danger zone header */}

- Burning tokens is permanent and irreversible. Burned tokens cannot be recovered. + Burning tokens is permanent and irreversible. Burned tokens cannot be + recovered.

{/* Token selector */}