cw-1584-ironwood-migration-modal (#3481)
What changed, and why it matters
This commit adds a user-interface feature for Zcash wallets that displays an informational modal when the user's funds are being automatically migrated to the new Zcash 'Ironwood' network upgrade. It does not change how funds are secured, sent, or received; it only shows explanatory text and a 'Learn more' link. There is no indication of a security vulnerability in this change.
No security action required. Treat as a normal feature review; verify the external documentation link is correct and that the modal does not inadvertently block critical wallet functionality.
Security signals we found
No changes to authentication, key storage, transaction signing, or network communication
No new dependencies or native code introduced
UI-only informational modal with an external documentation link
No input parsing, deserialization, or privilege changes
Evidence from the diff
The patch introduces UI-only support for the Zcash Ironwood migration: a new ZcashMigrationModal, a preference key to track whether the modal has been shown, reactions in DashboardViewModel and NewHomePage to trigger the modal when hasOrchardMigratableBalance() returns true, and updated strings/images. The underlying migration logic (_migratableOrchardTotal, auto-shielding) is pre-existing and only queried here. No cryptographic, networking, or authentication code is modified.
Changed components
cw_zcash/lib/src/zcash_wallet.dartlib/new-ui/pages/home_page.dartlib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dartlib/new-ui/widgets/coins_page/zcash_migration_modal.dartlib/view_model/dashboard/dashboard_view_model.dartlib/zcash/cw_zcash.dartlib/store/settings_store.dartlib/entities/preferences_key.dartInspect captured patch +260 / −8
diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart
index 6a09724f..98d4a92b 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<bool> 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<void> _$autoShield() async {
if (syncStatus is! SyncedSyncStatus) {
return;
diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart
index b12c5826..63c94bdb 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 8fd2e3e2..f6244095 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<NewHomePage> {
_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 461b8167..912a592d 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 00000000..e1f66b1f
--- /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 d67c1f04..95ab5d52 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 e16b0af3..fb2d53b7 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 76ecb5ca..51c9dc53 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<bool> 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 00000000..114e7cc1
--- /dev/null
+++ b/res/pictures/ironwood_migration.svg
@@ -0,0 +1,3 @@
+<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M14.7438 43.75H18.9104C19.391 39.6528 21.258 36.1979 24.5115 33.3854C27.7642 30.5729 31.6076 29.1667 36.0417 29.1667C38.8625 29.1667 41.8701 30.0736 45.0646 31.8875C48.259 33.7007 50.8979 36.266 52.9812 39.5833H43.4292V43.75H60.0958V27.0833H55.9292V36.5865C53.3868 32.6656 50.2767 29.7542 46.599 27.8521C42.9205 25.9507 39.4014 25 36.0417 25C30.4062 25 25.5944 26.8083 21.6063 30.425C17.6188 34.041 15.3313 38.4826 14.7438 43.75ZM37.5135 75C32.3281 75 27.4528 74.016 22.8875 72.0479C18.3229 70.0799 14.3521 67.409 10.975 64.0354C7.59792 60.6618 4.92465 56.6944 2.95521 52.1333C0.985069 47.5729 0 42.6997 0 37.5135C0 32.3281 0.984027 27.4528 2.95208 22.8875C4.92014 18.3229 7.59097 14.3521 10.9646 10.975C14.3382 7.59792 18.3056 4.92465 22.8667 2.95521C27.4271 0.985069 32.3003 0 37.4865 0C42.6719 0 47.5472 0.984031 52.1125 2.95209C56.6771 4.92014 60.6479 7.59097 64.025 10.9646C67.4021 14.3382 70.0753 18.3056 72.0448 22.8667C74.0149 27.4271 75 32.3003 75 37.4865C75 42.6719 74.016 47.5472 72.0479 52.1125C70.0799 56.6771 67.409 60.6479 64.0354 64.025C60.6618 67.4021 56.6944 70.0753 52.1333 72.0448C47.5729 74.0149 42.6997 75 37.5135 75Z" fill="#8C9FBB"/>
+</svg>
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index 9376a633..648568f3 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 6621fc21..66c30279 100644
--- a/tool/configure.dart
+++ b/tool/configure.dart
@@ -1770,6 +1770,7 @@ abstract class Zcash {
bool showMissingFundsCard(WalletBase wallet);
Future<void> rescanInternalChange(WalletBase wallet);
bool ironwoodActive(WalletAddresses walletAddresses);
+ Future<bool> hasOrchardMigratableBalance(WalletBase wallet);
}
""";
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.