Skip to content
6 changes: 0 additions & 6 deletions .cursorignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ app/android/dev-keystore
circuits/scripts/server/*.sh
!node_modules/**/*.sh

# Fastlane configuration (may contain secrets)
app/fastlane/Fastfile
app/fastlane/helpers.rb

# Test wallets and mock data
app/ios/passport.json
app/ios/OpenPassport/passport.json
Expand Down Expand Up @@ -100,7 +96,6 @@ contracts/ignition/deployments/
**/.pnp.*

# Mobile specific
app/ios/Podfile.lock
app/android/link-assets-manifest.json
app/ios/link-assets-manifest.json

Expand Down Expand Up @@ -162,7 +157,6 @@ app/android/android-passport-reader/app/src/main/assets/tessdata/
# IDE & Editor Files
# ========================================

.vscode/
.idea/
*.swp
*.swo
Expand Down
49 changes: 47 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,49 @@
{
// Performance Optimizations
"files.watcherExclude": {
"**/node_modules/**": true,
"**/.git/**": true,
"**/dist/**": true,
"**/build/**": true,
"**/vendor/**": true,
"**/coverage/**": true,
"**/.nyc_output/**": true,
"**/android/app/build/**": true,
"**/ios/build/**": true,
"**/circuits/build/**": true,
"**/Pods/**": true,
"**/.gradle/**": true,
"**/DerivedData/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/build": true,
"**/vendor": true,
"**/coverage": true,
"**/.nyc_output": true,
"**/android/app/build": true,
"**/ios/build": true,
"**/circuits/build": true
},
"files.exclude": {
"**/node_modules": false,
"**/.git": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},

// TypeScript Performance (Keep the good stuff)
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.suggest.autoImports": true,
"typescript.disableAutomaticTypeAcquisition": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.suggestionActions.enabled": true,

// Editor Performance (Sensible optimizations only)
"editor.minimap.enabled": false,
"editor.hover.delay": 500,

// Formatting & Linting
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
Expand All @@ -8,8 +53,8 @@
"editor.formatOnSave": true
},

// ESLint Configuration
"eslint.run": "onType",
// ESLint Configuration - Optimized for Performance
"eslint.run": "onSave",
"eslint.format.enable": true,
"eslint.lintTask.enable": true,
"eslint.quiet": false,
Expand Down
128 changes: 82 additions & 46 deletions app/src/providers/passportDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ import {
} from '@selfxyz/common/utils';

import { unsafe_getPrivateKey, useAuth } from '../providers/authProvider';
import { safeJsonParse } from '../utils/jsonUtils';

// Import testing utilities conditionally
let clearDocumentCatalogForMigrationTesting: (() => Promise<void>) | undefined;
if (__DEV__) {
try {
const testingUtils = require('../utils/testingUtils');
clearDocumentCatalogForMigrationTesting =
testingUtils.clearDocumentCatalogForMigrationTesting;
} catch (error) {
console.warn('Testing utilities not available:', error);
}
}

// Create safe wrapper functions to prevent undefined errors during early initialization
// These need to be declared early to avoid dependency issues
Expand Down Expand Up @@ -129,6 +142,10 @@ function inferDocumentCategory(documentType: string): DocumentCategory {
// Global flag to track if native modules are ready
let nativeModulesReady = false;

// Mutex to prevent concurrent initialization
let initializationInProgress = false;
let initializationPromise: Promise<boolean> | null = null;

export const PassportContext = createContext<IPassportContext>({
getData: () => Promise.resolve(null),
getSelectedData: () => Promise.resolve(null),
Expand All @@ -146,7 +163,7 @@ export const PassportContext = createContext<IPassportContext>({
migrateFromLegacyStorage: migrateFromLegacyStorage,
getCurrentDocumentType: getCurrentDocumentType,
clearDocumentCatalogForMigrationTesting:
clearDocumentCatalogForMigrationTesting,
clearDocumentCatalogForMigrationTesting || (() => Promise.resolve()),
markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered,
updateDocumentRegistrationState: updateDocumentRegistrationState,
checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration,
Expand All @@ -158,14 +175,17 @@ export const PassportProvider = ({ children }: PassportProviderProps) => {
const { _getSecurely } = useAuth();

const getData = useCallback(
() => _getSecurely<PassportData>(loadPassportData, str => JSON.parse(str)),
() =>
_getSecurely<PassportData>(loadPassportData, str =>
safeJsonParse(str, null as any),
),
[_getSecurely],
);

const getSelectedData = useCallback(() => {
return _getSecurely<PassportData>(
() => loadSelectedPassportData(),
str => JSON.parse(str),
str => safeJsonParse(str, null as any),
);
}, [_getSecurely]);

Expand All @@ -177,15 +197,15 @@ export const PassportProvider = ({ children }: PassportProviderProps) => {
() =>
_getSecurely<{ passportData: PassportData; secret: string }>(
loadPassportDataAndSecret,
str => JSON.parse(str),
str => safeJsonParse(str, null as any),
),
[_getSecurely],
);

const getSelectedPassportDataAndSecret = useCallback(() => {
return _getSecurely<{ passportData: PassportData; secret: string }>(
() => loadSelectedPassportDataAndSecret(),
str => JSON.parse(str),
str => safeJsonParse(str, null as any),
);
}, [_getSecurely]);

Expand All @@ -207,7 +227,7 @@ export const PassportProvider = ({ children }: PassportProviderProps) => {
migrateFromLegacyStorage: migrateFromLegacyStorage,
getCurrentDocumentType: getCurrentDocumentType,
clearDocumentCatalogForMigrationTesting:
clearDocumentCatalogForMigrationTesting,
clearDocumentCatalogForMigrationTesting || (() => Promise.resolve()),
markCurrentDocumentAsRegistered: markCurrentDocumentAsRegistered,
updateDocumentRegistrationState: updateDocumentRegistrationState,
checkIfAnyDocumentsNeedMigration: checkIfAnyDocumentsNeedMigration,
Expand Down Expand Up @@ -248,34 +268,8 @@ export async function checkIfAnyDocumentsNeedMigration(): Promise<boolean> {
}
}

export async function clearDocumentCatalogForMigrationTesting() {
console.log('Clearing document catalog for migration testing...');
const catalog = await loadDocumentCatalog();

// Delete all new-style documents
for (const doc of catalog.documents) {
try {
await Keychain.resetGenericPassword({ service: `document-${doc.id}` });
console.log(`Cleared document: ${doc.id}`);
} catch (error) {
console.log(`Document ${doc.id} not found or already cleared`);
}
}

// Clear the catalog itself
try {
await Keychain.resetGenericPassword({ service: 'documentCatalog' });
console.log('Cleared document catalog');
} catch (error) {
console.log('Document catalog not found or already cleared');
}

// Note: We intentionally do NOT clear legacy storage entries
// (passportData, mockPassportData, etc.) so migration can be tested
console.log(
'Document catalog cleared. Legacy storage preserved for migration testing.',
);
}
// clearDocumentCatalogForMigrationTesting has been moved to utils/testingUtils.ts
// to prevent it from being included in production builds

export async function clearPassportData() {
const catalog = await loadDocumentCatalog();
Expand Down Expand Up @@ -399,23 +393,47 @@ export async function initializeNativeModules(
maxRetries: number = 10,
delay: number = 500,
): Promise<boolean> {
// If already ready, return immediately
if (nativeModulesReady) {
return true;
}

// If initialization is already in progress, wait for it to complete
if (initializationInProgress && initializationPromise) {
return initializationPromise;
}

// Start new initialization
initializationInProgress = true;
initializationPromise = performInitialization(maxRetries, delay);

try {
const result = await initializationPromise;
return result;
} finally {
initializationInProgress = false;
initializationPromise = null;
}
}

async function performInitialization(
maxRetries: number = 10,
delay: number = 500,
): Promise<boolean> {
console.log('Initializing native modules...');

for (let i = 0; i < maxRetries; i++) {
try {
if (typeof Keychain.getGenericPassword === 'function') {
// Test if Keychain is actually available by making a safe call
await Keychain.getGenericPassword({ service: 'test-availability' });
// Non-mutating check: just verify the function exists without making a storage call
// This prevents creating unwanted storage entries
console.log('Keychain module is available');
nativeModulesReady = true;
console.log('Native modules ready!');
return true;
}
} catch (error) {
// If we get a "requiring unknown module" error, wait and retry
// Only retry for specific "requiring unknown module" errors
if (
error instanceof Error &&
error.message.includes('Requiring unknown module')
Expand All @@ -426,10 +444,23 @@ export async function initializeNativeModules(
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// For other errors (like service not found), assume Keychain is available
nativeModulesReady = true;
console.log('Native modules ready (with minor errors)!');
return true;

// For other errors, only set ready if it's a known safe error
// (like module not found, which indicates the module is available but the service doesn't exist)
if (
error instanceof Error &&
(error.message.includes('service not found') ||
error.message.includes('No password found'))
) {
nativeModulesReady = true;
console.log('Native modules ready (with expected errors)!');
return true;
}

// For unexpected errors, don't set ready and continue retrying
console.log(`Unexpected error during initialization: ${error}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}

Expand Down Expand Up @@ -653,7 +684,7 @@ interface IPassportContext {
deleteDocument: (documentId: string) => Promise<void>;
migrateFromLegacyStorage: () => Promise<void>;
getCurrentDocumentType: () => Promise<string | null>;
clearDocumentCatalogForMigrationTesting: () => Promise<void>;
clearDocumentCatalogForMigrationTesting?: () => Promise<void>;
markCurrentDocumentAsRegistered: () => Promise<void>;
updateDocumentRegistrationState: (
documentId: string,
Expand Down Expand Up @@ -693,12 +724,17 @@ export async function migrateFromLegacyStorage(): Promise<void> {
try {
const passportDataCreds = await Keychain.getGenericPassword({ service });
if (passportDataCreds !== false) {
const passportData: PassportData = JSON.parse(
const passportData: PassportData = safeJsonParse(
passportDataCreds.password,
null as any,
);
await storeDocumentWithDeduplication(passportData);
await Keychain.resetGenericPassword({ service });
console.log(`Migrated document from ${service}`);
if (passportData) {
await storeDocumentWithDeduplication(passportData);
await Keychain.resetGenericPassword({ service });
console.log(`Migrated document from ${service}`);
} else {
console.log(`Skipping corrupted data from ${service}`);
}
}
} catch (error) {
console.log(`Could not migrate from service ${service}:`, error);
Expand Down
3 changes: 1 addition & 2 deletions app/src/screens/dev/DevSettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
unsafe_clearSecrets,
unsafe_getPrivateKey,
} from '../../providers/authProvider';
import { usePassport } from '../../providers/passportDataProvider';
import { textBlack } from '../../utils/colors';
import { clearDocumentCatalogForMigrationTesting } from '../../utils/testingUtils';

import { useNavigation } from '@react-navigation/native';
import { Check, ChevronDown, Eraser } from '@tamagui/lucide-icons';
Expand Down Expand Up @@ -133,7 +133,6 @@ const ScreenSelector = ({}) => {
};

const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
const { clearDocumentCatalogForMigrationTesting } = usePassport();
const [privateKey, setPrivateKey] = useState<string | null>(
'Loading private key…',
);
Expand Down
49 changes: 49 additions & 0 deletions app/src/utils/jsonUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11

/**
* Safely parses a JSON string with error handling.
* Returns a default value if parsing fails.
*
* @param jsonString - The JSON string to parse
* @param defaultValue - The default value to return if parsing fails
* @returns The parsed object or the default value
*/
export function safeJsonParse<T>(
jsonString: string | null | undefined,
defaultValue: T,
): T {
if (jsonString == null) {
return defaultValue;
}

try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON, using default value:', error);
return defaultValue;
}
}

/**
* Safely stringifies an object with error handling.
* Returns a default string if stringification fails.
*
* @param obj - The object to stringify
* @param defaultValue - The default string to return if stringification fails
* @returns The JSON string or the default string
*/
export function safeJsonStringify<T>(
obj: T,
defaultValue: string = '{}',
): string {
if (obj == null) {
return defaultValue;
}

try {
return JSON.stringify(obj);
} catch (error) {
console.warn('Failed to stringify JSON, using default value:', error);
return defaultValue;
}
}
Loading
Loading