Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ jobs:
pnpm install --frozen-lockfile

if [ "${{ steps.changes.outputs.code }}" == "true" ]; then
# 运行所有 E2E 测试:Dev + Mock + Real(真实转账)
TASKS="typecheck:run build i18n:run test:run e2e:ci e2e:ci:mock e2e:ci:real"
# 运行所有测试:单元测试 + Storybook 组件测试 + E2E 测试
TASKS="typecheck:run build i18n:run test:run test:storybook e2e:ci e2e:ci:mock e2e:ci:real"
else
TASKS="typecheck:run build i18n:run test:run"
TASKS="typecheck:run build i18n:run test:run test:storybook"
fi

# Run turbo and capture output, pipefail ensures we get turbo's exit code
Expand Down Expand Up @@ -115,10 +115,10 @@ jobs:
E2E_TEST_MNEMONIC: ${{ secrets.E2E_TEST_MNEMONIC }}
run: |
if [ "${{ steps.changes.outputs.code }}" == "true" ]; then
# 运行所有 E2E 测试:Dev + Mock + Real(真实转账)
pnpm turbo run typecheck:run build i18n:run test:run e2e:ci e2e:ci:mock e2e:ci:real
# 运行所有测试:单元测试 + Storybook 组件测试 + E2E 测试
pnpm turbo run typecheck:run build i18n:run test:run test:storybook e2e:ci e2e:ci:mock e2e:ci:real
else
pnpm turbo run typecheck:run build i18n:run test:run
pnpm turbo run typecheck:run build i18n:run test:run test:storybook
fi

checks-standard:
Expand Down
27 changes: 22 additions & 5 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Preview, ReactRenderer } from '@storybook/react-vite'
import type { DecoratorFunction } from 'storybook/internal/types'
import { useEffect } from 'react'
import { useEffect, useMemo } from 'react'
import { I18nextProvider } from 'react-i18next'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import i18n, { languages, getLanguageDirection, type LanguageCode } from '../src/i18n'
import { currencies, preferencesActions, preferencesStore, type CurrencyCode } from '../src/stores/preferences'
import '../src/styles/globals.css'
Expand Down Expand Up @@ -123,13 +124,27 @@ const preview: Preview = {
},
},
decorators: [
// i18n + Theme + Direction decorator
// i18n + Theme + Direction + QueryClient decorator
((Story, context) => {
const locale = (context.globals['locale'] || 'zh-CN') as LanguageCode
const currency = isCurrencyCode(context.globals['currency']) ? context.globals['currency'] : 'USD'
const theme = context.globals['theme'] || 'light'
const direction = getLanguageDirection(locale)

// Create a stable QueryClient instance for each story
const queryClient = useMemo(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
}),
[]
)

useEffect(() => {
// 更新语言
if (i18n.language !== locale) {
Expand All @@ -154,9 +169,11 @@ const preview: Preview = {
}, [locale, currency, theme, direction])

return (
<I18nextProvider i18n={i18n}>
<Story />
</I18nextProvider>
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18n}>
<Story />
</I18nextProvider>
</QueryClientProvider>
)
}) as DecoratorFunction<ReactRenderer>,

Expand Down
49 changes: 49 additions & 0 deletions docs/white-book/04-服务篇/09-联系人服务/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,54 @@ function migrateV1ToV2(v1Contact: V1Contact): Contact {

---

## 联系人分享与扫码添加

### 联系人协议

支持通过二维码分享和添加联系人。

#### JSON 格式(推荐)

```json
{
"type": "contact",
"name": "张三",
"addresses": [
{ "chainType": "ethereum", "address": "0x742d35Cc..." },
{ "chainType": "bitcoin", "address": "bc1qar0..." }
],
"memo": "好友",
"avatar": "👨‍💼"
}
```

#### URI 格式

```
contact://张三?eth=0x742d35Cc...&btc=bc1qar0...&memo=好友
```

### 分享名片流程

1. 在通讯录页面,点击联系人菜单选择"分享"
2. 打开 `ContactShareJob` 显示联系人二维码
3. 可选择下载图片或分享给他人

### 扫码添加流程

1. 扫描联系人二维码
2. 解析为 `contact` 类型
3. 打开 `ContactAddConfirmJob` 确认界面
4. 用户确认后保存到通讯录

### 相关组件

- `ContactShareJob`: 分享联系人名片二维码
- `ContactAddConfirmJob`: 扫码后确认添加联系人
- 详见 [Scanner 组件](../../05-组件篇/02-通用组件/Scanner.md)

---

## 安全考虑

- 联系人数据不加密(非敏感数据)
Expand All @@ -245,3 +293,4 @@ function migrateV1ToV2(v1Contact: V1Contact): Contact {

- [08-钱包数据存储](../08-钱包数据存储/) - 数据存储规范
- [ITransactionService](../02-链服务/ITransactionService.md) - 交易服务
- [Scanner 组件](../../05-组件篇/02-通用组件/Scanner.md) - 二维码扫描与解析
222 changes: 222 additions & 0 deletions docs/white-book/05-组件篇/02-通用组件/Scanner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Scanner 二维码扫描器

> 高性能 QR 码扫描与解析

---

## 功能描述

使用 Web Worker 后台解码二维码,支持地址扫描、支付请求、深度链接和联系人协议。

---

## 架构设计

### 核心类

```typescript
// QRScanner - 主扫描器类
class QRScanner {
constructor(options: QRScannerOptions)
start(source: FrameSource): void
stop(): void
on(event: 'scan' | 'error', callback: Function): void
}

// FrameSource - 帧来源接口
interface FrameSource {
getFrame(): Promise<ImageData | null>
dispose(): void
}
```

### Web Worker 解码

```
┌─────────────────┐ ┌─────────────────┐
│ 主线程 │ │ Web Worker │
│ - 获取帧 │ --> │ - jsQR 解码 │
│ - 回调处理 │ <-- │ - 返回结果 │
└─────────────────┘ └─────────────────┘
```

---

## 解析类型

### ParsedQRContent 类型定义

```typescript
type ParsedQRContent =
| ParsedAddress // 纯地址
| ParsedPayment // 支付请求
| ParsedDeepLink // 深度链接
| ParsedContact // 联系人协议
| ParsedUnknown // 未知内容
```

### 支持的格式

| 类型 | 格式示例 | 说明 |
|-----|---------|------|
| address | `0x1234...` | 纯地址 |
| payment | `ethereum:0x...?value=1000` | EIP-681 支付请求 |
| deeplink | `keyapp://authorize?...` | 应用深度链接 |
| contact | `{"type":"contact",...}` | 联系人协议 |

---

## 联系人协议 (Contact Protocol)

### JSON 格式 (推荐)

```json
{
"type": "contact",
"name": "张三",
"addresses": [
{ "chainType": "ethereum", "address": "0x742d35Cc..." },
{ "chainType": "bitcoin", "address": "bc1qar0..." }
],
"memo": "好友",
"avatar": "👨‍💼"
}
```

### URI 格式

```
contact://张三?eth=0x742d35Cc...&btc=bc1qar0...&memo=好友
```

| 参数 | 说明 |
|-----|------|
| eth | Ethereum 地址 |
| btc | Bitcoin 地址 |
| trx | Tron 地址 |
| memo | 备注 |

---

## ScannerJob 组件

### 用途

Stackflow BottomSheet 模式的扫码器,用于在发送页面等场景扫描地址。

### 属性

| 属性 | 类型 | 说明 |
|-----|------|------|
| chainType | 'ethereum' \| 'bitcoin' \| 'tron' \| 'any' | 限制扫描的地址类型 |

### 使用示例

```tsx
// 在 SendPage 中使用
const flow = useFlow()
const callbackRef = useRef<(addr: string) => void>()

// 打开扫码器
callbackRef.current = (address) => {
setRecipientAddress(address)
}
setScannerResultCallback(callbackRef)
flow.push('ScannerJob', { chainType: 'ethereum' })
```

---

## 验证器规范

### 内置验证器

| 验证器 | 链类型 | 规则 |
|-------|-------|------|
| ethereumAddress | ethereum | `^0x[a-fA-F0-9]{40}$` |
| bitcoinAddress | bitcoin | legacy/segwit/bech32 |
| tronAddress | tron | `^T[a-zA-HJ-NP-Z1-9]{33}$` |
| anyAddress | any | 26-64 字符 |

### 自定义验证器

```typescript
type ScanValidator = (
content: string,
parsed: ParsedQRContent
) => true | string // true = 通过, string = 错误消息 i18n key
```

---

## 相关 Job 组件

### ContactAddConfirmJob

扫码获取联系人后的确认添加界面。

```
┌─────────────────────────────────┐
│ 添加联系人 │
├─────────────────────────────────┤
│ 👤 张三 │
│ ──────────────────────────── │
│ ETH: 0x742d... │
│ BTC: bc1qar... │
│ ──────────────────────────── │
│ 备注: [输入框] │
├─────────────────────────────────┤
│ [取消] [保存] │
└─────────────────────────────────┘
```

### ContactShareJob

展示联系人名片二维码用于分享。

```
┌─────────────────────────────────┐
│ 分享名片 │
├─────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ QR Code │ │
│ │ (联系人) │ │
│ └─────────────┘ │
│ │
│ 张三 · ETH │
│ │
├─────────────────────────────────┤
│ [下载图片] [分享] │
└─────────────────────────────────┘
```

---

## 测试覆盖

### 单元测试

- `qr-parser.test.ts`: 38 个测试用例
- `ScannerJob.test.ts`: 31 个测试用例
- `qr-scanner.test.ts`: 10 个测试用例

### Storybook Stories

- `ScannerJob.stories.tsx`: ValidatorDemo, AddressFormats, UIPreview
- `ContactJobs.stories.tsx`: ContactProtocolDemo, ContactAddConfirmPreview, ContactSharePreview, EdgeCases

### E2E 测试

- `scanner.spec.ts`: 扫描页面基础测试
- `contact-scanner.mock.spec.ts`: 联系人协议集成测试

---

## 性能指标

| 指标 | 目标 | 说明 |
|-----|------|------|
| 扫描帧率 | 6-7 FPS | 150ms 间隔 |
| 解码延迟 | < 100ms | Web Worker 后台处理 |
| 首次扫描 | < 500ms | 冷启动到首次成功 |
1 change: 1 addition & 0 deletions docs/white-book/05-组件篇/02-通用组件/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
| [AnimatedNumber](./AnimatedNumber) | 数字动画显示 | P0 |
| [TokenIcon](./TokenIcon) | 代币/链图标 | P0 |
| [QRCode](./QRCode) | 二维码生成与显示 | P0 |
| [Scanner](./Scanner) | 二维码扫描与解析 | P0 |
| Countdown | 倒计时 | P1 |
| CopyButton | 复制按钮 | P1 |
| EmptyState | 空状态占位 | P1 |
Expand Down
Loading