CW-809: Fix iOS iCloud Related Issues (#2334)

* feat(ios-storage-issue): Remove 2FA/PIN persistence across app re-installs in iOS This error occurs due to the keychain persisting auth data on user devices connected to the same iCloud * feat(reset-service): Remove PIN persistence for app reinstall * Update lib/main.dart [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Jul 16, 2025 at 04:54 UTC 80e24a8a1d5ab706216a6f2fbf639cd23b6020a6
3 files changed +178 -4
lib/core/reset_service.dart new
+162
@@ -0,0 +1,162 @@
1 +import 'package:cake_wallet/core/secure_storage.dart';
2 +import 'package:cake_wallet/entities/secret_store_key.dart';
3 +import 'package:cake_wallet/store/authentication_store.dart';
4 +import 'package:cake_wallet/store/settings_store.dart';
5 +import 'package:cake_wallet/entities/preferences_key.dart';
6 +import 'package:shared_preferences/shared_preferences.dart';
7 +import 'package:cw_core/utils/print_verbose.dart';
8 +
9 +class ResetService {
10 + ResetService({
11 + required this.secureStorage,
12 + required this.authenticationStore,
13 + required this.settingsStore,
14 + });
15 +
16 + final SecureStorage secureStorage;
17 + final AuthenticationStore authenticationStore;
18 + final SettingsStore settingsStore;
19 +
20 + static const List<String> _authKeys = [
21 + SecureKey.allowBiometricalAuthenticationKey,
22 + SecureKey.useTOTP2FA,
23 + SecureKey.shouldRequireTOTP2FAForAccessingWallet,
24 + SecureKey.shouldRequireTOTP2FAForSendsToContact,
25 + SecureKey.shouldRequireTOTP2FAForSendsToNonContact,
26 + SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets,
27 + SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets,
28 + SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets,
29 + SecureKey.shouldRequireTOTP2FAForAddingContacts,
30 + SecureKey.shouldRequireTOTP2FAForCreatingNewWallets,
31 + SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
32 + SecureKey.selectedCake2FAPreset,
33 + SecureKey.totpSecretKey,
34 + SecureKey.pinTimeOutDuration,
35 + SecureKey.lastAuthTimeMilliseconds,
36 + 'PIN_CODE_PASSWORD',
37 + ];
38 +
39 + static const List<String> _walletPreferencesKeys = [
40 + PreferencesKey.currentWalletName,
41 + PreferencesKey.currentWalletType,
42 + ];
43 +
44 + bool _isAuthKey(String key) => _authKeys.contains(key);
45 +
46 + /// Checks if this is a new install and clears any existing auth data from Storage
47 + Future<void> resetAuthDataOnNewInstall(SharedPreferences sharedPreferences) async {
48 + try {
49 + final isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? false;
50 +
51 + if (isNewInstall) {
52 + await _clearExistingAuthDataOnNewInstall(sharedPreferences);
53 + }
54 + } catch (e) {
55 + printV('Error during new install auth reset: $e');
56 + }
57 + }
58 +
59 + /// Checks if there's existing auth data that should be cleared on new install
60 + Future<void> _clearExistingAuthDataOnNewInstall(SharedPreferences sharedPreferences) async {
61 + final allKeys = await secureStorage.readAll();
62 + final authKeysFound = <String>[];
63 + final walletPrefsFound = <String>[];
64 +
65 + for (final key in allKeys.keys) {
66 + if (_isAuthKey(key)) {
67 + authKeysFound.add(key);
68 + }
69 + }
70 +
71 + for (final key in _walletPreferencesKeys) {
72 + if (sharedPreferences.containsKey(key)) {
73 + walletPrefsFound.add(key);
74 + }
75 + }
76 +
77 + if (authKeysFound.isNotEmpty || walletPrefsFound.isNotEmpty) {
78 + printV(
79 + 'Found ${authKeysFound.length} existing auth keys in storage: ${authKeysFound.join(', ')}',
80 + );
81 + printV(
82 + 'Found ${walletPrefsFound.length} existing wallet preferences: ${walletPrefsFound.join(', ')}',
83 + );
84 +
85 + await resetAuthenticationData(sharedPreferences);
86 + }
87 + }
88 +
89 + /// Resets authentication data auth store, storage and settings store
90 + Future<void> resetAuthenticationData(SharedPreferences sharedPreferences) async {
91 + try {
92 + authenticationStore.state = AuthenticationState.uninitialized;
93 +
94 + await Future.wait([
95 + _deleteAuthenticationKeys(),
96 + _resetSettingsStoreAuthData(),
97 + _clearWalletPreferences(sharedPreferences),
98 + ]);
99 + } catch (e) {
100 + printV('An error occurred during authentication reset: $e');
101 + rethrow;
102 + }
103 + }
104 +
105 + /// Resets authentication-related data in SettingsStore to default values
106 + Future<void> _resetSettingsStoreAuthData() async {
107 + settingsStore.useTOTP2FA = false;
108 + settingsStore.totpSecretKey = '';
109 + settingsStore.shouldRequireTOTP2FAForAccessingWallet = false;
110 + settingsStore.shouldRequireTOTP2FAForSendsToContact = false;
111 + settingsStore.shouldRequireTOTP2FAForSendsToNonContact = false;
112 + settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = false;
113 + settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = false;
114 + settingsStore.shouldRequireTOTP2FAForExchangesToExternalWallets = false;
115 + settingsStore.shouldRequireTOTP2FAForAddingContacts = false;
116 + settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = false;
117 + settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = false;
118 + settingsStore.allowBiometricalAuthentication = false;
119 + }
120 +
121 + Future<void> _clearWalletPreferences(SharedPreferences sharedPreferences) async {
122 + final failedDeletions = <String>[];
123 +
124 + for (final key in _walletPreferencesKeys) {
125 + try {
126 + await sharedPreferences.remove(key);
127 + } catch (e) {
128 + failedDeletions.add(key);
129 + }
130 + }
131 +
132 + if (failedDeletions.isNotEmpty) {
133 + printV(
134 + 'Warning: Failed to delete ${failedDeletions.length} wallet preferences: ${failedDeletions.join(', ')}',
135 + );
136 + } else {
137 + printV('All wallet preferences deleted successfully');
138 + }
139 + }
140 +
141 + Future<void> _deleteAuthenticationKeys() async {
142 + final failedDeletions = <String>[];
143 +
144 + final deletionFutures = _authKeys.map((key) async {
145 + try {
146 + await secureStorage.delete(key: key);
147 + } catch (e) {
148 + failedDeletions.add(key);
149 + }
150 + });
151 +
152 + await Future.wait(deletionFutures);
153 +
154 + if (failedDeletions.isNotEmpty) {
155 + printV(
156 + 'Warning: Failed to delete ${failedDeletions.length} auth keys: ${failedDeletions.join(', ')}',
157 + );
158 + } else {
159 + printV('All auth keys deleted successfully');
160 + }
161 + }
162 +}
lib/di.dart
+9
@@ -276,6 +276,7 @@ import 'src/screens/buy/buy_sell_page.dart';
276 import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
277 import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
278 import 'package:cake_wallet/core/trade_monitor.dart';
279 +import 'package:cake_wallet/core/reset_service.dart';
280
281 final getIt = GetIt.instance;
282
@@ -554,6 +555,14 @@ Future<void> setup({
555 ),
556 );
557
558 + getIt.registerFactory<ResetService>(
559 + () => ResetService(
560 + secureStorage: getIt.get<SecureStorage>(),
561 + authenticationStore: getIt.get<AuthenticationStore>(),
562 + settingsStore: getIt.get<SettingsStore>(),
563 + ),
564 + );
565 +
566 getIt.registerFactory<AuthViewModel>(() => AuthViewModel(getIt.get<AuthService>(),
567 getIt.get<SharedPreferences>(), getIt.get<SettingsStore>(), BiometricAuth()));
568
lib/main.dart
+7 -4
@@ -25,7 +25,6 @@ import 'package:cake_wallet/routes.dart';
25 import 'package:cake_wallet/src/screens/root/root.dart';
26 import 'package:cake_wallet/store/app_store.dart';
27 import 'package:cake_wallet/store/authentication_store.dart';
28 -import 'package:cake_wallet/themes/core/material_base_theme.dart';
28 import 'package:cake_wallet/themes/utils/theme_provider.dart';
29 import 'package:cake_wallet/store/settings_store.dart';
30 import 'package:cake_wallet/utils/device_info.dart';
@@ -56,6 +55,7 @@ import 'package:shared_preferences/shared_preferences.dart';
55 import 'package:cw_core/window_size.dart';
56 import 'package:logging/logging.dart';
57 import 'package:cake_wallet/core/trade_monitor.dart';
58 +import 'package:cake_wallet/core/reset_service.dart';
59
60 final navigatorKey = GlobalKey<NavigatorState>();
61 final rootKey = GlobalKey<RootState>();
@@ -202,8 +202,8 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
202 final powNodes =
203 await CakeHive.openBox<Node>(Node.boxName + "pow"); // must be different from Node.boxName
204 final transactionDescriptions = await CakeHive.openBox<TransactionDescription>(
205 - TransactionDescription.boxName,
206 - encryptionKey: transactionDescriptionsBoxKey);
205 + TransactionDescription.boxName,
206 + encryptionKey: transactionDescriptionsBoxKey);
207 final trades = await CakeHive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey);
208 final orders = await CakeHive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
209 final walletInfoSource = await CakeHive.openBox<WalletInfo>(WalletInfo.boxName);
@@ -287,6 +287,9 @@ Future<void> initialSetup({
287 navigatorKey: navigatorKey,
288 secureStorage: secureStorage,
289 );
290 +
291 + await getIt.get<ResetService>().resetAuthDataOnNewInstall(sharedPreferences);
292 +
293 await bootstrapOffline();
294 final settingsStore = getIt<SettingsStore>();
295 if (!settingsStore.currentBuiltinTor) {
@@ -314,7 +317,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
317 final statusBarColor = Colors.transparent;
318 final authenticationStore = getIt.get<AuthenticationStore>();
319 final initialRoute = authenticationStore.state == AuthenticationState.uninitialized
317 - ? Routes.welcome
320 + ? Routes.welcome
321 : settingsStore.currentBuiltinTor ? Routes.startTor : Routes.login;
322 final currentTheme = appStore.themeStore.currentTheme;
323 final statusBarBrightness = currentTheme.type == currentTheme.isDark