import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import '../../core/adjust/adjust_events.dart'; import '../../core/api/api_config.dart'; import '../../core/api/services/payment_api.dart'; import '../../core/auth/auth_service.dart'; import '../../core/user/account_refresh.dart'; import '../../core/log/app_logger.dart'; import '../../core/theme/app_colors.dart'; import '../../core/user/user_state.dart'; import '../../core/theme/app_spacing.dart'; import '../../core/theme/app_typography.dart'; import '../../shared/widgets/top_nav_bar.dart'; import 'google_play_purchase_service.dart'; import 'models/activity_item.dart'; import 'models/payment_method_item.dart'; import 'payment_webview_screen.dart'; /// Recharge screen - matches Pencil tPjdN /// Tier cards (DC6NS, YRaOG, QlNz6) 对接 getGooglePayActivities / getApplePayActivities class RechargeScreen extends StatefulWidget { static final _log = AppLogger('Recharge'); const RechargeScreen({super.key}); @override State createState() => _RechargeScreenState(); } class _RechargeScreenState extends State with WidgetsBindingObserver { List _activities = []; bool _loadingTiers = true; String? _tierError; /// 当前正在支付的商品 code,仅该 item 的 Buy 显示 loading String? _loadingProductId; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); refreshAccount(); _fetchActivities(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed && _loadingProductId != null && mounted) { setState(() => _loadingProductId = null); } } Future _fetchActivities() async { setState(() { _loadingTiers = true; _tierError = null; }); try { await AuthService.loginComplete; final country = UserState.navigate.value ?? ''; final res = defaultTargetPlatform == TargetPlatform.iOS ? await PaymentApi.getApplePayActivities( vambrace: country.isEmpty ? null : country, ) : await PaymentApi.getGooglePayActivities( vambrace: country.isEmpty ? null : country, ); if (!mounted) return; if (res.isSuccess && res.data != null) { final data = res.data as Map?; final summon = data?['summon'] as List? ?? []; final list = summon .whereType>() .map((e) => ActivityItem.fromJson(e)) .where((e) => e.code.isNotEmpty) .toList(); setState(() { _activities = list; _loadingTiers = false; }); } else { setState(() { _activities = []; _loadingTiers = false; _tierError = res.msg.isNotEmpty ? res.msg : 'Failed to load'; }); } } catch (e) { if (mounted) { setState(() { _activities = []; _loadingTiers = false; _tierError = e.toString(); }); } } } Future _onBuy(ActivityItem item) async { if (_loadingProductId != null) return; setState(() => _loadingProductId = item.code); final price = AdjustEvents.parsePrice(item.actualAmount); if (price != null) { final tierToken = AdjustEvents.tierTokenFromPrice(price); if (tierToken != null) { AdjustEvents.trackTier(tierToken); } } final titleLower = item.title.toLowerCase(); if (titleLower.contains('monthly vip')) { AdjustEvents.trackMonthlyVip(); } else if (titleLower.contains('weekly vip')) { AdjustEvents.trackWeeklyVip(); } final useThirdParty = UserState.enableThirdPartyPayment.value == true; final uid = UserState.userId.value; if (useThirdParty && uid != null && uid.isNotEmpty) { await _runThirdPartyPayment(item, uid); } else { await _runGooglePay(item); } } /// 第三方支付流程:获取支付方式 -> 弹窗选择 -> 创建订单 -> 打开支付页 Future _runThirdPartyPayment(ActivityItem item, String userId) async { final activityId = item.activityId; if (activityId == null || activityId.isEmpty) { if (mounted) { _showSnackBar(context, 'Invalid product', isError: true); } AdjustEvents.trackPaymentFailed(); return; } if (!mounted) return; try { final country = UserState.navigate.value; final methodsRes = await PaymentApi.getPaymentMethods( warrior: activityId, vambrace: country?.isEmpty == true ? null : country, ); if (!mounted) return; if (!methodsRes.isSuccess || methodsRes.data == null) { _showSnackBar( context, methodsRes.msg.isNotEmpty ? methodsRes.msg : 'Failed to load payment methods', isError: true, ); AdjustEvents.trackPaymentFailed(); return; } final methodsList = methodsRes.data is Map ? (methodsRes.data! as Map)['renew'] as List? : null; if (methodsList == null || methodsList.isEmpty) { _showSnackBar(context, 'No payment methods available', isError: true); AdjustEvents.trackPaymentFailed(); return; } final methods = methodsList .whereType>() .map((e) => PaymentMethodItem.fromJson(e)) .where((m) => m.paymentMethod.isNotEmpty) .toList(); if (methods.isEmpty) { _showSnackBar(context, 'No payment methods available', isError: true); AdjustEvents.trackPaymentFailed(); return; } if (!mounted) return; final selected = await showDialog( context: context, builder: (ctx) => _PaymentMethodDialog(methods: methods), ); if (!mounted) return; if (selected == null) { setState(() => _loadingProductId = null); return; } await _createOrderAndOpenUrl( userId: userId, activityId: activityId, productId: item.code, paymentMethod: selected.paymentMethod, subPaymentMethod: selected.subPaymentMethod?.isEmpty == true ? null : selected.subPaymentMethod, ); } catch (e) { if (mounted) { _showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { if (mounted) { setState(() => _loadingProductId = null); } } } /// 创建订单;若为 Google Pay 则调起内购并上报凭据,否则打开支付链接 Future _createOrderAndOpenUrl({ required String userId, required String activityId, required String productId, required String paymentMethod, String? subPaymentMethod, }) async { if (!mounted) return; try { final createRes = await PaymentApi.createPayment( sentinel: ApiConfig.appId, asset: userId, warrior: activityId, resource: paymentMethod, ceremony: subPaymentMethod, ); if (!mounted) return; if (!createRes.isSuccess) { _showSnackBar( context, createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order', isError: true, ); AdjustEvents.trackPaymentFailed(); return; } final data = createRes.data is Map ? createRes.data as Map : null; final orderId = data?['federation']?.toString(); if (_isGooglePay(paymentMethod, subPaymentMethod)) { if (defaultTargetPlatform != TargetPlatform.android) { _showSnackBar(context, 'Google Pay is only available on Android.', isError: true); AdjustEvents.trackPaymentFailed(); return; } final purchaseData = await GooglePlayPurchaseService.launchPurchaseAndReturnData(productId); if (!mounted) return; if (purchaseData != null && purchaseData.isNotEmpty && orderId != null && orderId.isNotEmpty) { RechargeScreen._log.d('googlepay 入参: federation=$orderId, asset=$userId'); RechargeScreen._log.d('googlepay 入参: merchant(length=${purchaseData.length}) ${purchaseData.length > 400 ? "${purchaseData.substring(0, 400)}..." : purchaseData}'); final googlepayRes = await PaymentApi.googlepay( merchant: purchaseData, federation: orderId, asset: userId, ); if (!mounted) return; if (googlepayRes.isSuccess) { _showSnackBar(context, 'Purchase completed.'); AdjustEvents.trackPurchaseSuccess(); } else { _showSnackBar(context, googlepayRes.msg.isNotEmpty ? googlepayRes.msg : 'Payment verification failed.', isError: true); AdjustEvents.trackPaymentFailed(); } } else { _showSnackBar(context, 'Purchase was cancelled or failed.', isError: true); AdjustEvents.trackPaymentFailed(); } return; } final payUrl = data?['convert']?.toString(); if (payUrl != null && payUrl.isNotEmpty) { if (mounted) { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PaymentWebViewScreen(paymentUrl: payUrl), ), ); _showSnackBar(context, 'Order created. Complete payment in the page.'); AdjustEvents.trackPurchaseSuccess(); } } else { if (mounted) { _showSnackBar(context, 'Order created. Awaiting payment confirmation.'); } AdjustEvents.trackPurchaseSuccess(); } } catch (e) { if (mounted) { _showSnackBar(context, 'Payment error: ${e.toString()}', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { if (mounted) { setState(() => _loadingProductId = null); } } } /// ceremony==GooglePay 或 resource==GOOGLEPAY 时走谷歌内购并上报 static bool _isGooglePay(String paymentMethod, String? subPaymentMethod) { final r = paymentMethod.trim().toLowerCase(); final c = (subPaymentMethod ?? '').trim().toLowerCase(); return r == 'googlepay' || c == 'googlepay'; } /// 三方为 false 时走谷歌应用内支付 Future _runGooglePay(ActivityItem item) async { if (defaultTargetPlatform != TargetPlatform.android) { _showSnackBar(context, 'Google Pay is only available on Android.', isError: true); AdjustEvents.trackPaymentFailed(); return; } await _launchGooglePlayPurchase(item); } /// 调起 Google Play 内购(商品 ID = item.code / helm) Future _launchGooglePlayPurchase(ActivityItem item) async { if (!mounted) return; try { final success = await GooglePlayPurchaseService.launchPurchase(item.code); if (!mounted) return; if (success) { _showSnackBar(context, 'Purchase completed.'); AdjustEvents.trackPurchaseSuccess(); } else { _showSnackBar(context, 'Purchase was cancelled or failed.', isError: true); AdjustEvents.trackPaymentFailed(); } } catch (e) { if (mounted) { _showSnackBar(context, 'Google Pay error: ${e.toString()}', isError: true); } AdjustEvents.trackPaymentFailed(); } finally { if (mounted) { setState(() => _loadingProductId = null); } } } void _showSnackBar(BuildContext context, String message, {bool isError = false}) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Colors.red.shade700 : null, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: PreferredSize( preferredSize: const Size.fromHeight(56), child: TopNavBar( title: 'Recharge', showBackButton: true, onBack: () => Navigator.of(context).pop(), ), ), body: SingleChildScrollView( padding: const EdgeInsets.only(bottom: AppSpacing.screenPaddingLarge), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _CreditsSection( currentCredits: UserCreditsData.of(context)?.creditsDisplay ?? '--', ), Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.screenPaddingLarge, vertical: AppSpacing.xxl, ), child: Text( 'Select Tier', style: AppTypography.bodyMedium.copyWith( color: AppColors.textPrimary, ), ), ), if (_loadingTiers) const Padding( padding: EdgeInsets.all(AppSpacing.xxl), child: Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ) else if (_tierError != null) Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.screenPadding), child: Column( children: [ Text( _tierError!, style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), const SizedBox(height: AppSpacing.md), TextButton( onPressed: _fetchActivities, child: const Text('Retry'), ), ], ), ) else if (_activities.isEmpty) Padding( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.screenPadding), child: Text( 'No packages available', style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), ) else ...List.generate(_activities.length, (i) { final item = _activities[i]; final isRecommended = i == 1; final isPopular = i == 2; return Padding( padding: EdgeInsets.only( bottom: i < _activities.length - 1 ? AppSpacing.xl : 0, ), child: _TierCardFromActivity( item: item, badge: isRecommended ? _TierBadge.recommended : isPopular ? _TierBadge.popular : _TierBadge.none, loading: _loadingProductId == item.code, onBuy: () => _onBuy(item), ), ); }), ], ), ), ); } } /// 支付方式选择弹窗(三方支付列表) class _PaymentMethodDialog extends StatelessWidget { const _PaymentMethodDialog({required this.methods}); final List methods; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Select payment method'), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: methods .map( (m) => ListTile( title: Text(m.displayName), subtitle: m.subPaymentMethod != null && m.subPaymentMethod!.isNotEmpty ? Text(m.subPaymentMethod!, style: AppTypography.caption) : null, trailing: m.recommend ? Text('Recommended', style: AppTypography.caption.copyWith(color: AppColors.primary)) : null, onTap: () => Navigator.of(context).pop(m), ), ) .toList(), ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel'), ), ], ); } } enum _TierBadge { none, recommended, popular } class _TierCardFromActivity extends StatelessWidget { const _TierCardFromActivity({ required this.item, required this.badge, required this.loading, required this.onBuy, }); final ActivityItem item; final _TierBadge badge; final bool loading; final VoidCallback onBuy; @override Widget build(BuildContext context) { final content = Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, vertical: AppSpacing.xl, ), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), boxShadow: [ BoxShadow( color: AppColors.shadowLight, blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.creditsDisplay, style: AppTypography.bodyLarge.copyWith( color: AppColors.textPrimary, ), ), SizedBox(height: badge == _TierBadge.none ? AppSpacing.xs : AppSpacing.sm), Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Text( item.priceDisplayWithDollar, style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, ), ), if (item.originAmount != null && item.originAmount!.isNotEmpty && item.originAmount != item.actualAmount) ...[ const SizedBox(width: AppSpacing.sm), Text( item.originAmount!, style: AppTypography.bodyRegular.copyWith( color: AppColors.textSecondary, decoration: TextDecoration.lineThrough, decorationColor: AppColors.textSecondary, ), ), ], if (item.bonus > 0) ...[ const SizedBox(width: AppSpacing.sm), Text( 'Bonus: ${item.bonus} credits', style: AppTypography.caption.copyWith( color: AppColors.primary, ), ), ], ], ), ], ), ), _BuyButton(onTap: onBuy, loading: loading), ], ), ); if (badge == _TierBadge.none) { return content; } return Stack( clipBehavior: Clip.none, children: [ content, Positioned( top: 0, right: AppSpacing.screenPadding, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.md, vertical: AppSpacing.xs, ), decoration: BoxDecoration( color: badge == _TierBadge.recommended ? AppColors.primaryLight : AppColors.accentOrangeLight, borderRadius: const BorderRadius.only( topRight: Radius.circular(16), bottomLeft: Radius.circular(16), ), ), child: Text( badge == _TierBadge.recommended ? 'Recommended' : 'Most Popular', style: AppTypography.label.copyWith( color: badge == _TierBadge.recommended ? AppColors.primary : AppColors.accentOrange, fontWeight: FontWeight.w600, ), ), ), ), ], ); } } class _CreditsSection extends StatelessWidget { const _CreditsSection({required this.currentCredits}); final String currentCredits; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.all(AppSpacing.screenPadding), padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xxl, vertical: AppSpacing.xl, ), decoration: BoxDecoration( color: AppColors.primary, borderRadius: BorderRadius.circular(16), border: Border.all( color: AppColors.primary.withValues(alpha: 0.5), ), boxShadow: [ BoxShadow( color: AppColors.primaryShadow.withValues(alpha: 0.25), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Icon(LucideIcons.sparkles, size: 28, color: AppColors.surface), const SizedBox(width: AppSpacing.md), Text( currentCredits, style: AppTypography.bodyLarge.copyWith( fontSize: 32, fontWeight: FontWeight.w700, color: AppColors.surface, ), ), const Spacer(), Text( 'Current Credits', style: AppTypography.caption.copyWith( color: AppColors.surface.withValues(alpha: 0.8), ), ), ], ), ); } } class _BuyButton extends StatelessWidget { const _BuyButton({required this.onTap, this.loading = false}); final VoidCallback onTap; final bool loading; @override Widget build(BuildContext context) { return GestureDetector( onTap: loading ? null : onTap, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xl, vertical: AppSpacing.md, ), decoration: BoxDecoration( color: loading ? AppColors.primary.withValues(alpha: 0.7) : AppColors.primary, borderRadius: BorderRadius.circular(12), ), child: loading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(AppColors.surface), ), ) : Text( 'Buy', style: AppTypography.bodyRegular.copyWith( color: AppColors.surface, fontWeight: FontWeight.w600, ), ), ), ); } }