From 0257f5cf76a33cf068ae2fc251b6e07a8283f4ae Mon Sep 17 00:00:00 2001 From: malik1004x Date: Tue, 4 Aug 2026 14:08:48 +0200 Subject: [PATCH] cw-1584-ironwood-migration-modal (#3481) * ironwood migration modal * auto-show when migration begins * remove restricted import * new condition * string fix * only activate if ironwood is active * reaction safeguards * migration* --- cw_zcash/lib/src/zcash_wallet.dart | 17 ++ lib/entities/preferences_key.dart | 1 + lib/new-ui/pages/home_page.dart | 22 +++ .../unconfirmed_balance_widget.dart | 26 ++- .../coins_page/zcash_migration_modal.dart | 152 ++++++++++++++++++ lib/store/settings_store.dart | 9 ++ .../dashboard/dashboard_view_model.dart | 24 +++ lib/zcash/cw_zcash.dart | 5 + res/pictures/ironwood_migration.svg | 3 + res/values/strings_en.arb | 8 +- tool/configure.dart | 1 + 11 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 lib/new-ui/widgets/coins_page/zcash_migration_modal.dart create mode 100644 res/pictures/ironwood_migration.svg diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index 6a09724fa..98d4a92b1 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -1214,6 +1214,23 @@ abstract class ZcashWalletBase return migratable; } + Future hasOrchardMigratableBalance() async { + + final (active, migratableOrchard) = await runWithCoin( + accountId: accountId, + func: (final coin) async => ( + await zkool_network.isIronwoodActive(c: coin), + await _migratableOrchardTotal(coin), + ), + ); + + if(!active) { + return false; + } + + return migratableOrchard > BigInt.zero; + } + Future _$autoShield() async { if (syncStatus is! SyncedSyncStatus) { return; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index b12c58265..63c94bdb7 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -145,4 +145,5 @@ class PreferencesKey { static const backgroundImage = 'background_image'; static const mwebAdDismissed = "mweb_ad_dismissed"; static const balanceHideCounter = "balance_hide_counter"; + static const zcashMigrationModalViewed = "zcash_migration_modal_viewed"; } diff --git a/lib/new-ui/pages/home_page.dart b/lib/new-ui/pages/home_page.dart index 8fd2e3e23..f62440952 100644 --- a/lib/new-ui/pages/home_page.dart +++ b/lib/new-ui/pages/home_page.dart @@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart'; import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart'; +import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart"; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; +import "package:cw_core/amount/money.dart"; +import "package:cw_core/crypto_currency.dart"; +import "package:cw_core/transaction_direction.dart"; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -48,6 +52,24 @@ class _NewHomePageState extends State { _lightningMode = false; }); }); + + reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) { + if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) { + if(!context.mounted) { + return; + } + widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true; + showMaterialModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => ZcashMigrationModal( + hasContinue: true, + balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable + .toStringWithSymbol(), + ), + ); + } + }); } void _setAccountViewModel() { diff --git a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart index 461b8167a..912a592dd 100644 --- a/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart +++ b/lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/generated/i18n.dart'; +import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart"; import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -21,6 +23,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget { return Observer(builder: (_) { final show = dashboardViewModel.balanceViewModel .hasAdditionalBalance(dashboardViewModel.wallet.currency); + final isIronwoodMigration = dashboardViewModel.isMigratingToIronwood; return AnimatedSize( duration: const Duration(milliseconds: 300), @@ -44,13 +47,17 @@ class UnconfirmedBalanceWidget extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: () { + final modal = isIronwoodMigration + ? ZcashMigrationModal( + hasContinue: false, balance: "${balance} ${currency.symbol}") + : UnconfirmedBalanceModal( + balance: balance.toStringWithSymbol(), + currencyIconPath: currency.iconPath ?? "", + ); showMaterialModalBottomSheet( backgroundColor: Colors.transparent, context: context, - builder: (context) => UnconfirmedBalanceModal( - balance: balance.toStringWithSymbol(), - currencyIconPath: currency.iconPath ?? "", - )); + builder: (context) => modal); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -63,8 +70,12 @@ class UnconfirmedBalanceWidget extends StatelessWidget { SizedBox( height: 20, width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, + child: isIronwoodMigration + ? CupertinoActivityIndicator( + color: Theme.of(context).colorScheme.primary, + ) + : CircularProgressIndicator( + strokeWidth: 2, backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(50), color: Theme.of(context).colorScheme.primary, @@ -80,7 +91,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), ), - Text(S.of(context).confirming), + Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming), ], ) ], @@ -172,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget { ); } } + diff --git a/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart b/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart new file mode 100644 index 000000000..e1f66b1fb --- /dev/null +++ b/lib/new-ui/widgets/coins_page/zcash_migration_modal.dart @@ -0,0 +1,152 @@ +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:flutter/material.dart"; +import "package:url_launcher/url_launcher.dart"; + +class ZcashMigrationModal extends StatelessWidget { + const ZcashMigrationModal({required this.hasContinue, required this.balance, super.key}); + + final String balance; + final bool hasContinue; + + @override + Widget build(BuildContext context) => SafeArea( + bottom: false, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),), + child: SafeArea( + top: false, + child: Column( + children: [ + ModalTopBar( + title: S.of(context).zcash_network_upgrade, + trailingIcon: const Icon(Icons.close), + onTrailingPressed: Navigator.of(context).pop, + trailingSemanticLabel: S.of(context).close, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Expanded( + child: Column( + spacing: 40, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 146, + height: 79, + child: Stack( + children: [ + const Positioned( + left: 67, + top: 0, + bottom: 0, + child: CakeImageWidget( + imageUrl: "assets/new-ui/ironwood_migration.svg", + width: 75, + height: 75, + ), + ), + Positioned( + left: 0, + child: Container( + width: 79, + height: 79, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99999999), + border: Border.all( + color: Theme.of(context).colorScheme.surface, + width: 4,),), + child: const CakeImageWidget( + imageUrl: "assets/new-ui/crypto_full_icons/zcash.svg", + height: 75, + width: 75, + ), + ), + ), + ], + ), + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Theme.of(context).colorScheme.surfaceContainer,), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4, + children: [ + Text( + balance, + style: TextStyle( + color: Theme.of(context).colorScheme.primary,), + ), + Text(S.of(context).is_currently_being_migrated), + ], + ), + ),), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_1}\n\n", + style: Theme.of(context).textTheme.bodyMedium,), + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_2}\n\n", + style: Theme.of(context).textTheme.bodyMedium,), + TextSpan( + text: "${S.of(context).zcash_network_upgrade_desc_3}\n\n", + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const Color(0xFFF9C03C), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + spacing: 12, + children: [ + NewPrimaryButton( + onPressed: _launchDocs, + text: S.of(context).learn_more, + color: Theme.of(context).colorScheme.surfaceContainer, + textColor: Theme.of(context).colorScheme.primary,), + if (hasContinue) + NewPrimaryButton( + onPressed: Navigator.of(context).pop, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary,), + ], + ), + ), + ], + ), + ), + ), + ); + + void _launchDocs() { + try { + launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/zcash/ironwood-migration")); + } catch (_) {} + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index d67c1f045..95ab5d52c 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -151,6 +151,7 @@ abstract class SettingsStoreBase with Store { required this.mwebNodeUri, required this.mwebAdDismissed, required this.balanceHideCounter, + required this.zcashMigrationModalViewed, required bool initialEnableAutomaticNodeSwitching, required String initialBackgroundImage, TransactionPriority? initialBitcoinTransactionPriority, @@ -766,6 +767,9 @@ abstract class SettingsStoreBase with Store { (int balanceHideCounter) => _sharedPreferences.setInt(PreferencesKey.balanceHideCounter, balanceHideCounter)); + reaction((_) => zcashMigrationModalViewed, + (val) => _sharedPreferences.setBool(PreferencesKey.zcashMigrationModalViewed, val)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -1075,6 +1079,9 @@ abstract class SettingsStoreBase with Store { @observable bool mwebAdDismissed; + @observable + bool zcashMigrationModalViewed; + final SecureStorage _secureStorage; final SharedPreferences _sharedPreferences; @@ -1350,6 +1357,7 @@ abstract class SettingsStoreBase with Store { final enableAutomaticNodeSwitching = sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? true; final backgroundImage = sharedPreferences.getString(PreferencesKey.backgroundImage) ?? ''; + final zcashMigrationModalViewed = sharedPreferences.getBool(PreferencesKey.zcashMigrationModalViewed) ?? false; // If no value if (pinLength == null || pinLength == 0) { @@ -1762,6 +1770,7 @@ abstract class SettingsStoreBase with Store { initialBuiltinTor: builtinTor, mwebAdDismissed: mwebAdDismissed, balanceHideCounter: balanceHideCounter, + zcashMigrationModalViewed: zcashMigrationModalViewed ); } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index e16b0af3a..fb2d53b74 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -96,6 +96,7 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false, isShowSecondYatIntroduction = false, isShowThirdYatIntroduction = false, + isMigratingToIronwood = false, filterItems = [], exchangeFilterItems = [], subname = '', @@ -250,6 +251,8 @@ abstract class DashboardViewModelBase with Store { reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id)); + addZcashMigrationReaction(); + tradeMonitor.monitorActiveTrades(wallet.id); } @@ -374,6 +377,22 @@ abstract class DashboardViewModelBase with Store { ); } + @observable + ReactionDisposer? zcashMigrationReactionDisposer; + + @action + void addZcashMigrationReaction() { + zcashMigrationReactionDisposer?.reaction.dispose(); + zcashMigrationReactionDisposer = null; + + if (wallet.type == WalletType.zcash) { + zcashMigrationReactionDisposer = reaction((_) => wallet.balance.values.first, (_) async { + isMigratingToIronwood = + wallet.type == WalletType.zcash && await zcash!.hasOrchardMigratableBalance(wallet); + }); + } + } + @computed bool get isSyncHeavy { if ([ @@ -1270,6 +1289,8 @@ abstract class DashboardViewModelBase with Store { name = wallet.name; loadFilterItems(); + addZcashMigrationReaction(); + if (wallet.type == WalletType.monero) { subname = monero!.getCurrentAccount(wallet).label; @@ -1551,6 +1572,9 @@ abstract class DashboardViewModelBase with Store { } } + @observable + bool isMigratingToIronwood; + String getTransactionType(TransactionInfo tx) { if (wallet.type == WalletType.bitcoin) { if (tx.isReplaced == true) return ' (replaced)'; diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart index 76ecb5ca6..51c9dc53e 100644 --- a/lib/zcash/cw_zcash.dart +++ b/lib/zcash/cw_zcash.dart @@ -213,6 +213,11 @@ class CWZcash extends Zcash { bool ironwoodActive(WalletAddresses walletAddresses) { return (walletAddresses as ZcashWalletAddresses).ironwoodActive; } + + @override + Future hasOrchardMigratableBalance(WalletBase wallet) { + return (wallet as ZcashWallet).hasOrchardMigratableBalance(); + } } const wordList = [ diff --git a/res/pictures/ironwood_migration.svg b/res/pictures/ironwood_migration.svg new file mode 100644 index 000000000..114e7cc1c --- /dev/null +++ b/res/pictures/ironwood_migration.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9376a6332..648568f30 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -571,6 +571,7 @@ "invalid_input": "Invalid input", "invalid_password": "Invalid password", "invoice_details": "Invoice details", + "is_currently_being_migrated": "is currently being migrated", "is_percentage": "is", "keys": "Keys", "label": "Label", @@ -646,6 +647,7 @@ "messages": "Messages", "method": "Method", "methods": "Methods", + "migrating_to_ironwood": "migrating to Ironwood", "min_amount": "Min: ${value}", "min_value": "Min: ${value} ${currency}", "mining": "Mining", @@ -1476,5 +1478,9 @@ "zcash_card_enable_later": "You can always enable this card later in settings", "zcash_card_missing_funds": "Missing funds?", "zcash_card_scan": "Scan", - "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch." + "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch.", + "zcash_network_upgrade": "Zcash Network Upgrade", + "zcash_network_upgrade_desc_1": "Zcash has undergone a network upgrade called Ironwood that requires migration of funds before they can be spent. This migration is automatic, privacy-preserving, and starts as soon as you open your Zcash wallet.", + "zcash_network_upgrade_desc_2": "When the migration finishes, your Available Balance will return to normal, and you can continue to send and receive Zcash as usual.", + "zcash_network_upgrade_desc_3": "Please allow your Zcash wallet to finishing syncing, and then leave the app open until migration finishes which can take a few minutes depending on the balance." } diff --git a/tool/configure.dart b/tool/configure.dart index 6621fc21a..66c302799 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1770,6 +1770,7 @@ abstract class Zcash { bool showMissingFundsCard(WalletBase wallet); Future rescanInternalChange(WalletBase wallet); bool ironwoodActive(WalletAddresses walletAddresses); + Future hasOrchardMigratableBalance(WalletBase wallet); } """;