diff --git a/assets/translations/en.json b/assets/translations/en.json index b33edc6036..60ab6a2a91 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -769,6 +769,7 @@ "noBannedPubkeys": "No banned pubkeys found", "unbanPubkeysFailed": "Failed to unban pubkeys", "privateKeyRetrievalFailed": "Failed to retrieve private keys. Please try again.", + "privateKeysEmptyError": "No private keys found. Assets may need to be activated. Please try again later.", "fetchingPrivateKeysTitle": "Fetching Private Keys...", "fetchingPrivateKeysMessage": "Please wait while we securely fetch your private keys...", "pubkeyType": "Type", diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index aeadb58ad1..ceb3c15a21 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -756,6 +756,7 @@ abstract class LocaleKeys { static const noBannedPubkeys = 'noBannedPubkeys'; static const unbanPubkeysFailed = 'unbanPubkeysFailed'; static const privateKeyRetrievalFailed = 'privateKeyRetrievalFailed'; + static const privateKeysEmptyError = 'privateKeysEmptyError'; static const fetchingPrivateKeysTitle = 'fetchingPrivateKeysTitle'; static const fetchingPrivateKeysMessage = 'fetchingPrivateKeysMessage'; static const pubkeyType = 'pubkeyType'; diff --git a/lib/views/settings/widgets/security_settings/security_settings_page.dart b/lib/views/settings/widgets/security_settings/security_settings_page.dart index 0cf6016229..14f8b06599 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -245,29 +245,44 @@ class _SecuritySettingsPageState extends State { /// /// This approach provides better UX by showing loading state during the entire operation. Future onViewPrivateKeysPressed(BuildContext context) async { + // IMPORTANT: Store keys in a local variable first to avoid state loss during async operations. + // The onPasswordValidated callback executes asynchronously within WidgetsBinding.instance.addPostFrameCallback, + // and setting _sdkPrivateKeys directly inside the callback may cause the state to be lost when the widget + // rebuilds or when the dialog closes. By storing data in a local variable first and then assigning it to + // _sdkPrivateKeys AFTER the dialog closes, we ensure the state is preserved when the widget rebuilds. + Map>? fetchedKeys; + bool isEmptyKeys = false; + + // Store SDK reference before async operations to avoid BuildContext usage across async gaps + final sdk = context.sdk; + final bool success = await walletPasswordDialogWithLoading( context, onPasswordValidated: (String password) async { try { // Fetch private keys directly into local UI state // This keeps sensitive data in minimal scope - final privateKeys = await context.sdk.security.getPrivateKeys(); + final privateKeys = await sdk.security.getPrivateKeys(); + + // Check if private keys are empty (e.g., when coins haven't been activated yet) + if (privateKeys.isEmpty) { + isEmptyKeys = true; + return false; // Failure - empty private keys + } // Filter out excluded assets (NFTs only) // Geo-blocked assets are handled by the UI toggle final filteredPrivateKeyEntries = privateKeys.entries.where( (entry) => !excludedAssetList.contains(entry.key.id), ); - _sdkPrivateKeys = Map.fromEntries(filteredPrivateKeyEntries); + fetchedKeys = Map.fromEntries(filteredPrivateKeyEntries); return true; // Success } catch (e) { - // Clear sensitive data on any error + isEmptyKeys = false; // Exception occurred, not empty keys + // Clear any previously stored private keys to prevent stale data from persisting _clearPrivateKeyData(); - // Log error for debugging - debugPrint('Failed to retrieve private keys: ${e.toString()}'); - return false; // Failure } }, @@ -277,16 +292,31 @@ class _SecuritySettingsPageState extends State { passwordFieldKey: 'confirmation-showing-private-keys', ); - if (!mounted) return; + if (!mounted) { + return; + } - if (success) { + if (success && fetchedKeys != null) { + // Set the keys AFTER dialog closes to ensure state is preserved + setState(() { + _sdkPrivateKeys = fetchedKeys; + }); + // Clear the local reference to minimize the number of places holding sensitive data + // Note: fetchedKeys is a reference to the same Map object, so we only set it to null + // to remove the extra reference, not clear() which would clear the data used by _sdkPrivateKeys + fetchedKeys = null; + // Private keys are ready, show the private keys screen // ignore: use_build_context_synchronously context.read().add(const ShowPrivateKeysEvent()); } else { // Show error to user + // Check if failure was due to empty private keys + final errorMessage = isEmptyKeys + ? LocaleKeys.privateKeysEmptyError.tr() + : LocaleKeys.privateKeyRetrievalFailed.tr(); // ignore: use_build_context_synchronously - _showPrivateKeyError(context, LocaleKeys.privateKeyRetrievalFailed.tr()); + _showPrivateKeyError(context, errorMessage); } }