Handle Network Connection Errors (#2213)

* fix(moralis-nft-errors): guard against concurrent NFT fetches and adjusts the message being presented to the user. Previously multiple calls to get NFTs for the currently opened wallet could overlap and queue the error bottom sheet multiple times. This change: - Registers the NFTViewModel as a lazySingleton so its isLoading flag persists. - Adds an early return in the call to fetch the wallet NFTs when isLoading is already true. - Cleans up the error message being displayed to the user when there is an error. * feat(moralis-nft-error): localize error message in NFTViewModel * feat(nft/wc-bottom-sheet): Revamped the flow, service, theme, and UI for smoother UX Revamps bottom‑sheet handling end‑to‑end to deliver a much more smoother experience. This change: - Refactors the BottomSheetService queueing logic to prevent races and ensure strict sequencing - Update theme extensions and styling for the bottom‑sheet components - Adds the option to either auto dismiss or allow user manually dismiss the bottomsheet * fix: Context clash when entering the wallets on airplane mode. The flushbar clashes with the bottomSheet and results in it blocking entry to the selected wallet. This change: - Moves the logic for fetching nft to the listing page, no need fetching if the user does not route to the page, - Routes to balance page when entering from wallet list page - Adds a fade transition when entering the dashboard - Reverts nftViewModel registeration to be a factory * fix: Revert animation for now, prior to when the UX overhaul for the app is done * fix: Remove duplicate registration

David Adegoke committed Apr 24, 2025 at 23:12 UTC b5ba9385e8f7861d8baf6a9dd5a7059250963a2e
6 files changed +64 -26
lib/di.dart
+1 -1
@@ -651,6 +651,7 @@ Future<void> setup({
651 return walletKitService;
652 });
653
654 + getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
655 getIt.registerFactory(() => BalancePage(
656 nftViewModel: getIt.get<NFTViewModel>(),
657 dashboardViewModel: getIt.get<DashboardViewModel>(),
@@ -1451,7 +1452,6 @@ Future<void> setup({
1452 () => WalletConnectConnectionsView(walletKitService: getIt.get<WalletKitService>()),
1453 );
1454
1454 - getIt.registerFactory(() => NFTViewModel(appStore, getIt.get<BottomSheetService>()));
1455 getIt.registerFactory<TorPage>(() => TorPage(getIt.get<AppStore>()));
1456
1457 getIt.registerFactory(() => SignViewModel(getIt.get<AppStore>().wallet!));
lib/entities/main_actions.dart
+8 -5
@@ -9,8 +9,7 @@ class MainActions {
9
10 final bool Function(DashboardViewModel viewModel)? isEnabled;
11 final bool Function(DashboardViewModel viewModel)? canShow;
12 - final Future<void> Function(
13 - BuildContext context, DashboardViewModel viewModel) onTap;
12 + final Future<void> Function(BuildContext context, DashboardViewModel viewModel) onTap;
13
14 MainActions._({
15 required this.name,
@@ -32,7 +31,12 @@ class MainActions {
31 name: (context) => S.of(context).wallets,
32 image: 'assets/images/wallet_new.png',
33 onTap: (BuildContext context, DashboardViewModel viewModel) async {
35 - Navigator.pushNamed(context, Routes.walletList);
34 + Navigator.pushNamed(
35 + context,
36 + Routes.walletList,
37 + arguments: (BuildContext context) =>
38 + Navigator.of(context).pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
39 + );
40 },
41 );
42
@@ -65,7 +69,6 @@ class MainActions {
69 },
70 );
71
68 -
72 static MainActions tradeAction = MainActions._(
73 name: (context) => S.of(context).exchange,
74 image: 'assets/images/buy_sell.png',
@@ -76,4 +79,4 @@ class MainActions {
79 await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
80 },
81 );
79 -}
\ No newline at end of file
82 +}
lib/src/screens/dashboard/pages/nft_listing_page.dart
+20 -4
@@ -11,11 +11,27 @@ import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart';
11 import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
12 import 'package:cw_core/wallet_type.dart';
13
14 -class NFTListingPage extends StatelessWidget {
14 +class NFTListingPage extends StatefulWidget {
15 final NFTViewModel nftViewModel;
16
17 const NFTListingPage({super.key, required this.nftViewModel});
18
19 + @override
20 + State<NFTListingPage> createState() => _NFTListingPageState();
21 +}
22 +
23 +class _NFTListingPageState extends State<NFTListingPage> {
24 + @override
25 + void initState() {
26 + super.initState();
27 +
28 + fetchNFTsForWallet();
29 + }
30 +
31 + Future<void> fetchNFTsForWallet() async {
32 + await widget.nftViewModel.getNFTAssetByWallet();
33 + }
34 +
35 @override
36 Widget build(BuildContext context) {
37 final dashboardTheme = Theme.of(context).extension<DashboardPageTheme>()!;
@@ -36,11 +52,11 @@ class NFTListingPage extends StatelessWidget {
52 onPressed: () => Navigator.pushNamed(
53 context,
54 Routes.importNFTPage,
39 - arguments: nftViewModel,
55 + arguments: widget.nftViewModel,
56 ),
57 ),
58 ),
43 - if (nftViewModel.isLoading)
59 + if (widget.nftViewModel.isLoading)
60 Expanded(
61 child: Center(
62 child: CircularProgressIndicator(
@@ -53,7 +69,7 @@ class NFTListingPage extends StatelessWidget {
69 )
70 else
71 Expanded(
56 - child: NFTListWidget(nftViewModel: nftViewModel),
72 + child: NFTListWidget(nftViewModel: widget.nftViewModel),
73 ),
74 ],
75 );
lib/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart
+25 -7
@@ -14,13 +14,30 @@ class BottomSheetMessageDisplayWidget extends StatelessWidget {
14 mainAxisSize: MainAxisSize.min,
15 crossAxisAlignment: CrossAxisAlignment.start,
16 children: [
17 - Text(
18 - isError ? S.current.error : S.current.successful,
19 - style: TextStyle(
20 - fontSize: 16,
21 - fontWeight: FontWeight.normal,
22 - color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
23 - ),
17 + Row(
18 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
19 + crossAxisAlignment: CrossAxisAlignment.center,
20 + children: [
21 + Text(
22 + isError ? S.current.error : S.current.successful,
23 + style: TextStyle(
24 + fontSize: 16,
25 + fontWeight: FontWeight.normal,
26 + color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
27 + ),
28 + ),
29 + IconButton(
30 + color: Theme.of(context).appBarTheme.titleTextStyle!.color!,
31 + padding: const EdgeInsets.all(0.0),
32 + visualDensity: VisualDensity.compact,
33 + onPressed: () {
34 + if (Navigator.canPop(context)) {
35 + Navigator.pop(context);
36 + }
37 + },
38 + icon: const Icon(Icons.close_sharp),
39 + ),
40 + ],
41 ),
42 SizedBox(height: 8),
43 Row(
@@ -37,6 +54,7 @@ class BottomSheetMessageDisplayWidget extends StatelessWidget {
54 ),
55 ],
56 ),
57 + SizedBox(height: 16),
58 ],
59 );
60 }
lib/src/widgets/setting_actions.dart
+5 -1
@@ -73,7 +73,11 @@ class SettingActions {
73 image: 'assets/images/wallet_menu.png',
74 onTap: (BuildContext context) {
75 Navigator.pop(context);
76 - Navigator.of(context).pushNamed(Routes.walletList);
76 + Navigator.of(context).pushNamed(
77 + Routes.walletList,
78 + arguments: (BuildContext context) =>
79 + Navigator.of(context).pushNamedAndRemoveUntil(Routes.dashboard, (route) => false),
80 + );
81 },
82 );
83
lib/view_model/dashboard/nft_view_model.dart
+5 -8
@@ -23,11 +23,7 @@ abstract class NFTViewModelBase with Store {
23 : isLoading = false,
24 isImportNFTLoading = false,
25 nftAssetByWalletModels = ObservableList(),
26 - solanaNftAssetModels = ObservableList() {
27 - getNFTAssetByWallet();
28 -
29 - reaction((_) => appStore.wallet, (_) => getNFTAssetByWallet());
30 - }
26 + solanaNftAssetModels = ObservableList();
27
28 final AppStore appStore;
29 final BottomSheetService bottomSheetService;
@@ -80,6 +76,8 @@ abstract class NFTViewModelBase with Store {
76 }
77
78 try {
79 + if (isLoading) return;
80 +
81 isLoading = true;
82
83 final response = await http.get(
@@ -114,10 +112,7 @@ abstract class NFTViewModelBase with Store {
112
113 nftAssetByWalletModels.addAll(result);
114 }
117 -
118 - isLoading = false;
115 } catch (e) {
120 - isLoading = false;
116 log(e.toString());
117 bottomSheetService.queueBottomSheet(
118 isModalDismissible: true,
@@ -125,6 +120,8 @@ abstract class NFTViewModelBase with Store {
120 message: S.current.moralis_nft_error,
121 ),
122 );
123 + } finally {
124 + isLoading = false;
125 }
126 }
127