Skip to content

Commit 92eb852

Browse files
authored
fix(miniapp): unify gesture service feedback for transfer/sign (#485)
* fix(miniapp): unify gesture service feedback for transfer/sign * ci: fallback to standard checks when self-hosted fast path fails * fix(ci): skip mkcert https setup in CI miniapp tests * fix(ci): allow checks-standard to run after fast-path failure
1 parent 59dda81 commit 92eb852

9 files changed

Lines changed: 309 additions & 50 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ jobs:
7272

7373
# ==================== GitHub-hosted 标准链路 ====================
7474
ci-standard:
75-
if: vars.USE_SELF_HOSTED != 'true'
75+
if: always() && (vars.USE_SELF_HOSTED != 'true' || needs.ci-fast.result != 'success')
76+
needs: [ci-fast]
7677
runs-on: ubuntu-latest
7778
timeout-minutes: 15
7879
steps:
@@ -131,7 +132,7 @@ jobs:
131132
fi
132133
133134
checks-standard:
134-
if: vars.USE_SELF_HOSTED != 'true'
135+
if: always() && needs.ci-standard.result == 'success'
135136
needs: ci-standard
136137
runs-on: ubuntu-latest
137138
steps:

miniapps/biobridge/vite.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs'
88

99
const E2E_SCREENSHOTS_DIR = resolve(__dirname, '../../e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts')
1010
const MANIFEST_PATH = resolve(__dirname, 'manifest.json')
11+
const ENABLE_HTTPS_DEV = !process.env.CI
1112

1213
function getShortId(): string {
1314
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'))
@@ -51,7 +52,7 @@ function miniappPlugin(): Plugin {
5152

5253
export default defineConfig({
5354
plugins: [
54-
mkcert(),
55+
...(ENABLE_HTTPS_DEV ? [mkcert()] : []),
5556
react(),
5657
tsconfigPaths(),
5758
tailwindcss(),
@@ -63,7 +64,7 @@ export default defineConfig({
6364
emptyOutDir: true,
6465
},
6566
server: {
66-
https: true,
67+
https: ENABLE_HTTPS_DEV,
6768
port: 5184,
6869
fs: {
6970
allow: [resolve(__dirname, '../..')],

miniapps/teleport/vite.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { existsSync, readFileSync, readdirSync } from 'fs'
88

99
const E2E_SCREENSHOTS_DIR = resolve(__dirname, '../../e2e/__screenshots__/Desktop-Chrome/miniapp-ui.mock.spec.ts')
1010
const MANIFEST_PATH = resolve(__dirname, 'manifest.json')
11+
const ENABLE_HTTPS_DEV = !process.env.CI
1112

1213
function getShortId(): string {
1314
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'))
@@ -51,7 +52,7 @@ function miniappPlugin(): Plugin {
5152

5253
export default defineConfig({
5354
plugins: [
54-
mkcert(),
55+
...(ENABLE_HTTPS_DEV ? [mkcert()] : []),
5556
react(),
5657
tsconfigPaths(),
5758
tailwindcss(),
@@ -63,7 +64,7 @@ export default defineConfig({
6364
emptyOutDir: true,
6465
},
6566
server: {
66-
https: true,
67+
https: ENABLE_HTTPS_DEV,
6768
port: 5185,
6869
fs: {
6970
allow: [resolve(__dirname, '../..')],

src/components/security/pattern-lock.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ export interface PatternLockProps {
1919
success?: boolean;
2020
/** 错误提示文案(可选,覆盖默认错误文案) */
2121
errorText?: string;
22+
/** 状态提示文案(可选,覆盖默认提示文案) */
23+
hintText?: string;
24+
/** 状态提示语气(仅在 hintText 生效时使用) */
25+
hintTone?: 'default' | 'destructive' | 'primary';
26+
/** 底部详情文案(可选) */
27+
footerText?: string;
2228
/** 额外的 className */
2329
className?: string;
2430
/** 网格大小 (默认 3x3) */
@@ -51,6 +57,9 @@ export function PatternLock({
5157
error = false,
5258
success = false,
5359
errorText,
60+
hintText,
61+
hintTone = 'default',
62+
footerText,
5463
className,
5564
size = 3,
5665
'data-testid': testId,
@@ -504,6 +513,19 @@ export function PatternLock({
504513
<p className="text-destructive text-sm">
505514
{errorText ?? t('patternLock.error')}
506515
</p>
516+
) : hintText ? (
517+
<p
518+
className={cn(
519+
'text-sm',
520+
hintTone === 'destructive'
521+
? 'text-destructive'
522+
: hintTone === 'primary'
523+
? 'text-primary'
524+
: 'text-muted-foreground',
525+
)}
526+
>
527+
{hintText}
528+
</p>
507529
) : selectedNodes.length === 0 ? (
508530
<p className="text-muted-foreground text-sm">
509531
{t('patternLock.hint', { min: minPoints })}
@@ -525,7 +547,9 @@ export function PatternLock({
525547

526548
{/* 清除按钮 - 固定高度避免布局抖动 */}
527549
<div className="h-5">
528-
{selectedNodes.length > 0 && !disabled && !isErrorAnimating && (
550+
{footerText ? (
551+
<p className="text-destructive/80 truncate text-left text-xs">{footerText}</p>
552+
) : selectedNodes.length > 0 && !disabled && !isErrorAnimating ? (
529553
<button
530554
type="button"
531555
onClick={handleClear}
@@ -534,7 +558,7 @@ export function PatternLock({
534558
>
535559
{t('patternLock.clear')}
536560
</button>
537-
)}
561+
) : null}
538562
</div>
539563
</div>
540564
);

src/stackflow/activities/sheets/MiniappConfirmJobs.regression.test.tsx

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,26 @@ vi.mock('@/components/wallet/chain-address-display', () => ({
7979
}));
8080

8181
vi.mock('@/components/security/pattern-lock', () => ({
82-
PatternLock: ({ onComplete }: { onComplete?: (nodes: number[]) => void }) => (
83-
<button
84-
type="button"
85-
data-testid="pattern-lock"
86-
onClick={() => onComplete?.([1, 2, 3, 4])}
87-
>
88-
pattern
89-
</button>
82+
PatternLock: ({
83+
onComplete,
84+
hintText,
85+
footerText,
86+
}: {
87+
onComplete?: (nodes: number[]) => void;
88+
hintText?: string;
89+
footerText?: string;
90+
}) => (
91+
<div>
92+
<button
93+
type="button"
94+
data-testid="pattern-lock"
95+
onClick={() => onComplete?.([1, 2, 3, 4])}
96+
>
97+
pattern
98+
</button>
99+
<span data-testid="pattern-lock-hint">{hintText ?? ''}</span>
100+
<span data-testid="pattern-lock-footer">{footerText ?? ''}</span>
101+
</div>
90102
),
91103
patternToString: (nodes: number[]) => nodes.join(''),
92104
}));
@@ -227,6 +239,39 @@ describe('miniapp confirm jobs regressions', () => {
227239
expect(screen.getByTestId('miniapp-sheet-header')).toBeInTheDocument();
228240
});
229241

242+
it('shows sign service feedback in pattern hint and detail footer', async () => {
243+
vi.mocked(signUnsignedTransaction).mockRejectedValueOnce(new Error('sign service timeout'));
244+
245+
render(
246+
<MiniappSignTransactionJob
247+
params={{
248+
appName: 'Org App',
249+
appIcon: '',
250+
from: 'b_sender_1',
251+
chain: 'BFMetaV2',
252+
unsignedTx: superjson.stringify({
253+
chainId: 'bfmetav2',
254+
intentType: 'transfer',
255+
data: { tx: 'unsigned' },
256+
}),
257+
}}
258+
/>,
259+
);
260+
261+
const signButton = screen.getByTestId('miniapp-sign-review-confirm');
262+
await waitFor(() => {
263+
expect(signButton).not.toBeDisabled();
264+
});
265+
266+
fireEvent.click(signButton);
267+
fireEvent.click(screen.getByTestId('pattern-lock'));
268+
269+
await waitFor(() => {
270+
expect(screen.getByTestId('pattern-lock-hint').textContent?.length).toBeGreaterThan(0);
271+
});
272+
expect(screen.getByTestId('pattern-lock-footer').textContent).toContain('sign service timeout');
273+
});
274+
230275
it('does not pass raw amount directly to display layer', () => {
231276
render(
232277
<MiniappTransferConfirmJob
@@ -1047,7 +1092,10 @@ describe('miniapp confirm jobs regressions', () => {
10471092
fireEvent.click(confirmButton);
10481093
fireEvent.click(screen.getByTestId('pattern-lock'));
10491094

1050-
expect(await screen.findByTestId('miniapp-transfer-error')).toBeInTheDocument();
1095+
await waitFor(() => {
1096+
expect(screen.getByTestId('pattern-lock-hint').textContent?.length).toBeGreaterThan(0);
1097+
});
1098+
expect(screen.getByTestId('pattern-lock-footer').textContent).toContain('Request timeout');
10511099
await waitFor(() => {
10521100
expect(screen.queryByTestId('miniapp-transfer-broadcasting-status')).not.toBeInTheDocument();
10531101
});

src/stackflow/activities/sheets/MiniappSignTransactionJob.tsx

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ type MiniappSignTransactionJobParams = {
4444

4545
type SignStep = MiniappSignFlowStep;
4646

47+
function collectErrorMessages(error: unknown): string[] {
48+
const messages: string[] = [];
49+
const visited = new Set<unknown>();
50+
let current: unknown = error;
51+
52+
while (current instanceof Error && !visited.has(current)) {
53+
visited.add(current);
54+
if (current.message) {
55+
messages.push(current.message);
56+
}
57+
current = (current as Error & { cause?: unknown }).cause;
58+
}
59+
60+
return messages;
61+
}
62+
63+
function extractSignErrorDetail(error: unknown): string | null {
64+
for (const message of collectErrorMessages(error)) {
65+
const normalized = message.trim();
66+
if (normalized.length > 0) {
67+
return normalized;
68+
}
69+
}
70+
return null;
71+
}
72+
4773
function MiniappSignTransactionJobContent() {
4874
const { t } = useTranslation('common');
4975
const { pop } = useFlow();
@@ -56,6 +82,7 @@ function MiniappSignTransactionJobContent() {
5682
const [patternError, setPatternError] = useState(false);
5783
const [twoStepSecretError, setTwoStepSecretError] = useState(false);
5884
const [errorMessage, setErrorMessage] = useState<string | null>(null);
85+
const [errorDetail, setErrorDetail] = useState<string | null>(null);
5986
const [isSubmitting, setIsSubmitting] = useState(false);
6087
const [requiresTwoStepSecret, setRequiresTwoStepSecret] = useState(false);
6188
const [isResolvingTwoStepSecret, setIsResolvingTwoStepSecret] = useState(true);
@@ -85,6 +112,7 @@ function MiniappSignTransactionJobContent() {
85112
setTwoStepSecret('');
86113
setTwoStepSecretError(false);
87114
setErrorMessage(null);
115+
setErrorDetail(null);
88116
}, []);
89117

90118
useEffect(() => {
@@ -120,25 +148,27 @@ function MiniappSignTransactionJobContent() {
120148

121149
const handlePatternChange = useCallback(
122150
(nextPattern: number[]) => {
123-
if (patternError || errorMessage || twoStepSecretError) {
151+
if (patternError || errorMessage || twoStepSecretError || errorDetail) {
124152
setPatternError(false);
125153
setTwoStepSecretError(false);
126154
setErrorMessage(null);
155+
setErrorDetail(null);
127156
}
128157
setPattern(nextPattern);
129158
},
130-
[patternError, errorMessage, twoStepSecretError],
159+
[patternError, errorMessage, twoStepSecretError, errorDetail],
131160
);
132161

133162
const handleTwoStepSecretChange = useCallback(
134163
(value: string) => {
135-
if (twoStepSecretError || errorMessage) {
164+
if (twoStepSecretError || errorMessage || errorDetail) {
136165
setTwoStepSecretError(false);
137166
setErrorMessage(null);
167+
setErrorDetail(null);
138168
}
139169
setTwoStepSecret(value);
140170
},
141-
[twoStepSecretError, errorMessage],
171+
[twoStepSecretError, errorMessage, errorDetail],
142172
);
143173

144174
const performSign = useCallback(
@@ -180,6 +210,7 @@ function MiniappSignTransactionJobContent() {
180210
setPatternError(false);
181211
setTwoStepSecretError(false);
182212
setErrorMessage(null);
213+
setErrorDetail(null);
183214

184215
try {
185216
setStep('signing');
@@ -189,15 +220,20 @@ function MiniappSignTransactionJobContent() {
189220
if (isMiniappWalletLockError(error)) {
190221
setPatternError(true);
191222
setErrorMessage(t('walletLock.error'));
223+
setErrorDetail(null);
224+
setStep('wallet_lock');
192225
} else if (isMiniappTwoStepSecretError(error)) {
193226
setPatternError(false);
194227
setTwoStepSecretError(true);
195228
setErrorMessage(t('transaction:sendPage.twoStepSecretError'));
229+
setErrorDetail(null);
196230
setStep('two_step_secret');
197231
} else {
198232
setPatternError(false);
199233
setTwoStepSecretError(false);
200234
setErrorMessage(t('signingFailed'));
235+
setErrorDetail(extractSignErrorDetail(error));
236+
setStep('wallet_lock');
201237
}
202238
setPattern([]);
203239
} finally {
@@ -216,6 +252,7 @@ function MiniappSignTransactionJobContent() {
216252
setIsSubmitting(true);
217253
setTwoStepSecretError(false);
218254
setErrorMessage(null);
255+
setErrorDetail(null);
219256

220257
try {
221258
setStep('signing');
@@ -225,14 +262,17 @@ function MiniappSignTransactionJobContent() {
225262
if (isMiniappTwoStepSecretError(error)) {
226263
setTwoStepSecretError(true);
227264
setErrorMessage(t('transaction:sendPage.twoStepSecretError'));
265+
setErrorDetail(null);
228266
setStep('two_step_secret');
229267
} else if (isMiniappWalletLockError(error)) {
230268
setPatternError(true);
231269
setErrorMessage(t('walletLock.error'));
270+
setErrorDetail(null);
232271
setStep('wallet_lock');
233272
} else {
234273
setTwoStepSecretError(false);
235274
setErrorMessage(t('signingFailed'));
275+
setErrorDetail(extractSignErrorDetail(error));
236276
setStep('two_step_secret');
237277
}
238278
} finally {
@@ -261,6 +301,9 @@ function MiniappSignTransactionJobContent() {
261301
}
262302
}, [unsignedTx]);
263303

304+
const walletLockServiceMessage = !patternError && errorMessage ? errorMessage : null;
305+
const walletLockErrorDetail = !patternError ? errorDetail : null;
306+
264307
return (
265308
<BottomSheet onCancel={handleCancel}>
266309
<div className="bg-background rounded-t-2xl">
@@ -343,6 +386,7 @@ function MiniappSignTransactionJobContent() {
343386
{t('cancel')}
344387
</button>
345388
<button
389+
data-testid="miniapp-sign-review-confirm"
346390
onClick={handleEnterWalletLockStep}
347391
disabled={isSubmitting || !unsignedTx || !walletId || isResolvingTwoStepSecret}
348392
className={cn(
@@ -372,12 +416,15 @@ function MiniappSignTransactionJobContent() {
372416
disabled={isSubmitting || !walletId}
373417
error={patternError}
374418
errorText={patternError ? t('walletLock.error') : undefined}
419+
hintText={walletLockServiceMessage ?? undefined}
420+
hintTone={walletLockServiceMessage ? 'destructive' : 'default'}
421+
footerText={
422+
walletLockErrorDetail
423+
? `${t('common:service.technicalDetails')}: ${walletLockErrorDetail}`
424+
: undefined
425+
}
375426
/>
376427

377-
{errorMessage && !patternError && (
378-
<div className="bg-destructive/10 text-destructive rounded-xl p-3 text-sm">{errorMessage}</div>
379-
)}
380-
381428
{isSubmitting && (
382429
<div className="bg-muted text-muted-foreground flex items-center justify-center gap-2 rounded-xl p-3 text-sm">
383430
<IconLoader2 className="size-4 animate-spin" />

0 commit comments

Comments
 (0)