Skip to content
Open
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
14 changes: 13 additions & 1 deletion app/src/navigation/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
import AccountRestoreScreen from '@/screens/account/recovery/AccountRestoreScreen';
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen';
import RecoverWithPhraseScreen from '@/screens/account/recovery/RecoverWithPhraseScreen';
import CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
Expand Down Expand Up @@ -81,7 +82,18 @@ const accountScreens = {
screens: {},
},
},

AccountRestore: {
screen: AccountRestoreScreen,
options: {
title: 'Restore Account',
headerStyle: {
backgroundColor: white,
},
headerTitleStyle: {
color: black,
},
} as NativeStackNavigationOptions,
},
ShowRecoveryPhrase: {
screen: ShowRecoveryPhraseScreen,
options: IS_EUCLID_ENABLED
Expand Down
59 changes: 59 additions & 0 deletions app/src/providers/passportDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
inferDocumentCategory,
} from '@selfxyz/common/utils';
import { parseCertificateSimple } from '@selfxyz/common/utils/certificate_parsing/parseCertificateSimple';
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
import type {
Comment on lines +57 to 58
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hard‑coding 'prod' when fetching Aadhaar protocol data in bulk restore

In restoreSecretForAllDocuments, Aadhaar keys are loaded with a hard‑coded production environment:

if (!useProtocolStore.getState().aadhaar.public_keys) {
  await useProtocolStore.getState().aadhaar.fetch_all('prod');
}

This couples bulk restore to prod only and will likely break or silently mis‑verify documents in staging/dev (or any non‑prod environment), since the commitment tree and public keys won’t match the environment the app/selfClient is actually using.

Consider deriving the environment from existing configuration (e.g., a field on selfClient, a global env setting, or whatever your other protocol-store fetches already use) and passing that through instead of a literal:

const env = /* derive from selfClient or config */;
if (!useProtocolStore.getState().aadhaar.public_keys) {
  await useProtocolStore.getState().aadhaar.fetch_all(env);
}

This keeps restore behavior aligned with the currently active network and avoids hard‑to‑debug environment mismatches.

Also applies to: 892-947

🤖 Prompt for AI Agents
In app/src/providers/passportDataProvider.tsx around lines 57-58 (and similarly
at 892-947), the code hard-codes the Aadhaar protocol fetch environment to
'prod' which can cause restore/verification mismatches in non-production
environments; change the call to derive the environment from the current
runtime/config (for example use a field on selfClient, a protocol-store helper,
or a global env/config value the app already uses) and pass that derived env
into useProtocolStore.getState().aadhaar.fetch_all(env) instead of 'prod';
ensure the derived value is validated (fallback to a sensible default) and
update all other instances (892-947) to use the same derived environment
variable so bulk restore aligns with the active network.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The screen where we hardcoded will be accessible only in the ‘prod’ environment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seshanthS, thanks for clarifying! If the restore feature is prod-only by design, then hard-coding 'prod' is reasonable.

To make that constraint more explicit for future maintainers, you could consider adding a comment above the fetch call:

// Restore feature is prod-only; always use prod environment
if (!useProtocolStore.getState().aadhaar.public_keys) {
  await useProtocolStore.getState().aadhaar.fetch_all('prod');
}

Or if you want to be extra defensive, add a runtime check that throws early if someone tries to use this in non-prod. But if the UI already enforces the prod-only access, the current approach is fine.


🧠 Learnings used
Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-25T14:06:55.970Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Implement certificate validation for passport data verification.

Learnt from: shazarre
Repo: selfxyz/self PR: 1041
File: app/src/providers/passportDataProvider.tsx:297-301
Timestamp: 2025-09-10T14:47:40.945Z
Learning: In app/src/providers/passportDataProvider.tsx: The deleteDocumentDirectlyFromKeychain function is a low-level utility used by the DocumentsAdapter and should not include error handling since callers like deleteDocument() already implement appropriate try/catch with logging for Keychain operations.

Learnt from: CR
Repo: selfxyz/self PR: 0
File: packages/mobile-sdk-alpha/AGENTS.md:0-0
Timestamp: 2025-11-25T14:08:51.177Z
Learning: Applies to packages/mobile-sdk-alpha/**/*.test.{ts,tsx} : Test `isPassportDataValid()` with realistic, synthetic passport data and never use real user PII

Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursor/rules/compliance-verification.mdc:0-0
Timestamp: 2025-11-25T14:07:28.188Z
Learning: Applies to **/{compliance,ofac,verification,identity}/**/*.{ts,tsx,js,py} : Validate passport numbers by removing whitespace/punctuation and performing country-specific format validation

Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursor/rules/compliance-verification.mdc:0-0
Timestamp: 2025-11-25T14:07:28.188Z
Learning: Applies to **/{compliance,ofac,verification,identity}/**/*.{ts,tsx,js,py} : Implement three-tier OFAC verification system: Passport Number Check (direct passport validation), Name + DOB Check (full name with exact date of birth), and Name + Year Check (name with year of birth, defaulting to Jan-01)

Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-25T14:06:55.970Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not log sensitive data in production, including identity verification and passport information.

Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursor/rules/mobile-sdk-migration.mdc:0-0
Timestamp: 2025-11-25T14:07:55.507Z
Learning: Applies to **/*.{ts,tsx,js} : Never log PII, credentials, or private keys in production code; use DEBUG_SECRETS_TOKEN flag for debug-level secrets

AadhaarData,
DocumentCatalog,
Expand Down Expand Up @@ -887,3 +888,61 @@ export const getAllDocumentsDirectlyFromKeychain = async (): Promise<{

return allDocs;
};

/**
* Verifies if the secret is valid for all documents and marks the status as registered.
* This can be used when a document registeration fails mid-process and we want to restore the status of the document,
* instead of starting the registration process again (MRZ, NFC, etc).
*/
export const restoreSecretForAllDocuments = async (
selfClient: SelfClient,
secret: string,
): Promise<string[]> => {
const catalog = await selfClient.loadDocumentCatalog();
const useProtocolStore = selfClient.useProtocolStore;
const successDocuments: string[] = [];
if (!useProtocolStore.getState().aadhaar.public_keys) {
await useProtocolStore.getState().aadhaar.fetch_all('prod');
}
for (const document of catalog.documents) {
try {
if (!document.isRegistered && document.mock === false) {
const data = await selfClient.loadDocumentById(document.id);
if (data) {
const { isRegistered: docIsRegistered, csca: docCsca } =
await isUserRegisteredWithAlternativeCSCA(data, secret as string, {
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}

return useProtocolStore.getState()[docCategory]
.alternative_csca;
},
});

if (!docIsRegistered) {
continue;
}

if (docIsRegistered && docCsca && isMRZDocument(data)) {
await reStorePassportDataWithRightCSCA(data, docCsca as string);
}
await updateDocumentRegistrationState(document.id, true);
successDocuments.push(document.id);
}
}
} catch (error) {
console.error('Error restoring document:', error);
}
}
return successDocuments;
};
4 changes: 3 additions & 1 deletion app/src/providers/selfClientProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {

addListener(SdkEvents.PROVING_ACCOUNT_RECOVERY_REQUIRED, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('AccountRecoveryChoice');
navigationRef.navigate('AccountRecoveryChoice', {
restoreAllDocuments: false,
});
}
});

Expand Down
132 changes: 83 additions & 49 deletions app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import React, { useCallback, useState } from 'react';
import { Separator, View, XStack, YStack } from 'tamagui';
import type { StaticScreenProps } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';

Expand Down Expand Up @@ -36,12 +37,20 @@ import { getPrivateKeyFromMnemonic, useAuth } from '@/providers/authProvider';
import {
loadPassportData,
reStorePassportDataWithRightCSCA,
restoreSecretForAllDocuments,
} from '@/providers/passportDataProvider';
import { STORAGE_NAME, useBackupMnemonic } from '@/services/cloud-backup';
import { useSettingStore } from '@/stores/settingStore';
import type { Mnemonic } from '@/types/mnemonic';

const AccountRecoveryChoiceScreen: React.FC = () => {
type AccountRecoveryChoiceScreenProps = StaticScreenProps<{
restoreAllDocuments?: boolean;
}>;

const AccountRecoveryChoiceScreen: React.FC<
AccountRecoveryChoiceScreenProps
> = ({ route }) => {
const restoreAllDocuments = route.params?.restoreAllDocuments ?? false;
const selfClient = useSelfClient();
const { useProtocolStore } = selfClient;
const { trackEvent } = useSelfClient();
Expand All @@ -63,13 +72,29 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
// );

const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess');
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase');
const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase', {
params: {
restoreAllDocuments: restoreAllDocuments,
},
});

// DISABLED FOR NOW: Turnkey functionality
// useEffect(() => {
// refreshWallets();
// }, [refreshWallets]);

const handleRestoreFailed = useCallback(
(reason: string, hasCSCA: boolean) => {
console.warn('Failed to restore account');
trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED, {
reason: reason,
hasCSCA: hasCSCA,
});
navigation.navigate({ name: 'Home', params: {} });
},
[trackEvent, navigation],
);
Comment on lines +86 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "AccountRecoveryChoiceScreen.tsx" | head -5

Repository: selfxyz/self

Length of output: 122


🏁 Script executed:

cat -n app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx

Repository: selfxyz/self

Length of output: 14363


🏁 Script executed:

cd app && yarn types 2>&1 | grep -A 5 "AccountRecoveryChoiceScreen"

Repository: selfxyz/self

Length of output: 38


🏁 Script executed:

cd app && yarn types 2>&1 | head -100

Repository: selfxyz/self

Length of output: 525


🏁 Script executed:

cd app && npx tsc --noEmit 2>&1 | grep -A 3 "AccountRecoveryChoiceScreen"

Repository: selfxyz/self

Length of output: 38


Move handleRestoreFailed inside restoreAccountFlow to fix scope error on setRestoring

Line 95 references setRestoring(false), but setRestoring only exists as a parameter to restoreAccountFlow (line 104), not in the scope where handleRestoreFailed is defined (lines 87-98). This causes a TypeScript error and would crash at runtime.

Move handleRestoreFailed inside restoreAccountFlow so it can close over setRestoring, and remove it from the dependency array at line 205. This preserves all analytics and navigation behavior while fixing the scope error.

🧰 Tools
🪛 GitHub Actions: Mobile CI

[error] 95-95: TypeScript error: Cannot find name 'setRestoring'. Ensure 'setRestoring' is defined (e.g., from useState) or fix its usage.


const restoreAccountFlow = useCallback(
async (
mnemonic: Mnemonic,
Expand Down Expand Up @@ -100,54 +125,61 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
return false;
}

const passportDataParsed = JSON.parse(passportData);

const { isRegistered, csca } =
await isUserRegisteredWithAlternativeCSCA(
passportDataParsed,
if (restoreAllDocuments) {
const successDocuments = await restoreSecretForAllDocuments(
selfClient,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}
);
if (successDocuments.length === 0) {
handleRestoreFailed('all_documents_not_registered', false);
setRestoring(false);
return false;
}
} else {
const passportDataParsed = JSON.parse(passportData);

return useProtocolStore.getState()[docCategory]
.alternative_csca;
const { isRegistered, csca } =
await isUserRegisteredWithAlternativeCSCA(
passportDataParsed,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory]
.commitment_tree;
},
getAltCSCA(docCategory) {
if (docCategory === 'aadhaar') {
const publicKeys =
useProtocolStore.getState().aadhaar.public_keys;
// Convert string[] to Record<string, string> format expected by AlternativeCSCA
return publicKeys
? Object.fromEntries(publicKeys.map(key => [key, key]))
: {};
}

return useProtocolStore.getState()[docCategory]
.alternative_csca;
},
},
},
);
if (!isRegistered) {
console.warn(
'Secret provided did not match a registered ID. Please try again.',
);
trackEvent(
BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED,
{
reason: 'document_not_registered',
hasCSCA: !!csca,
},
);
if (!isRegistered) {
console.warn(
'Secret provided did not match a registered ID. Please try again.',
);
handleRestoreFailed('document_not_registered', !!csca);
setRestoring(false);
return false;
}
if (isCloudRestore && !cloudBackupEnabled) {
toggleCloudBackupEnabled();
}
await reStorePassportDataWithRightCSCA(
Comment on lines +128 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cloud backup toggle is skipped for “restore all documents” cloud restores

In restoreAccountFlow, the isCloudRestore && !cloudBackupEnabled toggle only runs in the single-document branch:

if (restoreAllDocuments) {
  const successDocuments = await restoreSecretForAllDocuments(...);
  if (successDocuments.length === 0) {
    handleRestoreFailed(...);
    setRestoring(false);
    return false;
  }
} else {
  // ...
  if (isCloudRestore && !cloudBackupEnabled) {
    toggleCloudBackupEnabled();
  }
  await reStorePassportDataWithRightCSCA(...);
  await markCurrentDocumentAsRegistered(selfClient);
}

When restoreAllDocuments === true and the user comes via the cloud-restore path (isCloudRestore === true), a successful restore never enables cloudBackupEnabled. That’s a reliability issue: users who just recovered from cloud backup on the “restore all documents” path will silently remain without backup enabled.

You can restore the original “enable backup whenever a cloud restore succeeds” behavior (for both single-doc and all-doc flows) by moving the toggle to after the branch, just before logging success:

-          if (isCloudRestore && !cloudBackupEnabled) {
-            toggleCloudBackupEnabled();
-          }
-          await reStorePassportDataWithRightCSCA(
+          await reStorePassportDataWithRightCSCA(
             passportDataParsed,
             csca as string,
           );
           await markCurrentDocumentAsRegistered(selfClient);
         }
-
-        trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS);
+        if (isCloudRestore && !cloudBackupEnabled) {
+          toggleCloudBackupEnabled();
+        }
+
+        trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS);

This keeps the early-return failure paths unchanged while ensuring any successful cloud-based restore (single doc or all docs) leaves the account with cloud backup correctly enabled.

Also applies to: 183-187, 198-209

🤖 Prompt for AI Agents
In app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx around lines
128-176 (and also consider related sections at 183-187 and 198-209), the
cloud-backup toggle runs only in the single-document branch so successful cloud
restores that use the "restore all documents" path never enable cloud backup;
move the isCloudRestore && !cloudBackupEnabled check out of the else branch and
instead perform it once after the if/else completes successfully (i.e., after
the early-return failure checks for restoreAllDocuments and after the single-doc
registration check) so that for both the all-docs and single-doc successful
flows you call toggleCloudBackupEnabled() when needed while leaving all existing
early returns unchanged.

passportDataParsed,
csca as string,
);
navigation.navigate({ name: 'Home', params: {} });
setRestoring(false);
return false;
await markCurrentDocumentAsRegistered(selfClient);
}
if (isCloudRestore && !cloudBackupEnabled) {
toggleCloudBackupEnabled();
}
await reStorePassportDataWithRightCSCA(
passportDataParsed,
csca as string,
);
await markCurrentDocumentAsRegistered(selfClient);

trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS);
trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED);
onRestoreFromCloudNext();
Expand All @@ -164,14 +196,16 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
}
},
[
trackEvent,
restoreAccountFromMnemonic,
cloudBackupEnabled,
restoreAllDocuments,
trackEvent,
onRestoreFromCloudNext,
navigation,
toggleCloudBackupEnabled,
useProtocolStore,
selfClient,
handleRestoreFailed,
cloudBackupEnabled,
useProtocolStore,
toggleCloudBackupEnabled,
],
);

Expand Down
6 changes: 5 additions & 1 deletion app/src/screens/account/recovery/AccountRecoveryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';

const AccountRecoveryScreen: React.FC = () => {
const onRestoreAccountPress = useHapticNavigation('AccountRecoveryChoice');
const onRestoreAccountPress = useHapticNavigation('AccountRecoveryChoice', {
params: {
restoreAllDocuments: true,
},
});
const onCreateAccountPress = useHapticNavigation('CloudBackupSettings', {
params: {
nextScreen: 'SaveRecoveryPhrase',
Expand Down
Loading
Loading