feat(client): 购买页三档 + 支付页 render_type 多态(crypto/redirect/qr 预留)+ 导航接线
购买页三档 pro(月/季/年,金额来自 GET /v1/pay/catalog)选档 → 选支付方式 → 下单; 支付页按 session.render_type 多态渲染(crypto_address 地址+精确金额+复制、redirect 外链拉起、qr 预留复制兜底),轮询用 Task 6 的 paymentFlowProvider,409 CURRENCY_MISMATCH 走「换方式开新单」。同时收口 paymentFlowProvider 非 autoDispose 带来的轮询生命周期缺口——支付页 dispose 时停轮询(cancel() 延后到微任务执行,避免 在自身 unmount 期间同步改 state 炸 Riverpod 断言)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -4,10 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
/// 一级视图 + 账户子页。
|
||||
/// desktop 侧栏暴露 6 个一级项(connect..settings);mobile/tablet 取前 4 项,
|
||||
/// contact/settings 在 mobile 走账户子页。plans/redeem 是 account 的下钻页。
|
||||
enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices }
|
||||
enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices, purchase, payment }
|
||||
|
||||
/// 账户下钻子页(desktop 在内容区切换、顶栏带返回;mobile/tablet 走全屏 push)。
|
||||
const Set<NavView> kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices};
|
||||
const Set<NavView> kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices, NavView.purchase, NavView.payment};
|
||||
|
||||
/// 当前视图。
|
||||
final navViewProvider = StateProvider<NavView>((ref) => NavView.connect);
|
||||
|
||||
@@ -63,9 +63,15 @@ class PaymentFlowState {
|
||||
}
|
||||
|
||||
class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
PaymentFlowController(this._ref) : super(const PaymentFlowState());
|
||||
PaymentFlowController(Ref ref) : _ref = ref, super(const PaymentFlowState());
|
||||
|
||||
final Ref _ref;
|
||||
/// 测试专用 fixture:直接给定固定状态,`_ref` 留空、不发网络、不起 Timer
|
||||
/// (widget 测试用来渲染某一支付阶段的界面,不驱动状态机)。[cancel] 对
|
||||
/// `_ref == null` 短路安全——支付页 dispose 时无条件可调,不炸 fixture。
|
||||
@visibleForTesting
|
||||
PaymentFlowController.fixed(super.state) : _ref = null;
|
||||
|
||||
final Ref? _ref;
|
||||
Timer? _poll;
|
||||
bool _polling = false; // 再入保护(轮询慢于间隔时跳过本拍)
|
||||
|
||||
@@ -83,7 +89,7 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
_stopPolling();
|
||||
state = PaymentFlowState(phase: PaymentPhase.creating, item: item, method: method);
|
||||
try {
|
||||
final order = await _ref
|
||||
final order = await _ref!
|
||||
.read(paymentApiProvider)
|
||||
.createOrder(sku: item.sku, method: method, metadata: _metadata(method));
|
||||
if (!mounted) return;
|
||||
@@ -102,7 +108,7 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
final order = state.order;
|
||||
if (item == null || order == null) return;
|
||||
try {
|
||||
final next = await _ref
|
||||
final next = await _ref!
|
||||
.read(paymentApiProvider)
|
||||
.retry(order.orderNo, method: method, metadata: _metadata(method));
|
||||
if (!mounted) return;
|
||||
@@ -111,7 +117,7 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
if (!mounted) return;
|
||||
if (e.code == 'CURRENCY_MISMATCH') {
|
||||
try {
|
||||
await _ref.read(paymentApiProvider).cancel(order.orderNo);
|
||||
await _ref!.read(paymentApiProvider).cancel(order.orderNo);
|
||||
} catch (_) {} // 旧单取消失败不阻断新单
|
||||
await start(item, method);
|
||||
return;
|
||||
@@ -122,10 +128,11 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
|
||||
Future<void> cancel() async {
|
||||
final order = state.order;
|
||||
final ref = _ref;
|
||||
_stopPolling();
|
||||
if (order != null) {
|
||||
if (order != null && ref != null) {
|
||||
try {
|
||||
await _ref.read(paymentApiProvider).cancel(order.orderNo);
|
||||
await ref.read(paymentApiProvider).cancel(order.orderNo);
|
||||
} catch (_) {}
|
||||
}
|
||||
if (mounted) state = const PaymentFlowState();
|
||||
@@ -137,7 +144,7 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
if (order == null || _polling) return;
|
||||
_polling = true;
|
||||
try {
|
||||
final st = await _ref.read(paymentApiProvider).orderStatus(order.orderNo);
|
||||
final st = await _ref!.read(paymentApiProvider).orderStatus(order.orderNo);
|
||||
if (!mounted) return;
|
||||
if (st.activated) {
|
||||
_stopPolling();
|
||||
|
||||
Reference in New Issue
Block a user