Skip to content

Commit 27d9c23

Browse files
authored
Merge pull request #429 from BioforestChain/fix/miniapp-single-sheet-flow-v2
fix(miniapp): single-sheet transfer/sign flow + release v0.10.2
2 parents b337ba7 + 3c2253b commit 27d9c23

5 files changed

Lines changed: 437 additions & 231 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 更新日志
22

3+
## [0.10.2] - 2026-02-08
4+
5+
miniapp 转账/签名交易改为单弹窗多步骤流程
6+
7+
<!-- last-commit: b835a08357f30a053e6aa2943c0391aa9dc299c5 -->
8+
39
## [0.10.1] - 2026-02-08
410

511
Fix miniapp transfer tx object + FIFO sheets

manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
"author": [
1919
"@bfmeta.info"
2020
],
21-
"version": "0.10.1",
22-
"change_log": "Fix miniapp transfer tx object + FIFO sheets",
21+
"version": "0.10.2",
22+
"change_log": "miniapp 转账/签名交易改为单弹窗多步骤流程",
2323
"categories": [
2424
"application",
2525
"wallet"

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@biochain/keyapp",
33
"private": true,
4-
"version": "0.10.1",
4+
"version": "0.10.2",
55
"type": "module",
66
"packageManager": "pnpm@10.28.0",
77
"scripts": {
@@ -214,5 +214,5 @@
214214
"packages/*",
215215
"miniapps/*"
216216
],
217-
"lastChangelogCommit": "666f7eb19a259e8677c0b04945d4d6843ca7290b"
217+
"lastChangelogCommit": "b835a08357f30a053e6aa2943c0391aa9dc299c5"
218218
}

src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx

Lines changed: 177 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { cn } from '@/lib/utils';
1111
import { IconAlertTriangle, IconLoader2 } from '@tabler/icons-react';
1212
import { useFlow } from '../../stackflow';
1313
import { ActivityParamsProvider, useActivityParams } from '../../hooks';
14-
import { setWalletLockConfirmCallback } from './WalletLockConfirmJob';
1514
import { walletStore } from '@/stores';
1615
import { findMiniappWalletIdByAddress, resolveMiniappChainId } from './miniapp-wallet';
1716
import type { UnsignedTransaction } from '@/services/ecosystem';
@@ -20,6 +19,8 @@ import { signUnsignedTransaction } from '@/services/ecosystem/handlers';
2019
import { MiniappSheetHeader } from '@/components/ecosystem';
2120
import { ChainBadge } from '@/components/wallet/chain-icon';
2221
import { ChainAddressDisplay } from '@/components/wallet/chain-address-display';
22+
import { PatternLock, patternToString } from '@/components/security/pattern-lock';
23+
import { WalletStorageError, WalletStorageErrorCode } from '@/services/wallet-storage/types';
2324

2425
type MiniappSignTransactionJobParams = {
2526
/** 来源小程序名称 */
@@ -34,12 +35,32 @@ type MiniappSignTransactionJobParams = {
3435
unsignedTx: string;
3536
};
3637

38+
type SignStep = 'review' | 'wallet_lock';
39+
40+
function isWalletLockError(error: unknown): boolean {
41+
if (error instanceof WalletStorageError) {
42+
return (
43+
error.code === WalletStorageErrorCode.DECRYPTION_FAILED || error.code === WalletStorageErrorCode.INVALID_PASSWORD
44+
);
45+
}
46+
47+
if (error instanceof Error) {
48+
return /wrong password|decrypt mnemonic|invalid walletlock|wallet lock/i.test(error.message);
49+
}
50+
51+
return false;
52+
}
53+
3754
function MiniappSignTransactionJobContent() {
3855
const { t } = useTranslation('common');
39-
const { pop, push } = useFlow();
56+
const { pop } = useFlow();
4057
const params = useActivityParams<MiniappSignTransactionJobParams>();
4158
const { appName, appIcon, from, chain, unsignedTx: unsignedTxJson } = params;
4259

60+
const [step, setStep] = useState<SignStep>('review');
61+
const [pattern, setPattern] = useState<number[]>([]);
62+
const [patternError, setPatternError] = useState(false);
63+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
4364
const [isSubmitting, setIsSubmitting] = useState(false);
4465

4566
const unsignedTx = useMemo((): UnsignedTransaction | null => {
@@ -56,16 +77,49 @@ function MiniappSignTransactionJobContent() {
5677
return findMiniappWalletIdByAddress(resolvedChainId, from);
5778
}, [resolvedChainId, from]);
5879

59-
const targetWallet = walletStore.state.wallets.find((w) => w.id === walletId);
80+
const targetWallet = walletStore.state.wallets.find((wallet) => wallet.id === walletId);
6081
const walletName = targetWallet?.name || t('unknownWallet');
6182

62-
const handleConfirm = useCallback(() => {
83+
const resetWalletLockStepState = useCallback(() => {
84+
setPattern([]);
85+
setPatternError(false);
86+
setErrorMessage(null);
87+
}, []);
88+
89+
const handleEnterWalletLockStep = useCallback(() => {
90+
if (isSubmitting || !unsignedTx || !walletId) return;
91+
resetWalletLockStepState();
92+
setStep('wallet_lock');
93+
}, [isSubmitting, unsignedTx, walletId, resetWalletLockStepState]);
94+
95+
const handleBackToReview = useCallback(() => {
6396
if (isSubmitting) return;
64-
if (!unsignedTx) return;
65-
if (!walletId) return;
97+
resetWalletLockStepState();
98+
setStep('review');
99+
}, [isSubmitting, resetWalletLockStepState]);
66100

67-
setWalletLockConfirmCallback(async (password: string) => {
101+
const handlePatternChange = useCallback(
102+
(nextPattern: number[]) => {
103+
if (patternError || errorMessage) {
104+
setPatternError(false);
105+
setErrorMessage(null);
106+
}
107+
setPattern(nextPattern);
108+
},
109+
[patternError, errorMessage],
110+
);
111+
112+
const handlePatternComplete = useCallback(
113+
async (nodes: number[]) => {
114+
if (nodes.length < 4 || isSubmitting || !unsignedTx || !walletId) {
115+
return;
116+
}
117+
118+
const password = patternToString(nodes);
68119
setIsSubmitting(true);
120+
setPatternError(false);
121+
setErrorMessage(null);
122+
69123
try {
70124
const signedTx = await signUnsignedTransaction({
71125
walletId,
@@ -84,25 +138,22 @@ function MiniappSignTransactionJobContent() {
84138
window.dispatchEvent(event);
85139

86140
pop();
87-
return true;
88141
} catch (error) {
89-
console.error("[miniapp-sign-transaction]", error);
90-
throw error instanceof Error ? error : new Error("Sign transaction failed");
142+
console.error('[miniapp-sign-transaction]', error);
143+
if (isWalletLockError(error)) {
144+
setPatternError(true);
145+
setErrorMessage(t('walletLock.error'));
146+
} else {
147+
setPatternError(false);
148+
setErrorMessage(error instanceof Error ? error.message : t('walletLock.error'));
149+
}
150+
setPattern([]);
91151
} finally {
92152
setIsSubmitting(false);
93153
}
94-
});
95-
96-
push('WalletLockConfirmJob', {
97-
title: t('signTransaction'),
98-
description: appName || t('unknownDApp'),
99-
miniappName: appName,
100-
miniappIcon: appIcon,
101-
walletName,
102-
walletAddress: from,
103-
walletChainId: resolvedChainId,
104-
});
105-
}, [appIcon, appName, from, isSubmitting, pop, push, resolvedChainId, t, unsignedTx, walletId, walletName]);
154+
},
155+
[isSubmitting, unsignedTx, walletId, from, resolvedChainId, pop, t, tSecurity],
156+
);
106157

107158
const handleCancel = useCallback(() => {
108159
const event = new CustomEvent('miniapp-sign-transaction-confirm', {
@@ -130,7 +181,7 @@ function MiniappSignTransactionJobContent() {
130181

131182
<MiniappSheetHeader
132183
title={t('signTransaction')}
133-
description={appName || t('unknownDApp')}
184+
description={step === 'review' ? appName || t('unknownDApp') : t('drawPatternToConfirm')}
134185
appName={appName}
135186
appIcon={appIcon}
136187
walletInfo={{
@@ -140,67 +191,114 @@ function MiniappSignTransactionJobContent() {
140191
}}
141192
/>
142193

143-
<div className="space-y-4 p-4">
144-
{!unsignedTx && (
145-
<div className="bg-destructive/10 text-destructive rounded-xl p-3 text-sm">{t('invalidTransaction')}</div>
146-
)}
194+
{step === 'review' ? (
195+
<>
196+
<div className="space-y-4 p-4">
197+
{!unsignedTx && (
198+
<div className="bg-destructive/10 text-destructive rounded-xl p-3 text-sm">
199+
{t('invalidTransaction')}
200+
</div>
201+
)}
202+
203+
{unsignedTx && !walletId && (
204+
<div className="rounded-xl bg-amber-50 p-3 text-sm text-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
205+
{t('signingAddressNotFound')}
206+
</div>
207+
)}
208+
209+
<div className="bg-muted/50 rounded-xl p-3">
210+
<p className="text-muted-foreground mb-1 text-xs">{t('network')}</p>
211+
<ChainBadge chainId={resolvedChainId} />
212+
</div>
213+
214+
<div className="bg-muted/50 rounded-xl p-3">
215+
<p className="text-muted-foreground mb-1 text-xs">{t('signingAddress')}</p>
216+
<ChainAddressDisplay chainId={resolvedChainId} address={from} copyable={false} size="sm" />
217+
</div>
218+
219+
<div className="bg-muted/50 rounded-xl p-3">
220+
<p className="text-muted-foreground mb-1 text-xs">{t('transaction')}</p>
221+
<div className="max-h-44 overflow-y-auto">
222+
<pre className="font-mono text-xs break-all whitespace-pre-wrap">{rawPreview}</pre>
223+
</div>
224+
</div>
147225

148-
{unsignedTx && !walletId && (
149-
<div className="rounded-xl bg-amber-50 p-3 text-sm text-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
150-
{t('signingAddressNotFound')}
226+
<div className="flex items-start gap-2 rounded-xl bg-amber-50 p-3 dark:bg-amber-950/30">
227+
<IconAlertTriangle className="mt-0.5 size-5 shrink-0 text-amber-600" />
228+
<p className="text-sm text-amber-800 dark:text-amber-200">{t('signTxWarning')}</p>
229+
</div>
151230
</div>
152-
)}
153-
154-
<div className="bg-muted/50 rounded-xl p-3">
155-
<p className="text-muted-foreground mb-1 text-xs">{t('network')}</p>
156-
<ChainBadge chainId={resolvedChainId} />
157-
</div>
158-
159-
<div className="bg-muted/50 rounded-xl p-3">
160-
<p className="text-muted-foreground mb-1 text-xs">{t('signingAddress')}</p>
161-
<ChainAddressDisplay chainId={resolvedChainId} address={from} copyable={false} size="sm" />
162-
</div>
163-
164-
<div className="bg-muted/50 rounded-xl p-3">
165-
<p className="text-muted-foreground mb-1 text-xs">{t('transaction')}</p>
166-
<div className="max-h-44 overflow-y-auto">
167-
<pre className="font-mono text-xs break-all whitespace-pre-wrap">{rawPreview}</pre>
231+
232+
<div className="flex gap-3 p-4">
233+
<button
234+
onClick={handleCancel}
235+
disabled={isSubmitting}
236+
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors disabled:opacity-50"
237+
>
238+
{t('cancel')}
239+
</button>
240+
<button
241+
onClick={handleEnterWalletLockStep}
242+
disabled={isSubmitting || !unsignedTx || !walletId}
243+
className={cn(
244+
'flex-1 rounded-xl py-3 font-medium transition-colors',
245+
'bg-primary text-primary-foreground hover:bg-primary/90',
246+
'flex items-center justify-center gap-2 disabled:opacity-50',
247+
)}
248+
>
249+
{t('sign')}
250+
</button>
168251
</div>
169-
</div>
252+
</>
253+
) : (
254+
<>
255+
<div className="space-y-4 p-4">
256+
{!walletId && (
257+
<div className="rounded-xl bg-amber-50 p-3 text-sm text-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
258+
{t('signingAddressNotFound')}
259+
</div>
260+
)}
170261

171-
<div className="flex items-start gap-2 rounded-xl bg-amber-50 p-3 dark:bg-amber-950/30">
172-
<IconAlertTriangle className="mt-0.5 size-5 shrink-0 text-amber-600" />
173-
<p className="text-sm text-amber-800 dark:text-amber-200">{t('signTxWarning')}</p>
174-
</div>
175-
</div>
262+
<PatternLock
263+
value={pattern}
264+
onChange={handlePatternChange}
265+
onComplete={handlePatternComplete}
266+
minPoints={4}
267+
disabled={isSubmitting || !walletId}
268+
error={patternError}
269+
errorText={patternError ? t('walletLock.error') : undefined}
270+
/>
176271

177-
<div className="flex gap-3 p-4">
178-
<button
179-
onClick={handleCancel}
180-
disabled={isSubmitting}
181-
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors disabled:opacity-50"
182-
>
183-
{t('cancel')}
184-
</button>
185-
<button
186-
onClick={handleConfirm}
187-
disabled={isSubmitting || !unsignedTx || !walletId}
188-
className={cn(
189-
'flex-1 rounded-xl py-3 font-medium transition-colors',
190-
'bg-primary text-primary-foreground hover:bg-primary/90',
191-
'flex items-center justify-center gap-2 disabled:opacity-50',
192-
)}
193-
>
194-
{isSubmitting ? (
195-
<>
196-
<IconLoader2 className="size-4 animate-spin" />
197-
{t('signing')}
198-
</>
199-
) : (
200-
t('sign')
201-
)}
202-
</button>
203-
</div>
272+
{errorMessage && !patternError && (
273+
<div className="bg-destructive/10 text-destructive rounded-xl p-3 text-sm">{errorMessage}</div>
274+
)}
275+
276+
{isSubmitting && (
277+
<div className="bg-muted text-muted-foreground flex items-center justify-center gap-2 rounded-xl p-3 text-sm">
278+
<IconLoader2 className="size-4 animate-spin" />
279+
{t('signing')}
280+
</div>
281+
)}
282+
</div>
283+
284+
<div className="flex gap-3 p-4">
285+
<button
286+
onClick={handleBackToReview}
287+
disabled={isSubmitting}
288+
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors disabled:opacity-50"
289+
>
290+
{t('back')}
291+
</button>
292+
<button
293+
onClick={handleCancel}
294+
disabled={isSubmitting}
295+
className="bg-muted hover:bg-muted/80 flex-1 rounded-xl py-3 font-medium transition-colors disabled:opacity-50"
296+
>
297+
{t('cancel')}
298+
</button>
299+
</div>
300+
</>
301+
)}
204302

205303
<div className="h-[env(safe-area-inset-bottom)]" />
206304
</div>

0 commit comments

Comments
 (0)