// payment_provider.dart — 支付流状态机:选档下单 → 等待支付(轮询查单)→ 成功/失败。 // 轮询成功判据是 server 的 activated(webhook 已开通),不是 pay 的 succeeded—— // 保证用户看到「成功」时权益一定已到账。 import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../models/payment.dart'; import '../services/auth_api.dart'; import '../services/payment_api.dart'; import 'account_providers.dart'; final paymentApiProvider = Provider( (ref) => PaymentApi(ref.watch(apiClientProvider)), ); final payCatalogProvider = FutureProvider>( (ref) => ref.watch(paymentApiProvider).catalog(), ); enum PaymentPhase { idle, creating, awaitingPayment, succeeded, failed } @immutable class PaymentFlowState { const PaymentFlowState({ this.phase = PaymentPhase.idle, this.item, this.method = '', this.order, this.status, this.errorZh, this.errorEn, }); final PaymentPhase phase; final PayCatalogItem? item; final String method; // alipay | crypto final PayOrder? order; final PayOrderStatus? status; final String? errorZh; final String? errorEn; PaymentFlowState copyWith({ PaymentPhase? phase, PayCatalogItem? item, String? method, PayOrder? order, PayOrderStatus? status, String? errorZh, String? errorEn, }) => PaymentFlowState( phase: phase ?? this.phase, item: item ?? this.item, method: method ?? this.method, order: order ?? this.order, status: status ?? this.status, errorZh: errorZh, errorEn: errorEn, ); } class PaymentFlowController extends StateNotifier { PaymentFlowController(Ref ref) : _ref = ref, super(const PaymentFlowState()); /// 测试专用 fixture:直接给定固定状态,`_ref` 留空、不发网络、不起 Timer /// (widget 测试用来渲染某一支付阶段的界面,不驱动状态机)。[cancel] 对 /// `_ref == null` 短路安全——支付页 dispose 时无条件可调,不炸 fixture。 @visibleForTesting PaymentFlowController.fixed(super.state) : _ref = null; final Ref? _ref; Timer? _poll; bool _polling = false; // 再入保护(轮询慢于间隔时跳过本拍) static const _pollInterval = Duration(seconds: 3); bool get _isMobile { if (kIsWeb) return false; return Platform.isAndroid || Platform.isIOS; } Map _metadata(String method) => method == 'alipay' ? {'is_mobile': _isMobile ? '1' : '0'} : const {}; Future start(PayCatalogItem item, String method) async { _stopPolling(); state = PaymentFlowState(phase: PaymentPhase.creating, item: item, method: method); try { final order = await _ref! .read(paymentApiProvider) .createOrder(sku: item.sku, method: method, metadata: _metadata(method)); if (!mounted) return; state = state.copyWith(phase: PaymentPhase.awaitingPayment, order: order); _startPolling(); } on AuthApiException catch (e) { if (!mounted) return; state = state.copyWith(phase: PaymentPhase.failed, errorZh: e.messageZh, errorEn: e.messageEn); } } /// 换支付方式:先 retry;pay 回 409 CURRENCY_MISMATCH(跨结算币种)时, /// 按契约「换币种必须新单」→ cancel 旧单 + 新 method 重新下单。 Future switchMethod(String method) async { final item = state.item; final order = state.order; if (item == null || order == null) return; try { final next = await _ref! .read(paymentApiProvider) .retry(order.orderNo, method: method, metadata: _metadata(method)); if (!mounted) return; state = state.copyWith(phase: PaymentPhase.awaitingPayment, method: method, order: next); } on AuthApiException catch (e) { if (!mounted) return; if (e.code == 'CURRENCY_MISMATCH') { try { await _ref!.read(paymentApiProvider).cancel(order.orderNo); } catch (_) {} // 旧单取消失败不阻断新单 await start(item, method); return; } state = state.copyWith(phase: PaymentPhase.failed, errorZh: e.messageZh, errorEn: e.messageEn); } } Future cancel() async { final order = state.order; final ref = _ref; _stopPolling(); if (order != null && ref != null) { try { await ref.read(paymentApiProvider).cancel(order.orderNo); } catch (_) {} } if (mounted) state = const PaymentFlowState(); } /// 只停轮询 Timer,**不**远程 cancel、**不**改 state——供支付页 dispose() /// 用(离开页面 ≠ 放弃订单)。与 [cancel] 区分:后者是「取消订单」按钮的显式 /// 放弃路径(远程 cancel + 状态归 idle)。 void stopPolling() => _stopPolling(); /// 单拍轮询(Timer 驱动;测试直接调用,不依赖真实时钟)。 Future pollOnce() async { final order = state.order; if (order == null || _polling) return; _polling = true; try { final st = await _ref!.read(paymentApiProvider).orderStatus(order.orderNo); if (!mounted) return; if (st.activated) { _stopPolling(); state = state.copyWith(phase: PaymentPhase.succeeded, status: st); // 权益已变,best-effort 刷新「我的」——仅当 meProvider 已被真实页面读取过 // (ref.exists)才刷新;测试容器/尚未触达账户页时是空操作,不会凭空触发 // 首次网络请求(避免打真网络 / 触发 authProvider 的后台加载)。 if (_ref.exists(meProvider)) { try { await _ref.read(meProvider.notifier).refresh(); } catch (_) {} } } else { state = state.copyWith(status: st); } } on AuthApiException { // 单拍失败静默,下一拍重试(网络抖动不打断等待页)。 } finally { _polling = false; } } void _startPolling() { _poll?.cancel(); _poll = Timer.periodic(_pollInterval, (_) => pollOnce()); } void _stopPolling() { _poll?.cancel(); _poll = null; } @override void dispose() { _stopPolling(); super.dispose(); } } final paymentFlowProvider = StateNotifierProvider( (ref) => PaymentFlowController(ref), );