Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions lib/generated/codegen_loader.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,29 +245,44 @@ class _SecuritySettingsPageState extends State<SecuritySettingsPage> {
///
/// This approach provides better UX by showing loading state during the entire operation.
Future<void> 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<AssetId, List<PrivateKey>>? 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
}
},
Expand All @@ -277,16 +292,31 @@ class _SecuritySettingsPageState extends State<SecuritySettingsPage> {
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<SecuritySettingsBloc>().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);
}
}

Expand Down
Loading