From 3f9881e943dafbfd021ae8a0eafcd37cd71bcf2b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 16 Nov 2025 23:43:28 +0800 Subject: [PATCH] :sparkles: Fund creation and attach found to message --- assets/i18n/en-US.json | 6 +- lib/pods/chat/messages_notifier.dart | 3 + lib/screens/chat/room.dart | 10 +- lib/screens/posts/post_search.dart | 8 +- lib/screens/wallet.dart | 89 +- lib/screens/wallet.dart.backup | 1012 ----------------- lib/widgets/account/stellar_program_tab.dart | 10 +- lib/widgets/chat/chat_input.dart | 135 ++- lib/widgets/chat/chat_link_attachments.dart | 6 +- lib/widgets/debug_sheet.dart | 6 +- lib/widgets/poll/poll_submit.dart | 6 +- lib/widgets/post/compose_fund.dart | 388 +++++++ .../post/compose_link_attachments.dart | 6 +- 13 files changed, 589 insertions(+), 1096 deletions(-) delete mode 100644 lib/screens/wallet.dart.backup create mode 100644 lib/widgets/post/compose_fund.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index cddc3365..0039bd9e 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1329,5 +1329,9 @@ "more": "More", "collapse": "Collapse", "pollConfirmDiscard": "Are you sure you want to leave? All the poll data you're editing will not be saved.", - "discard": "Discard" + "discard": "Discard", + "fund": "Fund", + "fundsRecent": "Recent Funds", + "fundCreateNew": "Create New", + "fundCreateNewHint": "Create a new fund for your message. Select recipients and amount." } diff --git a/lib/pods/chat/messages_notifier.dart b/lib/pods/chat/messages_notifier.dart index ddf09e22..e404bca2 100644 --- a/lib/pods/chat/messages_notifier.dart +++ b/lib/pods/chat/messages_notifier.dart @@ -9,6 +9,7 @@ import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; import "package:island/models/poll.dart"; +import "package:island/models/wallet.dart"; import "package:island/pods/database.dart"; import "package:island/pods/lifecycle.dart"; import "package:island/pods/network.dart"; @@ -439,6 +440,7 @@ class MessagesNotifier extends _$MessagesNotifier { String content, List attachments, { SnPoll? poll, + SnWalletFund? fund, SnChatMessage? editingTo, SnChatMessage? forwardingTo, SnChatMessage? replyingTo, @@ -501,6 +503,7 @@ class MessagesNotifier extends _$MessagesNotifier { 'replied_message_id': replyingTo?.id, 'forwarded_message_id': forwardingTo?.id, 'poll_id': poll?.id, + 'fund_id': fund?.id, 'meta': {}, 'nonce': nonce, }, diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 45d7007c..8bdcebd5 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -12,6 +12,7 @@ import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; import "package:island/models/poll.dart"; +import "package:island/models/wallet.dart"; import "package:island/pods/chat/chat_rooms.dart"; import "package:island/pods/chat/chat_subscribe.dart"; import "package:island/pods/chat/messages_notifier.dart"; @@ -172,6 +173,7 @@ class ChatRoomScreen extends HookConsumerWidget { final messageForwardingTo = useState(null); final messageEditingTo = useState(null); final selectedPoll = useState(null); + final selectedFund = useState(null); final attachments = useState>([]); final attachmentProgress = useState>>({}); @@ -288,12 +290,14 @@ class ChatRoomScreen extends HookConsumerWidget { void sendMessage() { if (messageController.text.trim().isNotEmpty || attachments.value.isNotEmpty || - selectedPoll.value != null) { + selectedPoll.value != null || + selectedFund.value != null) { messagesNotifier.sendMessage( ref, messageController.text.trim(), attachments.value, poll: selectedPoll.value, + fund: selectedFund.value, editingTo: messageEditingTo.value, forwardingTo: messageForwardingTo.value, replyingTo: messageReplyingTo.value, @@ -309,6 +313,7 @@ class ChatRoomScreen extends HookConsumerWidget { messageReplyingTo.value = null; messageForwardingTo.value = null; selectedPoll.value = null; + selectedFund.value = null; attachments.value = []; } } @@ -1252,12 +1257,15 @@ class ChatRoomScreen extends HookConsumerWidget { messageReplyingTo.value = null; messageForwardingTo.value = null; selectedPoll.value = null; + selectedFund.value = null; }, messageEditingTo: messageEditingTo.value, messageReplyingTo: messageReplyingTo.value, messageForwardingTo: messageForwardingTo.value, selectedPoll: selectedPoll.value, onPollSelected: (poll) => selectedPoll.value = poll, + selectedFund: selectedFund.value, + onFundSelected: (fund) => selectedFund.value = fund, onPickFile: (bool isPhoto) { if (isPhoto) { pickPhotoMedia(); diff --git a/lib/screens/posts/post_search.dart b/lib/screens/posts/post_search.dart index 19ff6253..a6a248ee 100644 --- a/lib/screens/posts/post_search.dart +++ b/lib/screens/posts/post_search.dart @@ -237,7 +237,9 @@ class PostSearchScreen extends HookConsumerWidget { controller: pubNameController, decoration: InputDecoration( labelText: 'pubName'.tr(), - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), ), onChanged: (value) => onSearchWithFilters(searchController.text), @@ -247,7 +249,9 @@ class PostSearchScreen extends HookConsumerWidget { controller: realmController, decoration: InputDecoration( labelText: 'realm'.tr(), - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), ), onChanged: (value) => onSearchWithFilters(searchController.text), diff --git a/lib/screens/wallet.dart b/lib/screens/wallet.dart index 76f5de2a..4bc4916a 100644 --- a/lib/screens/wallet.dart +++ b/lib/screens/wallet.dart @@ -103,17 +103,9 @@ class _CreateFundSheetState extends State { 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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), @@ -136,17 +128,9 @@ class _CreateFundSheetState extends State { 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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), @@ -370,17 +354,9 @@ class _CreateFundSheetState extends State { 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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), @@ -526,11 +502,6 @@ class _CreateFundSheetState extends State { return; } - if (selectedRecipients.isEmpty) { - showErrorAlert('noRecipientsSelected'.tr()); - return; - } - final data = { 'currency': selectedCurrency, 'total_amount': amount, @@ -610,17 +581,9 @@ class _CreateTransferSheetState extends State { 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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), @@ -643,17 +606,9 @@ class _CreateTransferSheetState extends State { 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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), @@ -817,17 +772,9 @@ class _CreateTransferSheetState extends State { labelText: 'transferRemark'.tr(), hintText: 'addRemarkForTransfer'.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, + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), ), ), ), diff --git a/lib/screens/wallet.dart.backup b/lib/screens/wallet.dart.backup deleted file mode 100644 index 2926ed49..00000000 --- a/lib/screens/wallet.dart.backup +++ /dev/null @@ -1,1012 +0,0 @@ -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_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'; - -part 'wallet.g.dart'; - -@riverpod -Future walletCurrent(Ref ref) async { - try { - final apiClient = ref.watch(apiClientProvider); - final resp = await apiClient.get('/pass/wallets'); - return SnWallet.fromJson(resp.data); - } catch (err) { - if (err is DioException && err.response?.statusCode == 404) { - return null; - } - rethrow; - } -} - -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 _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(), - }; - - 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( - '/pass/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('/pass/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( - '/pass/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('/pass/wallets/funds/$fundId'); - return SnWalletFund.fromJson(resp.data); -} - -@riverpod -Future> walletFundStats(Ref ref) async { - final client = ref.watch(apiClientProvider); - final resp = await client.get('/pass/wallets/funds/stats'); - return resp.data as Map; -} - -class WalletScreen extends HookConsumerWidget { - const WalletScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final wallet = ref.watch(walletCurrentProvider); - final tabController = useTabController(initialLength: 3); - final fundStats = ref.watch(walletFundStatsProvider); - - Future createWallet() async { - final client = ref.read(apiClientProvider); - try { - await client.post('/pass/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 Column( - children: [ - // Wallet Overview - Column( - spacing: 8, - children: [ - // Pockets - ...data.pockets.map( - (pocket) => Card( - margin: EdgeInsets.zero, - child: 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()}', - ), - ), - ), - ), - - // Fund Stats - fundStats.when( - data: - (stats) => Card( - margin: EdgeInsets.zero, - 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), - Text( - 'fundOverview'.tr(), - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const Gap(8), - Row( - children: [ - Expanded( - child: _buildStatItem( - context, - 'totalFundsSent'.tr(), - '${stats['total_sent'] ?? 0}', - Icons.send, - ), - ), - Expanded( - child: _buildStatItem( - context, - 'totalFundsReceived'.tr(), - '${stats['total_received'] ?? 0}', - Icons.call_received, - ), - ), - ], - ), - ], - ), - ), - ), - loading: - () => const Card( - margin: EdgeInsets.zero, - child: Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ), - ), - error: (error, stack) => const SizedBox.shrink(), - ), - ], - ).padding(horizontal: 16, vertical: 16), - - // Tab Bar - TabBar( - controller: tabController, - tabs: [ - Tab(text: 'transactions'.tr()), - Tab(text: 'myFunds'.tr()), - ], - ), - - // Tab Content - Expanded( - child: TabBarView( - controller: tabController, - children: [ - // Transactions Tab - PagingHelperView( - provider: transactionListNotifierProvider, - futureRefreshable: transactionListNotifierProvider.future, - notifierRefreshable: - transactionListNotifierProvider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => ListView.builder( - padding: EdgeInsets.zero, - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - - final transaction = data.items[index]; - final isIncome = - transaction.payeeWalletId == wallet.value?.id; - - return ListTile( - key: ValueKey(transaction.id), - leading: Icon( - isIncome - ? Symbols.arrow_upward - : Symbols.arrow_downward, - ), - title: Text(transaction.remarks ?? ''), - 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.status == 1).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')), - ); - } - - Future _handleFundCreation( - BuildContext context, - WidgetRef ref, - Map fundData, - ) async { - final client = ref.read(apiClientProvider); - try { - showLoadingModal(context); - final resp = await client.post( - '/pass/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('/pass/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(walletFundStatsProvider); - 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; - } - } -} diff --git a/lib/widgets/account/stellar_program_tab.dart b/lib/widgets/account/stellar_program_tab.dart index b851c3ee..62f071ee 100644 --- a/lib/widgets/account/stellar_program_tab.dart +++ b/lib/widgets/account/stellar_program_tab.dart @@ -240,7 +240,11 @@ class _PurchaseGiftSheetState extends State { labelText: 'personalMessage'.tr(), hintText: 'addPersonalMessageForRecipient'.tr(), alignLabelWithHint: true, - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of( @@ -925,7 +929,9 @@ class StellarProgramTab extends HookConsumerWidget { decoration: InputDecoration( isDense: true, hintText: 'enterGiftCode'.tr(), - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index 26c6f0f8..77415d58 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -13,6 +13,7 @@ import "package:island/models/chat.dart"; import "package:island/models/file.dart"; import "package:island/models/poll.dart"; import "package:island/models/publisher.dart"; +import "package:island/models/wallet.dart"; import "package:island/models/realm.dart"; import "package:island/models/sticker.dart"; import "package:island/pods/config.dart"; @@ -28,6 +29,7 @@ import "package:material_symbols_icons/symbols.dart"; import "package:island/widgets/stickers/sticker_picker.dart"; import "package:island/pods/chat/chat_subscribe.dart"; import "package:island/widgets/post/compose_poll.dart"; +import "package:island/widgets/post/compose_fund.dart"; void _insertPlaceholder(TextEditingController controller, String placeholder) { final text = controller.text; @@ -47,11 +49,15 @@ class _ExpandedSection extends StatelessWidget { final TextEditingController messageController; final SnPoll? selectedPoll; final Function(SnPoll?) onPollSelected; + final SnWalletFund? selectedFund; + final Function(SnWalletFund?) onFundSelected; const _ExpandedSection({ required this.messageController, this.selectedPoll, required this.onPollSelected, + this.selectedFund, + required this.onFundSelected, }); @override @@ -132,7 +138,18 @@ class _ExpandedSection extends StatelessWidget { borderRadius: const BorderRadius.all( Radius.circular(8), ), - onTap: () {}, + onTap: () async { + final fund = + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => const ComposeFundSheet(), + ); + if (fund != null) { + onFundSelected(fund); + } + }, child: Card( margin: EdgeInsets.zero, color: @@ -145,7 +162,7 @@ class _ExpandedSection extends StatelessWidget { Icon(Symbols.currency_exchange), const Gap(4), Text( - 'Fund', + 'fund'.tr(), style: Theme.of(context).textTheme.bodySmall, ), @@ -195,6 +212,8 @@ class ChatInput extends HookConsumerWidget { final Map> attachmentProgress; final SnPoll? selectedPoll; final Function(SnPoll?) onPollSelected; + final SnWalletFund? selectedFund; + final Function(SnWalletFund?) onFundSelected; const ChatInput({ super.key, @@ -217,6 +236,8 @@ class ChatInput extends HookConsumerWidget { required this.attachmentProgress, this.selectedPoll, required this.onPollSelected, + this.selectedFund, + required this.onFundSelected, }); @override @@ -515,6 +536,114 @@ class ChatInput extends HookConsumerWidget { key: ValueKey('no-selected-poll'), ), ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (Widget child, Animation animation) { + return SlideTransition( + position: Tween( + begin: const Offset(0, -0.25), + end: Offset.zero, + ).animate(animation), + child: FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axisAlignment: -1.0, + child: child, + ), + ), + ); + }, + child: + selectedFund != null + ? Container( + key: const ValueKey('selected-fund'), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: + Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + margin: const EdgeInsets.only( + left: 8, + right: 8, + top: 8, + bottom: 8, + ), + child: Row( + children: [ + Icon( + Symbols.currency_exchange, + size: 18, + color: Theme.of(context).colorScheme.primary, + ), + const Gap(8), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${selectedFund!.totalAmount.toStringAsFixed(2)} ${selectedFund!.currency}', + style: Theme.of( + context, + ).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (selectedFund!.message != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + selectedFund!.message!, + style: Theme.of( + context, + ).textTheme.bodySmall!.copyWith( + fontSize: 10, + color: + Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.close, size: 18), + onPressed: () => onFundSelected(null), + tooltip: 'clear'.tr(), + ), + ), + ], + ), + ) + : const SizedBox.shrink( + key: ValueKey('no-selected-fund'), + ), + ), AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOutCubic, @@ -904,6 +1033,8 @@ class ChatInput extends HookConsumerWidget { messageController: messageController, selectedPoll: selectedPoll, onPollSelected: onPollSelected, + selectedFund: selectedFund, + onFundSelected: onFundSelected, ) : const SizedBox.shrink(key: ValueKey('collapsed')), ), diff --git a/lib/widgets/chat/chat_link_attachments.dart b/lib/widgets/chat/chat_link_attachments.dart index 70187a73..dfe5500b 100644 --- a/lib/widgets/chat/chat_link_attachments.dart +++ b/lib/widgets/chat/chat_link_attachments.dart @@ -144,7 +144,11 @@ class ChatLinkAttachment extends HookConsumerWidget { helperText: 'fileIdHint'.tr(), helperMaxLines: 3, errorText: errorMessage.value, - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + ), ), onTapOutside: (_) => diff --git a/lib/widgets/debug_sheet.dart b/lib/widgets/debug_sheet.dart index a211a3ed..a6bb79c5 100644 --- a/lib/widgets/debug_sheet.dart +++ b/lib/widgets/debug_sheet.dart @@ -28,7 +28,9 @@ Future _showSetTokenDialog(BuildContext context, WidgetRef ref) async { controller: controller, decoration: const InputDecoration( hintText: 'Enter access token', - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), ), autofocus: true, ), @@ -96,7 +98,7 @@ class DebugSheet extends HookConsumerWidget { 'Unable to check for updates', ); } - } + }, ), const Divider(height: 8), ListTile( diff --git a/lib/widgets/poll/poll_submit.dart b/lib/widgets/poll/poll_submit.dart index e241a76d..da961b80 100644 --- a/lib/widgets/poll/poll_submit.dart +++ b/lib/widgets/poll/poll_submit.dart @@ -455,7 +455,11 @@ class _PollSubmitState extends ConsumerState { return TextField( controller: _textController, maxLines: 6, - decoration: const InputDecoration(border: OutlineInputBorder()), + decoration: const InputDecoration( + border: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + ), ); } diff --git a/lib/widgets/post/compose_fund.dart b/lib/widgets/post/compose_fund.dart new file mode 100644 index 00000000..c6229493 --- /dev/null +++ b/lib/widgets/post/compose_fund.dart @@ -0,0 +1,388 @@ +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.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/wallet.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/screens/wallet.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:island/widgets/payment/payment_overlay.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; + +/// Bottom sheet for selecting or creating a fund. Returns SnWalletFund via Navigator.pop. +class ComposeFundSheet extends HookConsumerWidget { + const ComposeFundSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPushing = useState(false); + final errorText = useState(null); + + return SheetScaffold( + heightFactor: 0.6, + titleText: 'fund'.tr(), + child: DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TabBar( + tabs: [ + Tab(text: 'fundsRecent'.tr()), + Tab(text: 'fundCreateNew'.tr()), + ], + ), + Expanded( + child: TabBarView( + children: [ + // Link/Select existing fund list + ref + .watch(walletFundsProvider()) + .when( + data: + (funds) => + funds.isEmpty + ? Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Symbols.money_bag, + size: 48, + color: + Theme.of( + context, + ).colorScheme.outline, + ), + const Gap(16), + Text( + 'noFundsCreated'.tr(), + style: + Theme.of( + context, + ).textTheme.titleMedium, + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: funds.length, + itemBuilder: (context, index) { + final fund = funds[index]; + + return Card( + margin: const EdgeInsets.only( + bottom: 8, + ), + child: InkWell( + onTap: + () => Navigator.of( + context, + ).pop(fund), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Symbols.money_bag, + color: + Theme.of(context) + .colorScheme + .primary, + fill: 1, + ), + const Gap(8), + Expanded( + child: Text( + '${fund.totalAmount.toStringAsFixed(2)} ${fund.currency}', + style: TextStyle( + fontSize: 18, + fontWeight: + FontWeight.bold, + color: + Theme.of( + context, + ) + .colorScheme + .primary, + ), + ), + ), + 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, + ), + ), + ), + ], + ), + if (fund.message != null && + fund + .message! + .isNotEmpty) ...[ + const Gap(8), + Text( + fund.message!, + style: + Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + const Gap(8), + Text( + '${'recipients'.tr()}: ${fund.recipients.where((r) => r.isReceived).length}/${fund.recipients.length}', + 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')), + ), + + // Create new fund and return it + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'fundCreateNewHint', + ).tr().fontSize(13).opacity(0.85).padding(bottom: 8), + if (errorText.value != null) + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 4, + ), + child: Text( + errorText.value!, + style: TextStyle(color: Colors.red[700]), + ), + ), + const Gap(16), + Align( + alignment: Alignment.centerRight, + child: FilledButton.icon( + icon: + isPushing.value + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Symbols.add_circle), + label: Text('create'.tr()), + onPressed: + isPushing.value + ? null + : () async { + errorText.value = null; + + isPushing.value = true; + // Show modal bottom sheet with fund creation form and await result + final result = await showModalBottomSheet< + Map + >( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: + (context) => + const CreateFundSheet(), + ); + + if (result == null) { + isPushing.value = false; + return; + } + + try { + if (!context.mounted) return; + + final client = ref.read( + apiClientProvider, + ); + showLoadingModal(context); + + final resp = await client.post( + '/pass/wallets/funds', + data: result, + options: Options( + headers: {'X-Noop': true}, + ), + ); + + final fund = SnWalletFund.fromJson( + resp.data, + ); + + if (fund.status == 0) { + // Return the fund that was just created (but not yet paid) + if (context.mounted) { + hideLoadingModal(context); + } + Navigator.of(context).pop(fund); + return; + } + + final orderResp = await client.post( + '/pass/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 (paidOrder != null && + context.mounted) { + showLoadingModal(context); + + // Wait for server to handle order + await Future.delayed( + const Duration(seconds: 1), + ); + ref.invalidate(walletFundsProvider); + + // Return the created fund + final updatedResp = await client.get( + '/pass/wallets/funds/${fund.id}', + ); + final updatedFund = + SnWalletFund.fromJson( + updatedResp.data, + ); + + if (context.mounted) { + hideLoadingModal(context); + } + Navigator.of( + context, + ).pop(updatedFund); + } else { + isPushing.value = false; + } + } catch (err) { + if (context.mounted) { + hideLoadingModal(context); + } + errorText.value = err.toString(); + isPushing.value = false; + } + }, + ), + ), + ], + ).padding(horizontal: 24, vertical: 24), + ), + ], + ), + ), + ], + ), + ), + ); + } + + 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; + } + } +} diff --git a/lib/widgets/post/compose_link_attachments.dart b/lib/widgets/post/compose_link_attachments.dart index f4815747..1cd70556 100644 --- a/lib/widgets/post/compose_link_attachments.dart +++ b/lib/widgets/post/compose_link_attachments.dart @@ -143,7 +143,11 @@ class ComposeLinkAttachment extends HookConsumerWidget { helperText: 'fileIdHint'.tr(), helperMaxLines: 3, errorText: errorMessage.value, - border: OutlineInputBorder(), + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + ), ), onTapOutside: (_) =>