From 583c772e302345c73cf7d9c82bc05d2c992d1451 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 20 Jan 2026 12:01:52 +0800 Subject: [PATCH 1/2] action sheet logic factored out to service --- .../components/notification_card.dart | 14 ++++- .../components/transaction_list_item.dart | 58 +++++++++++-------- .../components/transactions_list.dart | 35 ++++++----- .../main/screens/transactions_screen.dart | 14 ++++- .../lib/services/transaction_service.dart | 41 +++++++++++++ 5 files changed, 119 insertions(+), 43 deletions(-) diff --git a/mobile-app/lib/features/components/notification_card.dart b/mobile-app/lib/features/components/notification_card.dart index 65385d63..712e5fdd 100644 --- a/mobile-app/lib/features/components/notification_card.dart +++ b/mobile-app/lib/features/components/notification_card.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/transaction_list_item.dart'; import 'package:resonance_network_wallet/models/notification_models.dart'; +import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; class NotificationCard extends ConsumerStatefulWidget { @@ -141,7 +142,18 @@ class _NotificationCardState extends ConsumerState with Ticker final transaction = txService.deserializeTxEventFromJsonIfPossible(widget.notification.metadata); if (transaction != null) { - showTransactionActionSheet(context, transaction: transaction, role: txService.getTransactionRole(transaction)); + final role = txService.getTransactionRole(transaction); + final activeAccount = ref.read(activeAccountProvider).value; + showTransactionActionSheet( + context, + transaction: transaction, + role: role, + config: TransactionService.getTransactionDetailViewConfig( + transaction: transaction, + role: role, + activeAccount: activeAccount, + ), + ); } } diff --git a/mobile-app/lib/features/components/transaction_list_item.dart b/mobile-app/lib/features/components/transaction_list_item.dart index 840f2df0..80edd77a 100644 --- a/mobile-app/lib/features/components/transaction_list_item.dart +++ b/mobile-app/lib/features/components/transaction_list_item.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/skeleton.dart'; @@ -11,7 +10,7 @@ import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_size_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; import 'package:resonance_network_wallet/models/transaction_role.dart'; -import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/services/transaction_service.dart'; import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart'; import 'package:resonance_network_wallet/shared/extensions/transaction_event_extension.dart'; @@ -19,8 +18,15 @@ class TransactionListItem extends StatefulWidget { final TransactionEvent transaction; final TransactionRole role; final bool showFromAndTo; + final TransactionDetailViewConfig actionSheetConfig; - const TransactionListItem({super.key, required this.transaction, required this.role, this.showFromAndTo = true}); + const TransactionListItem({ + super.key, + required this.transaction, + required this.role, + this.showFromAndTo = true, + this.actionSheetConfig = TransactionDetailViewConfig.normal, + }); @override TransactionListItemState createState() => TransactionListItemState(); @@ -144,7 +150,12 @@ class TransactionListItemState extends State { return InkWell( onTap: () { - showTransactionActionSheet(context, transaction: widget.transaction, role: widget.role); + showTransactionActionSheet( + context, + transaction: widget.transaction, + role: widget.role, + config: widget.actionSheetConfig, + ); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -234,31 +245,28 @@ class TransactionListItemState extends State { } } -void showTransactionActionSheet(BuildContext context, {required TransactionEvent transaction, required role}) { - final container = ProviderScope.containerOf(context, listen: false); - final activeDisplayAccount = container.read(activeAccountProvider).value; - EntrustedAccount? entrustedAccount; - if (activeDisplayAccount is EntrustedDisplayAccount) { - entrustedAccount = activeDisplayAccount.account; - } - final isEntrustedAccount = entrustedAccount != null; - +void showTransactionActionSheet( + BuildContext context, { + required TransactionEvent transaction, + required TransactionRole role, + required TransactionDetailViewConfig config, +}) { Widget sheet; - if (transaction is ReversibleTransferEvent) { - final reversibleTx = transaction; - if ((reversibleTx.isReversibleScheduled || reversibleTx.isReversibleCancelled) && - (role == TransactionRole.sender || role == TransactionRole.both)) { + switch (config.type) { + case TransactionViewType.normal: + sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); + case TransactionViewType.reversible: sheet = ReversibleTransactionActionSheet( - transaction: reversibleTx, - mode: isEntrustedAccount ? ReversibleTransactionMode.guardianIntercept : ReversibleTransactionMode.reversible, - entrustedAccount: entrustedAccount, + transaction: transaction as ReversibleTransferEvent, + mode: ReversibleTransactionMode.reversible, + ); + case TransactionViewType.guardianIntercept: + sheet = ReversibleTransactionActionSheet( + transaction: transaction as ReversibleTransferEvent, + mode: ReversibleTransactionMode.guardianIntercept, + entrustedAccount: config.entrustedAccount, ); - } else { - sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); - } - } else { - sheet = TransactionDetailsActionSheet(transaction: transaction, role: role); } showModalBottomSheet( diff --git a/mobile-app/lib/features/components/transactions_list.dart b/mobile-app/lib/features/components/transactions_list.dart index a6f7d00c..e7bc4e3d 100644 --- a/mobile-app/lib/features/components/transactions_list.dart +++ b/mobile-app/lib/features/components/transactions_list.dart @@ -4,11 +4,12 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/transaction_list_item.dart'; import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart'; import 'package:resonance_network_wallet/features/styles/app_text_theme.dart'; +import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; class RecentTransactionsList extends ConsumerWidget { final List transactions; - final List accountIds; // List of account IDs we're showing transactions for + final List accountIds; final bool Function(TransactionEvent)? filter; final Color backgroundColor; @@ -23,6 +24,7 @@ class RecentTransactionsList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final txService = ref.read(transactionServiceProvider); + final activeAccount = ref.watch(activeAccountProvider).value; final transactionsToShow = filter == null ? transactions : transactions.where(filter!).toList(); final scheduled = transactionsToShow @@ -37,6 +39,21 @@ class RecentTransactionsList extends ConsumerWidget { return true; }).toList(); + Widget buildItem(TransactionEvent transaction) { + final role = txService.getTransactionRole(transaction, accountIds: accountIds); + return TransactionListItem( + key: ValueKey(transaction.id), + transaction: transaction, + role: role, + showFromAndTo: accountIds.length > 1, + actionSheetConfig: TransactionService.getTransactionDetailViewConfig( + transaction: transaction, + role: role, + activeAccount: activeAccount, + ), + ); + } + return Container( padding: const EdgeInsets.all(10), decoration: ShapeDecoration( @@ -62,15 +79,9 @@ class RecentTransactionsList extends ConsumerWidget { padding: EdgeInsets.zero, itemCount: scheduled.length, itemBuilder: (context, index) { - final transaction = scheduled[index]; return Padding( padding: EdgeInsets.only(top: index == 0 ? 7.0 : 0), - child: TransactionListItem( - key: ValueKey(transaction.id), - transaction: transaction, - role: txService.getTransactionRole(transaction, accountIds: accountIds), - showFromAndTo: accountIds.length > 1, - ), + child: buildItem(scheduled[index]), ); }, separatorBuilder: (context, index) => const _Divider(), @@ -87,15 +98,9 @@ class RecentTransactionsList extends ConsumerWidget { padding: EdgeInsets.zero, itemCount: others.length, itemBuilder: (context, index) { - final transaction = others[index]; return Padding( padding: EdgeInsets.only(top: index == 0 ? 7.0 : 0), - child: TransactionListItem( - key: ValueKey(transaction.id), - transaction: transaction, - role: txService.getTransactionRole(transaction, accountIds: accountIds), - showFromAndTo: accountIds.length > 1, - ), + child: buildItem(others[index]), ); }, separatorBuilder: (context, index) => const _Divider(), diff --git a/mobile-app/lib/features/main/screens/transactions_screen.dart b/mobile-app/lib/features/main/screens/transactions_screen.dart index 855540da..fa142613 100644 --- a/mobile-app/lib/features/main/screens/transactions_screen.dart +++ b/mobile-app/lib/features/main/screens/transactions_screen.dart @@ -65,9 +65,19 @@ class _TransactionsScreenState extends ConsumerState { final txIntent = ref.read(transactionIntentProvider); if (txIntent != null) { - // After we consume the intent, we clean it up ref.read(transactionIntentProvider.notifier).state = null; - showTransactionActionSheet(context, transaction: txIntent, role: txService.getTransactionRole(txIntent)); + final role = txService.getTransactionRole(txIntent); + final activeAccount = ref.read(activeAccountProvider).value; + showTransactionActionSheet( + context, + transaction: txIntent, + role: role, + config: TransactionService.getTransactionDetailViewConfig( + transaction: txIntent, + role: role, + activeAccount: activeAccount, + ), + ); } }); } diff --git a/mobile-app/lib/services/transaction_service.dart b/mobile-app/lib/services/transaction_service.dart index 62bd83a1..e1455c88 100644 --- a/mobile-app/lib/services/transaction_service.dart +++ b/mobile-app/lib/services/transaction_service.dart @@ -7,6 +7,17 @@ final transactionServiceProvider = Provider((ref) { return TransactionService(ref); }); +enum TransactionViewType { normal, reversible, guardianIntercept } + +class TransactionDetailViewConfig { + final TransactionViewType type; + final EntrustedAccount? entrustedAccount; + + const TransactionDetailViewConfig({required this.type, this.entrustedAccount}); + + static const normal = TransactionDetailViewConfig(type: TransactionViewType.normal); +} + class TransactionService { final Ref _ref; @@ -94,4 +105,34 @@ class TransactionService { return event; } + + /// Basically deciding whether or not to show a reversible or intercept user interface, + /// or whether to just show a normal transaction detail view. + static TransactionDetailViewConfig getTransactionDetailViewConfig({ + required TransactionEvent transaction, + required TransactionRole role, + required DisplayAccount? activeAccount, + }) { + if (transaction is! ReversibleTransferEvent) { + return TransactionDetailViewConfig.normal; + } + + final isScheduled = transaction.status == ReversibleTransferStatus.SCHEDULED; + final isCancelled = transaction.status == ReversibleTransferStatus.CANCELLED; + final canTakeAction = isScheduled || isCancelled; + final isActorRole = role == TransactionRole.sender || role == TransactionRole.both; + + if (!canTakeAction || !isActorRole) { + return TransactionDetailViewConfig.normal; + } + + if (activeAccount is EntrustedDisplayAccount) { + return TransactionDetailViewConfig( + type: TransactionViewType.guardianIntercept, + entrustedAccount: activeAccount.account, + ); + } + + return const TransactionDetailViewConfig(type: TransactionViewType.reversible); + } } From e28decd3e0c31dd4e59e39133ebb2ca09d1f3c4f Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Tue, 20 Jan 2026 15:00:38 +0800 Subject: [PATCH 2/2] naming and added a comment --- mobile-app/lib/services/transaction_service.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/services/transaction_service.dart b/mobile-app/lib/services/transaction_service.dart index e1455c88..d1e97dd7 100644 --- a/mobile-app/lib/services/transaction_service.dart +++ b/mobile-app/lib/services/transaction_service.dart @@ -119,10 +119,12 @@ class TransactionService { final isScheduled = transaction.status == ReversibleTransferStatus.SCHEDULED; final isCancelled = transaction.status == ReversibleTransferStatus.CANCELLED; - final canTakeAction = isScheduled || isCancelled; + // Reversible transaction UX is shown for scheduled transactions, so user can intercept / revert them. + // It is also shown for canceled transactions, showing a "this transaction was canceled/intercepted/reverted" message. + final showReversibleTransactionUX = isScheduled || isCancelled; final isActorRole = role == TransactionRole.sender || role == TransactionRole.both; - if (!canTakeAction || !isActorRole) { + if (!showReversibleTransactionUX || !isActorRole) { return TransactionDetailViewConfig.normal; }