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*
malik1004x committed
Aug 4, 2026 at 14:08 UTC
0257f5cf76a33cf068ae2fc251b6e07a8283f4ae
11 files changed
+260
-8
cw_zcash/lib/src/zcash_wallet.dart
+17
@@ -1214,6 +1214,23 @@ abstract class ZcashWalletBase
1214
return migratable;
1215
}
1216
1217
+ Future<bool> hasOrchardMigratableBalance() async {
1218
+
1219
+ final (active, migratableOrchard) = await runWithCoin(
1220
+ accountId: accountId,
1221
+ func: (final coin) async => (
1222
+ await zkool_network.isIronwoodActive(c: coin),
1223
+ await _migratableOrchardTotal(coin),
1224
+ ),
1225
+ );
1226
+
1227
+ if(!active) {
1228
+ return false;
1229
+ }
1230
+
1231
+ return migratableOrchard > BigInt.zero;
1232
+ }
1233
+
1234
Future<void> _$autoShield() async {
1235
if (syncStatus is! SyncedSyncStatus) {
1236
return;
lib/entities/preferences_key.dart
+1
@@ -145,4 +145,5 @@ class PreferencesKey {
145
static const backgroundImage = 'background_image';
146
static const mwebAdDismissed = "mweb_ad_dismissed";
147
static const balanceHideCounter = "balance_hide_counter";
148
+ static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
149
}
lib/new-ui/pages/home_page.dart
+22
@@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
13
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
14
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
15
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
16
+import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
17
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
18
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
19
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
20
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
21
+import "package:cw_core/amount/money.dart";
22
+import "package:cw_core/crypto_currency.dart";
23
+import "package:cw_core/transaction_direction.dart";
24
import 'package:flutter/cupertino.dart';
25
import 'package:flutter/material.dart';
26
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -48,6 +52,24 @@ class _NewHomePageState extends State<NewHomePage> {
52
_lightningMode = false;
53
});
54
});
55
+
56
+ reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
57
+ if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
58
+ if(!context.mounted) {
59
+ return;
60
+ }
61
+ widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
62
+ showMaterialModalBottomSheet(
63
+ context: context,
64
+ backgroundColor: Colors.transparent,
65
+ builder: (context) => ZcashMigrationModal(
66
+ hasContinue: true,
67
+ balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable
68
+ .toStringWithSymbol(),
69
+ ),
70
+ );
71
+ }
72
+ });
73
}
74
75
void _setAccountViewModel() {
lib/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart
+19
-7
@@ -1,8 +1,10 @@
1
import 'package:cake_wallet/generated/i18n.dart';
2
+import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
3
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
4
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
5
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
6
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
7
+import "package:flutter/cupertino.dart";
8
import 'package:flutter/material.dart';
9
import 'package:flutter_mobx/flutter_mobx.dart';
10
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@@ -21,6 +23,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
23
return Observer(builder: (_) {
24
final show = dashboardViewModel.balanceViewModel
25
.hasAdditionalBalance(dashboardViewModel.wallet.currency);
26
+ final isIronwoodMigration = dashboardViewModel.isMigratingToIronwood;
27
28
return AnimatedSize(
29
duration: const Duration(milliseconds: 300),
@@ -44,13 +47,17 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
47
color: Colors.transparent,
48
child: InkWell(
49
onTap: () {
50
+ final modal = isIronwoodMigration
51
+ ? ZcashMigrationModal(
52
+ hasContinue: false, balance: "${balance} ${currency.symbol}")
53
+ : UnconfirmedBalanceModal(
54
+ balance: balance.toStringWithSymbol(),
55
+ currencyIconPath: currency.iconPath ?? "",
56
+ );
57
showMaterialModalBottomSheet(
58
backgroundColor: Colors.transparent,
59
context: context,
50
- builder: (context) => UnconfirmedBalanceModal(
51
- balance: balance.toStringWithSymbol(),
52
- currencyIconPath: currency.iconPath ?? "",
53
- ));
60
+ builder: (context) => modal);
61
},
62
child: Padding(
63
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@@ -63,8 +70,12 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
70
SizedBox(
71
height: 20,
72
width: 20,
66
- child: CircularProgressIndicator(
67
- strokeWidth: 2,
73
+ child: isIronwoodMigration
74
+ ? CupertinoActivityIndicator(
75
+ color: Theme.of(context).colorScheme.primary,
76
+ )
77
+ : CircularProgressIndicator(
78
+ strokeWidth: 2,
79
backgroundColor:
80
Theme.of(context).colorScheme.primary.withAlpha(50),
81
color: Theme.of(context).colorScheme.primary,
@@ -80,7 +91,7 @@ class UnconfirmedBalanceWidget extends StatelessWidget {
91
color: Theme.of(context).colorScheme.primary,
92
),
93
),
83
- Text(S.of(context).confirming),
94
+ Text(isIronwoodMigration ? S.of(context).migrating_to_ironwood : S.of(context).confirming),
95
],
96
)
97
],
@@ -172,3 +183,4 @@ class UnconfirmedBalanceModal extends StatelessWidget {
183
);
184
}
185
}
186
+
lib/new-ui/widgets/coins_page/zcash_migration_modal.dart
new
+152
@@ -0,0 +1,152 @@
1
+import "package:cake_wallet/generated/i18n.dart";
2
+import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
3
+import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
4
+import "package:cake_wallet/src/widgets/cake_image_widget.dart";
5
+import "package:flutter/material.dart";
6
+import "package:url_launcher/url_launcher.dart";
7
+
8
+class ZcashMigrationModal extends StatelessWidget {
9
+ const ZcashMigrationModal({required this.hasContinue, required this.balance, super.key});
10
+
11
+ final String balance;
12
+ final bool hasContinue;
13
+
14
+ @override
15
+ Widget build(BuildContext context) => SafeArea(
16
+ bottom: false,
17
+ child: Container(
18
+ decoration: BoxDecoration(
19
+ color: Theme.of(context).colorScheme.surface,
20
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),),
21
+ child: SafeArea(
22
+ top: false,
23
+ child: Column(
24
+ children: [
25
+ ModalTopBar(
26
+ title: S.of(context).zcash_network_upgrade,
27
+ trailingIcon: const Icon(Icons.close),
28
+ onTrailingPressed: Navigator.of(context).pop,
29
+ trailingSemanticLabel: S.of(context).close,
30
+ ),
31
+ Expanded(
32
+ child: Padding(
33
+ padding: const EdgeInsets.symmetric(horizontal: 16),
34
+ child: Column(
35
+ children: [
36
+ Expanded(
37
+ child: Column(
38
+ spacing: 40,
39
+ mainAxisAlignment: MainAxisAlignment.center,
40
+ children: [
41
+ SizedBox(
42
+ width: 146,
43
+ height: 79,
44
+ child: Stack(
45
+ children: [
46
+ const Positioned(
47
+ left: 67,
48
+ top: 0,
49
+ bottom: 0,
50
+ child: CakeImageWidget(
51
+ imageUrl: "assets/new-ui/ironwood_migration.svg",
52
+ width: 75,
53
+ height: 75,
54
+ ),
55
+ ),
56
+ Positioned(
57
+ left: 0,
58
+ child: Container(
59
+ width: 79,
60
+ height: 79,
61
+ decoration: BoxDecoration(
62
+ borderRadius: BorderRadius.circular(99999999),
63
+ border: Border.all(
64
+ color: Theme.of(context).colorScheme.surface,
65
+ width: 4,),),
66
+ child: const CakeImageWidget(
67
+ imageUrl: "assets/new-ui/crypto_full_icons/zcash.svg",
68
+ height: 75,
69
+ width: 75,
70
+ ),
71
+ ),
72
+ ),
73
+ ],
74
+ ),
75
+ ),
76
+ Container(
77
+ width: double.infinity,
78
+ decoration: BoxDecoration(
79
+ borderRadius: BorderRadius.circular(12),
80
+ color: Theme.of(context).colorScheme.surfaceContainer,),
81
+ child: Padding(
82
+ padding: const EdgeInsets.symmetric(vertical: 12),
83
+ child: Row(
84
+ mainAxisAlignment: MainAxisAlignment.center,
85
+ spacing: 4,
86
+ children: [
87
+ Text(
88
+ balance,
89
+ style: TextStyle(
90
+ color: Theme.of(context).colorScheme.primary,),
91
+ ),
92
+ Text(S.of(context).is_currently_being_migrated),
93
+ ],
94
+ ),
95
+ ),),
96
+ RichText(
97
+ textAlign: TextAlign.center,
98
+ text: TextSpan(
99
+ children: [
100
+ TextSpan(
101
+ text: "${S.of(context).zcash_network_upgrade_desc_1}\n\n",
102
+ style: Theme.of(context).textTheme.bodyMedium,),
103
+ TextSpan(
104
+ text: "${S.of(context).zcash_network_upgrade_desc_2}\n\n",
105
+ style: Theme.of(context).textTheme.bodyMedium,),
106
+ TextSpan(
107
+ text: "${S.of(context).zcash_network_upgrade_desc_3}\n\n",
108
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
109
+ color: const Color(0xFFF9C03C),
110
+ ),
111
+ ),
112
+ ],
113
+ ),
114
+ ),
115
+ ],
116
+ ),
117
+ ),
118
+ ],
119
+ ),
120
+ ),
121
+ ),
122
+ Padding(
123
+ padding: const EdgeInsets.all(12),
124
+ child: Column(
125
+ spacing: 12,
126
+ children: [
127
+ NewPrimaryButton(
128
+ onPressed: _launchDocs,
129
+ text: S.of(context).learn_more,
130
+ color: Theme.of(context).colorScheme.surfaceContainer,
131
+ textColor: Theme.of(context).colorScheme.primary,),
132
+ if (hasContinue)
133
+ NewPrimaryButton(
134
+ onPressed: Navigator.of(context).pop,
135
+ text: S.of(context).continue_text,
136
+ color: Theme.of(context).colorScheme.primary,
137
+ textColor: Theme.of(context).colorScheme.onPrimary,),
138
+ ],
139
+ ),
140
+ ),
141
+ ],
142
+ ),
143
+ ),
144
+ ),
145
+ );
146
+
147
+ void _launchDocs() {
148
+ try {
149
+ launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/zcash/ironwood-migration"));
150
+ } catch (_) {}
151
+ }
152
+}
lib/store/settings_store.dart
+9
@@ -151,6 +151,7 @@ abstract class SettingsStoreBase with Store {
151
required this.mwebNodeUri,
152
required this.mwebAdDismissed,
153
required this.balanceHideCounter,
154
+ required this.zcashMigrationModalViewed,
155
required bool initialEnableAutomaticNodeSwitching,
156
required String initialBackgroundImage,
157
TransactionPriority? initialBitcoinTransactionPriority,
@@ -766,6 +767,9 @@ abstract class SettingsStoreBase with Store {
767
(int balanceHideCounter) =>
768
_sharedPreferences.setInt(PreferencesKey.balanceHideCounter, balanceHideCounter));
769
770
+ reaction((_) => zcashMigrationModalViewed,
771
+ (val) => _sharedPreferences.setBool(PreferencesKey.zcashMigrationModalViewed, val));
772
+
773
this.nodes.observe((change) {
774
if (change.newValue != null && change.key != null) {
775
_saveCurrentNode(change.newValue!, change.key!);
@@ -1075,6 +1079,9 @@ abstract class SettingsStoreBase with Store {
1079
@observable
1080
bool mwebAdDismissed;
1081
1082
+ @observable
1083
+ bool zcashMigrationModalViewed;
1084
+
1085
final SecureStorage _secureStorage;
1086
final SharedPreferences _sharedPreferences;
1087
@@ -1350,6 +1357,7 @@ abstract class SettingsStoreBase with Store {
1357
final enableAutomaticNodeSwitching =
1358
sharedPreferences.getBool(PreferencesKey.enableAutomaticNodeSwitching) ?? true;
1359
final backgroundImage = sharedPreferences.getString(PreferencesKey.backgroundImage) ?? '';
1360
+ final zcashMigrationModalViewed = sharedPreferences.getBool(PreferencesKey.zcashMigrationModalViewed) ?? false;
1361
1362
// If no value
1363
if (pinLength == null || pinLength == 0) {
@@ -1762,6 +1770,7 @@ abstract class SettingsStoreBase with Store {
1770
initialBuiltinTor: builtinTor,
1771
mwebAdDismissed: mwebAdDismissed,
1772
balanceHideCounter: balanceHideCounter,
1773
+ zcashMigrationModalViewed: zcashMigrationModalViewed
1774
);
1775
}
1776
lib/view_model/dashboard/dashboard_view_model.dart
+24
@@ -96,6 +96,7 @@ abstract class DashboardViewModelBase with Store {
96
isShowFirstYatIntroduction = false,
97
isShowSecondYatIntroduction = false,
98
isShowThirdYatIntroduction = false,
99
+ isMigratingToIronwood = false,
100
filterItems = [],
101
exchangeFilterItems = [],
102
subname = '',
@@ -250,6 +251,8 @@ abstract class DashboardViewModelBase with Store {
251
252
reaction((_) => tradesStore.trades, (_) => tradeMonitor.monitorActiveTrades(wallet.id));
253
254
+ addZcashMigrationReaction();
255
+
256
tradeMonitor.monitorActiveTrades(wallet.id);
257
}
258
@@ -374,6 +377,22 @@ abstract class DashboardViewModelBase with Store {
377
);
378
}
379
380
+ @observable
381
+ ReactionDisposer? zcashMigrationReactionDisposer;
382
+
383
+ @action
384
+ void addZcashMigrationReaction() {
385
+ zcashMigrationReactionDisposer?.reaction.dispose();
386
+ zcashMigrationReactionDisposer = null;
387
+
388
+ if (wallet.type == WalletType.zcash) {
389
+ zcashMigrationReactionDisposer = reaction((_) => wallet.balance.values.first, (_) async {
390
+ isMigratingToIronwood =
391
+ wallet.type == WalletType.zcash && await zcash!.hasOrchardMigratableBalance(wallet);
392
+ });
393
+ }
394
+ }
395
+
396
@computed
397
bool get isSyncHeavy {
398
if ([
@@ -1270,6 +1289,8 @@ abstract class DashboardViewModelBase with Store {
1289
name = wallet.name;
1290
loadFilterItems();
1291
1292
+ addZcashMigrationReaction();
1293
+
1294
if (wallet.type == WalletType.monero) {
1295
subname = monero!.getCurrentAccount(wallet).label;
1296
@@ -1551,6 +1572,9 @@ abstract class DashboardViewModelBase with Store {
1572
}
1573
}
1574
1575
+ @observable
1576
+ bool isMigratingToIronwood;
1577
+
1578
String getTransactionType(TransactionInfo tx) {
1579
if (wallet.type == WalletType.bitcoin) {
1580
if (tx.isReplaced == true) return ' (replaced)';
lib/zcash/cw_zcash.dart
+5
@@ -213,6 +213,11 @@ class CWZcash extends Zcash {
213
bool ironwoodActive(WalletAddresses walletAddresses) {
214
return (walletAddresses as ZcashWalletAddresses).ironwoodActive;
215
}
216
+
217
+ @override
218
+ Future<bool> hasOrchardMigratableBalance(WalletBase wallet) {
219
+ return (wallet as ZcashWallet).hasOrchardMigratableBalance();
220
+ }
221
}
222
223
const wordList = [
res/pictures/ironwood_migration.svg
new
+3
@@ -0,0 +1,3 @@
1
+<svg width="75" height="75" viewBox="0 0 75 75" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <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"/>
3
+</svg>
res/values/strings_en.arb
+7
-1
@@ -571,6 +571,7 @@
571
"invalid_input": "Invalid input",
572
"invalid_password": "Invalid password",
573
"invoice_details": "Invoice details",
574
+ "is_currently_being_migrated": "is currently being migrated",
575
"is_percentage": "is",
576
"keys": "Keys",
577
"label": "Label",
@@ -646,6 +647,7 @@
647
"messages": "Messages",
648
"method": "Method",
649
"methods": "Methods",
650
+ "migrating_to_ironwood": "migrating to Ironwood",
651
"min_amount": "Min: ${value}",
652
"min_value": "Min: ${value} ${currency}",
653
"mining": "Mining",
@@ -1476,5 +1478,9 @@
1478
"zcash_card_enable_later": "You can always enable this card later in settings",
1479
"zcash_card_missing_funds": "Missing funds?",
1480
"zcash_card_scan": "Scan",
1479
- "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch."
1481
+ "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch.",
1482
+ "zcash_network_upgrade": "Zcash Network Upgrade",
1483
+ "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.",
1484
+ "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.",
1485
+ "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."
1486
}
tool/configure.dart
+1
@@ -1770,6 +1770,7 @@ abstract class Zcash {
1770
bool showMissingFundsCard(WalletBase wallet);
1771
Future<void> rescanInternalChange(WalletBase wallet);
1772
bool ironwoodActive(WalletAddresses walletAddresses);
1773
+ Future<bool> hasOrchardMigratableBalance(WalletBase wallet);
1774
}
1775
""";
1776