-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCryptoAuthorizeJob.tsx
More file actions
203 lines (186 loc) · 7.3 KB
/
CryptoAuthorizeJob.tsx
File metadata and controls
203 lines (186 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
* CryptoAuthorizeJob - Crypto 黑盒授权对话框
*
* 用于小程序请求加密操作授权,用户需输入手势密码确认
*/
import { useCallback, useState } from 'react';
import type { ActivityComponentType } from '@stackflow/react';
import { BottomSheet } from '@/components/layout/bottom-sheet';
import { useTranslation } from 'react-i18next';
import { IconLock, IconLoader2 } from '@tabler/icons-react';
import { useFlow } from '../../stackflow';
import { ActivityParamsProvider, useActivityParams } from '../../hooks';
import { MiniappSheetHeader } from '@/components/ecosystem';
import { PatternLock, patternToString } from '@/components/security/pattern-lock';
import {
type CryptoAction,
type TokenDuration,
CRYPTO_ACTION_LABELS,
TOKEN_DURATION_LABELS,
TOKEN_DURATION_OPTIONS,
} from '@/services/crypto-box';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { walletStore } from '@/stores';
import { superjson } from '@biochain/chain-effect';
import { verifyCryptoAuthorizePattern } from './crypto-authorize-pattern';
type CryptoAuthorizeJobParams = {
/** 请求的操作权限 (superjson 字符串) */
actions: string;
/** 授权时长 */
duration: string;
/** 使用的地址 */
address: string;
/** 链 ID */
chainId?: string;
/** 请求来源小程序名称 */
appName?: string;
/** 请求来源小程序图标 */
appIcon?: string;
};
function CryptoAuthorizeJobContent() {
const { t } = useTranslation('common');
const { pop } = useFlow();
const params = useActivityParams<CryptoAuthorizeJobParams>();
const actions = superjson.parse<CryptoAction[]>(params.actions);
const duration = params.duration as TokenDuration;
const { address, chainId, appName, appIcon } = params;
// 找到使用该地址的钱包
const targetWallet = walletStore.state.wallets.find((w) => w.chainAddresses.some((ca) => ca.address === address));
const walletName = targetWallet?.name;
const walletId = targetWallet?.id;
const [pattern, setPattern] = useState<number[]>([]);
const [error, setError] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
const [selectedDuration, setSelectedDuration] = useState<TokenDuration>(duration);
const handlePatternComplete = useCallback(
async (nodes: number[]) => {
setIsVerifying(true);
setError(false);
try {
const patternKey = patternToString(nodes);
// 验证手势密码必须匹配当前目标钱包,避免“任意钱包可解锁”导致后续执行失败
const isValid = await verifyCryptoAuthorizePattern(walletId, patternKey);
if (isValid && walletId) {
// 发送成功事件(包含 walletId 和 selectedDuration 用于 Token 创建)
const event = new CustomEvent('crypto-authorize-confirm', {
detail: { approved: true, patternKey, walletId, selectedDuration },
});
window.dispatchEvent(event);
pop();
} else {
setError(true);
setPattern([]);
}
} catch {
setError(true);
setPattern([]);
} finally {
setIsVerifying(false);
}
},
[pop, selectedDuration],
);
const handleCancel = useCallback(() => {
const event = new CustomEvent('crypto-authorize-confirm', {
detail: { approved: false },
});
window.dispatchEvent(event);
pop();
}, [pop]);
return (
<BottomSheet onCancel={handleCancel}>
<div className="bg-background flex max-h-[85vh] flex-col rounded-t-2xl">
{/* Handle */}
<div className="flex flex-shrink-0 justify-center py-2">
<div className="bg-muted h-1 w-10 rounded-full" />
</div>
{/* Header - 左侧 miniapp 信息,右侧钱包信息 */}
<MiniappSheetHeader
title={t('cryptoAuthorize')}
description={appName || t('unknownDApp')}
appName={appName}
appIcon={appIcon}
walletInfo={{
name: walletName || t('unknownWallet'),
address,
chainId: chainId || 'bfmeta',
}}
/>
{/* Content - 可滚动区域 */}
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3">
{/* 权限和授权时长 */}
<div className="bg-muted/50 space-y-2 rounded-xl p-3 text-sm">
{/* 请求权限 - 水平排列 */}
<div className="flex items-center gap-2">
<IconLock className="text-primary size-4 flex-shrink-0" />
<span className="text-muted-foreground">{t('permissions')}:</span>
<span className="truncate font-medium">
{actions.map((a) => CRYPTO_ACTION_LABELS[a]?.name || a).join('、')}
</span>
</div>
{/* 授权时长 */}
<div className="flex items-center gap-2">
<span className="text-muted-foreground ml-6">{t('duration')}:</span>
<Select value={selectedDuration} onValueChange={(value) => setSelectedDuration(value as TokenDuration)}>
<SelectTrigger className="h-7 w-24 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TOKEN_DURATION_OPTIONS.map((option) => (
<SelectItem key={option} value={option}>
{TOKEN_DURATION_LABELS[option]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 手势密码区域 - 固定尺寸不可压缩 */}
<div className="flex-shrink-0 px-4 pb-2">
<p className="text-muted-foreground mb-2 text-center text-xs">{t('drawPatternToConfirm')}</p>
{/* 固定尺寸容器,防止内容变化导致布局抖动 */}
<div className="flex justify-center">
<div className="w-[280px]">
<PatternLock
value={pattern}
onChange={setPattern}
onComplete={handlePatternComplete}
error={error}
disabled={isVerifying}
/>
</div>
</div>
{/* 验证中状态 - 固定高度 */}
<div className="flex h-6 items-center justify-center">
{isVerifying && (
<div className="text-muted-foreground flex items-center gap-2">
<IconLoader2 className="size-4 animate-spin" />
<span className="text-xs">{t('verifying')}</span>
</div>
)}
</div>
</div>
{/* Cancel button */}
<div className="flex-shrink-0 px-4 pb-2">
<button
onClick={handleCancel}
disabled={isVerifying}
className="bg-muted hover:bg-muted/80 w-full rounded-xl py-2.5 text-sm font-medium transition-colors disabled:opacity-50"
>
{t('cancel')}
</button>
</div>
{/* Safe area */}
<div className="h-[env(safe-area-inset-bottom)] flex-shrink-0" />
</div>
</BottomSheet>
);
}
export const CryptoAuthorizeJob: ActivityComponentType<CryptoAuthorizeJobParams> = ({ params }) => {
return (
<ActivityParamsProvider params={params}>
<CryptoAuthorizeJobContent />
</ActivityParamsProvider>
);
};