diff --git a/e2e/bioforest-full-flow.spec.ts b/e2e/bioforest-full-flow.spec.ts index 4cf50f639..05ce11b3a 100644 --- a/e2e/bioforest-full-flow.spec.ts +++ b/e2e/bioforest-full-flow.spec.ts @@ -268,8 +268,8 @@ async function performTransfer( await continueBtn.click() await page.waitForTimeout(500) - // 点击确认转账 - const confirmBtn = page.locator(`[data-testid="confirm-transfer-button"], button:has-text("${UI_TEXT.confirm.source}")`).first() + // 点击确认转账 (TransferPreviewJob) + const confirmBtn = page.locator(`[data-testid="confirm-preview-button"], button:has-text("${UI_TEXT.confirm.source}")`).first() await expect(confirmBtn).toBeVisible({ timeout: 5000 }) await confirmBtn.click() await page.waitForTimeout(500) diff --git a/e2e/bioforest-real-transfer.spec.ts b/e2e/bioforest-real-transfer.spec.ts index 9752e413f..1a9b4c9e3 100644 --- a/e2e/bioforest-real-transfer.spec.ts +++ b/e2e/bioforest-real-transfer.spec.ts @@ -178,7 +178,7 @@ async function doTransfer(page: Page, toAddress: string, amount: string, needPay await expect(continueBtn).toBeEnabled({ timeout: 15000 }) await continueBtn.click() - await page.locator('[data-testid="confirm-transfer-button"]').click() + await page.locator('[data-testid="confirm-preview-button"]').click() // 验证钱包锁 const patternInput = page.locator('[data-testid="wallet-pattern-input"]') diff --git a/e2e/bioforest-transfer.spec.ts b/e2e/bioforest-transfer.spec.ts index b79f2961c..8a4793e4a 100644 --- a/e2e/bioforest-transfer.spec.ts +++ b/e2e/bioforest-transfer.spec.ts @@ -201,9 +201,9 @@ describeOrSkip('BioForest 转账测试', () => { await continueBtn.click() console.log(' ✅ 继续到确认页') - // 7. 确认转账 + // 7. 确认转账(TransferPreviewJob) console.log('7. 确认转账...') - const confirmBtn = page.locator('[data-testid="confirm-transfer-button"]') + const confirmBtn = page.locator('[data-testid="confirm-preview-button"]') await expect(confirmBtn).toBeVisible({ timeout: 5000 }) await confirmBtn.click() console.log(' ✅ 点击确认') diff --git a/e2e/send-transaction.mock.spec.ts b/e2e/send-transaction.mock.spec.ts index c56109e18..6ec52f51f 100644 --- a/e2e/send-transaction.mock.spec.ts +++ b/e2e/send-transaction.mock.spec.ts @@ -344,14 +344,14 @@ test.describe('发送交易 - Job 弹窗流程', () => { await page.waitForTimeout(500) // 检查确认弹窗内容 - const confirmBtn = page.locator('[data-testid="confirm-transfer-button"]') - const cancelBtn = page.locator('[data-testid="cancel-transfer-button"]') + const confirmBtn = page.locator('[data-testid="confirm-preview-button"]') + const cancelBtn = page.locator('[data-testid="cancel-preview-button"]') // 至少一个按钮应该可见(确认或取消) const hasConfirmUI = await confirmBtn.isVisible() || await cancelBtn.isVisible() if (hasConfirmUI) { - console.log('TransferConfirmJob opened successfully') + console.log('TransferPreviewJob opened successfully') // 截图 await expect(page).toHaveScreenshot('send-confirm-job.png') @@ -365,7 +365,7 @@ test.describe('发送交易 - Job 弹窗流程', () => { await expect(page.locator('[data-testid="send-continue-button"]')).toBeVisible() } } else { - console.log('TransferConfirmJob may not have opened - check mock configuration') + console.log('TransferPreviewJob may not have opened - check mock configuration') } } else { console.log('Continue button not enabled - mock service may not be configured correctly') @@ -390,7 +390,7 @@ test.describe('发送交易 - Job 弹窗流程', () => { await continueBtn.click() await page.waitForTimeout(500) - const confirmBtn = page.locator('[data-testid="confirm-transfer-button"]') + const confirmBtn = page.locator('[data-testid="confirm-preview-button"]') if (await confirmBtn.isVisible()) { await confirmBtn.click() diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png index c84c7a6bd..f2a47e2c0 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/01-connect.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png index bcea56e74..c5d71a549 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/02-swap.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png index 62525e2de..b6ef16466 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/03-swap-amount.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png index bcea56e74..c5d71a549 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/04-swap-tokens.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png index ea0ba8d75..d7ff2c569 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/05-confirm.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png index db7a41a19..38846eee3 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/06-error.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png index cc1aaac77..4635cfeb8 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/07-flow-complete.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png index c84c7a6bd..f2a47e2c0 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09-mode-tabs.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png index 3d6920842..40ca84a32 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09b-redemption-mode.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png index 7f26b2884..b438abac8 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09c-redemption-form.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png index f2f6883f0..d5a0c515f 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/09d-redemption-amount.png differ diff --git a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png index 66d80d8e6..51ea8a879 100644 Binary files a/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png and b/miniapps/forge/e2e/__screenshots__/Mobile-Chrome/ui.spec.ts/12-api-failure-confirm.png differ diff --git a/miniapps/forge/e2e/helpers/i18n.ts b/miniapps/forge/e2e/helpers/i18n.ts index 9214b5d72..0d61ee938 100644 --- a/miniapps/forge/e2e/helpers/i18n.ts +++ b/miniapps/forge/e2e/helpers/i18n.ts @@ -10,7 +10,7 @@ export const UI_TEXT = { subtitle: { source: '跨链桥接', pattern: /跨链桥接|Cross-chain Bridge/i }, }, mode: { - recharge: { source: '充值', pattern: /充值|Recharge/i }, + recharge: { source: '锻造', pattern: /锻造|Forge/i }, redemption: { source: '赎回', pattern: /赎回|Redemption/i }, }, connect: { diff --git a/miniapps/forge/e2e/ui.spec.ts b/miniapps/forge/e2e/ui.spec.ts index 694a8ceab..caf2bc708 100644 --- a/miniapps/forge/e2e/ui.spec.ts +++ b/miniapps/forge/e2e/ui.spec.ts @@ -254,7 +254,7 @@ test.describe('BioBridge UI', () => { await page.waitForLoadState('networkidle') // Wait for config to load - mode tabs should be visible since redemption is enabled - await expect(page.locator('text=充值')).toBeVisible({ timeout: 10000 }) + await expect(page.locator('text=锻造')).toBeVisible({ timeout: 10000 }) await expect(page.locator('text=赎回')).toBeVisible({ timeout: 5000 }) await expect(page).toHaveScreenshot('09-mode-tabs.png') diff --git a/miniapps/forge/package.json b/miniapps/forge/package.json index dbbb7b769..324675c80 100644 --- a/miniapps/forge/package.json +++ b/miniapps/forge/package.json @@ -31,6 +31,7 @@ "@biochain/key-ui": "workspace:*", "@biochain/key-utils": "workspace:*", "@biochain/keyapp-sdk": "workspace:*", + "@fontsource-variable/noto-sans-sc": "^5.2.10", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", diff --git a/miniapps/forge/src/i18n/index.ts b/miniapps/forge/src/i18n/index.ts index 6da1678e1..f21df82ff 100644 --- a/miniapps/forge/src/i18n/index.ts +++ b/miniapps/forge/src/i18n/index.ts @@ -16,6 +16,20 @@ export type LanguageCode = keyof typeof languages export const defaultLanguage: LanguageCode = 'zh-CN' +// Try to get language from local storage (aligns with shell/e2e) +let savedLanguage: LanguageCode = defaultLanguage +try { + const prefs = localStorage.getItem('bfm_preferences') + if (prefs) { + const parsed = JSON.parse(prefs) + if (parsed.language && Object.keys(languages).includes(parsed.language)) { + savedLanguage = parsed.language as LanguageCode + } + } +} catch (e) { + // Ignore error +} + export function getLanguageDirection(lang: LanguageCode): 'ltr' | 'rtl' { return languages[lang]?.dir ?? 'ltr' } @@ -31,7 +45,7 @@ i18n.use(initReactI18next).init({ 'zh-CN': { translation: zhCN }, 'zh-TW': { translation: zhTW }, }, - lng: defaultLanguage, + lng: savedLanguage, fallbackLng: { 'zh-CN': ['zh'], 'zh-TW': ['zh'], diff --git a/miniapps/forge/src/i18n/locales/en.json b/miniapps/forge/src/i18n/locales/en.json index ccee562ec..81eabca3a 100644 --- a/miniapps/forge/src/i18n/locales/en.json +++ b/miniapps/forge/src/i18n/locales/en.json @@ -5,7 +5,7 @@ "description": "Bridge assets between external chains and Bio ecosystem" }, "mode": { - "recharge": "Recharge", + "recharge": "Forge", "redemption": "Redemption" }, "connect": { diff --git a/miniapps/forge/src/i18n/locales/zh-CN.json b/miniapps/forge/src/i18n/locales/zh-CN.json index c0b9e3c92..aaa1ea03a 100644 --- a/miniapps/forge/src/i18n/locales/zh-CN.json +++ b/miniapps/forge/src/i18n/locales/zh-CN.json @@ -5,7 +5,7 @@ "description": "在外链与 Bio 生态之间桥接资产" }, "mode": { - "recharge": "充值", + "recharge": "锻造", "redemption": "赎回" }, "connect": { diff --git a/miniapps/forge/src/i18n/locales/zh-TW.json b/miniapps/forge/src/i18n/locales/zh-TW.json index 43f9b3103..cb82a024b 100644 --- a/miniapps/forge/src/i18n/locales/zh-TW.json +++ b/miniapps/forge/src/i18n/locales/zh-TW.json @@ -5,7 +5,7 @@ "description": "在外鏈與 Bio 生態之間橋接資產" }, "mode": { - "recharge": "充值", + "recharge": "鍛造", "redemption": "贖回" }, "connect": { diff --git a/miniapps/forge/src/i18n/locales/zh.json b/miniapps/forge/src/i18n/locales/zh.json index c0b9e3c92..aaa1ea03a 100644 --- a/miniapps/forge/src/i18n/locales/zh.json +++ b/miniapps/forge/src/i18n/locales/zh.json @@ -5,7 +5,7 @@ "description": "在外链与 Bio 生态之间桥接资产" }, "mode": { - "recharge": "充值", + "recharge": "锻造", "redemption": "赎回" }, "connect": { diff --git a/miniapps/forge/src/index.css b/miniapps/forge/src/index.css index 310514b2c..6c9b0bb70 100644 --- a/miniapps/forge/src/index.css +++ b/miniapps/forge/src/index.css @@ -116,6 +116,7 @@ } body { @apply bg-background text-foreground; + font-family: 'Noto Sans SC Variable', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } diff --git a/miniapps/forge/src/main.tsx b/miniapps/forge/src/main.tsx index 81c689d23..cfa4bd4dd 100644 --- a/miniapps/forge/src/main.tsx +++ b/miniapps/forge/src/main.tsx @@ -1,4 +1,5 @@ import './index.css' +import '@fontsource-variable/noto-sans-sc' import './i18n' import '@biochain/bio-sdk' import { StrictMode } from 'react' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 469e9bd49..09d0cd918 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -378,6 +378,9 @@ importers: '@biochain/keyapp-sdk': specifier: workspace:* version: link:../../packages/keyapp-sdk + '@fontsource-variable/noto-sans-sc': + specifier: ^5.2.10 + version: 5.2.10 '@radix-ui/react-avatar': specifier: ^1.1.11 version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2036,6 +2039,9 @@ packages: '@fontsource-variable/figtree@5.2.10': resolution: {integrity: sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==} + '@fontsource-variable/noto-sans-sc@5.2.10': + resolution: {integrity: sha512-zdk10i5HrDQTXI7ldD61zToX1fsgig8vDTsu7zB48SXOitWfuX0e5viZAwnkHuhwh096PU6X6i1AyAsbBCISpA==} + '@fontsource/dm-mono@5.2.7': resolution: {integrity: sha512-Ma1az2atTVgQWuOWwjuxx26p/6A6CU9HBNKq1CFV6YKpKhpswnf9ry9Ql4+T6bTZzkdtSfS6tjJvqZOljVzIFQ==} @@ -9655,6 +9661,8 @@ snapshots: '@fontsource-variable/figtree@5.2.10': {} + '@fontsource-variable/noto-sans-sc@5.2.10': {} + '@fontsource/dm-mono@5.2.7': {} '@fontsource/dm-serif-display@5.2.8': {} @@ -11015,7 +11023,7 @@ snapshots: '@vitest/browser': 4.0.16(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vitest@4.0.16) '@vitest/browser-playwright': 4.0.16(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(playwright@1.57.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2))(vitest@4.0.16) '@vitest/runner': 4.0.16 - vitest: 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)) + vitest: 4.0.16(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3)) transitivePeerDependencies: - react - react-dom @@ -11535,7 +11543,7 @@ snapshots: '@vitest/mocker': 4.0.16(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)) playwright: 1.57.0 tinyrainbow: 3.0.3 - vitest: 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)) + vitest: 4.0.16(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3)) transitivePeerDependencies: - bufferutil - msw @@ -11551,7 +11559,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 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)) + vitest: 4.0.16(@types/node@24.10.4)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.4)(typescript@5.9.3)) ws: 8.18.3 transitivePeerDependencies: - bufferutil diff --git a/src/components/wallet/wallet-address-portfolio-from-provider.tsx b/src/components/wallet/wallet-address-portfolio-from-provider.tsx new file mode 100644 index 000000000..9a38fd0a4 --- /dev/null +++ b/src/components/wallet/wallet-address-portfolio-from-provider.tsx @@ -0,0 +1,107 @@ +import { useMemo } from 'react' +import { WalletAddressPortfolioView, type WalletAddressPortfolioViewProps } from './wallet-address-portfolio-view' +import type { ChainType } from '@/stores' +import { ChainProviderGate, useChainProvider } from '@/contexts' +import type { TokenInfo } from '@/components/token/token-item' +import { toTransactionInfoList } from '@/components/transaction/adapters' + +export interface WalletAddressPortfolioFromProviderProps { + chainId: ChainType + address: string + chainName?: string + onTokenClick?: WalletAddressPortfolioViewProps['onTokenClick'] + onTransactionClick?: WalletAddressPortfolioViewProps['onTransactionClick'] + className?: string + testId?: string +} + +/** + * 从 Provider 获取地址资产组合(内部实现) + * + * 使用 ChainProvider 响应式 API 获取数据,复用 WalletAddressPortfolioView 展示。 + */ +function WalletAddressPortfolioFromProviderInner({ + chainId, + address, + chainName, + onTokenClick, + onTransactionClick, + className, + testId, +}: WalletAddressPortfolioFromProviderProps) { + const chainProvider = useChainProvider() + + // 使用新的响应式 API + const { data: tokens = [], isLoading: tokensLoading } = chainProvider.allBalances.useState( + { address }, + { enabled: !!address } + ) + + const { data: txResult, isLoading: transactionsLoading } = chainProvider.transactionHistory.useState( + { address, limit: 50 }, + { enabled: !!address } + ) + + // 转换为 TokenInfo 格式 + const tokenInfoList: TokenInfo[] = useMemo(() => { + return tokens.map((token) => ({ + symbol: token.symbol, + name: token.name, + chain: chainId, + balance: token.amount.toFormatted(), + decimals: token.decimals, + fiatValue: undefined, + change24h: 0, + icon: token.icon, + })) + }, [tokens, chainId]) + + // 转换交易历史格式 + const transactions = useMemo(() => { + if (!txResult) return [] + return toTransactionInfoList(txResult, chainId) + }, [txResult, chainId]) + + return ( + + ) +} + +/** + * 从 Provider 获取地址资产组合 + * + * 使用 ChainProviderGate 确保 ChainProvider 可用,再使用响应式 API 获取数据。 + * 适用于 Stories 测试和任意地址查询场景。 + */ +export function WalletAddressPortfolioFromProvider(props: WalletAddressPortfolioFromProviderProps) { + return ( + +
+

Chain not supported: {props.chainId}

+
+ + } + > + +
+ ) +} diff --git a/src/i18n/locales/en/transaction.json b/src/i18n/locales/en/transaction.json index 226ddfd02..8a476f426 100644 --- a/src/i18n/locales/en/transaction.json +++ b/src/i18n/locales/en/transaction.json @@ -60,7 +60,8 @@ "twoStepSecretDescription": "This address has a security password set. Please enter your security password to confirm the transfer.", "twoStepSecretPlaceholder": "Enter security password", "twoStepSecretError": "Incorrect security password", - "fee": "Fee" + "fee": "Fee", + "previewTitle": "Transaction Preview" }, "destroyPage": { "title": "Destroy", diff --git a/src/i18n/locales/zh-CN/transaction.json b/src/i18n/locales/zh-CN/transaction.json index b3c8b5a22..cb5017c59 100644 --- a/src/i18n/locales/zh-CN/transaction.json +++ b/src/i18n/locales/zh-CN/transaction.json @@ -60,7 +60,8 @@ "twoStepSecretDescription": "该地址已设置安全密码,请输入安全密码确认转账。", "twoStepSecretPlaceholder": "输入安全密码", "twoStepSecretError": "安全密码错误", - "fee": "手续费" + "fee": "手续费", + "previewTitle": "交易预览" }, "destroyPage": { "title": "销毁", diff --git a/src/pages/send/index.tsx b/src/pages/send/index.tsx index af1aee480..bd2ec942e 100644 --- a/src/pages/send/index.tsx +++ b/src/pages/send/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigation, useActivityParams, useFlow } from '@/stackflow'; -import { setTransferConfirmCallback, setTransferWalletLockCallback, setScannerResultCallback } from '@/stackflow/activities/sheets'; +import { setTransferPreviewCallback, setTransferWalletLockCallback, setScannerResultCallback } from '@/stackflow/activities/sheets'; import type { Contact, ContactAddress } from '@/stores'; import { addressBookStore, addressBookSelectors, preferencesActions } from '@/stores'; import { PageHeader } from '@/components/layout/page-header'; @@ -237,8 +237,8 @@ function SendPageContent() { haptics.impact('light'); - // Set up callback: TransferConfirm -> TransferWalletLock (合并的钱包锁+二次签名) - setTransferConfirmCallback( + // Set up callback: TransferPreview -> TransferWalletLock (合并的钱包锁+二次签名) + setTransferPreviewCallback( async () => { if (isWalletLockSheetOpen.current) return; isWalletLockSheetOpen.current = true; @@ -300,13 +300,17 @@ function SendPageContent() { } ); - push('TransferConfirmJob', { + push('TransferPreviewJob', { amount: state.amount?.toFormatted() ?? '0', symbol, + decimals: String(state.asset?.decimals ?? chainConfig?.decimals ?? 8), + fromAddress: currentChainAddress?.address ?? '', toAddress: state.toAddress, feeAmount: state.feeAmount?.toFormatted() ?? '0', feeSymbol: state.feeSymbol, feeLoading: state.feeLoading ? 'true' : 'false', + chainId: selectedChain, + chainName: selectedChainName, }); }; diff --git a/src/services/ecosystem/handlers/wallet.test.ts b/src/services/ecosystem/handlers/wallet.test.ts new file mode 100644 index 000000000..811db7026 --- /dev/null +++ b/src/services/ecosystem/handlers/wallet.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { handleChainId } from './wallet' +import { walletStore } from '@/stores/wallet' +import type { HandlerContext } from '../types' + +describe('handleChainId', () => { + const mockContext: HandlerContext = { + appId: 'test-app', + appName: 'Test App', + origin: 'https://test.com', + permissions: [], + } + + beforeEach(() => { + // Reset store state + walletStore.setState(() => ({ + wallets: [], + currentWalletId: null, + selectedChain: 'bfmeta', + chainPreferences: {}, + isLoading: false, + isInitialized: false, + migrationRequired: false, + })) + }) + + it('should return the currently selected chain ID', async () => { + // Set a different chain + walletStore.setState((state) => ({ + ...state, + selectedChain: 'ethereum', + })) + + const chainId = await handleChainId({}, mockContext) + expect(chainId).toBe('ethereum') + }) + + it('should return bfmeta if that is the selected chain', async () => { + walletStore.setState((state) => ({ + ...state, + selectedChain: 'bfmeta', + })) + + const chainId = await handleChainId({}, mockContext) + expect(chainId).toBe('bfmeta') + }) +}) diff --git a/src/services/ecosystem/handlers/wallet.ts b/src/services/ecosystem/handlers/wallet.ts index 6a58b14eb..b7236dfe3 100644 --- a/src/services/ecosystem/handlers/wallet.ts +++ b/src/services/ecosystem/handlers/wallet.ts @@ -6,6 +6,7 @@ import type { MethodHandler, BioAccount } from '../types' import { BioErrorCodes } from '../types' import { HandlerContext } from './context' import { getChainProvider } from '@/services/chain-adapter/providers' +import { walletStore } from '@/stores/wallet' // 兼容旧 API,逐步迁移到 HandlerContext let _showWalletPicker: ((opts?: { chain?: string; exclude?: string }) => Promise) | null = null @@ -104,8 +105,7 @@ export const handlePickWallet: MethodHandler = async (params, context) => { /** bio_chainId - Get current chain ID */ export const handleChainId: MethodHandler = async (_params, _context) => { - // TODO: Get from current selected chain - return 'bfmeta' + return walletStore.state.selectedChain } /** bio_getBalance - Get balance */ diff --git a/src/stackflow/activities/sheets/TransferPreviewJob.tsx b/src/stackflow/activities/sheets/TransferPreviewJob.tsx new file mode 100644 index 000000000..25a5e0dd2 --- /dev/null +++ b/src/stackflow/activities/sheets/TransferPreviewJob.tsx @@ -0,0 +1,250 @@ +/** + * 转账预览确认组件 + * + * 显示完整的交易详情预览,替代简单的 TransferConfirmJob + * 流程: Send页面(填写) → TransferPreviewJob(交易详情预览) → TransferWalletLockJob(签名+广播+状态) + */ +import { useState, useCallback, useRef, useEffect } from 'react'; +import type { ActivityComponentType } from '@stackflow/react'; +import { BottomSheet, SheetContent } from '@/components/layout/bottom-sheet'; +import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; +import { FeeDisplay } from '@/components/transaction/fee-display'; +import { AddressDisplay } from '@/components/wallet/address-display'; +import { ChainIcon } from '@/components/wallet/chain-icon'; +import { AmountDisplay } from '@/components/common'; +import type { ChainType } from '@/stores'; +import { + IconArrowUp as ArrowUp, + IconArrowDown as ArrowDown, +} from '@tabler/icons-react'; +import { useFlow } from '../../stackflow'; +import { ActivityParamsProvider, useActivityParams } from '../../hooks'; +import { setFeeEditCallback } from './FeeEditJob'; + +interface TransferPreviewConfig { + onConfirm: () => Promise; + minFee?: string | undefined; + onFeeChange?: ((newFee: string) => void) | undefined; +} + +let pendingConfig: TransferPreviewConfig | null = null; + +export function setTransferPreviewCallback( + onConfirm: () => Promise, + options?: { minFee?: string; onFeeChange?: (newFee: string) => void } +) { + pendingConfig = { + onConfirm, + minFee: options?.minFee, + onFeeChange: options?.onFeeChange, + }; +} + +function clearTransferPreviewCallback() { + pendingConfig = null; +} + +type TransferPreviewJobParams = { + /** 转账金额 */ + amount: string; + /** 资产符号 */ + symbol: string; + /** 资产小数位 */ + decimals?: string; + /** 法币价值 */ + fiatValue?: string; + /** 发送地址 */ + fromAddress: string; + /** 接收地址 */ + toAddress: string; + /** 手续费金额 */ + feeAmount: string; + /** 手续费符号 */ + feeSymbol: string; + /** 手续费法币价值 */ + feeFiatValue?: string; + /** 手续费加载中 */ + feeLoading?: string; + /** 链ID */ + chainId: string; + /** 链名称 */ + chainName?: string; +}; + +function TransferPreviewJobContent() { + const { t } = useTranslation(['transaction', 'common']); + const { pop, push } = useFlow(); + const params = useActivityParams(); + + const [isConfirming, setIsConfirming] = useState(false); + const [customFee, setCustomFee] = useState(null); + + // 捕获配置 + const configRef = useRef(pendingConfig); + const initialized = useRef(false); + + if (!initialized.current && pendingConfig) { + configRef.current = pendingConfig; + clearTransferPreviewCallback(); + initialized.current = true; + } + + useEffect(() => { + return () => { + clearTransferPreviewCallback(); + }; + }, []); + + const feeLoading = params.feeLoading === 'true'; + const displayFee = customFee ?? params.feeAmount; + const canEditFee = !!configRef.current?.onFeeChange; + const decimals = params.decimals ? parseInt(params.decimals, 10) : 8; + const amountNum = parseFloat(params.amount) || 0; + + const handleEditFee = useCallback(() => { + const config = configRef.current; + if (!config?.onFeeChange) return; + + setFeeEditCallback( + { + currentFee: displayFee, + minFee: config.minFee ?? params.feeAmount, + symbol: params.feeSymbol, + }, + (result) => { + setCustomFee(result.fee); + config.onFeeChange?.(result.fee); + } + ); + push('FeeEditJob', {}); + }, [displayFee, params.feeAmount, params.feeSymbol, push]); + + const handleConfirm = useCallback(async () => { + const config = configRef.current; + + if (!config?.onConfirm || isConfirming) return; + + setIsConfirming(true); + try { + pop(); + await config.onConfirm(); + } finally { + setIsConfirming(false); + } + }, [isConfirming, pop]); + + const handleClose = useCallback(() => { + pop(); + }, [pop]); + + return ( + + +
+ {/* 金额头部 */} +
+
+ +
+ +
+

{t('type.send')}

+ + {params.fiatValue && ( +

≈ ${params.fiatValue}

+ )} +
+
+ + {/* 交易详情 */} +
+

{t('detail.info')}

+ + {/* 发送地址 */} +
+ {t('detail.fromAddress')} + +
+ +
+ + {/* 接收地址 */} +
+ {t('detail.toAddress')} + +
+ +
+ + {/* 网络 */} +
+ {t('detail.network')} +
+ + {params.chainName ?? params.chainId} +
+
+ +
+ + {/* 手续费 */} +
+ {t('detail.fee')} + +
+
+ + {/* 操作按钮 */} +
+ + +
+
+ + + ); +} + +export const TransferPreviewJob: ActivityComponentType = ({ params }) => { + return ( + + + + ); +}; diff --git a/src/stackflow/activities/sheets/TransferWalletLockJob.tsx b/src/stackflow/activities/sheets/TransferWalletLockJob.tsx index b313eaf17..4b5100b75 100644 --- a/src/stackflow/activities/sheets/TransferWalletLockJob.tsx +++ b/src/stackflow/activities/sheets/TransferWalletLockJob.tsx @@ -55,7 +55,7 @@ type Step = 'wallet_lock' | 'two_step_secret'; function TransferWalletLockJobContent() { const { t } = useTranslation(["security", "transaction", "common"]); - const { pop, push } = useFlow(); + const { pop, push, replace } = useFlow(); const { title } = useActivityParams(); const clipboard = useClipboard(); const toast = useToast(); @@ -110,7 +110,12 @@ function TransferWalletLockJobContent() { return unsubscribe; }, [txHash, selectedChain, currentWallet?.address]); - // 上链成功后 5 秒倒计时自动关闭 + // Navigate back to home (replace entire stack) + const goToHome = useCallback(() => { + replace('MainTabsActivity', {}, { animate: true }); + }, [replace]); + + // 上链成功后 5 秒倒计时自动关闭,返回首页 useEffect(() => { if (txStatus !== 'confirmed') { setCountdown(null); @@ -122,7 +127,7 @@ function TransferWalletLockJobContent() { setCountdown((prev) => { if (prev === null || prev <= 1) { clearInterval(timer); - pop(); + goToHome(); return null; } return prev - 1; @@ -130,7 +135,7 @@ function TransferWalletLockJobContent() { }, 1000); return () => clearInterval(timer); - }, [txStatus, pop]); + }, [txStatus, goToHome]); // Get chain config for explorer URL const chainConfig = useMemo(() => { diff --git a/src/stackflow/activities/sheets/index.ts b/src/stackflow/activities/sheets/index.ts index b7040f658..4466a8ce9 100644 --- a/src/stackflow/activities/sheets/index.ts +++ b/src/stackflow/activities/sheets/index.ts @@ -11,6 +11,7 @@ export { WalletAddJob } from "./WalletAddJob"; export { WalletListJob } from "./WalletListJob"; export { SecurityWarningJob, setSecurityWarningConfirmCallback } from "./SecurityWarningJob"; export { TransferConfirmJob, setTransferConfirmCallback } from "./TransferConfirmJob"; +export { TransferPreviewJob, setTransferPreviewCallback } from "./TransferPreviewJob"; export { TransferWalletLockJob, setTransferWalletLockCallback } from "./TransferWalletLockJob"; export { FeeEditJob, setFeeEditCallback, type FeeEditConfig, type FeeEditResult } from "./FeeEditJob"; export { ScannerJob, setScannerResultCallback, scanValidators, getValidatorForChain, type ScannerJobParams, type ScannerResultEvent, type ScanValidator } from "./ScannerJob"; diff --git a/src/stackflow/stackflow.ts b/src/stackflow/stackflow.ts index 9f41f8cd8..f81b1af91 100644 --- a/src/stackflow/stackflow.ts +++ b/src/stackflow/stackflow.ts @@ -48,6 +48,7 @@ import { WalletListJob, SecurityWarningJob, TransferConfirmJob, + TransferPreviewJob, TransferWalletLockJob, FeeEditJob, ScannerJob, @@ -115,6 +116,7 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ WalletListJob: '/job/wallet-list', SecurityWarningJob: '/job/security-warning', TransferConfirmJob: '/job/transfer-confirm', + TransferPreviewJob: '/job/transfer-preview', TransferWalletLockJob: '/job/transfer-wallet-lock', FeeEditJob: '/job/fee-edit', ScannerJob: '/job/scanner', @@ -181,6 +183,7 @@ export const { Stack, useFlow, useStepFlow, activities } = stackflow({ WalletListJob, SecurityWarningJob, TransferConfirmJob, + TransferPreviewJob, TransferWalletLockJob, FeeEditJob, ScannerJob,