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..4fd8230eb --- /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/stackflow/activities/sheets/TransferPreviewJob.tsx b/src/stackflow/activities/sheets/TransferPreviewJob.tsx new file mode 100644 index 000000000..f7baa133f --- /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/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..a45b97715 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',