import 'dart:io'; import 'package:flutter/material.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; import '../../../../app/di/injection.dart'; import '../../../../core/auth/session_store.dart'; import '../../../../core/logging/logger.dart'; import '../../../../core/network/api_problem_mapper.dart'; import '../../../../data/network/api_client.dart'; import '../../../../data/storage/local_kv_store.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/app_color_palette.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../../payments/data/services/apple_iap_service.dart'; import '../../../points/data/apis/points_api.dart'; import '../../../points/data/models/package_info.dart'; import '../widgets/settings_section_widgets.dart'; class CoinCenterScreen extends StatefulWidget { const CoinCenterScreen({ super.key, required this.balance, required this.userId, required this.onBalanceChanged, }); final int balance; final String userId; final void Function(int newBalance) onBalanceChanged; @override State createState() => _CoinCenterScreenState(); } class _CoinCenterScreenState extends State { final Logger _logger = getLogger('features.settings.coin_center_screen'); List? _packages; bool _isLoading = true; AppleIapService? _iapService; @override void initState() { super.initState(); _loadPackages(); } Future _loadPackages() async { try { final sessionStore = SessionStore(LocalKvStore()); final apiClient = ApiClient( baseUrl: appDependencies.backendUrl, tokenProvider: sessionStore.getToken, ); final api = PointsApi(apiClient.rawDio); final result = await api.getPackages(); final service = AppleIapService( apiClient: apiClient, userId: widget.userId, ); service.init(); service.addListener(_onPurchaseStateChanged); if (await InAppPurchase.instance.isAvailable()) { await service.loadStoreKitProducts(result.packages); } if (mounted) { setState(() { _packages = result.packages; _isLoading = false; _iapService = service; }); } } catch (e, stackTrace) { _logger.error( message: 'Failed to load packages', error: e, stackTrace: stackTrace, ); if (mounted) { setState(() { _isLoading = false; }); } } } Future _reloadPackages() async { try { final sessionStore = SessionStore(LocalKvStore()); final apiClient = ApiClient( baseUrl: appDependencies.backendUrl, tokenProvider: sessionStore.getToken, ); final api = PointsApi(apiClient.rawDio); final result = await api.getPackages(); await _iapService?.loadStoreKitProducts(result.packages); if (mounted) { setState(() { _packages = result.packages; }); } } catch (e, stackTrace) { _logger.warning( message: 'Failed to reload packages after purchase', extra: {'error': e.toString()}, ); } } void _onPurchaseStateChanged() { final service = _iapService; if (service == null || !mounted) return; final l10n = AppLocalizations.of(context)!; switch (service.state) { case PurchaseFlowState.success: Toast.show(context, l10n.paymentSuccess, type: ToastType.success); _refreshBalance(); _reloadPackages(); service.resetState(); break; case PurchaseFlowState.failed: final apiProblem = service.lastApiProblem; final error = apiProblem != null ? mapApiProblemToMessage(apiProblem, l10n) : (service.lastError ?? l10n.paymentVerifyFailed); Toast.show(context, error, type: ToastType.error); service.resetState(); break; case PurchaseFlowState.purchasing: case PurchaseFlowState.verifying: case PurchaseFlowState.idle: break; } setState(() {}); } Future _refreshBalance() async { try { final sessionStore = SessionStore(LocalKvStore()); final apiClient = ApiClient( baseUrl: appDependencies.backendUrl, tokenProvider: sessionStore.getToken, ); final response = await apiClient.getJson('/api/v1/points/balance'); final newBalance = response['availableBalance'] as int; widget.onBalanceChanged(newBalance); } catch (e, stackTrace) { _logger.warning( message: 'Failed to refresh balance after purchase: $e', extra: {'stackTrace': stackTrace.toString()}, ); } } @override void dispose() { _iapService?.removeListener(_onPurchaseStateChanged); _iapService?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final colors = Theme.of(context).colorScheme; final palette = Theme.of(context).extension()!; return Scaffold( backgroundColor: colors.surfaceContainerLow, appBar: AppBar( title: Text(l10n.settingsCoinCenterTitle), centerTitle: true, backgroundColor: colors.surfaceContainerLow, surfaceTintColor: colors.surfaceContainerLow, ), body: ListView( padding: const EdgeInsets.all(AppSpacing.lg), children: [ Container( padding: const EdgeInsets.all(AppSpacing.xl), decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppRadius.xl), gradient: LinearGradient( colors: [colors.primary, palette.accentPurple], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.monetization_on_rounded, color: colors.onPrimary, size: 34, ), const SizedBox(height: AppSpacing.md), Text( l10n.settingsCoinBalanceLabel, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: colors.onPrimary), ), const SizedBox(height: AppSpacing.xs), Text( l10n.settingsCoinBalanceValue(widget.balance), style: Theme.of( context, ).textTheme.headlineMedium?.copyWith(color: colors.onPrimary), ), const SizedBox(height: AppSpacing.sm), Text( l10n.settingsCoinCenterDescription, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colors.onPrimary.withValues(alpha: 0.88), ), ), ], ), ), const SizedBox(height: AppSpacing.xl), SectionLabel(text: l10n.settingsCoinRechargeSection), ..._buildPackageCards(l10n, colors), ], ), ); } List _buildPackageCards(AppLocalizations l10n, ColorScheme colors) { if (_isLoading) { return [ const Padding( padding: EdgeInsets.all(AppSpacing.xl), child: Center(child: CircularProgressIndicator()), ), ]; } if (_packages == null || _packages!.isEmpty) { return []; } final isBusy = _iapService?.state == PurchaseFlowState.purchasing || _iapService?.state == PurchaseFlowState.verifying; final isPending = _iapService?.state == PurchaseFlowState.purchasing; return List.generate(_packages!.length, (index) { final pkg = _packages![index]; final storeKitProduct = _iapService?.getStoreKitProduct(pkg.appStoreProductId); final isAvailable = storeKitProduct != null; return Column( children: [ if (index > 0) const SizedBox(height: AppSpacing.md), if (isPending && !isBusy) Padding( padding: const EdgeInsets.only(bottom: AppSpacing.sm), child: Text( l10n.paymentPending, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colors.primary, ), ), ), CoinPackageCard( title: _getPackageTitle(pkg, l10n), price: _getDisplayPrice(pkg), amount: pkg.credits, badge: _getPackageBadge(pkg, l10n), onPurchase: () => _handlePurchase(pkg), isPurchasing: isBusy, isAvailable: isAvailable, unavailableMessage: isAvailable ? null : l10n.paymentProductUnavailable, ), ], ); }); } String _getDisplayPrice(PackageInfo pkg) { final product = _iapService?.getStoreKitProduct(pkg.appStoreProductId); if (product != null) { return product.price; } return ''; } Future _handlePurchase(PackageInfo pkg) async { final l10n = AppLocalizations.of(context)!; if (_iapService == null) { Toast.show(context, l10n.paymentVerifyFailed, type: ToastType.error); return; } if (!Platform.isIOS) { Toast.show(context, l10n.paymentVerifyFailed, type: ToastType.error); return; } await _iapService!.purchase(pkg); } String? _getPackageBadge(PackageInfo pkg, AppLocalizations l10n) { if (pkg.productCode == ProductCode.newUserPack) { return l10n.settingsCoinPackNewUserBadge; } if (pkg.productCode == ProductCode.popularPack) { return l10n.settingsCoinPackPopularBadge; } return null; } String _getPackageTitle(PackageInfo pkg, AppLocalizations l10n) { return switch (pkg.productCode) { ProductCode.newUserPack => l10n.settingsCoinPackStarter, ProductCode.starterPack => l10n.settingsCoinPackBasic, ProductCode.popularPack => l10n.settingsCoinPackPopular, ProductCode.premiumPack => l10n.settingsCoinPackPremium, }; } }