fix(client/payment): switchMethod 补 catch-all 兜网络/超时错(与 start/resume 对齐)防卡 awaitingPayment

switchMethod 的首个 retry() 调用只 catch AuthApiException,网络/超时/未知异常会穿出到
无 try/catch 的 _pickAndSwitch,UI 卡在 awaitingPayment 无任何提示。补一个 catch-all,
与 start()/resume() 同一处理形状落 failed 相位(自动经 build() 切到 _failed() 呈现);
payment_page.dart 的 _pickAndSwitch 再加一层防御性 try/catch + toast,防任何逃逸异常
变成无提示的 unhandled rejection。补单测覆盖非 Auth 异常路径。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
This commit is contained in:
wangjia
2026-07-13 15:53:30 +08:00
parent 3acd3ca59d
commit 301e13f477
3 changed files with 31 additions and 1 deletions
+8 -1
View File
@@ -245,9 +245,16 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
]);
}
// switchMethod 内部已把网络/超时/未知异常兜到 failed 相位(与 start/resume 对齐,
// 会经 build() 的 phase 分支自动切到 _failed() 呈现)——这里的 try/catch 是最后一道
// 防线,防止任何逃逸异常变成无提示的 unhandled rejection(照抄 _openRedirectUrl 的写法)。
Future<void> _pickAndSwitch(BuildContext context, WidgetRef ref, PaymentFlowState s) async {
final other = s.method == 'crypto' ? 'nezha' : 'crypto';
await ref.read(paymentFlowProvider.notifier).switchMethod(other);
try {
await ref.read(paymentFlowProvider.notifier).switchMethod(other);
} catch (_) {
if (context.mounted) showPangolinToast(context, t.orderCreateFailed);
}
}
Widget _succeeded(BuildContext context, PaymentFlowState s) {
+9
View File
@@ -169,6 +169,15 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
return;
}
state = state.copyWith(phase: PaymentPhase.failed, errorZh: e.messageZh, errorEn: e.messageEn);
} catch (_) {
// 网络/超时等非 Auth 异常也要落 failed —— 与 start()/resume() 同一形状,否则
// 穿出到无 try/catch 的 _pickAndSwitch,UI 卡在 awaitingPayment 无提示。
if (!mounted) return;
state = state.copyWith(
phase: PaymentPhase.failed,
errorZh: '下单失败,请检查网络后重试',
errorEn: 'Failed to create order, please check your network and retry',
);
}
}
+14
View File
@@ -105,6 +105,20 @@ void main() {
expect(c.read(paymentFlowProvider).order?.orderNo, 'pay2');
});
test('switchMethod 遇非 Auth 异常(网络/超时)→ 落 failed 兜底,不卡 awaitingPayment', () async {
final api = _FakePaymentApi();
final c = _container(api);
final ctl = c.read(paymentFlowProvider.notifier);
await ctl.start(item, 'crypto');
api.retryError = Exception('network timeout');
await ctl.switchMethod('alipay');
expect(c.read(paymentFlowProvider).phase, PaymentPhase.failed);
expect(c.read(paymentFlowProvider).errorZh, isNotNull);
expect(c.read(paymentFlowProvider).errorZh, isNotEmpty);
expect(c.read(paymentFlowProvider).errorEn, isNotNull);
expect(c.read(paymentFlowProvider).errorEn, isNotEmpty);
});
test('resume() 成功 → retry 恢复原单会话(不新下单),phase=awaitingPayment', () async {
final api = _FakePaymentApi();
final c = _container(api);