import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/widgets/account/account_pfc.dart'; import 'package:island/widgets/account/account_picker.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/payment/payment_overlay.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; part 'wallet.g.dart'; @riverpod Future walletCurrent(Ref ref) async { try { final apiClient = ref.watch(apiClientProvider); final resp = await apiClient.get('/id/wallets'); return SnWallet.fromJson(resp.data); } catch (err) { if (err is DioException && err.response?.statusCode == 404) { return null; } rethrow; } } @riverpod Future walletStats(Ref ref) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/id/wallets/stats'); return SnWalletStats.fromJson(resp.data); } class CreateFundSheet extends StatefulWidget { const CreateFundSheet({super.key}); @override State createState() => _CreateFundSheetState(); } class _CreateFundSheetState extends State { final amountController = TextEditingController(); final messageController = TextEditingController(); String selectedCurrency = 'golds'; int selectedSplitType = 0; // 0: even, 1: random List selectedRecipients = []; @override void dispose() { amountController.dispose(); messageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SheetScaffold( titleText: 'createFund'.tr(), child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Amount Section Text( 'fundAmount'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(8), TextField( controller: amountController, keyboardType: TextInputType.numberWithOptions( decimal: true, ), inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'^\d+\.?\d{0,2}'), ), ], decoration: InputDecoration( labelText: 'enterAmount'.tr(), hintText: '0.00', prefixIcon: Icon(kCurrencyIconData[selectedCurrency]), 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, ), ), ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), const Gap(16), // Currency Selection Text( 'selectCurrency'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(8), DropdownButtonFormField( value: selectedCurrency, decoration: InputDecoration( 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, ), ), ), items: kCurrencyIconData.keys.map((currency) { return DropdownMenuItem( value: currency, child: Row( children: [ Icon(kCurrencyIconData[currency]), const Gap(8), Text( 'walletCurrency${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}' .tr(), ), ], ), ); }).toList(), onChanged: (value) { if (value != null) { setState(() => selectedCurrency = value); } }, ), // Split Type Section (only show when there are 2+ recipients) if (selectedRecipients.length >= 2) ...[ const Gap(16), Text( 'splitType'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(8), Row( children: [ Expanded( child: RadioListTile( title: Text('evenSplit'.tr()), subtitle: Text('equalAmountEach'.tr()), value: 0, groupValue: selectedSplitType, onChanged: (value) { if (value != null) { setState(() => selectedSplitType = value); } }, ), ), Expanded( child: RadioListTile( title: Text('randomSplit'.tr()), subtitle: Text('randomAmountEach'.tr()), value: 1, groupValue: selectedSplitType, onChanged: (value) { if (value != null) { setState(() => selectedSplitType = value); } }, ), ), ], ), ], const Gap(16), // Recipient Selection Section Text( 'selectRecipients'.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: selectedRecipients.isNotEmpty ? Column( children: [ ...selectedRecipients.map((recipient) { return ListTile( contentPadding: const EdgeInsets.only( left: 20, right: 12, ), leading: ProfilePictureWidget( file: recipient.profile.picture, ), title: Text( recipient.nick, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), subtitle: Text( 'selectedRecipient'.tr(), style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), trailing: IconButton( onPressed: () => setState( () => selectedRecipients.remove( recipient, ), ), icon: Icon( Icons.clear, color: Theme.of(context).colorScheme.error, ), tooltip: 'Remove recipient', ), ); }), if (selectedRecipients.length < 10) OutlinedButton.icon( onPressed: () async { final recipient = await showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, builder: (context) => const AccountPickerSheet(), ); if (recipient != null && !selectedRecipients.contains( recipient, )) { setState( () => selectedRecipients.add(recipient), ); } }, icon: const Icon(Icons.person_add), label: Text('addRecipient'.tr()), style: OutlinedButton.styleFrom( minimumSize: const Size( double.infinity, 48, ), ), ).padding(all: 16), ], ) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.person_add_outlined, size: 48, color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), const Gap(8), Text( 'noRecipientsSelected'.tr(), style: Theme.of( context, ).textTheme.bodyMedium?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), ), const Gap(4), Text( 'selectRecipientsToSendFund'.tr(), style: Theme.of( context, ).textTheme.bodySmall?.copyWith( color: Theme.of( context, ).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ).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 && !selectedRecipients.contains(recipient)) { setState(() => selectedRecipients.add(recipient)); } }, icon: const Icon(Icons.person_search), label: Text( selectedRecipients.isNotEmpty ? 'addMoreRecipients'.tr() : 'selectRecipients'.tr(), ), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ), const Gap(16), // 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: 'personalMessage'.tr(), hintText: 'addPersonalMessageForRecipients'.tr(), 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(), child: Text('cancel'.tr()), ), ), const Gap(8), Expanded( child: FilledButton( onPressed: _createFund, child: Text('createFund'.tr()), ), ), ], ), ), ], ), ); } Future _showPinVerificationDialog(BuildContext context) async { String enteredPin = ''; await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, useSafeArea: true, builder: (context) => Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.vertical( top: Radius.circular(16), ), ), child: Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SheetScaffold( titleText: 'enterPinToConfirm'.tr(), heightFactor: 0.5, child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'enterPinToConfirm'.tr(), style: Theme.of(context).textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), const Gap(24), OtpTextField( numberOfFields: 6, borderColor: Theme.of(context).colorScheme.outline, focusedBorderColor: Theme.of(context).colorScheme.primary, showFieldAsBox: true, obscureText: true, keyboardType: TextInputType.number, fieldWidth: 48, fieldHeight: 56, borderRadius: BorderRadius.circular(8), borderWidth: 1, textStyle: Theme.of(context) .textTheme .headlineSmall ?.copyWith(fontWeight: FontWeight.w600), onSubmit: (pin) { enteredPin = pin; Navigator.of(context).pop(pin); }, onCodeChanged: (String code) { enteredPin = code; }, ), ], ), ), const Gap(24), Row( children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.of(context).pop(), child: Text('cancel'.tr()), ), ), if (enteredPin.length == 6) ...[ const Gap(12), Expanded( child: FilledButton( onPressed: () { Navigator.of(context).pop(enteredPin); }, child: Text('confirm'.tr()), ), ), ], ], ), ], ), ), ), ), ), ); return enteredPin.isNotEmpty ? enteredPin : null; } Future _createFund() async { final amount = double.tryParse(amountController.text); if (amount == null || amount <= 0) { showErrorAlert('invalidAmount'.tr()); return; } if (selectedRecipients.isEmpty) { showErrorAlert('noRecipientsSelected'.tr()); return; } final data = { 'currency': selectedCurrency, 'total_amount': amount, 'split_type': selectedSplitType, 'recipient_account_ids': selectedRecipients.map((r) => r.id).toList(), 'message': messageController.text.trim().isEmpty ? null : messageController.text.trim(), 'pin_code': '', // Will be filled by PIN verification }; // Ask for PIN confirmation before creating fund final enteredPin = await _showPinVerificationDialog(context); if (enteredPin == null || enteredPin.isEmpty) return; // Add PIN to the fund data data['pin_code'] = enteredPin; if (mounted) Navigator.of(context).pop(data); } } const Map kCurrencyIconData = { 'points': Symbols.toll, 'golds': Symbols.attach_money, }; @riverpod class TransactionListNotifier extends _$TransactionListNotifier with CursorPagingNotifierMixin { static const int _pageSize = 20; @override Future> build() => fetch(cursor: null); @override Future> fetch({ required String? cursor, }) async { final client = ref.read(apiClientProvider); final offset = cursor == null ? 0 : int.parse(cursor); final queryParams = {'offset': offset, 'take': _pageSize}; final response = await client.get( '/id/wallets/transactions', queryParameters: queryParams, ); final total = int.parse(response.headers.value('X-Total') ?? '0'); final List data = response.data; final transactions = data.map((json) => SnTransaction.fromJson(json)).toList(); final hasMore = offset + transactions.length < total; final nextCursor = hasMore ? (offset + transactions.length).toString() : null; return CursorPagingData( items: transactions, hasMore: hasMore, nextCursor: nextCursor, ); } } @riverpod Future> walletFunds( Ref ref, { int offset = 0, int take = 20, }) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/id/wallets/funds?offset=$offset&take=$take'); return (resp.data as List).map((e) => SnWalletFund.fromJson(e)).toList(); } @riverpod Future> walletFundRecipients( Ref ref, { int offset = 0, int take = 20, }) async { final client = ref.watch(apiClientProvider); final resp = await client.get( '/id/wallets/funds/recipients?offset=$offset&take=$take', ); return (resp.data as List) .map((e) => SnWalletFundRecipient.fromJson(e)) .toList(); } @riverpod Future walletFund(Ref ref, String fundId) async { final client = ref.watch(apiClientProvider); final resp = await client.get('/id/wallets/funds/$fundId'); return SnWalletFund.fromJson(resp.data); } class TransactionDetailSheet extends StatelessWidget { final SnTransaction transaction; const TransactionDetailSheet({super.key, required this.transaction}); @override Widget build(BuildContext context) { final isIncome = transaction.payeeWalletId == null || transaction.payeeWallet?.accountId == null; return SheetScaffold( titleText: 'transactionDetails'.tr(), heightFactor: 0.75, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Amount Text( 'amount'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), Text( '${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isIncome ? Colors.green : Colors.red, ), ), const Gap(16), // Remarks if (transaction.remarks != null && transaction.remarks!.isNotEmpty) ...[ Text( 'remarks'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), Text( transaction.remarks!, style: Theme.of(context).textTheme.bodyMedium, ), const Gap(16), ], // Date Text( 'date'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), Text( DateFormat.yMd().add_Hm().format(transaction.createdAt), style: Theme.of(context).textTheme.bodyMedium, ), const Gap(16), // Payer Text( 'payer'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), AccountPfcGestureDetector( uname: transaction.payerWallet?.account?.name, child: Row( spacing: 8, children: [ if (transaction.payerWallet?.account != null) ProfilePictureWidget( file: transaction.payerWallet!.account!.profile.picture, radius: 12, ), Text( transaction.payerWallet?.account?.nick ?? 'systemWallet'.tr(), style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), const Gap(16), // Payee Text( 'payee'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), AccountPfcGestureDetector( uname: transaction.payeeWallet?.account?.name, child: Row( spacing: 8, children: [ if (transaction.payeeWallet?.account != null) ProfilePictureWidget( file: transaction.payeeWallet!.account!.profile.picture, radius: 12, ), Text( transaction.payeeWallet?.account?.nick ?? 'systemWallet'.tr(), style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), const Gap(16), // Transaction Type Text( 'transactionType'.tr(), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.primary, ), ), const Gap(4), Text( _getTransactionTypeText(transaction.type), style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), ); } String _getTransactionTypeText(int type) { // Assuming types: 0: transfer, 1: payment, etc. Adjust based on actual types switch (type) { case 0: return 'transfer'.tr(); case 1: return 'payment'.tr(); default: return 'unknown'.tr(); } } } class WalletScreen extends HookConsumerWidget { const WalletScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final wallet = ref.watch(walletCurrentProvider); final tabController = useTabController(initialLength: 2); Future createWallet() async { final client = ref.read(apiClientProvider); try { await client.post('/id/wallets'); ref.invalidate(walletCurrentProvider); } catch (err) { showErrorAlert(err); } } Future createFund() async { final result = await showModalBottomSheet>( context: context, useRootNavigator: true, isScrollControlled: true, builder: (context) => const CreateFundSheet(), ); if (result != null && context.mounted) { await _handleFundCreation(context, ref, result); } } String getCurrencyTranslationKey(String currency, {bool isShort = false}) { return 'walletCurrency${isShort ? 'Short' : ''}${currency[0].toUpperCase()}${currency.substring(1).toLowerCase()}'; } return AppScaffold( appBar: AppBar( title: Text('wallet').tr(), actions: [ IconButton( icon: const Icon(Symbols.add), onPressed: createFund, tooltip: 'createFund'.tr(), ), const Gap(8), ], ), body: wallet.when( data: (data) { if (data == null) { return ConstrainedBox( constraints: BoxConstraints(maxWidth: 280), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('walletNotFound').tr().fontSize(16).bold(), Text('walletCreateHint', textAlign: TextAlign.center).tr(), TextButton( onPressed: createWallet, child: Text('walletCreate').tr(), ), ], ), ).center(); } return NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ // Wallet Overview SliverToBoxAdapter( child: Column( spacing: 8, children: [ // Wallet Stats _buildCompactStatsWidget(context, ref), // Pockets Card( margin: EdgeInsets.zero, child: Column( children: [ ...data.pockets.map( (pocket) => ListTile( leading: Icon( kCurrencyIconData[pocket.currency] ?? Symbols.universal_currency_alt, ), title: Text( getCurrencyTranslationKey( pocket.currency, ), ).tr(), subtitle: Text( '${pocket.amount.toStringAsFixed(2)} ${getCurrencyTranslationKey(pocket.currency, isShort: true).tr()}', ), ), ), ], ), ), ], ).padding(horizontal: 12, top: 12), ), // Tab Bar SliverToBoxAdapter( child: TabBar( controller: tabController, tabs: [ Tab(text: 'transactions'.tr()), Tab(text: 'myFunds'.tr()), ], ), ), ], body: TabBarView( controller: tabController, children: [ // Transactions Tab CustomScrollView( slivers: [ PagingHelperSliverView( provider: transactionListNotifierProvider, futureRefreshable: transactionListNotifierProvider.future, notifierRefreshable: transactionListNotifierProvider.notifier, contentBuilder: ( data, widgetCount, endItemView, ) => SliverList.builder( itemCount: widgetCount, itemBuilder: (context, index) { if (index == widgetCount - 1) { return endItemView; } final transaction = data.items[index]; final isIncome = transaction.payeeWalletId == wallet.value?.id; return InkWell( onTap: () { showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, builder: (context) => TransactionDetailSheet( transaction: transaction, ), ); }, child: ListTile( key: ValueKey(transaction.id), leading: Icon( isIncome ? Symbols.payment_arrow_down : Symbols.paid, ), title: Text( transaction.remarks ?? '', maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( DateFormat.yMd().add_Hm().format( transaction.createdAt, ), ), trailing: Text( '${isIncome ? '+' : '-'}${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', style: TextStyle( color: isIncome ? Colors.green : Colors.red, ), ), ), ); }, ), ), ], ), // My Funds Tab _buildFundsList(context, ref), ], ), ); }, error: (error, stackTrace) => ResponseErrorWidget( error: error, onRetry: () => ref.invalidate(walletCurrentProvider), ), loading: () => const Center(child: CircularProgressIndicator()), ), ); } Widget _buildStatItem( BuildContext context, String label, String value, IconData icon, ) { return Column( children: [ Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary), const Gap(4), Text( value, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, ), ], ); } Widget _buildFundsList(BuildContext context, WidgetRef ref) { final funds = ref.watch(walletFundsProvider()); return funds.when( data: (fundList) { if (fundList.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Symbols.celebration, size: 48, color: Theme.of(context).colorScheme.outline, ), const Gap(16), Text( 'noFundsCreated'.tr(), style: Theme.of(context).textTheme.titleMedium, ), const Gap(8), Text( 'createYourFirstFund'.tr(), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, ), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(16), itemCount: fundList.length, itemBuilder: (context, index) { final fund = fundList[index]; final claimedCount = fund.recipients.where((r) => r.isReceived).length; final totalRecipients = fund.recipients.length; return Card( margin: const EdgeInsets.only(bottom: 8), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Symbols.celebration, color: Theme.of(context).colorScheme.primary, ), const Gap(8), Expanded( child: Text( '${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: _getFundStatusColor( context, fund.status, ).withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( _getFundStatusText(fund.status), style: TextStyle( color: _getFundStatusColor(context, fund.status), fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ], ), const Gap(8), Text( '${'recipients'.tr()}: $claimedCount/$totalRecipients', style: Theme.of(context).textTheme.bodyMedium, ), if (fund.message != null && fund.message!.isNotEmpty) ...[ const Gap(4), Text( fund.message!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], const Gap(8), Text( DateFormat.yMd().add_Hm().format(fund.createdAt), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), ), ); }, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), ); } Widget _buildCompactStatsWidget(BuildContext context, WidgetRef ref) { final stats = ref.watch(walletStatsProvider); return stats.when( data: (statsData) { return Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'walletStats'.tr(), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const Spacer(), Text( '${DateFormat.yMd().format(statsData.periodBegin)} - ${DateFormat.yMd().format(statsData.periodEnd)}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ], ), const Gap(12), Row( children: [ Expanded( child: _buildStatItem( context, 'totalTransactions'.tr(), statsData.totalTransactions.toString(), Symbols.swap_horiz, ), ), const Gap(16), Expanded( child: _buildStatItem( context, 'totalOrders'.tr(), statsData.totalOrders.toString(), Symbols.receipt_long, ), ), ], ), const Gap(12), Row( children: [ Expanded( child: _buildStatItem( context, 'totalIncome'.tr(), statsData.totalIncome.toStringAsFixed(2), Symbols.arrow_upward, ), ), const Gap(16), Expanded( child: _buildStatItem( context, 'totalOutgoing'.tr(), statsData.totalOutgoing.toStringAsFixed(2), Symbols.arrow_downward, ), ), const Gap(16), Expanded( child: _buildStatItem( context, 'netBalance'.tr(), statsData.sum.toStringAsFixed(2), Symbols.account_balance, ), ), ], ), ], ), ), ); }, loading: () => Card( margin: EdgeInsets.zero, child: const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), ), error: (error, stack) => Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Center(child: Text('Error loading stats')), ), ), ); } Future _handleFundCreation( BuildContext context, WidgetRef ref, Map fundData, ) async { final client = ref.read(apiClientProvider); try { showLoadingModal(context); final resp = await client.post( '/id/wallets/funds', data: fundData, options: Options(headers: {'X-Noop': true}), ); final fund = SnWalletFund.fromJson(resp.data); if (fund.status == 0) return; // Already created final orderResp = await client.post('/id/wallets/funds/${fund.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)); ref.invalidate(walletFundsProvider); ref.invalidate(walletCurrentProvider); if (context.mounted) { showSnackBar('fundCreatedSuccessfully'.tr()); } } } catch (err) { showErrorAlert(err); } finally { if (context.mounted) hideLoadingModal(context); } } String _getFundStatusText(int status) { switch (status) { case 0: return 'fundStatusCreated'.tr(); case 1: return 'fundStatusPartial'.tr(); case 2: return 'fundStatusCompleted'.tr(); case 3: return 'fundStatusExpired'.tr(); default: return 'fundStatusUnknown'.tr(); } } Color _getFundStatusColor(BuildContext context, int status) { switch (status) { case 0: return Colors.blue; case 1: return Colors.orange; case 2: return Colors.green; case 3: return Colors.red; default: return Theme.of(context).colorScheme.primary; } } }