diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index eaa8293..4262583 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -513,5 +513,12 @@ "membershipFeatureAllNova": "All Nova features", "membershipFeatureExclusiveContent": "Exclusive content", "membershipFeatureVipSupport": "VIP support", - "membershipCurrentBadge": "CURRENT" + "membershipCurrentBadge": "CURRENT", + "restorePurchase": "Restore Purchase", + "restorePurchaseDescription": "Enter your payment provider and order ID to restore your purchase.", + "provider": "Provider", + "selectProvider": "Select a provider", + "orderId": "Order ID", + "enterOrderId": "Enter your order ID", + "restore": "Restore" } diff --git a/lib/screens/account/leveling.dart b/lib/screens/account/leveling.dart index 9e68ae4..1c8d7a6 100644 --- a/lib/screens/account/leveling.dart +++ b/lib/screens/account/leveling.dart @@ -10,6 +10,7 @@ import 'package:island/pods/userinfo.dart'; import 'package:island/services/responsive.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/account/leveling_progress.dart'; +import 'package:island/widgets/account/restore_purchase_sheet.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/payment/payment_overlay.dart'; @@ -54,61 +55,69 @@ class LevelingScreen extends HookConsumerWidget { appBar: AppBar(title: Text('levelingProgress'.tr())), body: SingleChildScrollView( padding: getTabbedPadding(context, horizontal: 20, vertical: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Current Progress Card - LevelingProgressCard( - level: currentLevel, - experience: currentExp, - progress: progress, - ), - const Gap(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Current Progress Card + LevelingProgressCard( + level: currentLevel, + experience: currentExp, + progress: progress, + ), + const Gap(24), - // Level Stairs Graph - Text( - 'levelProgress'.tr(), - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), - ), - const Gap(16), - - // Stairs visualization with fixed height and horizontal scroll - _buildLevelStairs(context, currentLevel), - - const Gap(24), - - // Membership section - _buildMembershipSection(context, ref, stellarSubscription), - const Gap(16), - - // Unlocked features section - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'unlockedFeatures'.tr(), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + // Level Stairs Graph + Text( + 'levelProgress'.tr(), + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, ), - const Gap(8), - Text( - 'unlockedFeaturesDescription'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + ), + const Gap(16), + + // Stairs visualization with fixed height and horizontal scroll + _buildLevelStairs(context, currentLevel), + + const Gap(24), + + // Membership section + _buildMembershipSection(context, ref, stellarSubscription), + const Gap(16), + + // Unlocked features section + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), ), - ], - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'unlockedFeatures'.tr(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Gap(8), + Text( + 'unlockedFeaturesDescription'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], ), - ], + ), ), ), ); @@ -330,6 +339,17 @@ class LevelingScreen extends HookConsumerWidget { const Gap(12), _buildMembershipTiers(context, ref, membership), + const Gap(12), + + // Restore Purchase Button + OutlinedButton.icon( + onPressed: () => _showRestorePurchaseSheet(context, ref), + icon: const Icon(Icons.restore), + label: Text('restorePurchase'.tr()), + style: OutlinedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + ), ], ), ); @@ -565,6 +585,16 @@ class LevelingScreen extends HookConsumerWidget { } } + Future _showRestorePurchaseSheet( + BuildContext context, + WidgetRef ref, + ) async { + await showModalBottomSheet( + context: context, + builder: (context) => const RestorePurchaseSheet(), + ); + } + Future _purchaseMembership( BuildContext context, WidgetRef ref, diff --git a/lib/widgets/account/restore_purchase_sheet.dart b/lib/widgets/account/restore_purchase_sheet.dart new file mode 100644 index 0000000..ea1d2b6 --- /dev/null +++ b/lib/widgets/account/restore_purchase_sheet.dart @@ -0,0 +1,142 @@ +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/pods/network.dart'; +import 'package:island/screens/account/me/settings_connections.dart'; +import 'package:island/services/responsive.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class RestorePurchaseSheet extends HookConsumerWidget { + const RestorePurchaseSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedProvider = useState(null); + final orderIdController = useTextEditingController(); + final isLoading = useState(false); + + final providers = ['afdian']; + + Future restorePurchase() async { + if (selectedProvider.value == null || + orderIdController.text.trim().isEmpty) { + showErrorAlert('Please fill in all fields'); + return; + } + + isLoading.value = true; + try { + final client = ref.read(apiClientProvider); + await client.post( + '/wallet/subscriptions/order/restore/${selectedProvider.value!}', + data: {'order_id': orderIdController.text.trim()}, + ); + + if (context.mounted) { + Navigator.pop(context); + showSnackBar(context, 'Purchase restored successfully!'); + } + } catch (err) { + showErrorAlert(err); + } finally { + isLoading.value = false; + } + } + + return SheetScaffold( + titleText: 'restorePurchase'.tr(), + child: SingleChildScrollView( + padding: getTabbedPadding(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'restorePurchaseDescription'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const Gap(24), + + // Provider Selection + Text( + 'provider'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(8), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedProvider.value, + hint: Text('selectProvider'.tr()), + isExpanded: true, + padding: const EdgeInsets.symmetric(horizontal: 12), + items: + providers.map((provider) { + return DropdownMenuItem( + value: provider, + child: Row( + children: [ + getProviderIcon( + provider, + size: 20, + color: Theme.of(context).colorScheme.onSurface, + ), + const Gap(12), + Text(getLocalizedProviderName(provider)), + ], + ), + ); + }).toList(), + onChanged: (value) { + selectedProvider.value = value; + }, + ), + ), + ), + const Gap(16), + + // Order ID Input + Text( + 'orderId'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + const Gap(8), + TextField( + controller: orderIdController, + decoration: InputDecoration( + hintText: 'enterOrderId'.tr(), + border: const OutlineInputBorder(), + ), + ), + const Gap(24), + + // Restore Button + FilledButton( + onPressed: isLoading.value ? null : restorePurchase, + child: + isLoading.value + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text('restore'.tr()), + ), + const Gap(16), + ], + ).padding(all: 16), + ), + ); + } +}