From b363a282a4542971c2f6937c2441ea94dc13fe4c Mon Sep 17 00:00:00 2001 From: Gaubee Date: Thu, 29 Jan 2026 16:00:28 +0800 Subject: [PATCH] feat: complete implementation --- deno.lock | 2 +- e2e/ecosystem-miniapp.mock.spec.ts | 12 +- e2e/forge-auth.mock.spec.ts | 14 +- e2e/miniapp-ui.mock.spec.ts | 8 +- miniapps/{forge => biobridge}/.oxlintrc.json | 0 .../{forge => biobridge}/.storybook/main.ts | 0 .../.storybook/preview.tsx | 0 .../.storybook/vitest.setup.ts | 0 miniapps/{forge => biobridge}/components.json | 0 .../Mobile-Chrome/ui.spec.ts/01-connect.png | Bin .../Mobile-Chrome/ui.spec.ts/02-swap.png | Bin .../ui.spec.ts/03-swap-amount.png | Bin .../ui.spec.ts/04-swap-tokens.png | Bin .../Mobile-Chrome/ui.spec.ts/05-confirm.png | Bin .../Mobile-Chrome/ui.spec.ts/06-error.png | Bin .../ui.spec.ts/07-flow-complete.png | Bin .../Mobile-Chrome/ui.spec.ts/09-mode-tabs.png | Bin .../ui.spec.ts/09b-redemption-mode.png | Bin .../ui.spec.ts/09c-redemption-form.png | Bin .../ui.spec.ts/09d-redemption-amount.png | Bin .../ui.spec.ts/12-api-failure-confirm.png | Bin .../{forge => biobridge}/e2e/helpers/i18n.ts | 0 miniapps/{forge => biobridge}/e2e/ui.spec.ts | 0 miniapps/{forge => biobridge}/index.html | 0 miniapps/{forge => biobridge}/manifest.json | 0 miniapps/{forge => biobridge}/package.json | 0 .../{forge => biobridge}/playwright.config.ts | 0 miniapps/{forge => biobridge}/public/icon.svg | 0 miniapps/{forge => biobridge}/scripts/e2e.ts | 0 .../{forge => biobridge}/src/App.stories.tsx | 0 .../{forge => biobridge}/src/App.test.tsx | 0 miniapps/{forge => biobridge}/src/App.tsx | 240 +++++++---- .../{forge => biobridge}/src/api/client.ts | 0 .../{forge => biobridge}/src/api/config.ts | 2 + .../src/api/helpers.test.ts | 0 .../{forge => biobridge}/src/api/helpers.ts | 0 .../{forge => biobridge}/src/api/index.ts | 0 .../src/api/recharge.test.ts | 0 .../{forge => biobridge}/src/api/recharge.ts | 15 + .../src/api/redemption.ts | 0 .../{forge => biobridge}/src/api/schemas.ts | 11 + .../{forge => biobridge}/src/api/types.ts | 12 + miniapps/{forge => biobridge}/src/bio.d.ts | 0 .../src/components/BackgroundBeams.tsx | 0 .../src/components/FireButton.stories.tsx | 0 .../src/components/FireButton.tsx | 0 .../src/components/ModeTabs.tsx | 0 .../src/components/RedemptionForm.tsx | 185 ++++++--- .../src/components/ui/avatar.tsx | 0 .../src/components/ui/badge.tsx | 0 .../src/components/ui/button.tsx | 0 .../src/components/ui/card.tsx | 0 .../src/components/ui/input.tsx | 0 .../src/components/ui/separator.tsx | 0 .../{forge => biobridge}/src/hooks/index.ts | 1 + .../src/hooks/useContractPool.ts | 0 .../src/hooks/useForge.test.ts | 31 ++ .../src/hooks/useForge.ts | 16 +- .../src/hooks/useRechargeConfig.test.ts | 28 ++ .../src/hooks/useRechargeConfig.ts | 17 +- .../src/hooks/useRechargeRecords.ts | 0 .../src/hooks/useRedemption.ts | 18 +- .../src/hooks/useRedemptionRecords.ts | 0 .../biobridge/src/hooks/useTokenInfoMap.ts | 105 +++++ .../src/i18n/index.test.ts | 0 .../{forge => biobridge}/src/i18n/index.ts | 0 .../src/i18n/locales/en.json | 3 + .../src/i18n/locales/zh-CN.json | 3 + .../src/i18n/locales/zh-TW.json | 3 + .../src/i18n/locales/zh.json | 3 + miniapps/{forge => biobridge}/src/index.css | 0 .../src/lib/chain.test.ts | 0 .../{forge => biobridge}/src/lib/chain.ts | 0 miniapps/{forge => biobridge}/src/lib/fee.ts | 0 .../src/lib/tron-address.test.ts | 0 .../src/lib/tron-address.ts | 0 .../{forge => biobridge}/src/lib/utils.ts | 0 miniapps/{forge => biobridge}/src/main.tsx | 0 .../{forge => biobridge}/src/test-setup.ts | 0 .../{forge => biobridge}/src/vite-env.d.ts | 0 miniapps/{forge => biobridge}/tsconfig.json | 0 miniapps/{forge => biobridge}/vite.config.ts | 0 .../{forge => biobridge}/vitest.config.ts | 0 miniapps/forge/src/i18n/i18next.d.ts | 12 - package.json | 2 +- pnpm-lock.yaml | 2 +- scripts/dev.ts | 173 +++++++- scripts/dev/control-panel.ts | 25 +- .../ecosystem/ecosystem-desktop.stories.tsx | 14 +- src/components/layout/bottom-sheet.tsx | 36 +- .../transaction/transaction-item.tsx | 8 +- src/i18n/locales/ar/devtools.json | 3 + src/i18n/locales/en/devtools.json | 3 + src/i18n/locales/zh-CN/devtools.json | 3 + src/i18n/locales/zh-TW/devtools.json | 3 + src/lib/url-resolver.ts | 2 +- .../bioforest/transaction-mixin.ts | 3 + src/services/chain-adapter/types.ts | 1 + .../ecosystem/handlers/transaction.ts | 46 ++- src/services/ecosystem/my-apps.ts | 4 +- src/services/ecosystem/types.ts | 4 + .../mock-devtools/components/ConsolePanel.tsx | 2 + .../components/DevPanelShell.tsx | 375 ++++++++++++++++++ .../mock-devtools/components/MockDevTools.tsx | 320 ++------------- src/services/mock-devtools/index.tsx | 6 + .../activities/sheets/ChainSelectorJob.tsx | 5 +- .../sheets/ChainSwitchConfirmJob.tsx | 2 +- .../activities/sheets/ClearDataConfirmJob.tsx | 7 +- .../sheets/ContactAddConfirmJob.tsx | 2 +- .../activities/sheets/ContactEditJob.tsx | 7 +- .../activities/sheets/ContactPickerJob.tsx | 5 +- .../activities/sheets/ContactShareJob.tsx | 7 +- .../activities/sheets/CryptoAuthorizeJob.tsx | 2 +- .../sheets/MiniappDestroyConfirmJob.tsx | 2 +- .../sheets/MiniappSignTransactionJob.tsx | 2 +- .../sheets/MiniappTransferConfirmJob.tsx | 2 +- .../activities/sheets/MnemonicOptionsJob.tsx | 5 +- .../sheets/PermissionRequestJob.tsx | 2 +- .../activities/sheets/ScannerJob.tsx | 2 +- .../activities/sheets/SetTwoStepSecretJob.tsx | 32 +- .../activities/sheets/SigningConfirmJob.tsx | 2 +- .../activities/sheets/TransferConfirmJob.tsx | 2 +- .../sheets/TransferWalletLockJob.tsx | 14 +- .../sheets/TwoStepSecretConfirmJob.tsx | 7 +- .../activities/sheets/WalletAddJob.tsx | 5 +- .../activities/sheets/WalletDeleteJob.tsx | 8 +- .../activities/sheets/WalletListJob.tsx | 5 +- .../sheets/WalletLockConfirmJob.tsx | 7 +- .../activities/sheets/WalletPickerJob.tsx | 9 +- .../activities/sheets/WalletRenameJob.tsx | 7 +- src/stores/ecosystem.test.ts | 6 +- 131 files changed, 1373 insertions(+), 559 deletions(-) rename miniapps/{forge => biobridge}/.oxlintrc.json (100%) rename miniapps/{forge => biobridge}/.storybook/main.ts (100%) rename miniapps/{forge => biobridge}/.storybook/preview.tsx (100%) rename miniapps/{forge => biobridge}/.storybook/vitest.setup.ts (100%) rename miniapps/{forge => biobridge}/components.json (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png (100%) rename miniapps/{forge => biobridge}/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png (100%) rename miniapps/{forge => biobridge}/e2e/helpers/i18n.ts (100%) rename miniapps/{forge => biobridge}/e2e/ui.spec.ts (100%) rename miniapps/{forge => biobridge}/index.html (100%) rename miniapps/{forge => biobridge}/manifest.json (100%) rename miniapps/{forge => biobridge}/package.json (100%) rename miniapps/{forge => biobridge}/playwright.config.ts (100%) rename miniapps/{forge => biobridge}/public/icon.svg (100%) rename miniapps/{forge => biobridge}/scripts/e2e.ts (100%) rename miniapps/{forge => biobridge}/src/App.stories.tsx (100%) rename miniapps/{forge => biobridge}/src/App.test.tsx (100%) rename miniapps/{forge => biobridge}/src/App.tsx (77%) rename miniapps/{forge => biobridge}/src/api/client.ts (100%) rename miniapps/{forge => biobridge}/src/api/config.ts (96%) rename miniapps/{forge => biobridge}/src/api/helpers.test.ts (100%) rename miniapps/{forge => biobridge}/src/api/helpers.ts (100%) rename miniapps/{forge => biobridge}/src/api/index.ts (100%) rename miniapps/{forge => biobridge}/src/api/recharge.test.ts (100%) rename miniapps/{forge => biobridge}/src/api/recharge.ts (88%) rename miniapps/{forge => biobridge}/src/api/redemption.ts (100%) rename miniapps/{forge => biobridge}/src/api/schemas.ts (91%) rename miniapps/{forge => biobridge}/src/api/types.ts (97%) rename miniapps/{forge => biobridge}/src/bio.d.ts (100%) rename miniapps/{forge => biobridge}/src/components/BackgroundBeams.tsx (100%) rename miniapps/{forge => biobridge}/src/components/FireButton.stories.tsx (100%) rename miniapps/{forge => biobridge}/src/components/FireButton.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ModeTabs.tsx (100%) rename miniapps/{forge => biobridge}/src/components/RedemptionForm.tsx (72%) rename miniapps/{forge => biobridge}/src/components/ui/avatar.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ui/badge.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ui/button.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ui/card.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ui/input.tsx (100%) rename miniapps/{forge => biobridge}/src/components/ui/separator.tsx (100%) rename miniapps/{forge => biobridge}/src/hooks/index.ts (92%) rename miniapps/{forge => biobridge}/src/hooks/useContractPool.ts (100%) rename miniapps/{forge => biobridge}/src/hooks/useForge.test.ts (91%) rename miniapps/{forge => biobridge}/src/hooks/useForge.ts (92%) rename miniapps/{forge => biobridge}/src/hooks/useRechargeConfig.test.ts (85%) rename miniapps/{forge => biobridge}/src/hooks/useRechargeConfig.ts (82%) rename miniapps/{forge => biobridge}/src/hooks/useRechargeRecords.ts (100%) rename miniapps/{forge => biobridge}/src/hooks/useRedemption.ts (88%) rename miniapps/{forge => biobridge}/src/hooks/useRedemptionRecords.ts (100%) create mode 100644 miniapps/biobridge/src/hooks/useTokenInfoMap.ts rename miniapps/{forge => biobridge}/src/i18n/index.test.ts (100%) rename miniapps/{forge => biobridge}/src/i18n/index.ts (100%) rename miniapps/{forge => biobridge}/src/i18n/locales/en.json (95%) rename miniapps/{forge => biobridge}/src/i18n/locales/zh-CN.json (94%) rename miniapps/{forge => biobridge}/src/i18n/locales/zh-TW.json (94%) rename miniapps/{forge => biobridge}/src/i18n/locales/zh.json (94%) rename miniapps/{forge => biobridge}/src/index.css (100%) rename miniapps/{forge => biobridge}/src/lib/chain.test.ts (100%) rename miniapps/{forge => biobridge}/src/lib/chain.ts (100%) rename miniapps/{forge => biobridge}/src/lib/fee.ts (100%) rename miniapps/{forge => biobridge}/src/lib/tron-address.test.ts (100%) rename miniapps/{forge => biobridge}/src/lib/tron-address.ts (100%) rename miniapps/{forge => biobridge}/src/lib/utils.ts (100%) rename miniapps/{forge => biobridge}/src/main.tsx (100%) rename miniapps/{forge => biobridge}/src/test-setup.ts (100%) rename miniapps/{forge => biobridge}/src/vite-env.d.ts (100%) rename miniapps/{forge => biobridge}/tsconfig.json (100%) rename miniapps/{forge => biobridge}/vite.config.ts (100%) rename miniapps/{forge => biobridge}/vitest.config.ts (100%) delete mode 100644 miniapps/forge/src/i18n/i18next.d.ts create mode 100644 src/services/mock-devtools/components/DevPanelShell.tsx diff --git a/deno.lock b/deno.lock index 9b712a535..ca59cf627 100644 --- a/deno.lock +++ b/deno.lock @@ -10258,7 +10258,7 @@ ] }, "members": { - "miniapps/forge": { + "miniapps/biobridge": { "packageJson": { "dependencies": [ "npm:@base-ui/react@1", diff --git a/e2e/ecosystem-miniapp.mock.spec.ts b/e2e/ecosystem-miniapp.mock.spec.ts index 47cb3577e..6c8627f9e 100644 --- a/e2e/ecosystem-miniapp.mock.spec.ts +++ b/e2e/ecosystem-miniapp.mock.spec.ts @@ -113,7 +113,7 @@ test.describe('生态 Tab 截图测试', () => { await page.addInitScript(() => { localStorage.setItem('ecosystem_my_apps', JSON.stringify([ { appId: 'xin.dweb.teleport', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }, - { appId: 'xin.dweb.forge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, + { appId: 'xin.dweb.biobridge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, ])) }) @@ -138,7 +138,7 @@ test.describe('生态 Tab 截图测试', () => { await page.addInitScript(() => { localStorage.setItem('ecosystem_my_apps', JSON.stringify([ { appId: 'xin.dweb.teleport', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }, - { appId: 'xin.dweb.forge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, + { appId: 'xin.dweb.biobridge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, ])) }) @@ -222,7 +222,7 @@ test.describe('生态 Tab 截图测试', () => { await page.addInitScript(() => { localStorage.setItem('ecosystem_my_apps', JSON.stringify([ { appId: 'xin.dweb.teleport', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }, - { appId: 'xin.dweb.forge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, + { appId: 'xin.dweb.biobridge', installedAt: Date.now() - 7200000, lastUsedAt: Date.now() - 3600000 }, ])) }) @@ -457,7 +457,7 @@ test.describe('签名消息变体截图测试', () => { message: longMessage, address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', appName: 'DeFi Protocol', - appIcon: '/miniapps/forge/icon.svg', + appIcon: '/miniapps/biobridge/icon.svg', }) await page.goto(`/#/job/signing-confirm?${params}`) await page.waitForLoadState('networkidle') @@ -475,7 +475,7 @@ test.describe('转账金额变体截图测试', () => { test('大额转账', async ({ page }) => { const params = new URLSearchParams({ appName: '交易所提现', - appIcon: '/miniapps/forge/icon.svg', + appIcon: '/miniapps/biobridge/icon.svg', from: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', to: 'c8X7yWePwIqsQxf6R0AwXs8DqQo6Nl6Yz4', amount: '999999.12345678', @@ -665,7 +665,7 @@ test.describe('安全警告截图测试', () => { message: hexMessage, address: 'c7R6wVdPvHqvRxe5Q9ZvWr7CpPn5Mk5Xz3', appName: '未知 DApp', - appIcon: '/miniapps/forge/icon.svg', + appIcon: '/miniapps/biobridge/icon.svg', }) await page.goto(`/#/job/signing-confirm?${params}`) await page.waitForLoadState('networkidle') diff --git a/e2e/forge-auth.mock.spec.ts b/e2e/forge-auth.mock.spec.ts index c87976633..59486e636 100644 --- a/e2e/forge-auth.mock.spec.ts +++ b/e2e/forge-auth.mock.spec.ts @@ -62,7 +62,7 @@ async function injectTestData(page: import('@playwright/test').Page) { localStorage.setItem('bfm_wallets', JSON.stringify(data.wallet)) localStorage.setItem('bfm_ecosystem', JSON.stringify(data.ecosystem)) localStorage.setItem('ecosystem_my_apps', JSON.stringify([ - { appId: 'xin.dweb.forge', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }, + { appId: 'xin.dweb.biobridge', installedAt: Date.now() - 3600000, lastUsedAt: Date.now() - 1800000 }, ])) }, { wallet: TEST_WALLET_DATA, ecosystem: TEST_ECOSYSTEM_DATA }) } @@ -95,12 +95,12 @@ test.describe('Forge 小程序授权流程', () => { await page.waitForTimeout(300) // 右键菜单 -> 打开 - await page.locator('[data-testid="ios-app-icon-xin.dweb.forge"]').click({ button: 'right' }) + await page.locator('[data-testid="ios-app-icon-xin.dweb.biobridge"]').click({ button: 'right' }) await page.waitForTimeout(200) await page.locator('button:has-text("打开")').click() // 等待 Forge iframe 加载 - const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.forge"]') + const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.biobridge"]') // 验证连接按钮可见 (Forge 显示"开始兑换"或连接按钮) await expect(forgeFrame.getByTestId('connect-button')).toBeVisible({ timeout: 15000 }) @@ -120,12 +120,12 @@ test.describe('Forge 小程序授权流程', () => { await page.waitForTimeout(300) // 打开 Forge - await page.locator('[data-testid="ios-app-icon-xin.dweb.forge"]').click({ button: 'right' }) + await page.locator('[data-testid="ios-app-icon-xin.dweb.biobridge"]').click({ button: 'right' }) await page.waitForTimeout(200) await page.locator('button:has-text("打开")').click() // 等待 Forge iframe 加载 - const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.forge"]') + const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.biobridge"]') const connectButton = forgeFrame.getByTestId('connect-button') // 等待按钮可见 @@ -175,12 +175,12 @@ test.describe('Forge 小程序授权流程', () => { await page.waitForTimeout(300) // 打开 Forge - await page.locator('[data-testid="ios-app-icon-xin.dweb.forge"]').click({ button: 'right' }) + await page.locator('[data-testid="ios-app-icon-xin.dweb.biobridge"]').click({ button: 'right' }) await page.waitForTimeout(200) await page.locator('button:has-text("打开")').click() // 等待 Forge iframe 加载 - const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.forge"]') + const forgeFrame = page.frameLocator('iframe[data-app-id="xin.dweb.biobridge"]') await forgeFrame.getByTestId('connect-button').waitFor({ state: 'visible', timeout: 15000 }) // 验证没有 "Method not found: bio_closeSplashScreen" 错误 diff --git a/e2e/miniapp-ui.mock.spec.ts b/e2e/miniapp-ui.mock.spec.ts index 8c26cf7ae..9fad59d08 100644 --- a/e2e/miniapp-ui.mock.spec.ts +++ b/e2e/miniapp-ui.mock.spec.ts @@ -68,12 +68,12 @@ test.describe('Forge 小程序 UI', () => { }) test('连接页面 - 初始状态', async ({ page }) => { - await gotoMiniapp(page, 'xin.dweb.forge') + await gotoMiniapp(page, 'xin.dweb.biobridge') await expect(page).toHaveScreenshot('forge-01-connect.png') }) test('连接页面 - 暗色主题', async ({ page }) => { - await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark') + await gotoMiniapp(page, 'xin.dweb.biobridge', '?colorMode=dark') await page.evaluate(() => { document.documentElement.classList.add('dark') @@ -84,7 +84,7 @@ test.describe('Forge 小程序 UI', () => { }) test('连接页面 - 自定义主题色', async ({ page }) => { - await gotoMiniapp(page, 'xin.dweb.forge', '?primaryHue=145') + await gotoMiniapp(page, 'xin.dweb.biobridge', '?primaryHue=145') await page.evaluate(() => { document.documentElement.style.setProperty('--primary-hue', '145') @@ -114,7 +114,7 @@ test.describe('小程序主题同步', () => { test('Forge - 暗色 + 自定义主题色', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) - await gotoMiniapp(page, 'xin.dweb.forge', '?colorMode=dark&primaryHue=280') + await gotoMiniapp(page, 'xin.dweb.biobridge', '?colorMode=dark&primaryHue=280') await page.evaluate(() => { document.documentElement.classList.add('dark') diff --git a/miniapps/forge/.oxlintrc.json b/miniapps/biobridge/.oxlintrc.json similarity index 100% rename from miniapps/forge/.oxlintrc.json rename to miniapps/biobridge/.oxlintrc.json diff --git a/miniapps/forge/.storybook/main.ts b/miniapps/biobridge/.storybook/main.ts similarity index 100% rename from miniapps/forge/.storybook/main.ts rename to miniapps/biobridge/.storybook/main.ts diff --git a/miniapps/forge/.storybook/preview.tsx b/miniapps/biobridge/.storybook/preview.tsx similarity index 100% rename from miniapps/forge/.storybook/preview.tsx rename to miniapps/biobridge/.storybook/preview.tsx diff --git a/miniapps/forge/.storybook/vitest.setup.ts b/miniapps/biobridge/.storybook/vitest.setup.ts similarity index 100% rename from miniapps/forge/.storybook/vitest.setup.ts rename to miniapps/biobridge/.storybook/vitest.setup.ts diff --git a/miniapps/forge/components.json b/miniapps/biobridge/components.json similarity index 100% rename from miniapps/forge/components.json rename to miniapps/biobridge/components.json diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png b/miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png similarity index 100% rename from miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png rename to miniapps/biobridge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png diff --git a/miniapps/forge/e2e/helpers/i18n.ts b/miniapps/biobridge/e2e/helpers/i18n.ts similarity index 100% rename from miniapps/forge/e2e/helpers/i18n.ts rename to miniapps/biobridge/e2e/helpers/i18n.ts diff --git a/miniapps/forge/e2e/ui.spec.ts b/miniapps/biobridge/e2e/ui.spec.ts similarity index 100% rename from miniapps/forge/e2e/ui.spec.ts rename to miniapps/biobridge/e2e/ui.spec.ts diff --git a/miniapps/forge/index.html b/miniapps/biobridge/index.html similarity index 100% rename from miniapps/forge/index.html rename to miniapps/biobridge/index.html diff --git a/miniapps/forge/manifest.json b/miniapps/biobridge/manifest.json similarity index 100% rename from miniapps/forge/manifest.json rename to miniapps/biobridge/manifest.json diff --git a/miniapps/forge/package.json b/miniapps/biobridge/package.json similarity index 100% rename from miniapps/forge/package.json rename to miniapps/biobridge/package.json diff --git a/miniapps/forge/playwright.config.ts b/miniapps/biobridge/playwright.config.ts similarity index 100% rename from miniapps/forge/playwright.config.ts rename to miniapps/biobridge/playwright.config.ts diff --git a/miniapps/forge/public/icon.svg b/miniapps/biobridge/public/icon.svg similarity index 100% rename from miniapps/forge/public/icon.svg rename to miniapps/biobridge/public/icon.svg diff --git a/miniapps/forge/scripts/e2e.ts b/miniapps/biobridge/scripts/e2e.ts similarity index 100% rename from miniapps/forge/scripts/e2e.ts rename to miniapps/biobridge/scripts/e2e.ts diff --git a/miniapps/forge/src/App.stories.tsx b/miniapps/biobridge/src/App.stories.tsx similarity index 100% rename from miniapps/forge/src/App.stories.tsx rename to miniapps/biobridge/src/App.stories.tsx diff --git a/miniapps/forge/src/App.test.tsx b/miniapps/biobridge/src/App.test.tsx similarity index 100% rename from miniapps/forge/src/App.test.tsx rename to miniapps/biobridge/src/App.test.tsx diff --git a/miniapps/forge/src/App.tsx b/miniapps/biobridge/src/App.tsx similarity index 77% rename from miniapps/forge/src/App.tsx rename to miniapps/biobridge/src/App.tsx index 8c30046db..7fb2631a8 100644 --- a/miniapps/forge/src/App.tsx +++ b/miniapps/biobridge/src/App.tsx @@ -8,12 +8,13 @@ import { useTranslation } from 'react-i18next'; import type { BioAccount } from '@biochain/bio-sdk'; import { normalizeChainId } from '@biochain/bio-sdk'; import { getChainType, getEvmChainIdFromApi } from '@/lib/chain'; +import { parseAmount } from '@/lib/fee'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { BackgroundBeams } from './components/BackgroundBeams'; import { ModeTabs } from './components/ModeTabs'; @@ -33,7 +34,7 @@ import { ArrowLeftRight, } from 'lucide-react'; -import { useRechargeConfig, useForge, type ForgeOption } from '@/hooks'; +import { useRechargeConfig, useForge, useTokenInfoMap, getTokenInfoKey, type ForgeOption } from '@/hooks'; import type { BridgeMode } from '@/api/types'; type RechargeStep = 'connect' | 'swap' | 'confirm' | 'processing' | 'success'; @@ -47,7 +48,15 @@ const TOKEN_COLORS: Record = { BFC: 'bg-blue-600', }; -function TokenAvatar({ symbol, size = 'sm' }: { symbol: string; size?: 'sm' | 'md' }) { +function TokenAvatar({ + symbol, + logo, + size = 'sm', +}: { + symbol: string; + logo?: string; + size?: 'sm' | 'md'; +}) { const iconSize = size === 'md' ? 'size-5' : 'size-4'; const icons: Record = { ETH: , @@ -58,10 +67,19 @@ function TokenAvatar({ symbol, size = 'sm' }: { symbol: string; size?: 'sm' | 'm USDT: , }; return ( - - - {icons[symbol] || } - + + {logo ? ( + + ) : ( + + {icons[symbol] || } + + )} ); } @@ -134,19 +152,23 @@ export default function App() { [forgeHook], ); - const handleConnect = useCallback(async () => { + const handleConnect = useCallback(async (option?: ForgeOption) => { + const activeOption = option ?? selectedOption; + if (option) { + setSelectedOption(option); + } if (!window.bio) { setError('Bio SDK not initialized'); return; } - if (!selectedOption) { + if (!activeOption) { setError('Please select an option'); return; } setLoading(true); setError(null); try { - const externalChain = selectedOption.externalChain; + const externalChain = activeOption.externalChain; const chainType = getChainType(externalChain); let extAcc: BioAccount; @@ -198,7 +220,7 @@ export default function App() { const intAcc = await window.bio.request({ method: 'bio_selectAccount', - params: [{ chain: selectedOption.internalChain }], + params: [{ chain: activeOption.internalChain }], }); setInternalAccount(intAcc); @@ -220,35 +242,58 @@ export default function App() { }; const externalDecimals = - typeof selectedOption?.externalInfo.decimals === 'number' && Number.isFinite(selectedOption.externalInfo.decimals) + typeof selectedOption?.externalInfo.decimals === 'number' && + Number.isFinite(selectedOption.externalInfo.decimals) && + selectedOption.externalInfo.decimals > 0 ? selectedOption.externalInfo.decimals : undefined; + const tokenInfoTargets = useMemo( + () => + forgeOptions.map((option) => ({ + chain: option.externalChain, + address: option.externalInfo.contract, + })), + [forgeOptions], + ); + const { tokenInfoMap, loadingMap } = useTokenInfoMap(tokenInfoTargets); const externalTokenAddress = selectedOption?.externalInfo.contract?.trim(); - const externalDecimalsFallback = externalTokenAddress - ? (() => { - switch (selectedOption?.externalChain) { - case 'ETH': - case 'BSC': - return 18; - case 'TRON': - return 6; - default: - return undefined; - } - })() - : undefined; - const resolvedExternalDecimals = externalDecimals ?? externalDecimalsFallback; - const usingFallbackDecimals = - Boolean(externalTokenAddress) && externalDecimals === undefined && resolvedExternalDecimals !== undefined; + const tokenInfoKey = + selectedOption?.externalChain && externalTokenAddress + ? getTokenInfoKey(selectedOption.externalChain, externalTokenAddress) + : undefined; + const tokenInfo = tokenInfoKey ? tokenInfoMap[tokenInfoKey] : undefined; + const tokenInfoLoading = tokenInfoKey ? Boolean(loadingMap[tokenInfoKey]) : false; + const resolvedExternalDecimals = tokenInfo?.decimals ?? externalDecimals; + const usingRemoteDecimals = Boolean(externalTokenAddress) && tokenInfo?.decimals !== undefined; + const resolvedExternalSymbol = tokenInfo?.symbol ?? selectedOption?.externalAsset ?? ''; + const resolvedExternalLogo = selectedOption?.externalLogo ?? tokenInfo?.icon; const handleConfirm = useCallback(async () => { if (!externalAccount || !internalAccount || !selectedOption) return; const tokenAddress = selectedOption.externalInfo.contract?.trim(); + if (tokenAddress && tokenInfoLoading) { + setError(t('error.decimalsLoading')); + return; + } if (tokenAddress && resolvedExternalDecimals === undefined) { setError(t('error.missingDecimals')); return; } + let amountInUnits: string | undefined; + if (typeof resolvedExternalDecimals === 'number') { + try { + amountInUnits = parseAmount(amount, resolvedExternalDecimals).toString(); + } catch { + setError(t('error.invalidAmount')); + return; + } + if (amountInUnits === '0') { + setError(t('error.invalidAmount')); + return; + } + } + setError(null); setRechargeStep('processing'); @@ -256,6 +301,7 @@ export default function App() { externalChain: selectedOption.externalChain, externalAsset: selectedOption.externalAsset, externalDecimals: resolvedExternalDecimals, + amountInUnits, depositAddress: selectedOption.externalInfo.depositAddress, externalContract: tokenAddress, amount, @@ -294,17 +340,6 @@ export default function App() { setPickerOpen(false); }; - const handleSelectExternalChain = useCallback( - (externalChain: string) => { - const options = groupedOptions[externalChain]; - const first = options?.[0]; - if (first) { - setSelectedOption(first); - } - }, - [groupedOptions], - ); - // Check if redemption is available const hasRedemptionOptions = useMemo(() => { if (!config) return false; @@ -408,37 +443,67 @@ export default function App() {

{t('app.description')}

- {/* Available chains preview */} {forgeOptions.length > 0 && ( -
- {Object.keys(groupedOptions).map((chain) => ( - - - +
+
+ {options.map((option, index) => { + const isSelected = + selectedOption?.externalAsset === option.externalAsset && + selectedOption?.externalChain === option.externalChain; + const isPrimary = isSelected || (!selectedOption && index === 0); + const optionTokenInfoKey = option.externalInfo.contract?.trim() + ? getTokenInfoKey(option.externalChain, option.externalInfo.contract.trim()) + : undefined; + const optionTokenInfo = optionTokenInfoKey ? tokenInfoMap[optionTokenInfoKey] : undefined; + const externalSymbol = optionTokenInfo?.symbol ?? option.externalAsset; + const externalName = optionTokenInfo?.name; + const externalLogo = option.externalLogo ?? optionTokenInfo?.icon; + const externalLabel = `${externalSymbol} (${getChainName(option.externalChain)})`; + const internalLabel = `${option.internalAsset} (${getChainName(option.internalChain)})`; + const secondaryLine = `${externalName ? `${externalName} · ` : ''}${externalLabel} ${t('common.arrow')} ${internalLabel}`; + return ( + + ); + })} +
+ ))} )} - - )} @@ -446,9 +511,9 @@ export default function App() { {rechargeStep === 'swap' && selectedOption && ( {/* From Card (External Chain) */} @@ -464,8 +529,8 @@ export default function App() {
- - {selectedOption.externalAsset} + + {resolvedExternalSymbol}
-
{t('forge.amountHint', { symbol: selectedOption.externalAsset })}
+
{t('forge.amountHint', { symbol: resolvedExternalSymbol })}
+ {tokenInfoLoading && ( +
{t('forge.decimalsLoading')}
+ )} {typeof resolvedExternalDecimals === 'number' && (
{t('forge.decimalsHint', { decimals: resolvedExternalDecimals })} - {usingFallbackDecimals ? ` (${t('forge.decimalsFallback')})` : null} + {usingRemoteDecimals ? ` (${t('forge.decimalsFromTokenInfo')})` : null}
)} {contractDisplay && ( @@ -518,7 +586,7 @@ export default function App() {
- + {selectedOption.internalAsset}
@@ -566,9 +634,9 @@ export default function App() { {rechargeStep === 'confirm' && selectedOption && ( @@ -581,8 +649,8 @@ export default function App() { })}
- - {amount} {selectedOption.externalAsset} + + {amount} {resolvedExternalSymbol}
@@ -600,7 +668,7 @@ export default function App() { })}
- + {amount} {selectedOption.internalAsset}
@@ -630,9 +698,15 @@ export default function App() {
-
{t('forge.amountHint', { symbol: selectedOption.externalAsset })}
- {typeof externalDecimals === 'number' && ( -
{t('forge.decimalsHint', { decimals: externalDecimals })}
+
{t('forge.amountHint', { symbol: resolvedExternalSymbol })}
+ {tokenInfoLoading && ( +
{t('forge.decimalsLoading')}
+ )} + {typeof resolvedExternalDecimals === 'number' && ( +
+ {t('forge.decimalsHint', { decimals: resolvedExternalDecimals })} + {usingRemoteDecimals ? ` (${t('forge.decimalsFromTokenInfo')})` : null} +
)} {contractDisplay && (
@@ -747,7 +821,7 @@ export default function App() { onClick={() => handleSelectOption(option)} > - +
{t('picker.optionAssets', { diff --git a/miniapps/forge/src/api/client.ts b/miniapps/biobridge/src/api/client.ts similarity index 100% rename from miniapps/forge/src/api/client.ts rename to miniapps/biobridge/src/api/client.ts diff --git a/miniapps/forge/src/api/config.ts b/miniapps/biobridge/src/api/config.ts similarity index 96% rename from miniapps/forge/src/api/config.ts rename to miniapps/biobridge/src/api/config.ts index 3db6699be..c52f4f654 100644 --- a/miniapps/forge/src/api/config.ts +++ b/miniapps/biobridge/src/api/config.ts @@ -36,6 +36,8 @@ export const API_ENDPOINTS = { RECHARGE_RETRY_EXTERNAL: '/cot/recharge/retryExternalOnChain', /** 充值内链上链重试 */ RECHARGE_RETRY_INTERNAL: '/cot/recharge/retryInternalOnChain', + /** 获取合约代币信息 */ + CONTRACT_TOKEN_INFO: '/wallet/contractTokenInfo/getTokenInfo', // ============================================================================ // Redemption Endpoints (赎回) diff --git a/miniapps/forge/src/api/helpers.test.ts b/miniapps/biobridge/src/api/helpers.test.ts similarity index 100% rename from miniapps/forge/src/api/helpers.test.ts rename to miniapps/biobridge/src/api/helpers.test.ts diff --git a/miniapps/forge/src/api/helpers.ts b/miniapps/biobridge/src/api/helpers.ts similarity index 100% rename from miniapps/forge/src/api/helpers.ts rename to miniapps/biobridge/src/api/helpers.ts diff --git a/miniapps/forge/src/api/index.ts b/miniapps/biobridge/src/api/index.ts similarity index 100% rename from miniapps/forge/src/api/index.ts rename to miniapps/biobridge/src/api/index.ts diff --git a/miniapps/forge/src/api/recharge.test.ts b/miniapps/biobridge/src/api/recharge.test.ts similarity index 100% rename from miniapps/forge/src/api/recharge.test.ts rename to miniapps/biobridge/src/api/recharge.test.ts diff --git a/miniapps/forge/src/api/recharge.ts b/miniapps/biobridge/src/api/recharge.ts similarity index 88% rename from miniapps/forge/src/api/recharge.ts rename to miniapps/biobridge/src/api/recharge.ts index 96ff7d28b..1e31bf17c 100644 --- a/miniapps/forge/src/api/recharge.ts +++ b/miniapps/biobridge/src/api/recharge.ts @@ -10,6 +10,7 @@ import { rechargeSubmitSchema, rechargeRecordsSchema, rechargeRecordDetailSchema, + contractTokenInfoSchema, } from './schemas' import type { RechargeSupportResDto, @@ -23,6 +24,7 @@ import type { RechargeRecordDetailResDto, RetryOnChainReqDto, ExternalAssetInfoItem, + ContractTokenInfo, } from './types' /** @@ -95,6 +97,19 @@ export const rechargeApi = { return parsed.data }, + /** 获取合约代币信息(精度/图标等) */ + async getTokenInfo(params: { contractAddress: string; chainName: string }): Promise { + const raw = await apiClient.get(API_ENDPOINTS.CONTRACT_TOKEN_INFO, { + contractAddress: params.contractAddress, + chainName: params.chainName, + }) + const parsed = contractTokenInfoSchema.safeParse(raw) + if (!parsed.success) { + throw new ApiError('Invalid contract token info response', 0, parsed.error.flatten()) + } + return parsed.data + }, + /** 获取合约池信息 */ getContractPoolInfo(params: RechargeContractPoolReqDto): Promise { return apiClient.get(API_ENDPOINTS.CONTRACT_POOL_INFO, { internalChainName: params.internalChainName }) diff --git a/miniapps/forge/src/api/redemption.ts b/miniapps/biobridge/src/api/redemption.ts similarity index 100% rename from miniapps/forge/src/api/redemption.ts rename to miniapps/biobridge/src/api/redemption.ts diff --git a/miniapps/forge/src/api/schemas.ts b/miniapps/biobridge/src/api/schemas.ts similarity index 91% rename from miniapps/forge/src/api/schemas.ts rename to miniapps/biobridge/src/api/schemas.ts index 66f9a48a3..853a188d0 100644 --- a/miniapps/forge/src/api/schemas.ts +++ b/miniapps/biobridge/src/api/schemas.ts @@ -45,6 +45,17 @@ export const rechargeSubmitSchema = z.object({ orderId: z.string(), }).passthrough() +export const contractTokenInfoSchema = z.object({ + chain: z.string(), + address: z.string(), + name: z.string().optional(), + symbol: z.string().optional(), + decimals: z.coerce.number(), + icon: z.string().optional(), + totalSupply: z.string().optional(), + website: z.string().optional(), +}).passthrough() + const recordTxInfoSchema = z.object({ chainName: z.string(), assetType: z.string(), diff --git a/miniapps/forge/src/api/types.ts b/miniapps/biobridge/src/api/types.ts similarity index 97% rename from miniapps/forge/src/api/types.ts rename to miniapps/biobridge/src/api/types.ts index 3fc2be322..af5951988 100644 --- a/miniapps/forge/src/api/types.ts +++ b/miniapps/biobridge/src/api/types.ts @@ -68,6 +68,18 @@ export interface RechargeSupportResDto { recharge: RechargeConfig } +/** 合约代币信息 */ +export interface ContractTokenInfo { + chain: string + address: string + name?: string + symbol?: string + decimals: number + icon?: string + totalSupply?: string + website?: string +} + /** TRON 交易体 */ export interface TronTransaction { txID: string diff --git a/miniapps/forge/src/bio.d.ts b/miniapps/biobridge/src/bio.d.ts similarity index 100% rename from miniapps/forge/src/bio.d.ts rename to miniapps/biobridge/src/bio.d.ts diff --git a/miniapps/forge/src/components/BackgroundBeams.tsx b/miniapps/biobridge/src/components/BackgroundBeams.tsx similarity index 100% rename from miniapps/forge/src/components/BackgroundBeams.tsx rename to miniapps/biobridge/src/components/BackgroundBeams.tsx diff --git a/miniapps/forge/src/components/FireButton.stories.tsx b/miniapps/biobridge/src/components/FireButton.stories.tsx similarity index 100% rename from miniapps/forge/src/components/FireButton.stories.tsx rename to miniapps/biobridge/src/components/FireButton.stories.tsx diff --git a/miniapps/forge/src/components/FireButton.tsx b/miniapps/biobridge/src/components/FireButton.tsx similarity index 100% rename from miniapps/forge/src/components/FireButton.tsx rename to miniapps/biobridge/src/components/FireButton.tsx diff --git a/miniapps/forge/src/components/ModeTabs.tsx b/miniapps/biobridge/src/components/ModeTabs.tsx similarity index 100% rename from miniapps/forge/src/components/ModeTabs.tsx rename to miniapps/biobridge/src/components/ModeTabs.tsx diff --git a/miniapps/forge/src/components/RedemptionForm.tsx b/miniapps/biobridge/src/components/RedemptionForm.tsx similarity index 72% rename from miniapps/forge/src/components/RedemptionForm.tsx rename to miniapps/biobridge/src/components/RedemptionForm.tsx index a2403de28..a318ad889 100644 --- a/miniapps/forge/src/components/RedemptionForm.tsx +++ b/miniapps/biobridge/src/components/RedemptionForm.tsx @@ -6,18 +6,20 @@ import { useState, useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import type { BioAccount } from '@biochain/bio-sdk' +import { normalizeChainId } from '@biochain/bio-sdk' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardDescription } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' -import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Separator } from '@/components/ui/separator' import { motion } from 'framer-motion' import { cn } from '@/lib/utils' -import { ArrowDown, AlertCircle, Check, Coins, DollarSign, Leaf } from 'lucide-react' +import { ArrowDown, AlertCircle, Check, Coins, DollarSign, Leaf, Loader2 } from 'lucide-react' import type { RechargeConfig, ExternalChainName, RechargeItem } from '@/api/types' import { useRedemption } from '@/hooks/useRedemption' +import { useTokenInfoMap, getTokenInfoKey } from '@/hooks' import { calculateRedemptionFee, formatAmount, parseAmount, isAmountWithinLimits } from '@/lib/fee' interface RedemptionFormProps { @@ -32,6 +34,9 @@ interface RedemptionOption { externalChain: ExternalChainName externalAsset: string rechargeItem: RechargeItem + internalLogo?: string + externalLogo?: string + externalContract?: string } const TOKEN_COLORS: Record = { @@ -43,7 +48,18 @@ const TOKEN_COLORS: Record = { BFC: 'bg-blue-600', } -function TokenAvatar({ symbol, size = 'sm' }: { symbol: string; size?: 'sm' | 'md' }) { +/** Chain display order: BSC -> TRON -> ETH */ +const CHAIN_ORDER: Record = { BSC: 1, TRON: 2, ETH: 3 } + +function TokenAvatar({ + symbol, + logo, + size = 'sm', +}: { + symbol: string + logo?: string + size?: 'sm' | 'md' +}) { const iconSize = size === 'md' ? 'size-5' : 'size-4' const icons: Record = { ETH: , @@ -54,10 +70,19 @@ function TokenAvatar({ symbol, size = 'sm' }: { symbol: string; size?: 'sm' | 'm USDT: , } return ( - - - {icons[symbol] || } - + + {logo ? ( + + ) : ( + + {icons[symbol] || } + + )} ) } @@ -73,6 +98,7 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { const [externalAddress, setExternalAddress] = useState('') const [step, setStep] = useState<'select' | 'form' | 'confirm' | 'processing' | 'success'>('select') const [error, setError] = useState(null) + const [connecting, setConnecting] = useState(false) // Parse available redemption options from config const redemptionOptions = useMemo(() => { @@ -91,6 +117,9 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { externalChain: externalChain as ExternalChainName, externalAsset: externalInfo.assetType, rechargeItem: item, + internalLogo: item.logo, + externalLogo: externalInfo.logo, + externalContract: externalInfo.contract?.trim(), }) } } @@ -99,6 +128,36 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { return options }, [config]) + const groupedOptions = useMemo(() => { + const groups: Record = {} + for (const option of redemptionOptions) { + if (!groups[option.externalChain]) { + groups[option.externalChain] = [] + } + groups[option.externalChain].push(option) + } + return Object.entries(groups) + .sort( + ([a], [b]) => (CHAIN_ORDER[a] ?? 99) - (CHAIN_ORDER[b] ?? 99), + ) + }, [redemptionOptions]) + + const tokenInfoTargets = useMemo( + () => + redemptionOptions.map((option) => ({ + chain: option.externalChain, + address: option.externalContract, + })), + [redemptionOptions], + ) + const { tokenInfoMap } = useTokenInfoMap(tokenInfoTargets) + const selectedTokenInfo = useMemo(() => { + if (!selectedOption?.externalContract) return undefined + return tokenInfoMap[getTokenInfoKey(selectedOption.externalChain, selectedOption.externalContract)] + }, [selectedOption, tokenInfoMap]) + const selectedExternalSymbol = selectedTokenInfo?.symbol ?? selectedOption?.externalAsset ?? '' + const selectedExternalLogo = selectedOption?.externalLogo ?? selectedTokenInfo?.icon + // Auto-select first option useEffect(() => { if (redemptionOptions.length > 0 && !selectedOption) { @@ -141,21 +200,28 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { return isAmountWithinLimits(amountInUnits, selectedOption.rechargeItem.redemption) }, [selectedOption, amount]) - const handleConnect = useCallback(async () => { - if (!window.bio || !selectedOption) return - + const handleConnect = useCallback(async (option?: RedemptionOption) => { + const activeOption = option ?? selectedOption + if (!window.bio || !activeOption || connecting) return + + if (option) { + setSelectedOption(option) + } + setConnecting(true) setError(null) try { const acc = await window.bio.request({ method: 'bio_selectAccount', - params: [{ chain: selectedOption.internalChain }], + params: [{ chain: normalizeChainId(activeOption.internalChain) }], }) setInternalAccount(acc) setStep('form') } catch (err) { setError(err instanceof Error ? err.message : 'Connection failed') + } finally { + setConnecting(false) } - }, [selectedOption]) + }, [selectedOption, connecting]) const handlePreview = () => { if (!amount || !externalAddress) { @@ -244,39 +310,68 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {

{t('redemption.description')}

- {/* Chain selection */} -
- {redemptionOptions.map((opt) => ( - - + ) })} - - +
+
))}
- - )} {/* Form */} {step === 'form' && selectedOption && internalAccount && ( {/* From (Internal) */} @@ -292,7 +387,7 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {
- + {selectedOption.internalAsset}
- - {selectedOption.externalAsset} + + {selectedExternalSymbol}
{feeResult?.isValid ? formatAmount(feeResult.receivable) : '0.00'} @@ -367,7 +462,7 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) {
{t('redemption.receivable')} - {formatAmount(feeResult.receivable)} {selectedOption.externalAsset} + {formatAmount(feeResult.receivable)} {selectedExternalSymbol}
@@ -385,8 +480,8 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { {/* Confirm */} {step === 'confirm' && selectedOption && feeResult && ( @@ -399,7 +494,7 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { })}
- + {amount} {selectedOption.internalAsset}
@@ -418,8 +513,8 @@ export function RedemptionForm({ config, onSuccess }: RedemptionFormProps) { })}
- - {formatAmount(feeResult.receivable)} {selectedOption.externalAsset} + + {formatAmount(feeResult.receivable)} {selectedExternalSymbol}
diff --git a/miniapps/forge/src/components/ui/avatar.tsx b/miniapps/biobridge/src/components/ui/avatar.tsx similarity index 100% rename from miniapps/forge/src/components/ui/avatar.tsx rename to miniapps/biobridge/src/components/ui/avatar.tsx diff --git a/miniapps/forge/src/components/ui/badge.tsx b/miniapps/biobridge/src/components/ui/badge.tsx similarity index 100% rename from miniapps/forge/src/components/ui/badge.tsx rename to miniapps/biobridge/src/components/ui/badge.tsx diff --git a/miniapps/forge/src/components/ui/button.tsx b/miniapps/biobridge/src/components/ui/button.tsx similarity index 100% rename from miniapps/forge/src/components/ui/button.tsx rename to miniapps/biobridge/src/components/ui/button.tsx diff --git a/miniapps/forge/src/components/ui/card.tsx b/miniapps/biobridge/src/components/ui/card.tsx similarity index 100% rename from miniapps/forge/src/components/ui/card.tsx rename to miniapps/biobridge/src/components/ui/card.tsx diff --git a/miniapps/forge/src/components/ui/input.tsx b/miniapps/biobridge/src/components/ui/input.tsx similarity index 100% rename from miniapps/forge/src/components/ui/input.tsx rename to miniapps/biobridge/src/components/ui/input.tsx diff --git a/miniapps/forge/src/components/ui/separator.tsx b/miniapps/biobridge/src/components/ui/separator.tsx similarity index 100% rename from miniapps/forge/src/components/ui/separator.tsx rename to miniapps/biobridge/src/components/ui/separator.tsx diff --git a/miniapps/forge/src/hooks/index.ts b/miniapps/biobridge/src/hooks/index.ts similarity index 92% rename from miniapps/forge/src/hooks/index.ts rename to miniapps/biobridge/src/hooks/index.ts index e91810a2a..209132a9b 100644 --- a/miniapps/forge/src/hooks/index.ts +++ b/miniapps/biobridge/src/hooks/index.ts @@ -6,6 +6,7 @@ export { useRechargeConfig, type ForgeOption, type RechargeConfigState } from './useRechargeConfig' export { useForge, type ForgeParams, type ForgeState, type ForgeStep } from './useForge' export { useContractPool, type ContractPoolState } from './useContractPool' +export { useTokenInfoMap, getTokenInfoKey } from './useTokenInfoMap' export { useRechargeRecords, useRechargeRecordDetail, diff --git a/miniapps/forge/src/hooks/useContractPool.ts b/miniapps/biobridge/src/hooks/useContractPool.ts similarity index 100% rename from miniapps/forge/src/hooks/useContractPool.ts rename to miniapps/biobridge/src/hooks/useContractPool.ts diff --git a/miniapps/forge/src/hooks/useForge.test.ts b/miniapps/biobridge/src/hooks/useForge.test.ts similarity index 91% rename from miniapps/forge/src/hooks/useForge.test.ts rename to miniapps/biobridge/src/hooks/useForge.test.ts index 0319260a9..effaaf43f 100644 --- a/miniapps/forge/src/hooks/useForge.test.ts +++ b/miniapps/biobridge/src/hooks/useForge.test.ts @@ -19,6 +19,7 @@ const mockForgeParams: ForgeParams = { externalAsset: 'ETH', depositAddress: '0x1234567890abcdef1234567890abcdef12345678', externalContract: undefined, + externalDecimals: 18, amount: '1.5', externalAccount: { address: '0xabcdef1234567890abcdef1234567890abcdef12', chain: 'eth', publicKey: '0x' }, internalChain: 'bfmeta', @@ -189,6 +190,36 @@ describe('useForge', () => { expect(submitCall.fromTrJson.eth?.signTransData).toBe('0xsignedEthTx') }) + it('should scale external amount by decimals when provided', async () => { + mockBio.request + .mockResolvedValueOnce({ txHash: 'unsigned123' }) + .mockResolvedValueOnce({ data: '0xsigned123' }) + .mockResolvedValueOnce({ signature: 'signature123', publicKey: 'pubkey123' }) + + vi.mocked(rechargeApi.submitRecharge).mockResolvedValue({ orderId: 'order123' }) + + const { result } = renderHook(() => useForge()) + + act(() => { + result.current.forge({ + ...mockForgeParams, + amount: '10', + externalDecimals: 18, + }) + }) + + await waitFor(() => { + expect(result.current.step).toBe('success') + }) + + const createTxCall = mockBio.request.mock.calls[0][0] as { + method: string + params: Array<{ amount: string }> + } + expect(createTxCall.method).toBe('bio_createTransaction') + expect(createTxCall.params[0]?.amount).toBe('10000000000000000000') + }) + it('should build correct fromTrJson for BSC', async () => { mockBio.request .mockResolvedValueOnce({ txHash: 'unsigned' }) diff --git a/miniapps/forge/src/hooks/useForge.ts b/miniapps/biobridge/src/hooks/useForge.ts similarity index 92% rename from miniapps/forge/src/hooks/useForge.ts rename to miniapps/biobridge/src/hooks/useForge.ts index 28378889f..373a307f0 100644 --- a/miniapps/forge/src/hooks/useForge.ts +++ b/miniapps/biobridge/src/hooks/useForge.ts @@ -8,6 +8,7 @@ import { normalizeChainId } from '@biochain/bio-sdk' import { rechargeApi } from '@/api' import { encodeRechargeV2ToTrInfoData, createRechargeMessage } from '@/api/helpers' import { validateDepositAddress } from '@/lib/chain' +import { parseAmount } from '@/lib/fee' import { superjson } from '@biochain/chain-effect' import type { ExternalChainName, @@ -38,6 +39,8 @@ export interface ForgeParams { externalContract?: string /** 转账金额 */ amount: string + /** 转账金额(外链最小单位) */ + amountInUnits?: string /** 外链账户(已连接) */ externalAccount: BioAccount /** 内链名称 */ @@ -123,6 +126,7 @@ export function useForge() { externalChain, externalAsset, externalDecimals, + amountInUnits, depositAddress, externalContract, amount, @@ -152,12 +156,22 @@ export function useForge() { const externalKeyAppChainId = normalizeChainId(externalChain) + let normalizedAmount = amountInUnits ?? amount + if (amountInUnits === undefined && typeof externalDecimals === 'number') { + try { + normalizedAmount = parseAmount(amount, externalDecimals).toString() + } catch { + setState({ step: 'error', orderId: null, error: 'Invalid amount' }) + return + } + } + const unsignedTx = await window.bio.request({ method: 'bio_createTransaction', params: [{ from: externalAccount.address, to: depositAddress, - amount, + amount: normalizedAmount, chain: externalKeyAppChainId, asset: externalAsset, tokenAddress, diff --git a/miniapps/forge/src/hooks/useRechargeConfig.test.ts b/miniapps/biobridge/src/hooks/useRechargeConfig.test.ts similarity index 85% rename from miniapps/forge/src/hooks/useRechargeConfig.test.ts rename to miniapps/biobridge/src/hooks/useRechargeConfig.test.ts index 7d66e27fd..da685f73b 100644 --- a/miniapps/forge/src/hooks/useRechargeConfig.test.ts +++ b/miniapps/biobridge/src/hooks/useRechargeConfig.test.ts @@ -176,4 +176,32 @@ describe('useRechargeConfig', () => { expect(result.current.forgeOptions).toHaveLength(1) expect(result.current.forgeOptions[0].externalChain).toBe('ETH') }) + + it('should normalize invalid decimals to undefined', async () => { + const configWithInvalidDecimals: RechargeSupportResDto = { + recharge: { + bfmeta: { + BFM: { + enable: true, + chainName: 'bfmeta', + assetType: 'BFM', + applyAddress: 'bfm-apply', + supportChain: { + ETH: { enable: true, assetType: 'ETH', depositAddress: '0x1', decimals: 0 }, + }, + }, + }, + }, + } + + vi.mocked(rechargeApi.getSupport).mockResolvedValue(configWithInvalidDecimals) + + const { result } = renderHook(() => useRechargeConfig()) + + await waitFor(() => { + expect(result.current.isLoading).toBe(false) + }) + + expect(result.current.forgeOptions[0]?.externalInfo.decimals).toBeUndefined() + }) }) diff --git a/miniapps/forge/src/hooks/useRechargeConfig.ts b/miniapps/biobridge/src/hooks/useRechargeConfig.ts similarity index 82% rename from miniapps/forge/src/hooks/useRechargeConfig.ts rename to miniapps/biobridge/src/hooks/useRechargeConfig.ts index 5a732b3fb..1dd777b8d 100644 --- a/miniapps/forge/src/hooks/useRechargeConfig.ts +++ b/miniapps/biobridge/src/hooks/useRechargeConfig.ts @@ -23,13 +23,20 @@ export interface ForgeOption { internalChain: string /** 内链资产类型 */ internalAsset: string - /** Logo */ - logo?: string + /** 外链 Logo */ + externalLogo?: string + /** 内链 Logo */ + internalLogo?: string } /** Chain display order: BSC -> TRON -> ETH */ const CHAIN_ORDER: Record = { BSC: 1, TRON: 2, ETH: 3 } +function normalizeDecimals(value: number | undefined): number | undefined { + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined + return value +} + /** * Parse recharge config to forge options */ @@ -42,14 +49,16 @@ function parseForgeOptions(config: RechargeConfig): ForgeOption[] { for (const [externalChain, externalInfo] of Object.entries(item.supportChain)) { if (!externalInfo?.enable) continue + const normalizedDecimals = normalizeDecimals(externalInfo.decimals) options.push({ externalChain: externalChain as ExternalChainName, externalAsset: externalInfo.assetType, - externalInfo, + externalInfo: { ...externalInfo, decimals: normalizedDecimals }, internalChain, internalAsset, - logo: item.logo || externalInfo.logo, + externalLogo: externalInfo.logo, + internalLogo: item.logo, }) } } diff --git a/miniapps/forge/src/hooks/useRechargeRecords.ts b/miniapps/biobridge/src/hooks/useRechargeRecords.ts similarity index 100% rename from miniapps/forge/src/hooks/useRechargeRecords.ts rename to miniapps/biobridge/src/hooks/useRechargeRecords.ts diff --git a/miniapps/forge/src/hooks/useRedemption.ts b/miniapps/biobridge/src/hooks/useRedemption.ts similarity index 88% rename from miniapps/forge/src/hooks/useRedemption.ts rename to miniapps/biobridge/src/hooks/useRedemption.ts index 041ca2687..7f6f734d9 100644 --- a/miniapps/forge/src/hooks/useRedemption.ts +++ b/miniapps/biobridge/src/hooks/useRedemption.ts @@ -5,6 +5,7 @@ import { useState, useCallback } from 'react' import type { BioAccount } from '@biochain/bio-sdk' +import { normalizeChainId } from '@biochain/bio-sdk' import { redemptionApi } from '@/api' import type { ExternalChainName, @@ -90,16 +91,19 @@ export function useRedemption() { } // Create DestroyAsset transaction via Bio SDK + const internalChainId = normalizeChainId(internalChain) + const unsignedTx = await window.bio.request({ method: 'bio_createTransaction', params: [{ - type: 'destroyAsset', + type: 'destroy', from: internalAccount.address, - to: applyAddress, // recipientId = 内链币发行地址 + to: applyAddress, + recipientId: applyAddress, // asset issuer amount, - chain: internalChain, + chain: internalChainId, asset: internalAsset, - remark: JSON.stringify(remark), + remark, }], }) @@ -108,11 +112,11 @@ export function useRedemption() { const unsignedTxSafe = toJsonSafe(unsignedTx) - const signedTx = await window.bio.request<{ trJson: unknown }>({ + const signedTx = await window.bio.request<{ trJson?: unknown; data?: unknown }>({ method: 'bio_signTransaction', params: [{ from: internalAccount.address, - chain: internalChain, + chain: internalChainId, unsignedTx: unsignedTxSafe, }], }) @@ -124,7 +128,7 @@ export function useRedemption() { fromTrJson: { bcf: { chainName: internalChain, - trJson: signedTx.trJson || signedTx, + trJson: signedTx.trJson ?? signedTx.data ?? signedTx, }, }, } diff --git a/miniapps/forge/src/hooks/useRedemptionRecords.ts b/miniapps/biobridge/src/hooks/useRedemptionRecords.ts similarity index 100% rename from miniapps/forge/src/hooks/useRedemptionRecords.ts rename to miniapps/biobridge/src/hooks/useRedemptionRecords.ts diff --git a/miniapps/biobridge/src/hooks/useTokenInfoMap.ts b/miniapps/biobridge/src/hooks/useTokenInfoMap.ts new file mode 100644 index 000000000..55d699ced --- /dev/null +++ b/miniapps/biobridge/src/hooks/useTokenInfoMap.ts @@ -0,0 +1,105 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { rechargeApi } from '@/api' +import type { ContractTokenInfo } from '@/api/types' + +export interface TokenInfoTarget { + chain: string + address?: string | null +} + +export function getTokenInfoKey(chain: string, address: string): string { + return `${chain}:${address.toLowerCase()}` +} + +export function useTokenInfoMap(targets: TokenInfoTarget[]) { + const [tokenInfoMap, setTokenInfoMap] = useState>({}) + const [loadingMap, setLoadingMap] = useState>({}) + const tokenInfoRef = useRef(tokenInfoMap) + const loadingRef = useRef(new Set()) + + useEffect(() => { + tokenInfoRef.current = tokenInfoMap + }, [tokenInfoMap]) + + const targetKeys = useMemo(() => { + return targets + .map((target) => { + const address = target.address?.trim() + if (!address) return null + return getTokenInfoKey(target.chain, address) + }) + .filter((key): key is string => Boolean(key)) + .sort() + .join('|') + }, [targets]) + + useEffect(() => { + let cancelled = false + + const toFetch = targets + .map((target) => { + const address = target.address?.trim() + if (!address) return null + const key = getTokenInfoKey(target.chain, address) + return { key, chain: target.chain, address } + }) + .filter((entry): entry is { key: string; chain: string; address: string } => Boolean(entry)) + .filter((entry) => !tokenInfoRef.current[entry.key] && !loadingRef.current.has(entry.key)) + + if (toFetch.length === 0) return + + toFetch.forEach((entry) => loadingRef.current.add(entry.key)) + setLoadingMap((prev) => { + const next = { ...prev } + toFetch.forEach((entry) => { + next[entry.key] = true + }) + return next + }) + + Promise.all( + toFetch.map(async (entry) => { + try { + const info = await rechargeApi.getTokenInfo({ + contractAddress: entry.address, + chainName: entry.chain, + }) + return { key: entry.key, info } + } catch { + return null + } + }), + ) + .then((results) => { + if (cancelled) return + setTokenInfoMap((prev) => { + const next = { ...prev } + results.forEach((result) => { + if (result) { + next[result.key] = result.info + } + }) + return next + }) + }) + .finally(() => { + if (cancelled) return + loadingRef.current = new Set( + [...loadingRef.current].filter((key) => !toFetch.some((entry) => entry.key === key)), + ) + setLoadingMap((prev) => { + const next = { ...prev } + toFetch.forEach((entry) => { + delete next[entry.key] + }) + return next + }) + }) + + return () => { + cancelled = true + } + }, [targets, targetKeys]) + + return { tokenInfoMap, loadingMap } +} diff --git a/miniapps/forge/src/i18n/index.test.ts b/miniapps/biobridge/src/i18n/index.test.ts similarity index 100% rename from miniapps/forge/src/i18n/index.test.ts rename to miniapps/biobridge/src/i18n/index.test.ts diff --git a/miniapps/forge/src/i18n/index.ts b/miniapps/biobridge/src/i18n/index.ts similarity index 100% rename from miniapps/forge/src/i18n/index.ts rename to miniapps/biobridge/src/i18n/index.ts diff --git a/miniapps/forge/src/i18n/locales/en.json b/miniapps/biobridge/src/i18n/locales/en.json similarity index 95% rename from miniapps/forge/src/i18n/locales/en.json rename to miniapps/biobridge/src/i18n/locales/en.json index e4ab0299d..840d777c3 100644 --- a/miniapps/forge/src/i18n/locales/en.json +++ b/miniapps/biobridge/src/i18n/locales/en.json @@ -25,6 +25,8 @@ "continue": "Continue", "amountHint": "Amount is in {{symbol}}", "decimalsHint": "Token decimals: {{decimals}}", + "decimalsLoading": "Loading decimals...", + "decimalsFromTokenInfo": "token info", "decimalsFallback": "default", "contractHint": "Contract: {{address}}" }, @@ -76,6 +78,7 @@ "sdkNotInit": "Bio SDK not initialized", "connectionFailed": "Connection failed", "invalidAmount": "Please enter a valid amount", + "decimalsLoading": "Token decimals are loading", "missingDecimals": "Missing token decimals configuration", "forgeFailed": "Operation failed" }, diff --git a/miniapps/forge/src/i18n/locales/zh-CN.json b/miniapps/biobridge/src/i18n/locales/zh-CN.json similarity index 94% rename from miniapps/forge/src/i18n/locales/zh-CN.json rename to miniapps/biobridge/src/i18n/locales/zh-CN.json index 3661545c8..cc751d727 100644 --- a/miniapps/forge/src/i18n/locales/zh-CN.json +++ b/miniapps/biobridge/src/i18n/locales/zh-CN.json @@ -25,6 +25,8 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsLoading": "精度读取中...", + "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, @@ -76,6 +78,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "decimalsLoading": "精度信息加载中,请稍候", "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/biobridge/src/i18n/locales/zh-TW.json similarity index 94% rename from miniapps/forge/src/i18n/locales/zh-TW.json rename to miniapps/biobridge/src/i18n/locales/zh-TW.json index b90475f4b..4561fc844 100644 --- a/miniapps/forge/src/i18n/locales/zh-TW.json +++ b/miniapps/biobridge/src/i18n/locales/zh-TW.json @@ -25,6 +25,8 @@ "continue": "繼續", "amountHint": "數量單位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsLoading": "精度讀取中...", + "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "預設", "contractHint": "合約:{{address}}" }, @@ -76,6 +78,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "連接失敗", "invalidAmount": "請輸入有效金額", + "decimalsLoading": "精度資訊載入中,請稍候", "missingDecimals": "缺少資產精度配置", "forgeFailed": "操作失敗" }, diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/biobridge/src/i18n/locales/zh.json similarity index 94% rename from miniapps/forge/src/i18n/locales/zh.json rename to miniapps/biobridge/src/i18n/locales/zh.json index 3661545c8..cc751d727 100644 --- a/miniapps/forge/src/i18n/locales/zh.json +++ b/miniapps/biobridge/src/i18n/locales/zh.json @@ -25,6 +25,8 @@ "continue": "继续", "amountHint": "数量单位:{{symbol}}", "decimalsHint": "精度:{{decimals}} 位", + "decimalsLoading": "精度读取中...", + "decimalsFromTokenInfo": "TokenInfo", "decimalsFallback": "默认", "contractHint": "合约:{{address}}" }, @@ -76,6 +78,7 @@ "sdkNotInit": "Bio SDK 未初始化", "connectionFailed": "连接失败", "invalidAmount": "请输入有效金额", + "decimalsLoading": "精度信息加载中,请稍候", "missingDecimals": "缺少资产精度配置", "forgeFailed": "操作失败" }, diff --git a/miniapps/forge/src/index.css b/miniapps/biobridge/src/index.css similarity index 100% rename from miniapps/forge/src/index.css rename to miniapps/biobridge/src/index.css diff --git a/miniapps/forge/src/lib/chain.test.ts b/miniapps/biobridge/src/lib/chain.test.ts similarity index 100% rename from miniapps/forge/src/lib/chain.test.ts rename to miniapps/biobridge/src/lib/chain.test.ts diff --git a/miniapps/forge/src/lib/chain.ts b/miniapps/biobridge/src/lib/chain.ts similarity index 100% rename from miniapps/forge/src/lib/chain.ts rename to miniapps/biobridge/src/lib/chain.ts diff --git a/miniapps/forge/src/lib/fee.ts b/miniapps/biobridge/src/lib/fee.ts similarity index 100% rename from miniapps/forge/src/lib/fee.ts rename to miniapps/biobridge/src/lib/fee.ts diff --git a/miniapps/forge/src/lib/tron-address.test.ts b/miniapps/biobridge/src/lib/tron-address.test.ts similarity index 100% rename from miniapps/forge/src/lib/tron-address.test.ts rename to miniapps/biobridge/src/lib/tron-address.test.ts diff --git a/miniapps/forge/src/lib/tron-address.ts b/miniapps/biobridge/src/lib/tron-address.ts similarity index 100% rename from miniapps/forge/src/lib/tron-address.ts rename to miniapps/biobridge/src/lib/tron-address.ts diff --git a/miniapps/forge/src/lib/utils.ts b/miniapps/biobridge/src/lib/utils.ts similarity index 100% rename from miniapps/forge/src/lib/utils.ts rename to miniapps/biobridge/src/lib/utils.ts diff --git a/miniapps/forge/src/main.tsx b/miniapps/biobridge/src/main.tsx similarity index 100% rename from miniapps/forge/src/main.tsx rename to miniapps/biobridge/src/main.tsx diff --git a/miniapps/forge/src/test-setup.ts b/miniapps/biobridge/src/test-setup.ts similarity index 100% rename from miniapps/forge/src/test-setup.ts rename to miniapps/biobridge/src/test-setup.ts diff --git a/miniapps/forge/src/vite-env.d.ts b/miniapps/biobridge/src/vite-env.d.ts similarity index 100% rename from miniapps/forge/src/vite-env.d.ts rename to miniapps/biobridge/src/vite-env.d.ts diff --git a/miniapps/forge/tsconfig.json b/miniapps/biobridge/tsconfig.json similarity index 100% rename from miniapps/forge/tsconfig.json rename to miniapps/biobridge/tsconfig.json diff --git a/miniapps/forge/vite.config.ts b/miniapps/biobridge/vite.config.ts similarity index 100% rename from miniapps/forge/vite.config.ts rename to miniapps/biobridge/vite.config.ts diff --git a/miniapps/forge/vitest.config.ts b/miniapps/biobridge/vitest.config.ts similarity index 100% rename from miniapps/forge/vitest.config.ts rename to miniapps/biobridge/vitest.config.ts diff --git a/miniapps/forge/src/i18n/i18next.d.ts b/miniapps/forge/src/i18n/i18next.d.ts deleted file mode 100644 index 37a273dbd..000000000 --- a/miniapps/forge/src/i18n/i18next.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import 'i18next' - -import type translation from './locales/zh-CN.json' - -declare module 'i18next' { - interface CustomTypeOptions { - defaultNS: 'translation' - resources: { - translation: typeof translation - } - } -} diff --git a/package.json b/package.json index 1dbad3a3f..fc99809ca 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "sdk:dev": "pnpm --filter @biochain/bio-sdk dev", "miniapps:build": "pnpm --filter \"./miniapps/*\" build", "miniapps:dev:teleport": "pnpm --filter @biochain/miniapp-teleport dev", - "miniapps:dev:forge": "pnpm --filter @biochain/miniapp-forge dev" + "miniapps:dev:biobridge": "pnpm --filter @biochain/miniapp-biobridge dev" }, "dependencies": { "@base-ui/react": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34675ab56..9856ab1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -400,7 +400,7 @@ importers: specifier: ^4.0.15 version: 4.0.16(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(yaml@2.8.2) - miniapps/forge: + miniapps/biobridge: dependencies: '@base-ui/react': specifier: ^1.0.0 diff --git a/scripts/dev.ts b/scripts/dev.ts index e0163dab2..36eef34e1 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -9,6 +9,7 @@ const processManager = new ProcessManager(); processManager.setCommands(commands); const tabState: TabState = { activeTab: 0, selectedCommand: 0 }; +const scrollState = new Map(); const controlPanel = new ControlPanel(commands, (id) => processManager.getState(id)); @@ -32,20 +33,37 @@ function renderProcessOutput(commandId: string) { const state = processManager.getState(commandId); if (state) { console.clear(); - console.log(state.buffer.join('')); + const header = controlPanel.renderHeader(tabState); + const headerLines = header.split('\n'); + const rows = process.stdout.rows || 24; + const outputHeight = Math.max(1, rows - headerLines.length); + const { lines, totalLines } = normalizeBuffer(state.buffer); + const { top, follow } = getScrollState(commandId, totalLines, outputHeight); + const maxTop = Math.max(0, totalLines - outputHeight); + const finalTop = follow ? maxTop : Math.min(Math.max(0, top), maxTop); + const view = renderOutputLines(lines, finalTop, totalLines, outputHeight, process.stdout.columns || 80); + setScrollState(commandId, finalTop, follow && finalTop === maxTop); + + console.log(header); + if (view.length > 0) { + console.log(view.join('\n')); + } } } -processManager.onOutput = (commandId, text) => { +processManager.onOutput = (commandId, _text) => { + const state = processManager.getState(commandId); + if (!state) return; const runningCommands = controlPanel.getRunningCommands(); const cmdIndex = runningCommands.findIndex((c) => c.id === commandId); + updateScrollStateForAppend(commandId, state.buffer); if (tabState.activeTab === cmdIndex + 1) { - process.stdout.write(text); + renderProcessOutput(commandId); } }; async function handleKeypress(key: string): Promise { - if (key === '\x1b[1;3D') { + if (key === '\x1b[1;3D' || key === '\x1bb') { tabState.activeTab = Math.max(0, tabState.activeTab - 1); if (tabState.activeTab === 0) { render(); @@ -56,7 +74,7 @@ async function handleKeypress(key: string): Promise { return true; } - if (key === '\x1b[1;3C') { + if (key === '\x1b[1;3C' || key === '\x1bf') { const maxTab = getMaxTabIndex(); tabState.activeTab = Math.min(maxTab, tabState.activeTab + 1); if (tabState.activeTab === 0) { @@ -106,12 +124,51 @@ async function handleKeypress(key: string): Promise { return false; } } else { + const cmd = getRunningCommandAtTabIndex(tabState.activeTab); + if (!cmd) return true; + if (key === 'q' || key === 'Q') { tabState.activeTab = 0; render(); return true; } + if (key === '\x1b[A') { + updateScroll(commandIdFromTab(cmd.id), -1); + renderProcessOutput(cmd.id); + return true; + } + + if (key === '\x1b[B') { + updateScroll(commandIdFromTab(cmd.id), 1); + renderProcessOutput(cmd.id); + return true; + } + + if (key === '\x1b[D') { + updateScroll(commandIdFromTab(cmd.id), -getPageSize()); + renderProcessOutput(cmd.id); + return true; + } + + if (key === '\x1b[C') { + updateScroll(commandIdFromTab(cmd.id), getPageSize()); + renderProcessOutput(cmd.id); + return true; + } + + if (key === '\x1b[5~') { + updateScroll(commandIdFromTab(cmd.id), -getPageSize()); + renderProcessOutput(cmd.id); + return true; + } + + if (key === '\x1b[6~') { + updateScroll(commandIdFromTab(cmd.id), getPageSize()); + renderProcessOutput(cmd.id); + return true; + } + if (key === '\x03') { return false; } @@ -192,3 +249,109 @@ main().catch(async (err) => { await cleanup(); process.exit(1); }); + +function commandIdFromTab(commandId: string): string { + return commandId; +} + +function normalizeBuffer(buffer: string[]): { lines: string[]; totalLines: number } { + const text = buffer.join(''); + const lines = text.length > 0 ? text.split('\n') : []; + return { lines, totalLines: lines.length }; +} + +function getScrollState(commandId: string, totalLines: number, height: number): { top: number; follow: boolean } { + const existing = scrollState.get(commandId); + const maxTop = Math.max(0, totalLines - height); + if (!existing) { + const top = maxTop; + scrollState.set(commandId, { top, follow: true }); + return { top, follow: true }; + } + const top = Math.min(Math.max(0, existing.top), maxTop); + return { top, follow: existing.follow }; +} + +function setScrollState(commandId: string, top: number, follow: boolean): void { + scrollState.set(commandId, { top, follow }); +} + +function updateScrollStateForAppend(commandId: string, buffer: string[]): void { + const { totalLines } = normalizeBuffer(buffer); + if (totalLines === 0) return; + const rows = process.stdout.rows || 24; + const headerLines = controlPanel.renderHeader(tabState).split('\n').length; + const height = Math.max(1, rows - headerLines); + const maxTop = Math.max(0, totalLines - height); + const current = getScrollState(commandId, totalLines, height); + if (current.follow) { + setScrollState(commandId, maxTop, true); + } else { + setScrollState(commandId, current.top, false); + } +} + +function updateScroll(commandId: string, delta: number): void { + const state = processManager.getState(commandId); + if (!state) return; + const headerLines = controlPanel.renderHeader(tabState).split('\n').length; + const height = Math.max(1, (process.stdout.rows || 24) - headerLines); + const { totalLines } = normalizeBuffer(state.buffer); + const maxTop = Math.max(0, totalLines - height); + const current = getScrollState(commandId, totalLines, height); + const nextTop = Math.min(Math.max(0, current.top + delta), maxTop); + setScrollState(commandId, nextTop, nextTop === maxTop); +} + +function getPageSize(): number { + const headerLines = controlPanel.renderHeader(tabState).split('\n').length; + return Math.max(1, (process.stdout.rows || 24) - headerLines); +} + +function renderOutputLines( + lines: string[], + top: number, + totalLines: number, + height: number, + width: number, +): string[] { + const view: string[] = []; + const maxTop = Math.max(0, totalLines - height); + const safeTop = Math.min(Math.max(0, top), maxTop); + const contentWidth = Math.max(1, width - 1); + const bar = getScrollbar(height, totalLines, safeTop); + + for (let i = 0; i < height; i++) { + const line = lines[safeTop + i] ?? ''; + const padded = padLine(line, contentWidth); + view.push(`${padded}${bar[i] ?? ' '}`); + } + return view; +} + +function padLine(line: string, width: number): string { + const len = stripAnsi(line).length; + if (len >= width) return line; + return line + ' '.repeat(width - len); +} + +function stripAnsi(input: string): string { + return input.replace( + /\x1b(\[(\[H\x1b\[2J|\d+;\d+H|\d+(;\d+;\d+(;\d+;\d+)?m|[mABCDFGd])|[HJK]|1K)|[78]|\d*[PMX]|\(B\x1b\[m)/g, + '', + ); +} + +function getScrollbar(height: number, totalLines: number, top: number): string[] { + const bar = Array.from({ length: height }, () => ' '); + if (totalLines <= height) { + return bar; + } + const maxTop = Math.max(0, totalLines - height); + const barHeight = Math.max(1, Math.round((height * height) / totalLines)); + const barTop = maxTop === 0 ? 0 : Math.round((top / maxTop) * (height - barHeight)); + for (let i = 0; i < height; i++) { + bar[i] = i >= barTop && i < barTop + barHeight ? '█' : '│'; + } + return bar; +} diff --git a/scripts/dev/control-panel.ts b/scripts/dev/control-panel.ts index cb251a7c0..d655249e1 100644 --- a/scripts/dev/control-panel.ts +++ b/scripts/dev/control-panel.ts @@ -11,14 +11,17 @@ export class ControlPanel { this.getState = getState; } + renderHeader(tabState: TabState): string { + const width = process.stdout.columns || 80; + return this.getHeaderLines(tabState, width).join('\n'); + } + render(tabState: TabState): string { const lines: string[] = []; const width = process.stdout.columns || 80; - lines.push('\x1b[90m' + '─'.repeat(width) + '\x1b[0m'); + lines.push(...this.getHeaderLines(tabState, width)); lines.push('\x1b[1m Dev Runner \x1b[0m'); - lines.push('\x1b[90m Option+← / Option+→ 切换Tab │ ↑↓ 选择 │ Enter 启动 │ S 停止 │ R 重启 │ Q 退出\x1b[0m'); - lines.push('\x1b[90m' + '─'.repeat(width) + '\x1b[0m'); lines.push(''); this.commands.forEach((cmd, index) => { @@ -36,6 +39,11 @@ export class ControlPanel { lines.push(''); lines.push('\x1b[90m' + '─'.repeat(width) + '\x1b[0m'); + return lines.join('\n'); + } + + private getHeaderLines(tabState: TabState, width: number): string[] { + const lines: string[] = []; const tabs = [tabState.activeTab === 0 ? '\x1b[47m\x1b[30m[0] Control\x1b[0m' : '[0] Control']; let tabIndex = 1; this.commands.forEach((cmd) => { @@ -46,9 +54,16 @@ export class ControlPanel { tabIndex++; } }); - lines.push(tabs.join(' ')); - return lines.join('\n'); + lines.push('\x1b[90m' + '─'.repeat(width) + '\x1b[0m'); + lines.push(tabs.join(' ')); + const helpText = + tabState.activeTab === 0 + ? 'Option+← / Option+→ 切换Tab │ ↑↓ 选择 │ Enter 启动 │ S 停止 │ R 重启 │ Q 退出' + : 'Option+← / Option+→ 切换Tab │ ↑↓ 滚动 │ ←→ 翻页 │ Q 返回'; + lines.push(`\x1b[90m ${helpText}\x1b[0m`); + lines.push('\x1b[90m' + '─'.repeat(width) + '\x1b[0m'); + return lines; } private getStatusIcon(status?: string): string { diff --git a/src/components/ecosystem/ecosystem-desktop.stories.tsx b/src/components/ecosystem/ecosystem-desktop.stories.tsx index d3104559e..27acf7652 100644 --- a/src/components/ecosystem/ecosystem-desktop.stories.tsx +++ b/src/components/ecosystem/ecosystem-desktop.stories.tsx @@ -9,10 +9,10 @@ import type { MiniappManifest } from '@/services/ecosystem'; // Mock 数据 const mockApps: MiniappManifest[] = [ { - id: 'forge', - name: '锻造', - description: '铸造和管理你的 NFT 资产', - icon: '/miniapps/forge/logo.webp', + id: 'biobridge', + name: '跨链通', + description: '跨链资产桥接与充值/赎回', + icon: '/miniapps/biobridge/icon.svg', url: 'https://localhost:5182/', version: '1.0.0', themeColor: '#FF6B35', @@ -24,7 +24,7 @@ const mockApps: MiniappManifest[] = [ id: 'teleport', name: '传送', description: '跨链资产转移', - icon: '/miniapps/teleport/logo.webp', + icon: '/miniapps/teleport/icon.svg', url: 'https://localhost:5183/', version: '1.0.0', themeColor: '#6366F1', @@ -36,7 +36,7 @@ const mockApps: MiniappManifest[] = [ id: 'market', name: '市场', description: 'NFT 交易市场', - icon: '/miniapps/forge/logo.webp', + icon: '/miniapps/biobridge/icon.svg', url: 'https://example.com/market', version: '1.0.0', themeColor: '#10B981', @@ -47,7 +47,7 @@ const mockApps: MiniappManifest[] = [ id: 'wallet', name: '钱包', description: '数字资产管理', - icon: '/miniapps/teleport/logo.webp', + icon: '/miniapps/teleport/icon.svg', url: 'https://example.com/wallet', version: '1.0.0', themeColor: '#F59E0B', diff --git a/src/components/layout/bottom-sheet.tsx b/src/components/layout/bottom-sheet.tsx index 33cce992a..a6324bada 100644 --- a/src/components/layout/bottom-sheet.tsx +++ b/src/components/layout/bottom-sheet.tsx @@ -8,13 +8,45 @@ import { BottomSheet as StackflowBottomSheet, Modal as StackflowModal } from '@s import type { ComponentProps, ReactNode } from 'react'; import { cn } from '@/lib/utils'; -export type BottomSheetProps = ComponentProps; +export type BottomSheetProps = ComponentProps & { + /** + * Whether the sheet supports cancel (outside click). + * Defaults to true to preserve existing behavior. + */ + cancelable?: boolean; + /** + * Cancel handler invoked when outside click happens. + * If provided, it is responsible for closing the sheet (call pop). + */ + onCancel?: () => void; +}; export type ModalProps = ComponentProps; /** * Re-export Stackflow BottomSheet */ -export const BottomSheet = StackflowBottomSheet; +export function BottomSheet({ + cancelable, + onCancel, + onOutsideClick, + ...props +}: BottomSheetProps) { + const resolvedCancelable = cancelable ?? Boolean(onCancel); + const handleOutsideClick: BottomSheetProps['onOutsideClick'] = (event) => { + if (!resolvedCancelable) { + event.preventDefault(); + return; + } + if (onCancel) { + onCancel(); + event.preventDefault(); + return; + } + onOutsideClick?.(event); + }; + + return ; +} /** * Re-export Stackflow Modal diff --git a/src/components/transaction/transaction-item.tsx b/src/components/transaction/transaction-item.tsx index c09e38b01..c12af432c 100644 --- a/src/components/transaction/transaction-item.tsx +++ b/src/components/transaction/transaction-item.tsx @@ -40,9 +40,11 @@ export function TransactionItem({ const Icon = typeMeta.Icon; // Get the amount value for display - const amountValue = transaction.type === 'send' - ? -Math.abs(transaction.amount.toNumber()) - : transaction.amount.toNumber(); + const negativeTypes = new Set(['send', 'destroy', 'destroyEntity', 'emigrate']); + const rawAmount = transaction.amount.toNumber(); + const amountValue = negativeTypes.has(transaction.type) + ? -Math.abs(rawAmount) + : rawAmount; return (
'/miniapps/forge/icon.svg' + * resolve('./biobridge/icon.svg') // => '/miniapps/biobridge/icon.svg' * resolve('https://example.com/icon.svg') // => 'https://example.com/icon.svg' (保持不变) * ``` */ diff --git a/src/services/chain-adapter/bioforest/transaction-mixin.ts b/src/services/chain-adapter/bioforest/transaction-mixin.ts index 354025d41..a243f1f8c 100644 --- a/src/services/chain-adapter/bioforest/transaction-mixin.ts +++ b/src/services/chain-adapter/bioforest/transaction-mixin.ts @@ -105,6 +105,7 @@ export function BioforestTransactionMixin } const feeEstimate = unsignedTx.estimatedFee @@ -282,6 +284,7 @@ export function BioforestTransactionMixin } /** 设置支付密码意图(BioChain) */ diff --git a/src/services/ecosystem/handlers/transaction.ts b/src/services/ecosystem/handlers/transaction.ts index 0c420a7f7..5d1bfd7d6 100644 --- a/src/services/ecosystem/handlers/transaction.ts +++ b/src/services/ecosystem/handlers/transaction.ts @@ -56,9 +56,22 @@ async function getChainConfigOrThrow(chainId: string) { /** bio_createTransaction - build unsigned tx */ export const handleCreateTransaction: MethodHandler = async (params, _context) => { const opts = params as Partial | undefined - if (!opts?.from || !opts?.to || !opts?.amount || !opts?.chain) { + const intentType = opts?.type ?? 'transfer' + if (!opts?.from || !opts?.amount || !opts?.chain) { throw Object.assign( - new Error('Missing required parameters: from, to, amount, chain'), + new Error('Missing required parameters: from, amount, chain'), + { code: BioErrorCodes.INVALID_PARAMS }, + ) + } + if (intentType === 'transfer' && !opts?.to) { + throw Object.assign( + new Error('Missing required parameters: to'), + { code: BioErrorCodes.INVALID_PARAMS }, + ) + } + if (intentType === 'destroy' && !opts?.recipientId && !opts?.to) { + throw Object.assign( + new Error('Missing required parameters: recipientId'), { code: BioErrorCodes.INVALID_PARAMS }, ) } @@ -87,15 +100,26 @@ export const handleCreateTransaction: MethodHandler = async (params, _context) = throw Object.assign(new Error(`Chain ${chainConfig.id} does not support transaction building`), { code: BioErrorCodes.UNSUPPORTED_METHOD }) } - const unsignedTx = await buildTransaction({ - type: 'transfer', - from: opts.from, - to: opts.to, - amount, - tokenAddress, - ...(opts.asset ? { bioAssetType: opts.asset } : {}), - ...(opts.remark ? { remark: opts.remark } : {}), - }) + const unsignedTx = await buildTransaction( + intentType === 'destroy' + ? { + type: 'destroy', + from: opts.from, + recipientId: opts.recipientId ?? opts.to!, + amount, + bioAssetType: opts.asset ?? chainConfig.symbol, + ...(opts.remark ? { remark: opts.remark } : {}), + } + : { + type: 'transfer', + from: opts.from, + to: opts.to!, + amount, + tokenAddress, + ...(opts.asset ? { bioAssetType: opts.asset } : {}), + ...(opts.remark ? { remark: opts.remark } : {}), + }, + ) if (tokenAddress && chainConfig.chainKind === 'tron') { const data = unsignedTx.data as Record | null diff --git a/src/services/ecosystem/my-apps.ts b/src/services/ecosystem/my-apps.ts index 7c2e3b6b6..7707ce300 100644 --- a/src/services/ecosystem/my-apps.ts +++ b/src/services/ecosystem/my-apps.ts @@ -10,7 +10,8 @@ const MY_APPS_KEY = 'ecosystem_my_apps' const LEGACY_APP_ID_MAP: Record = { teleport: 'xin.dweb.teleport', - forge: 'xin.dweb.forge', + forge: 'xin.dweb.biobridge', + biobridge: 'xin.dweb.biobridge', } export function normalizeAppId(appId: string): string { @@ -42,4 +43,3 @@ export function saveMyApps(apps: MyAppRecord[]): void { localStorage.setItem(MY_APPS_KEY, JSON.stringify(apps)) } - diff --git a/src/services/ecosystem/types.ts b/src/services/ecosystem/types.ts index d51a4db92..ede372e51 100644 --- a/src/services/ecosystem/types.ts +++ b/src/services/ecosystem/types.ts @@ -46,6 +46,10 @@ export interface EcosystemTransferParams { amount: string; // RPC 参数是字符串 chain: string; asset?: string; + /** 交易类型(默认 transfer) */ + type?: 'transfer' | 'destroy' | 'setPayPassword'; + /** 销毁资产接收方(资产发行者) */ + recipientId?: string; /** 代币合约地址(用于 EVM/TRON Token 转账) */ tokenAddress?: string; /** 资产精度(用于 EVM/TRON Token 转账) */ diff --git a/src/services/mock-devtools/components/ConsolePanel.tsx b/src/services/mock-devtools/components/ConsolePanel.tsx index 80a916236..aff9f3e7f 100644 --- a/src/services/mock-devtools/components/ConsolePanel.tsx +++ b/src/services/mock-devtools/components/ConsolePanel.tsx @@ -304,6 +304,7 @@ export function ConsolePanel({ e.preventDefault() executeCode(input) } else if (e.key === 'ArrowUp') { + if (e.altKey || e.metaKey || e.ctrlKey) return e.preventDefault() if (history.length > 0) { const newIndex = historyIndex < history.length - 1 ? historyIndex + 1 : historyIndex @@ -311,6 +312,7 @@ export function ConsolePanel({ setInput(history[history.length - 1 - newIndex] || '') } } else if (e.key === 'ArrowDown') { + if (e.altKey || e.metaKey || e.ctrlKey) return e.preventDefault() if (historyIndex > 0) { const newIndex = historyIndex - 1 diff --git a/src/services/mock-devtools/components/DevPanelShell.tsx b/src/services/mock-devtools/components/DevPanelShell.tsx new file mode 100644 index 000000000..823c3884e --- /dev/null +++ b/src/services/mock-devtools/components/DevPanelShell.tsx @@ -0,0 +1,375 @@ +/** + * DevPanelShell - 可复用的浮动调试面板外壳 + */ + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import type { ComponentType, ReactNode } from 'react' +import { IconX as X } from '@tabler/icons-react' +import { cn } from '@/lib/utils' + +export type DevPanelPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' + +export type DevPanelTab = { + id: T + icon: ComponentType<{ className?: string }> + label: string + badge?: number +} + +export interface DevPanelShellProps { + title: string + icon: ComponentType<{ className?: string }> + buttonTitle: string + tabs: DevPanelTab[] + activeTab: T + onTabChange: (tab: T) => void + children: ReactNode + footer?: ReactNode + helpText?: ReactNode + defaultOpen?: boolean + position?: DevPanelPosition + buttonBadge?: number + buttonPulse?: boolean + panelSize?: { width: number; height: number } + buttonSize?: { width: number; height: number } + panelClassName?: string + enableTabShortcuts?: boolean +} + +const DEFAULT_BUTTON_SIZE = { width: 48, height: 48 } +const DEFAULT_PANEL_SIZE = { width: 380, height: 480 } +const PADDING = 16 + +function getWindowSize(): { width: number; height: number } { + if (typeof window === 'undefined') { + return { width: 0, height: 0 } + } + return { width: window.innerWidth, height: window.innerHeight } +} + +/** + * 根据相对位置(0-1)、元素尺寸、屏幕尺寸计算实际像素位置 + * 自动处理边缘碰撞检测 + */ +function calculatePosition( + relativePos: { x: number; y: number }, + elementSize: { width: number; height: number }, + screenSize: { width: number; height: number }, +): { x: number; y: number } { + const availableWidth = screenSize.width - elementSize.width - PADDING * 2 + const availableHeight = screenSize.height - elementSize.height - PADDING * 2 + + const targetX = PADDING + relativePos.x * Math.max(0, availableWidth) + const targetY = PADDING + relativePos.y * Math.max(0, availableHeight) + + const x = Math.max(PADDING, Math.min(screenSize.width - elementSize.width - PADDING, targetX)) + const y = Math.max(PADDING, Math.min(screenSize.height - elementSize.height - PADDING, targetY)) + + return { x, y } +} + +/** + * 从像素位置反推相对位置(0-1) + */ +function pixelToRelative( + pixelPos: { x: number; y: number }, + elementSize: { width: number; height: number }, + screenSize: { width: number; height: number }, +): { x: number; y: number } { + const availableWidth = screenSize.width - elementSize.width - PADDING * 2 + const availableHeight = screenSize.height - elementSize.height - PADDING * 2 + + const x = availableWidth > 0 ? (pixelPos.x - PADDING) / availableWidth : 0 + const y = availableHeight > 0 ? (pixelPos.y - PADDING) / availableHeight : 0 + + return { + x: Math.max(0, Math.min(1, x)), + y: Math.max(0, Math.min(1, y)), + } +} + +function getInitialRelativePos(position: DevPanelPosition): { x: number; y: number } { + switch (position) { + case 'bottom-left': + return { x: 0, y: 1 } + case 'top-right': + return { x: 1, y: 0 } + case 'top-left': + return { x: 0, y: 0 } + case 'bottom-right': + default: + return { x: 1, y: 1 } + } +} + +export function DevPanelShell({ + title, + icon: Icon, + buttonTitle, + tabs, + activeTab, + onTabChange, + children, + footer, + helpText, + defaultOpen = false, + position = 'bottom-right', + buttonBadge, + buttonPulse, + panelSize = DEFAULT_PANEL_SIZE, + buttonSize = DEFAULT_BUTTON_SIZE, + panelClassName, + enableTabShortcuts = true, +}: DevPanelShellProps) { + const [isOpen, setIsOpen] = useState(defaultOpen) + const [relativePos, setRelativePos] = useState(() => getInitialRelativePos(position)) + const [screenSize, setScreenSize] = useState(() => getWindowSize()) + const [isDragging, setIsDragging] = useState(false) + const dragStartRef = useRef<{ x: number; y: number; relX: number; relY: number } | null>(null) + const hasDraggedRef = useRef(false) + const DRAG_THRESHOLD = 5 + + const elementSize = isOpen ? panelSize : buttonSize + const buttonPos = useMemo(() => calculatePosition(relativePos, buttonSize, screenSize), [relativePos, buttonSize, screenSize]) + const panelPos = useMemo(() => calculatePosition(relativePos, panelSize, screenSize), [relativePos, panelSize, screenSize]) + + useEffect(() => { + if (!isOpen || !enableTabShortcuts) return undefined + + const handleKeyDown = (event: KeyboardEvent) => { + if (!event.altKey || event.metaKey || event.ctrlKey) return + + const key = event.key + const code = event.code + const lowerKey = key.toLowerCase() + const isLeft = + key === 'ArrowLeft' || + code === 'ArrowLeft' || + lowerKey === 'arrowleft' || + lowerKey === 'b' + const isRight = + key === 'ArrowRight' || + code === 'ArrowRight' || + lowerKey === 'arrowright' || + lowerKey === 'f' + if (!isLeft && !isRight) return + + event.preventDefault() + event.stopPropagation() + + const currentIndex = tabs.findIndex((tab) => tab.id === activeTab) + if (currentIndex === -1) return + + const nextIndex = isLeft + ? Math.max(0, currentIndex - 1) + : Math.min(tabs.length - 1, currentIndex + 1) + + if (nextIndex !== currentIndex) { + onTabChange(tabs[nextIndex].id) + } + } + + document.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => document.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, [activeTab, enableTabShortcuts, isOpen, onTabChange, tabs]) + + useEffect(() => { + const handleResize = () => { + setScreenSize(getWindowSize()) + } + if (typeof window !== 'undefined') { + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + } + return undefined + }, []) + + const handleDragStart = useCallback( + (e: React.MouseEvent | React.TouchEvent) => { + e.stopPropagation() + const clientX = 'touches' in e ? e.touches[0]?.clientX ?? 0 : e.clientX + const clientY = 'touches' in e ? e.touches[0]?.clientY ?? 0 : e.clientY + dragStartRef.current = { + x: clientX, + y: clientY, + relX: relativePos.x, + relY: relativePos.y, + } + hasDraggedRef.current = false + setIsDragging(true) + }, + [relativePos], + ) + + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + if (hasDraggedRef.current) { + e.preventDefault() + return + } + setIsOpen(true) + }, + [], + ) + + useEffect(() => { + if (!isDragging) return undefined + + const handleMove = (e: MouseEvent | TouchEvent) => { + if (!dragStartRef.current) return + + e.preventDefault() + e.stopPropagation() + + const clientX = 'touches' in e ? e.touches[0]?.clientX ?? 0 : e.clientX + const clientY = 'touches' in e ? e.touches[0]?.clientY ?? 0 : e.clientY + + const deltaX = clientX - dragStartRef.current.x + const deltaY = clientY - dragStartRef.current.y + + if (!hasDraggedRef.current && (Math.abs(deltaX) > DRAG_THRESHOLD || Math.abs(deltaY) > DRAG_THRESHOLD)) { + hasDraggedRef.current = true + } + + if (hasDraggedRef.current) { + const startPixelPos = calculatePosition( + { x: dragStartRef.current.relX, y: dragStartRef.current.relY }, + elementSize, + screenSize, + ) + const newPixelPos = { x: startPixelPos.x + deltaX, y: startPixelPos.y + deltaY } + const newRelativePos = pixelToRelative(newPixelPos, elementSize, screenSize) + setRelativePos(newRelativePos) + } + } + + const handleEnd = (e: MouseEvent | TouchEvent) => { + e.stopPropagation() + setIsDragging(false) + dragStartRef.current = null + } + + document.addEventListener('mousemove', handleMove, { capture: true }) + document.addEventListener('mouseup', handleEnd, { capture: true }) + document.addEventListener('touchmove', handleMove, { passive: false, capture: true }) + document.addEventListener('touchend', handleEnd, { capture: true }) + + return () => { + document.removeEventListener('mousemove', handleMove, { capture: true }) + document.removeEventListener('mouseup', handleEnd, { capture: true }) + document.removeEventListener('touchmove', handleMove, { capture: true } as EventListenerOptions) + document.removeEventListener('touchend', handleEnd, { capture: true }) + } + }, [isDragging, elementSize, screenSize]) + + const stopPropagation = useCallback((e: React.MouseEvent | React.TouchEvent) => { + e.stopPropagation() + }, []) + + if (!isOpen) { + return ( + + ) + } + + return ( +
+ {/* Tabs + Title (顶部) */} +
+
+ + {title} +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ + {/* Help line */} + {helpText && ( +
+ {helpText} +
+ )} + +
{children}
+ + {footer && ( +
+ {footer} +
+ )} +
+ ) +} diff --git a/src/services/mock-devtools/components/MockDevTools.tsx b/src/services/mock-devtools/components/MockDevTools.tsx index 7dbed7f16..59809dfcb 100644 --- a/src/services/mock-devtools/components/MockDevTools.tsx +++ b/src/services/mock-devtools/components/MockDevTools.tsx @@ -3,11 +3,10 @@ * 提供统一的 Mock 服务调试界面 */ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react' +import { useState, useEffect, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { IconBug as Bug, - IconX as X, IconRefresh as RefreshCw, IconAlertTriangle as AlertTriangle, IconClock as Clock, @@ -48,6 +47,7 @@ import { } from '@/lib/service-meta' import { BreakpointPanel } from './BreakpointPanel' import { ConsolePanel } from './ConsolePanel' +import { DevPanelShell } from './DevPanelShell' /** 合并两个日志系统的日志 */ function getLogs(): RequestLogEntry[] { @@ -427,77 +427,9 @@ function SettingsPanel() { ) } -/** 元素尺寸常量 */ -const BUTTON_SIZE = { width: 48, height: 48 } -const PANEL_SIZE = { width: 380, height: 480 } -const PADDING = 16 - -/** - * 统一的弹簧位置算法 - * 根据相对位置(0-1)、元素尺寸、屏幕尺寸计算实际像素位置 - * 自动处理边缘碰撞检测 - */ -function calculatePosition( - relativePos: { x: number; y: number }, - elementSize: { width: number; height: number }, - screenSize: { width: number; height: number }, -): { x: number; y: number } { - // 可用空间 = 屏幕尺寸 - 元素尺寸 - 两边padding - const availableWidth = screenSize.width - elementSize.width - PADDING * 2 - const availableHeight = screenSize.height - elementSize.height - PADDING * 2 - - // 根据相对位置计算目标位置 - const targetX = PADDING + relativePos.x * Math.max(0, availableWidth) - const targetY = PADDING + relativePos.y * Math.max(0, availableHeight) - - // 边缘碰撞检测:确保元素完全在屏幕内 - const x = Math.max(PADDING, Math.min(screenSize.width - elementSize.width - PADDING, targetX)) - const y = Math.max(PADDING, Math.min(screenSize.height - elementSize.height - PADDING, targetY)) - - return { x, y } -} - -/** - * 从像素位置反推相对位置(0-1) - */ -function pixelToRelative( - pixelPos: { x: number; y: number }, - elementSize: { width: number; height: number }, - screenSize: { width: number; height: number }, -): { x: number; y: number } { - const availableWidth = screenSize.width - elementSize.width - PADDING * 2 - const availableHeight = screenSize.height - elementSize.height - PADDING * 2 - - // 相对位置 = (像素位置 - padding) / 可用空间 - const x = availableWidth > 0 ? (pixelPos.x - PADDING) / availableWidth : 0 - const y = availableHeight > 0 ? (pixelPos.y - PADDING) / availableHeight : 0 - - // 限制在 0-1 范围内 - return { - x: Math.max(0, Math.min(1, x)), - y: Math.max(0, Math.min(1, y)), - } -} - -/** 获取初始相对位置 */ -function getInitialRelativePos(position: MockDevToolsProps['position']): { x: number; y: number } { - switch (position) { - case 'bottom-left': - return { x: 0, y: 1 } - case 'top-right': - return { x: 1, y: 0 } - case 'top-left': - return { x: 0, y: 0 } - case 'bottom-right': - default: - return { x: 1, y: 1 } - } -} - /** MockDevTools 主组件 */ export function MockDevTools({ defaultOpen = false, position = 'bottom-right' }: MockDevToolsProps) { const { t } = useTranslation('devtools') - const [isOpen, setIsOpen] = useState(defaultOpen) const [activeTab, setActiveTab] = useState('logs') const [logFilter, setLogFilter] = useState('') const [logCount, setLogCount] = useState(() => getLogs().length) @@ -512,28 +444,6 @@ export function MockDevTools({ defaultOpen = false, position = 'bottom-right' }: setActiveTab('console') }, []) - // 统一的相对位置 (0-1),按钮和面板共享 - const [relativePos, setRelativePos] = useState(() => getInitialRelativePos(position)) - const [screenSize, setScreenSize] = useState(() => ({ - width: window.innerWidth, - height: window.innerHeight, - })) - const [isDragging, setIsDragging] = useState(false) - const dragStartRef = useRef<{ x: number; y: number; relX: number; relY: number } | null>(null) - // 跟踪是否发生了实际拖动(移动超过阈值) - const hasDraggedRef = useRef(false) - const DRAG_THRESHOLD = 5 - - // 根据相对位置计算实际像素位置 - const buttonPos = useMemo( - () => calculatePosition(relativePos, BUTTON_SIZE, screenSize), - [relativePos, screenSize] - ) - const panelPos = useMemo( - () => calculatePosition(relativePos, PANEL_SIZE, screenSize), - [relativePos, screenSize] - ) - useEffect(() => { const updateLogCount = () => setLogCount(getLogs().length) const updatePausedCount = () => @@ -554,118 +464,6 @@ export function MockDevTools({ defaultOpen = false, position = 'bottom-right' }: } }, []) - // 窗口 resize 时更新屏幕尺寸(相对位置不变,像素位置自动重算) - useEffect(() => { - const handleResize = () => { - setScreenSize({ width: window.innerWidth, height: window.innerHeight }) - } - window.addEventListener('resize', handleResize) - return () => window.removeEventListener('resize', handleResize) - }, []) - - // 拖动处理 - 统一更新相对位置 - const handleDragStart = useCallback( - (e: React.MouseEvent | React.TouchEvent) => { - // 阻止事件冒泡到 Stackflow - e.stopPropagation() - - const clientX = 'touches' in e ? e.touches[0]?.clientX ?? 0 : e.clientX - const clientY = 'touches' in e ? e.touches[0]?.clientY ?? 0 : e.clientY - - dragStartRef.current = { - x: clientX, - y: clientY, - relX: relativePos.x, - relY: relativePos.y, - } - hasDraggedRef.current = false - setIsDragging(true) - }, - [relativePos], - ) - - // 处理点击 - 只在没有发生拖动时触发 - const handleClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation() - // 如果发生了拖动,忽略点击 - if (hasDraggedRef.current) { - e.preventDefault() - return - } - setIsOpen(true) - }, - [], - ) - - useEffect(() => { - if (!isDragging) return - - // 当前拖动的元素尺寸 - const elementSize = isOpen ? PANEL_SIZE : BUTTON_SIZE - - const handleMove = (e: MouseEvent | TouchEvent) => { - if (!dragStartRef.current) return - - e.preventDefault() - e.stopPropagation() - - const clientX = 'touches' in e ? e.touches[0]?.clientX ?? 0 : e.clientX - const clientY = 'touches' in e ? e.touches[0]?.clientY ?? 0 : e.clientY - - const deltaX = clientX - dragStartRef.current.x - const deltaY = clientY - dragStartRef.current.y - - // 检测是否超过拖动阈值 - if (!hasDraggedRef.current && (Math.abs(deltaX) > DRAG_THRESHOLD || Math.abs(deltaY) > DRAG_THRESHOLD)) { - hasDraggedRef.current = true - } - - // 只有实际拖动时才更新位置 - if (hasDraggedRef.current) { - // 计算起始相对位置对应的像素位置 - const startPixelPos = calculatePosition( - { x: dragStartRef.current.relX, y: dragStartRef.current.relY }, - elementSize, - screenSize - ) - - // 新的像素位置 - const newPixelPos = { - x: startPixelPos.x + deltaX, - y: startPixelPos.y + deltaY, - } - - // 转换回相对位置(自动处理边界) - const newRelativePos = pixelToRelative(newPixelPos, elementSize, screenSize) - setRelativePos(newRelativePos) - } - } - - const handleEnd = (e: MouseEvent | TouchEvent) => { - e.stopPropagation() - setIsDragging(false) - dragStartRef.current = null - } - - document.addEventListener('mousemove', handleMove, { capture: true }) - document.addEventListener('mouseup', handleEnd, { capture: true }) - document.addEventListener('touchmove', handleMove, { passive: false, capture: true }) - document.addEventListener('touchend', handleEnd, { capture: true }) - - return () => { - document.removeEventListener('mousemove', handleMove, { capture: true }) - document.removeEventListener('mouseup', handleEnd, { capture: true }) - document.removeEventListener('touchmove', handleMove, { capture: true } as EventListenerOptions) - document.removeEventListener('touchend', handleEnd, { capture: true }) - } - }, [isDragging, isOpen, screenSize]) - - // 阻止面板内部事件冒泡到 Stackflow(必须在条件返回前定义) - const stopPropagation = useCallback((e: React.MouseEvent | React.TouchEvent) => { - e.stopPropagation() - }, []) - const tabs: { id: TabId; icon: typeof List; label: string; badge: number | undefined }[] = [ { id: 'logs', icon: List, label: t('tabs.logs'), badge: logCount > 0 ? logCount : undefined }, { id: 'intercept', icon: Terminal, label: t('tabs.breakpoints'), badge: pausedCount > 0 ? pausedCount : undefined }, @@ -673,99 +471,31 @@ export function MockDevTools({ defaultOpen = false, position = 'bottom-right' }: { id: 'settings', icon: Settings, label: t('tabs.settings'), badge: undefined }, ] - if (!isOpen) { - return ( - - ) - } - return ( -
0 ? pausedCount : undefined} + buttonPulse={pausedCount > 0} + footer={ + <> + {t('footer.mode')} • {t('footer.logs', { count: logCount })} + {pausedCount > 0 && • {t('footer.paused', { count: pausedCount })}} + + } > - {/* Header - 可拖动区域 */} -
-
- - {t('title')} -
- -
- - {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Content */} -
- {activeTab === 'logs' && } - {activeTab === 'intercept' && } - {activeTab === 'console' && } - {activeTab === 'settings' && } -
- - {/* Footer */} -
- {t('footer.mode')} • {t('footer.logs', { count: logCount })} - {pausedCount > 0 && • {t('footer.paused', { count: pausedCount })}} -
-
+ {activeTab === 'logs' && } + {activeTab === 'intercept' && } + {activeTab === 'console' && } + {activeTab === 'settings' && } + ) } diff --git a/src/services/mock-devtools/index.tsx b/src/services/mock-devtools/index.tsx index 88dcc3d81..6dfbd7c74 100644 --- a/src/services/mock-devtools/index.tsx +++ b/src/services/mock-devtools/index.tsx @@ -4,6 +4,12 @@ */ export { MockDevTools, type MockDevToolsProps } from './components/MockDevTools' +export { + DevPanelShell, + type DevPanelTab, + type DevPanelPosition, + type DevPanelShellProps, +} from './components/DevPanelShell' // 状态管理 (供 MockDevTools UI 使用) export { diff --git a/src/stackflow/activities/sheets/ChainSelectorJob.tsx b/src/stackflow/activities/sheets/ChainSelectorJob.tsx index 2b520ceb4..a6588305b 100644 --- a/src/stackflow/activities/sheets/ChainSelectorJob.tsx +++ b/src/stackflow/activities/sheets/ChainSelectorJob.tsx @@ -29,9 +29,12 @@ export const ChainSelectorJob: ActivityComponentType = () => { walletActions.setSelectedChain(chain); pop(); }; + const handleCancel = () => { + pop(); + }; return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx index fa569350e..1c7ed6143 100644 --- a/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx +++ b/src/stackflow/activities/sheets/ChainSwitchConfirmJob.tsx @@ -74,7 +74,7 @@ function ChainSwitchConfirmJobContent() { }; return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/ClearDataConfirmJob.tsx b/src/stackflow/activities/sheets/ClearDataConfirmJob.tsx index a51b5c875..fc861cf49 100644 --- a/src/stackflow/activities/sheets/ClearDataConfirmJob.tsx +++ b/src/stackflow/activities/sheets/ClearDataConfirmJob.tsx @@ -17,9 +17,12 @@ function ClearDataConfirmJobContent() { const baseUri = import.meta.env.BASE_URL || '/'; window.location.href = `${baseUri}clear.html`; }, []); + const handleCancel = () => { + pop(); + }; return ( - +
{/* Handle */}
@@ -57,7 +60,7 @@ function ClearDataConfirmJobContent() {

{t('addressBook.shareContact')}

diff --git a/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx b/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx index a067dbd1b..08ff146e7 100644 --- a/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx +++ b/src/stackflow/activities/sheets/CryptoAuthorizeJob.tsx @@ -117,7 +117,7 @@ function CryptoAuthorizeJobContent() { }, [pop]); return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx b/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx index 91a0b0a5b..19453bfd8 100644 --- a/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx +++ b/src/stackflow/activities/sheets/MiniappDestroyConfirmJob.tsx @@ -127,7 +127,7 @@ function MiniappDestroyConfirmJobContent() { }, [pop]); return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx b/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx index 48292d5f8..2996e8fb6 100644 --- a/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx +++ b/src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx @@ -128,7 +128,7 @@ function MiniappSignTransactionJobContent() { }, [unsignedTx]); return ( - +
diff --git a/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx b/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx index b6da1d29b..6c30d895c 100644 --- a/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx +++ b/src/stackflow/activities/sheets/MiniappTransferConfirmJob.tsx @@ -124,7 +124,7 @@ function MiniappTransferConfirmJobContent() { const displayAsset = asset || chain.toUpperCase(); return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx b/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx index 1b43c6157..0b54e5ae4 100644 --- a/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx +++ b/src/stackflow/activities/sheets/MnemonicOptionsJob.tsx @@ -64,9 +64,12 @@ function MnemonicOptionsJobContent() { clearMnemonicOptionsCallback(); pop(); }; + const handleCancel = () => { + pop(); + }; return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/PermissionRequestJob.tsx b/src/stackflow/activities/sheets/PermissionRequestJob.tsx index 92d498814..e27f4a3e1 100644 --- a/src/stackflow/activities/sheets/PermissionRequestJob.tsx +++ b/src/stackflow/activities/sheets/PermissionRequestJob.tsx @@ -76,7 +76,7 @@ function PermissionRequestJobContent() { }; return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/ScannerJob.tsx b/src/stackflow/activities/sheets/ScannerJob.tsx index b5247bb2f..e0da5fb60 100644 --- a/src/stackflow/activities/sheets/ScannerJob.tsx +++ b/src/stackflow/activities/sheets/ScannerJob.tsx @@ -319,7 +319,7 @@ function ScannerJobContent() { }, [stopCamera, pop]) return ( - +
{/* Handle */}
diff --git a/src/stackflow/activities/sheets/SetTwoStepSecretJob.tsx b/src/stackflow/activities/sheets/SetTwoStepSecretJob.tsx index a146f5208..c0f0dd007 100644 --- a/src/stackflow/activities/sheets/SetTwoStepSecretJob.tsx +++ b/src/stackflow/activities/sheets/SetTwoStepSecretJob.tsx @@ -129,6 +129,20 @@ function SetTwoStepSecretJobContent() { } }, [step, newTwoStepSecret, confirmTwoStepSecret, t]); + const handleCancel = useCallback(() => { + if (step === "input") { + pop(); + } else if (step === "confirm") { + setStep("input"); + setError(undefined); + } else { + setStep("confirm"); + setError(undefined); + setPatternError(false); + setWalletLockPattern([]); + } + }, [step, pop]); + const handlePatternComplete = useCallback( async (nodes: number[]) => { const callback = callbackRef.current; @@ -167,7 +181,7 @@ function SetTwoStepSecretJobContent() { // 广播成功后的状态显示 if (txStatus !== "idle") { return ( - +
@@ -192,7 +206,7 @@ function SetTwoStepSecretJobContent() { } return ( - +
{/* Handle */}
@@ -329,19 +343,7 @@ function SetTwoStepSecretJobContent() {