import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/account.dart'; import 'package:island/models/wallet.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/account/account_pfc.dart'; import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/account/restore_purchase_sheet.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/payment/payment_overlay.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; part 'stellar_program_tab.g.dart'; @riverpod Future accountStellarSubscription(Ref ref) async { try { final client = ref.watch(apiClientProvider); final resp = await client.get('/id/subscriptions/fuzzy/solian.stellar'); return SnWalletSubscription.fromJson(resp.data); } catch (err) { if (err is DioException && err.response?.statusCode == 404) return null; rethrow; } } @riverpod Future> accountSentGifts( Ref ref, { int offset = 0, int take = 20, }) async { final client = ref.watch(apiClientProvider); final resp = await client.get( '/id/subscriptions/gifts/sent?offset=$offset&take=$take', ); return (resp.data as List).map((e) => SnWalletGift.fromJson(e)).toList(); } @riverpod Future> accountReceivedGifts( Ref ref, { int offset = 0, int take = 20, }) async { final client = ref.watch(apiClientProvider); final resp = await client.get( '/id/subscriptions/gifts/received?offset=$offset&take=$take', ); return (resp.data as List).map((e) => SnWalletGift.fromJson(e)).toList(); } @riverpod Future accountGift(Ref ref, String giftId) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/id/subscriptions/gifts/$giftId'); return SnWalletGift.fromJson(resp.data); } class PurchaseGiftSheet extends StatefulWidget { const PurchaseGiftSheet({super.key}); @override State createState() => _PurchaseGiftSheetState(); } class _PurchaseGiftSheetState extends State { SnAccount? selectedRecipient; final messageController = TextEditingController(); @override void dispose() { messageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SheetScaffold( titleText: 'purchaseGift'.tr(), child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Recipient Selection Section Text( 'selectRecipient'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(8), Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of( context, ).colorScheme.outline.withOpacity(0.2), ), ), child: selectedRecipient != null ? ListTile( contentPadding: const EdgeInsets.only( left: 20, right: 12, ), leading: ProfilePictureWidget( file: selectedRecipient!.profile.picture, ), title: Text( selectedRecipient!.nick, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), subtitle: Text( 'Selected recipient', style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), trailing: IconButton( onPressed: () => setState( () => selectedRecipient = null, ), icon: Icon( Icons.clear, color: Theme.of(context).colorScheme.error, ), tooltip: 'Clear selection', ), ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.person_add_outlined, size: 48, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), const Gap(8), Text( 'No recipient selected', style: Theme.of( context, ).textTheme.bodyMedium?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), const Gap(4), Text( 'This will be an open gift', style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), ], ).padding(vertical: 32), ), const Gap(12), OutlinedButton.icon( onPressed: () async { final recipient = await showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, builder: (context) => const AccountPickerSheet(), ); if (recipient != null) { setState(() => selectedRecipient = recipient); } }, icon: const Icon(Icons.person_search), label: Text( selectedRecipient != null ? 'changeRecipient'.tr() : 'selectRecipient'.tr(), ), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ), const Gap(24), // Message Section Text( 'addMessage'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(8), TextField( controller: messageController, decoration: InputDecoration( labelText: 'Personal Message', hintText: 'Add a personal message for the recipient', alignLabelWithHint: true, border: OutlineInputBorder(), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of( context, ).colorScheme.outline.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, ), ), ), maxLines: 3, onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ], ), ), ), // Action Buttons Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop({ 'recipient': null, 'message': messageController.text.trim().isEmpty ? null : messageController.text.trim(), }), child: Text('skipRecipient'.tr()), ), ), const Gap(8), Expanded( child: FilledButton( onPressed: () => Navigator.of(context).pop({ 'recipient': selectedRecipient, 'message': messageController.text.trim().isEmpty ? null : messageController.text.trim(), }), child: Text('purchaseGift'.tr()), ), ), ], ), ), ], ), ); } } class StellarProgramTab extends HookConsumerWidget { const StellarProgramTab({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final stellarSubscription = ref.watch(accountStellarSubscriptionProvider); return SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildMembershipSection(context, ref, stellarSubscription), const Gap(16), _buildGiftingSection(context, ref), const Gap(16), ], ), ); } Widget _buildMembershipSection( BuildContext context, WidgetRef ref, AsyncValue stellarSubscriptionAsync, ) { return stellarSubscriptionAsync.when( data: (membership) => _buildMembershipContent(context, ref, membership), loading: () => Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.secondaryContainer, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: const Center(child: CircularProgressIndicator()), ), error: (error, stack) => Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Theme.of(context).colorScheme.primaryContainer, Theme.of(context).colorScheme.secondaryContainer, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), ), child: Text('Error loading membership: $error'), ), ); } Widget _buildMembershipContent( BuildContext context, WidgetRef ref, SnWalletSubscription? membership, ) { final isActive = membership?.isActive ?? false; Future membershipCancel() async { if (!isActive || membership == null) return; final confirm = await showConfirmAlert( 'membershipCancelHint'.tr(), 'membershipCancelConfirm'.tr(), ); if (!confirm || !context.mounted) return; try { showLoadingModal(context); final client = ref.watch(apiClientProvider); await client.post('/id/subscriptions/${membership.identifier}/cancel'); ref.invalidate(accountStellarSubscriptionProvider); ref.read(userInfoProvider.notifier).fetchUser(); if (context.mounted) { hideLoadingModal(context); showSnackBar('membershipCancelSuccess'.tr()); } } catch (err) { if (context.mounted) hideLoadingModal(context); showErrorAlert(err); } } return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon( isActive ? Icons.star_rounded : Icons.star_border_rounded, color: Theme.of(context).colorScheme.primary, size: 24, ), const Gap(8), Text( 'stellarMembership'.tr(), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ), const Gap(12), if (isActive) ...[ _buildCurrentMembershipCard(context, membership!), const Gap(12), FilledButton.icon( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.error, ), foregroundColor: WidgetStateProperty.all( Theme.of(context).colorScheme.onError, ), ), onPressed: membershipCancel, icon: const Icon(Symbols.cancel), label: Text('membershipCancel'.tr()), ), ], if (!isActive) ...[ Text( 'chooseYourPlan'.tr(), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const Gap(12), _buildMembershipTiers(context, ref, membership), ], // Restore Purchase Button // As you know Apple platform need IAP if (kIsWeb || !(Platform.isIOS || Platform.isMacOS)) OutlinedButton.icon( onPressed: () => _showRestorePurchaseSheet(context, ref), icon: const Icon(Icons.restore), label: Text('restorePurchase'.tr()), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ).padding(top: 12), ], ).padding(all: 16), ); } Widget _buildCurrentMembershipCard( BuildContext context, SnWalletSubscription membership, ) { final tierName = _getMembershipTierName(membership.identifier); final tierColor = _getMembershipTierColor(context, membership.identifier); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: tierColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: tierColor, width: 1), ), child: Row( children: [ Icon(Icons.verified, color: tierColor, size: 20), const Gap(8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'currentMembership'.tr(args: [tierName]), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: tierColor, ), ), if (membership.endedAt != null) Text( 'membershipExpires'.tr( args: [membership.endedAt!.formatSystem()], ), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), ], ), ); } Widget _buildMembershipTiers( BuildContext context, WidgetRef ref, SnWalletSubscription? currentMembership, ) { final tiers = [ { 'id': 'solian.stellar.primary', 'name': 'membershipTierStellar'.tr(), 'price': 'membershipPriceStellar'.tr(), 'color': Colors.blue, }, { 'id': 'solian.stellar.nova', 'name': 'membershipTierNova'.tr(), 'price': 'membershipPriceNova'.tr(), 'color': Color.fromRGBO(57, 197, 187, 1), }, { 'id': 'solian.stellar.supernova', 'name': 'membershipTierSupernova'.tr(), 'price': 'membershipPriceSupernova'.tr(), 'color': Colors.orange, }, ]; return Column( children: tiers.map((tier) { final isCurrentTier = currentMembership?.identifier == tier['id']; final tierColor = tier['color'] as Color; return Container( margin: const EdgeInsets.only(bottom: 8), child: Material( color: Colors.transparent, child: InkWell( onTap: isCurrentTier ? null : () => _purchaseMembership( context, ref, tier['id'] as String, ), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isCurrentTier ? tierColor.withOpacity(0.1) : Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: isCurrentTier ? tierColor : Theme.of( context, ).colorScheme.outline.withOpacity(0.2), width: isCurrentTier ? 2 : 1, ), ), child: Row( children: [ Container( width: 4, height: 40, decoration: BoxDecoration( color: tierColor, borderRadius: BorderRadius.circular(2), ), ), const Gap(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( tier['name'] as String, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isCurrentTier ? tierColor : null, ), ), const Gap(8), if (isCurrentTier) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: tierColor, borderRadius: BorderRadius.circular(4), ), child: Text( 'membershipCurrentBadge'.tr(), style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ], ), Text( tier['price'] as String, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), ], ), ), if (!isCurrentTier) Icon( Icons.arrow_forward_ios, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ], ), ), ), ), ); }).toList(), ); } String _getMembershipTierName(String identifier) { switch (identifier) { case 'solian.stellar.primary': return 'membershipTierStellar'.tr(); case 'solian.stellar.nova': return 'membershipTierNova'.tr(); case 'solian.stellar.supernova': return 'membershipTierSupernova'.tr(); default: return 'membershipTierUnknown'.tr(); } } Color _getMembershipTierColor(BuildContext context, String identifier) { switch (identifier) { case 'solian.stellar.primary': return Colors.blue; case 'solian.stellar.nova': return Colors.purple; case 'solian.stellar.supernova': return Colors.orange; default: return Theme.of(context).colorScheme.primary; } } Future _showRestorePurchaseSheet( BuildContext context, WidgetRef ref, ) async { await showModalBottomSheet( context: context, builder: (context) => const RestorePurchaseSheet(), ); } Future _purchaseMembership( BuildContext context, WidgetRef ref, String tierId, ) async { final client = ref.watch(apiClientProvider); try { showLoadingModal(context); final resp = await client.post( '/id/subscriptions', data: { 'identifier': tierId, 'payment_method': 'solian.wallet', 'payment_details': {'currency': 'golds'}, 'cycle_duration_days': 30, }, options: Options(headers: {'X-Noop': true}), ); final subscription = SnWalletSubscription.fromJson(resp.data); if (subscription.status == 1) return; final orderResp = await client.post( '/id/subscriptions/${subscription.identifier}/order', ); final order = SnWalletOrder.fromJson(orderResp.data); if (context.mounted) hideLoadingModal(context); // Show payment overlay to complete the payment if (!context.mounted) return; final paidOrder = await PaymentOverlay.show( context: context, order: order, enableBiometric: true, ); if (context.mounted) showLoadingModal(context); if (paidOrder != null) { // Wait for server to handle order await Future.delayed(const Duration(seconds: 1)); ref.invalidate(accountStellarSubscriptionProvider); ref.read(userInfoProvider.notifier).fetchUser(); if (context.mounted) { showSnackBar('membershipPurchaseSuccess'.tr()); } } } catch (err) { showErrorAlert(err); } finally { if (context.mounted) hideLoadingModal(context); } } Widget _buildGiftingSection(BuildContext context, WidgetRef ref) { final sentGifts = ref.watch(accountSentGiftsProvider()); final receivedGifts = ref.watch(accountReceivedGiftsProvider()); return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon( Icons.card_giftcard, color: Theme.of(context).colorScheme.primary, size: 24, ), const Gap(8), Text( 'Gift Subscriptions', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ], ), const Gap(12), // Purchase Gift Section Text( 'Purchase a Gift', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const Gap(8), _buildGiftPurchaseOptions(context, ref), const Gap(16), // Redeem Gift Section Text( 'Redeem a Gift', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const Gap(8), _buildGiftRedeemSection(context, ref), const Gap(16), // Gift History Text( 'Gift History', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const Gap(8), _buildGiftHistory(context, ref, sentGifts, receivedGifts), ], ).padding(all: 16), ); } Widget _buildGiftPurchaseOptions(BuildContext context, WidgetRef ref) { final tiers = [ { 'id': 'solian.stellar.primary', 'name': 'Stellar Gift', 'price': 'Same as membership', 'color': Colors.blue, }, { 'id': 'solian.stellar.nova', 'name': 'Nova Gift', 'price': 'Same as membership', 'color': Color.fromRGBO(57, 197, 187, 1), }, { 'id': 'solian.stellar.supernova', 'name': 'Supernova Gift', 'price': 'Same as membership', 'color': Colors.orange, }, ]; return Column( children: tiers.map((tier) { final tierColor = tier['color'] as Color; return Container( margin: const EdgeInsets.only(bottom: 8), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showPurchaseGiftDialog( context, ref, tier['id'] as String, ), borderRadius: BorderRadius.circular(8), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of( context, ).colorScheme.outline.withOpacity(0.2), width: 1, ), ), child: Row( children: [ Container( width: 4, height: 40, decoration: BoxDecoration( color: tierColor, borderRadius: BorderRadius.circular(2), ), ), const Gap(12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tier['name'] as String, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), Text( tier['price'] as String, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), ], ), ), Icon( Icons.arrow_forward_ios, size: 16, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ], ), ), ), ), ); }).toList(), ); } Widget _buildGiftRedeemSection(BuildContext context, WidgetRef ref) { final codeController = useTextEditingController(); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Enter gift code to redeem', style: Theme.of(context).textTheme.bodyMedium, ), const Gap(8), TextField( controller: codeController, decoration: InputDecoration( isDense: true, hintText: 'Enter gift code', border: OutlineInputBorder(), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.primary, ), ), suffixIcon: IconButton( icon: Icon(Icons.redeem), onPressed: () => _redeemGift(context, ref, codeController.text.trim()), ), ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onSubmitted: (code) => _redeemGift(context, ref, code.trim()), ), ], ), ); } Widget _buildGiftHistory( BuildContext context, WidgetRef ref, AsyncValue> sentGifts, AsyncValue> receivedGifts, ) { return Row( children: [ Expanded( child: OutlinedButton( onPressed: () => _showGiftHistorySheet(context, ref, sentGifts, true), child: Text('Sent Gifts'), ), ), const Gap(8), Expanded( child: OutlinedButton( onPressed: () => _showGiftHistorySheet(context, ref, receivedGifts, false), child: Text('Received Gifts'), ), ), ], ); } Future _showGiftHistorySheet( BuildContext context, WidgetRef ref, AsyncValue> giftsAsync, bool isSent, ) async { await showModalBottomSheet( isScrollControlled: true, useRootNavigator: true, context: context, builder: (context) => SheetScaffold( titleText: isSent ? 'Sent Gifts' : 'Received Gifts', child: giftsAsync.when( data: (gifts) => gifts.isEmpty ? Padding( padding: const EdgeInsets.all(16), child: Text( isSent ? 'No sent gifts' : 'No received gifts', ), ) : ListView.builder( itemCount: gifts.length, itemBuilder: (context, index) => _buildGiftItem( context, ref, gifts[index], isSent, ), ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), ), ), ); } Widget _buildGiftItem( BuildContext context, WidgetRef ref, SnWalletGift gift, bool isSent, ) { final statusText = _getGiftStatusText(gift.status); final statusColor = _getGiftStatusColor(context, gift.status); final canCancel = isSent && (gift.status == 0 || gift.status == 1); return Container( margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Row( children: [ Text( 'Code: ', style: TextStyle(fontWeight: FontWeight.w600), ), Expanded( child: Text( gift.giftCode, style: TextStyle( fontWeight: FontWeight.w600, fontFamily: 'monospace', ), ), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Row( spacing: 6, children: [ Text( statusText, style: TextStyle( color: statusColor, fontSize: 12, fontWeight: FontWeight.w600, ), ), if (gift.status == 2 && gift.redeemer != null) AccountPfcGestureDetector( uname: gift.redeemer!.name, child: ProfilePictureWidget( file: gift.redeemer!.profile.picture, radius: 8, ), ), ], ), ), ], ), const Gap(4), Text( 'Subscription: ${_getMembershipTierName(gift.subscriptionIdentifier)}', style: Theme.of(context).textTheme.bodySmall, ), if (gift.recipient != null && isSent) ...[ const Gap(4), Text( 'To: ${gift.recipient!.name}', style: Theme.of(context).textTheme.bodySmall, ), ], if (gift.gifter != null && !isSent) ...[ const Gap(4), Text( 'From: ${gift.gifter!.name}', style: Theme.of(context).textTheme.bodySmall, ), ], if (gift.message != null && gift.message!.isNotEmpty) ...[ const Gap(4), Text( 'Message: ${gift.message}', style: Theme.of(context).textTheme.bodySmall, ), ], const Gap(8), Row( mainAxisAlignment: MainAxisAlignment.end, spacing: 8, children: [ FilledButton.tonalIcon( onPressed: () async { await Clipboard.setData(ClipboardData(text: gift.giftCode)); if (context.mounted) { showSnackBar('Gift code copied to clipboard'); } }, icon: const Icon(Icons.copy, size: 16), label: Text('Copy'), style: FilledButton.styleFrom( visualDensity: VisualDensity.compact, ), ), if (canCancel) ...[ OutlinedButton.icon( onPressed: () => _cancelGift(context, ref, gift), icon: const Icon(Icons.cancel, size: 16), label: const Text('Cancel'), style: OutlinedButton.styleFrom( visualDensity: VisualDensity.compact, foregroundColor: Theme.of(context).colorScheme.error, side: BorderSide( color: Theme.of(context).colorScheme.error, ), ), ), ], ], ), ], ), ); } String _getGiftStatusText(int status) { switch (status) { case 0: return 'Created'; case 1: return 'Sent'; case 2: return 'Redeemed'; case 3: return 'Cancelled'; case 4: return 'Expired'; default: return 'Unknown'; } } Color _getGiftStatusColor(BuildContext context, int status) { switch (status) { case 0: return Colors.grey; case 1: return Colors.blue; case 2: return Colors.green; case 3: return Colors.red; case 4: return Colors.orange; default: return Theme.of(context).colorScheme.primary; } } Future _showPurchaseGiftDialog( BuildContext context, WidgetRef ref, String subscriptionId, ) async { final result = await showModalBottomSheet>( isScrollControlled: true, useRootNavigator: true, context: context, builder: (context) => const PurchaseGiftSheet(), ); if (result != null && context.mounted) { final recipient = result['recipient'] as SnAccount?; final message = result['message'] as String?; await _purchaseGift(context, ref, subscriptionId, recipient?.id, message); } } Future _purchaseGift( BuildContext context, WidgetRef ref, String subscriptionId, String? recipientId, String? message, ) async { final client = ref.watch(apiClientProvider); try { showLoadingModal(context); final resp = await client.post( '/id/subscriptions/gifts/purchase', data: { 'subscription_identifier': subscriptionId, if (recipientId != null) 'recipient_id': recipientId, 'payment_method': 'solian.wallet', 'payment_details': {'currency': 'golds'}, if (message != null) 'message': message, 'gift_duration_days': 30, 'subscription_duration_days': 30, }, options: Options(headers: {'X-Noop': true}), ); final gift = SnWalletGift.fromJson(resp.data); if (gift.status == 1) return; // Already paid final orderResp = await client.post( '/id/subscriptions/gifts/${gift.id}/order', ); final order = SnWalletOrder.fromJson(orderResp.data); if (context.mounted) hideLoadingModal(context); // Show payment overlay to complete the payment if (!context.mounted) return; final paidOrder = await PaymentOverlay.show( context: context, order: order, enableBiometric: true, ); if (context.mounted) showLoadingModal(context); if (paidOrder != null) { // Wait for server to handle order await Future.delayed(const Duration(seconds: 1)); // Get the updated gift final giftResp = await client.get('/id/subscriptions/gifts/${gift.id}'); final updatedGift = SnWalletGift.fromJson(giftResp.data); if (context.mounted) hideLoadingModal(context); // Show gift code bottom sheet if (context.mounted) { await showModalBottomSheet( context: context, builder: (context) => SheetScaffold( titleText: 'Gift Purchased!', child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of( context, ).colorScheme.outline.withOpacity(0.2), ), ), child: Row( children: [ Expanded( child: Text( updatedGift.giftCode, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, fontFamily: 'monospace', ), ), ), IconButton( onPressed: () async { await Clipboard.setData( ClipboardData(text: updatedGift.giftCode), ); if (context.mounted) { showSnackBar( 'Gift code copied to clipboard', ); } }, icon: const Icon(Icons.copy), tooltip: 'Copy gift code', ), ], ), ), const Gap(16), Text( 'Share this code with the recipient to redeem the gift.', style: Theme.of(context).textTheme.bodyMedium, ), if (updatedGift.recipientId == null) ...[ const Gap(8), Text( 'This is an open gift that anyone can redeem.', style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), ], const Gap(24), FilledButton( onPressed: () => Navigator.of(context).pop(), child: Text('OK'), ), ], ), ), ), ); } } ref.invalidate(accountSentGiftsProvider); } catch (err) { showErrorAlert(err); } finally { if (context.mounted) hideLoadingModal(context); } } Future _redeemGift( BuildContext context, WidgetRef ref, String giftCode, ) async { final client = ref.watch(apiClientProvider); try { showLoadingModal(context); // First check if gift can be redeemed final checkResp = await client.get( '/id/subscriptions/gifts/check/$giftCode', ); final checkData = checkResp.data as Map; if (!checkData['can_redeem']) { if (context.mounted) hideLoadingModal(context); showErrorAlert(checkData['error'] ?? 'Gift cannot be redeemed'); return; } // Redeem the gift await client.post( '/id/subscriptions/gifts/redeem', data: {'gift_code': giftCode}, ); if (context.mounted) { hideLoadingModal(context); await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Gift Redeemed!'), content: Text( 'You have successfully redeemed the gift. Your new subscription is now active.', ), actions: [ FilledButton( onPressed: () => Navigator.of(context).pop(), child: Text('OK'), ), ], ), ); } ref.invalidate(accountReceivedGiftsProvider); ref.invalidate(accountStellarSubscriptionProvider); ref.read(userInfoProvider.notifier).fetchUser(); } catch (err) { if (context.mounted) hideLoadingModal(context); showErrorAlert(err); } } Future _cancelGift( BuildContext context, WidgetRef ref, SnWalletGift gift, ) async { final confirm = await showConfirmAlert( 'Cancel Gift', 'Are you sure you want to cancel this gift? This action cannot be undone.', ); if (!confirm || !context.mounted) return; final client = ref.watch(apiClientProvider); try { showLoadingModal(context); await client.post('/id/subscriptions/gifts/${gift.id}/cancel'); ref.invalidate(accountSentGiftsProvider); if (context.mounted) { hideLoadingModal(context); showSnackBar('Gift cancelled successfully'); } } catch (err) { if (context.mounted) hideLoadingModal(context); showErrorAlert(err); } } }