diff --git a/lib/core/api/services/payment_api.dart b/lib/core/api/services/payment_api.dart index 6cd5f96..673bcf1 100644 --- a/lib/core/api/services/payment_api.dart +++ b/lib/core/api/services/payment_api.dart @@ -88,6 +88,21 @@ abstract final class PaymentApi { ); } + /// 获取订单详情(用于三方支付 webview 关闭后轮询) + static Future getOrderDetail({ + required String asset, + required String federation, + }) async { + return _client.request( + path: '/v1/payment/getOrderDetail', + method: 'GET', + queryParams: { + 'asset': asset, + 'federation': federation, + }, + ); + } + /// Google 支付结果回调。body 为 sample(signature)、merchant(purchaseData)、federation(id)、asset(userId),见 docs/googlepay.md。 static Future googlepay({ required String sample, diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index ac073c9..dd78a23 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -80,7 +80,7 @@ class _ProfileScreenState extends State { icon: LucideIcons.chevron_right, onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => PaymentWebViewScreen( + builder: (_) => const PaymentWebViewScreen( paymentUrl: 'http://www.petsheroai.xyz/privacy.html', title: 'Privacy Policy', ), @@ -92,7 +92,7 @@ class _ProfileScreenState extends State { icon: LucideIcons.chevron_right, onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => PaymentWebViewScreen( + builder: (_) => const PaymentWebViewScreen( paymentUrl: 'http://www.petsheroai.xyz/terms.html', title: 'User Agreement', ), @@ -143,18 +143,18 @@ class _ProfileHeader extends StatelessWidget { ? CachedNetworkImage( imageUrl: avatarUrl!, fit: BoxFit.cover, - placeholder: (_, __) => Icon( + placeholder: (_, __) => const Icon( LucideIcons.user, size: 40, color: AppColors.textSecondary, ), - errorWidget: (_, __, ___) => Icon( + errorWidget: (_, __, ___) => const Icon( LucideIcons.user, size: 40, color: AppColors.textSecondary, ), ) - : Icon( + : const Icon( LucideIcons.user, size: 40, color: AppColors.textSecondary, @@ -162,7 +162,7 @@ class _ProfileHeader extends StatelessWidget { ), const SizedBox(height: AppSpacing.lg), Text( - userName ?? 'Guest', + userName ?? 'VIP', style: AppTypography.bodyLarge.copyWith( color: AppColors.textPrimary, ), @@ -210,11 +210,11 @@ class _BalanceCard extends StatelessWidget { decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), - boxShadow: [ + boxShadow: const [ BoxShadow( color: AppColors.shadowLight, blurRadius: 8, - offset: const Offset(0, 2), + offset: Offset(0, 2), ), ], ), @@ -314,11 +314,11 @@ class _MenuItem extends StatelessWidget { decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), - boxShadow: [ + boxShadow: const [ BoxShadow( color: AppColors.shadowLight, blurRadius: 6, - offset: const Offset(0, 2), + offset: Offset(0, 2), ), ], ), diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart index 7f37f79..e122d0c 100644 --- a/lib/features/recharge/recharge_screen.dart +++ b/lib/features/recharge/recharge_screen.dart @@ -275,6 +275,9 @@ class _RechargeScreenState extends State builder: (_) => PaymentWebViewScreen(paymentUrl: payUrl), ), ); + if (mounted && orderId != null && orderId.isNotEmpty) { + _startOrderPolling(orderId: orderId, userId: userId); + } _showSnackBar( context, 'Order created. Complete payment in the page.'); AdjustEvents.trackPurchaseSuccess(); @@ -299,6 +302,42 @@ class _RechargeScreenState extends State } } + /// 三方支付 webview 关闭后轮询订单详情,间隔 1/3/7/15/31/63 秒;status 为 SUCCESS|FAILED|CANCELED 或订单不存在或轮询结束则停止;SUCCESS 时刷新用户信息 + void _startOrderPolling({required String orderId, required String userId}) { + const delays = [1, 2, 4, 8, 16, 32]; // 累计 1,3,7,15,31,63 秒 + Future poll(int index) async { + if (index >= delays.length) return; + await Future.delayed(Duration(seconds: delays[index])); + if (!mounted) return; + try { + final res = await PaymentApi.getOrderDetail( + asset: userId, + federation: orderId, + ); + if (!mounted) return; + if (!res.isSuccess || res.data == null) { + RechargeScreen._log.d('订单轮询 orderId=$orderId 订单不存在或请求失败'); + return; + } + final data = res.data as Map?; + final status = data?['line']?.toString().toUpperCase(); + RechargeScreen._log.d('订单轮询 orderId=$orderId status=$status'); + if (status == 'SUCCESS' || status == 'FAILED' || status == 'CANCELED') { + if (status == 'SUCCESS') { + refreshAccount(); + } + return; + } + poll(index + 1); + } catch (e) { + RechargeScreen._log.w('订单轮询异常 orderId=$orderId: $e'); + poll(index + 1); + } + } + + poll(0); + } + /// ceremony==GooglePay 或 resource==GOOGLEPAY 时走谷歌内购并上报 static bool _isGooglePay(String paymentMethod, String? subPaymentMethod) { final r = paymentMethod.trim().toLowerCase(); @@ -534,8 +573,8 @@ class _RechargeScreenState extends State else ...List.generate(_activities.length, (i) { final item = _activities[i]; - final isRecommended = false; - final isPopular = false; + const isRecommended = false; + const isPopular = false; return Padding( padding: EdgeInsets.only( bottom: i < _activities.length - 1 ? AppSpacing.xl : 0, @@ -760,13 +799,13 @@ class _PaymentIcon extends StatelessWidget { ? Image.network( iconUrl!, fit: BoxFit.contain, - errorBuilder: (_, __, ___) => Icon( + errorBuilder: (_, __, ___) => const Icon( LucideIcons.credit_card, size: 24, color: AppColors.primary, ), ) - : Icon( + : const Icon( LucideIcons.credit_card, size: 24, color: AppColors.primary, @@ -825,11 +864,11 @@ class _TierCardFromActivity extends StatelessWidget { color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), - boxShadow: [ + boxShadow: const [ BoxShadow( color: AppColors.shadowLight, blurRadius: 6, - offset: const Offset(0, 2), + offset: Offset(0, 2), ), ], ), @@ -962,7 +1001,7 @@ class _CreditsSection extends StatelessWidget { ), child: Row( children: [ - Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface), + const Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface), const SizedBox(width: AppSpacing.md), Text( currentCredits,