Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
128f80e
fix: use dynamic genesis beginEpochTime for BioChain transaction time…
Gaubee Jan 16, 2026
c35ca9b
fix(receive): fix QR code display and add snapdom screenshot
Gaubee Jan 16, 2026
95cf90f
feat(my-card): implement My Business Card feature
Gaubee Jan 16, 2026
5c5d5c7
test(my-card): add comprehensive TDD acceptance tests
Gaubee Jan 16, 2026
161b309
feat(receive): upgrade ReceivePage to use ContactCard with user profile
Gaubee Jan 16, 2026
a05d3ab
fix(my-card): wallet chips use chain colors with auto text contrast
Gaubee Jan 16, 2026
a6a3ba2
fix(my-card): use wallet.themeHue for chip colors instead of chain co…
Gaubee Jan 16, 2026
5515585
fix(my-card): use resolveBackgroundStops for wallet chip colors
Gaubee Jan 16, 2026
f1867bb
fix(my-card): add AppScreen wrapper to MyCardActivity
Gaubee Jan 16, 2026
e5488a6
feat(settings): replace wallet header with My Card entry
Gaubee Jan 16, 2026
2d5db28
refactor(my-card): simplify wallet chips to show only name with color
Gaubee Jan 16, 2026
409f9fe
Revert "refactor(my-card): simplify wallet chips to show only name wi…
Gaubee Jan 16, 2026
7fe6c51
fix(my-card): remove chain name from ContactCard chips
Gaubee Jan 16, 2026
bd515ff
feat(my-card): add delete buttons to wallet selection chips
Gaubee Jan 16, 2026
42d444c
fix(contact-card): add all chain colors including BioForest chains
Gaubee Jan 16, 2026
0ac11f0
fix(contact-card): use address format standard colors
Gaubee Jan 16, 2026
548e4f6
refactor: extract useSnapdomShare hook for code consolidation
Gaubee Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/common/time-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short', // 显示时区信息以避免歧义
});
break;
case 'time':
Expand All @@ -72,6 +73,7 @@ export function TimeDisplay({ value, format = 'relative', className }: TimeDispl
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
});

return (
Expand Down
41 changes: 35 additions & 6 deletions src/components/contact/contact-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,48 @@ import { ContactAvatar } from '@/components/common/contact-avatar';
import { generateAvatarFromAddress } from '@/lib/avatar-codec';
import { detectAddressFormat } from '@/lib/address-format';
import type { ContactAddressInfo } from '@/lib/qr-parser';
import { isBioforestChain } from '@/lib/crypto';

const CHAIN_COLORS: Record<string, string> = {
ethereum: '#627EEA',
bitcoin: '#F7931A',
tron: '#FF0013',
/** Address format standard colors */
const ADDRESS_FORMAT_COLORS = {
evm: '#627EEA', // EVM (0x...) - ethereum, binance
bitcoin: '#F7931A', // Bitcoin (1.../3.../bc1...)
tron: '#FF0013', // TRON (T...)
bioforest: '#6366F1', // BioForest (all BioForest chains share same format)
};

/** Get color based on address format standard */
function getAddressFormatColor(chainType: string | null): string {
if (!chainType) return '#6B7280';

// EVM-compatible chains
if (chainType === 'ethereum' || chainType === 'binance') {
return ADDRESS_FORMAT_COLORS.evm;
}

// Bitcoin
if (chainType === 'bitcoin') {
return ADDRESS_FORMAT_COLORS.bitcoin;
}

// TRON
if (chainType === 'tron') {
return ADDRESS_FORMAT_COLORS.tron;
}

// BioForest chains (all use same address format)
if (isBioforestChain(chainType)) {
return ADDRESS_FORMAT_COLORS.bioforest;
}

return '#6B7280';
}

/** 获取地址显示标签和颜色(只显示自定义 label) */
function getAddressDisplay(addr: ContactAddressInfo): { label: string; color: string } | null {
if (!addr.label) return null;
const detected = detectAddressFormat(addr.address);
const chainType = detected.chainType;
const color = chainType ? CHAIN_COLORS[chainType] || '#6B7280' : '#6B7280';
const color = getAddressFormatColor(detected.chainType);
return { label: addr.label, color };
}

Expand Down
90 changes: 90 additions & 0 deletions src/hooks/useSnapdomShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* useSnapdomShare - 截图分享 hook
*
* 封装 snapdom 的下载和分享逻辑,供 ContactCard 相关页面复用
*/

import { useCallback, useState, type RefObject } from 'react';

export interface UseSnapdomShareOptions {
/** 下载/分享的文件名(不含扩展名) */
filename: string;
/** 分享时的标题 */
shareTitle?: string;
/** 分享时的描述文本 */
shareText?: string;
}

export interface UseSnapdomShareResult {
/** 是否正在处理(下载或分享中) */
isProcessing: boolean;
/** 下载为 PNG 图片 */
download: () => Promise<void>;
/** 使用系统分享功能 */
share: () => Promise<void>;
/** 浏览器是否支持分享 */
canShare: boolean;
}

/**
* 截图分享 hook
* @param cardRef 需要截图的元素引用
* @param options 配置选项
*/
export function useSnapdomShare(
cardRef: RefObject<HTMLElement | null>,
options: UseSnapdomShareOptions
): UseSnapdomShareResult {
const [isProcessing, setIsProcessing] = useState(false);
const { filename, shareTitle, shareText } = options;

const download = useCallback(async () => {
const element = cardRef.current;
if (!element || isProcessing) return;

setIsProcessing(true);
try {
const { snapdom } = await import('@zumer/snapdom');
await snapdom.download(element, {
type: 'png',
filename: `${filename}.png`,
scale: 2,
quality: 1,
});
} catch (error) {
console.error('Download failed:', error);
} finally {
setIsProcessing(false);
}
}, [cardRef, filename, isProcessing]);

const share = useCallback(async () => {
const element = cardRef.current;
if (!element || !navigator.share || isProcessing) return;

setIsProcessing(true);
try {
const { snapdom } = await import('@zumer/snapdom');
const result = await snapdom(element, { scale: 2 });
const blob = await result.toBlob();
const file = new File([blob], `${filename}.png`, { type: 'image/png' });

await navigator.share({
title: shareTitle,
text: shareText,
files: [file],
});
} catch {
// User cancelled or share failed
} finally {
setIsProcessing(false);
}
}, [cardRef, filename, shareTitle, shareText, isProcessing]);

return {
isProcessing,
download,
share,
canShare: typeof navigator !== 'undefined' && 'share' in navigator,
};
}
16 changes: 15 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,20 @@
"scanToAdd": "Scan to add contact",
"changeAvatar": "Click to change avatar"
},
"myCard": {
"title": "My Business Card",
"defaultName": "My Card",
"editUsername": "Tap to edit username",
"usernamePlaceholder": "Enter username",
"changeAvatar": "Tap to change avatar",
"selectWallets": "Select Wallets",
"addWallet": "Add Wallet",
"maxWallets": "Select up to {{max}} wallets",
"noWalletsSelected": "Please select at least one wallet",
"scanToAdd": "Scan to add me as contact",
"walletAddress": "{{wallet}} ({{chain}})",
"currentChain": "Current chain"
},
"time": {
"justNow": "Just now",
"minutesAgo": "{{count}} min ago",
Expand Down Expand Up @@ -373,4 +387,4 @@
"openExplorer": "Open {{name}} Explorer",
"viewOnExplorer": "View on {{name}}"
}
}
}
3 changes: 2 additions & 1 deletion src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"about": "About"
},
"items": {
"myCard": "My Business Card",
"walletManagement": "Wallet Management",
"walletChains": "Wallet Networks",
"addressBook": "Address Book",
Expand Down Expand Up @@ -146,4 +147,4 @@
"migrationDesc": "Detected old data format. Please clear the local database to continue. Your mnemonic and private keys are not affected, but you will need to re-import your wallet.",
"goToClear": "Go to Clear Data"
}
}
}
6 changes: 4 additions & 2 deletions src/i18n/locales/en/transaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"share": "Share",
"shareTitle": "BFM Pay Receive Address",
"addressCopied": "Address copied",
"networkWarning": "Only {{chain}} network assets can be transferred in, assets from other networks cannot be recovered"
"networkWarning": "Only {{chain}} network assets can be transferred in, assets from other networks cannot be recovered",
"saveImage": "Save Image",
"imageSaved": "Image saved"
},
"sendResult": {
"success": "Transfer Successful",
Expand Down Expand Up @@ -339,4 +341,4 @@
"viewAll": "View all {{count}} pending transactions",
"clearAllFailed": "Clear failed"
}
}
}
16 changes: 15 additions & 1 deletion src/i18n/locales/zh-CN/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,20 @@
"scanToAdd": "扫码添加联系人",
"changeAvatar": "点击切换头像"
},
"myCard": {
"title": "我的名片",
"defaultName": "我的名片",
"editUsername": "点击编辑用户名",
"usernamePlaceholder": "输入用户名",
"changeAvatar": "点击换头像",
"selectWallets": "选择钱包",
"addWallet": "添加钱包",
"maxWallets": "最多选择 {{max}} 个钱包",
"noWalletsSelected": "请选择至少一个钱包",
"scanToAdd": "扫码添加我为联系人",
"walletAddress": "{{wallet}} ({{chain}})",
"currentChain": "当前链"
},
"time": {
"justNow": "刚刚",
"minutesAgo": "{{count}} 分钟前",
Expand Down Expand Up @@ -351,4 +365,4 @@
"openExplorer": "打开 {{name}} 浏览器",
"viewOnExplorer": "在 {{name}} 浏览器中查看"
}
}
}
3 changes: 2 additions & 1 deletion src/i18n/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"about": "关于"
},
"items": {
"myCard": "我的名片",
"walletManagement": "钱包管理",
"walletChains": "钱包网络",
"addressBook": "地址簿",
Expand Down Expand Up @@ -146,4 +147,4 @@
"migrationDesc": "检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。",
"goToClear": "前往清理数据"
}
}
}
6 changes: 4 additions & 2 deletions src/i18n/locales/zh-CN/transaction.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"share": "分享",
"shareTitle": "BFM Pay 收款地址",
"addressCopied": "地址已复制",
"networkWarning": "仅支持 {{chain}} 网络资产转入,其他网络资产转入将无法找回"
"networkWarning": "仅支持 {{chain}} 网络资产转入,其他网络资产转入将无法找回",
"saveImage": "保存图片",
"imageSaved": "图片已保存"
},
"sendResult": {
"success": "转账成功",
Expand Down Expand Up @@ -339,4 +341,4 @@
"viewAll": "查看全部 {{count}} 条待处理交易",
"clearAllFailed": "清除失败"
}
}
}
40 changes: 40 additions & 0 deletions src/pages/my-card/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MyCardPage } from './index';

/**
* MyCardPage - 我的名片页面
*
* 功能:
* - 显示和编辑用户名
* - 点击头像随机更换
* - 选择最多3个钱包显示在名片上
* - QR码使用 contact 协议
* - 下载/分享功能
*/
const meta: Meta<typeof MyCardPage> = {
title: 'Pages/MyCardPage',
component: MyCardPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: '我的名片页面 - 用于分享个人钱包地址二维码',
},
},
},
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof MyCardPage>;

export const Default: Story = {};

export const WithUsername: Story = {
play: async () => {
// Pre-set username for testing
const { userProfileActions } = await import('@/stores');
userProfileActions.setUsername('测试用户');
userProfileActions.randomizeAvatar();
},
};
Loading