diff --git a/.cursorignore b/.cursorignore index caed5f49a..95ea3e898 100644 --- a/.cursorignore +++ b/.cursorignore @@ -199,9 +199,6 @@ app/ios/App Thinning Size Report.txt local.properties app/android/android-passport-nfc-reader/examples/ -# React Native config -app/react-native.config.cjs - # ======================================== # Miscellaneous # ======================================== diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..11bc0fbaf --- /dev/null +++ b/.eslintignore @@ -0,0 +1,15 @@ +node_modules +dist +build +coverage +ios/build +android/build +android/app/build +app/vendor +circuits/build +contracts/artifacts +contracts/cache +contracts/typechain-types +**/*.js +**/*.cjs +**/*.mjs diff --git a/.watchmanconfig b/.watchmanconfig index 2c63c0851..0955181ce 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -1,2 +1,12 @@ { + "ignore_dirs": [ + ".git", + ".hg", + "node_modules", + "ios/build", + "android/build", + "android/app/build", + "dist", + "build" + ] } diff --git a/app/Gemfile.lock b/app/Gemfile.lock index aaefd7336..6d8fcf190 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -25,7 +25,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1167.0) + aws-partitions (1.1168.0) aws-sdk-core (3.233.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index b8e7323ed..6b3cbd91f 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -1520,7 +1520,7 @@ PODS: - React-Core - react-native-netinfo (11.4.1): - React-Core - - react-native-nfc-manager (3.16.3): + - react-native-nfc-manager (3.17.1): - React-Core - react-native-safe-area-context (5.6.1): - DoubleConversion @@ -1904,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDeviceInfo (14.0.4): + - RNDeviceInfo (14.1.1): - React-Core - RNFBApp (19.3.0): - Firebase/CoreOnly (= 10.24.0) @@ -2113,7 +2113,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - segment-analytics-react-native (2.21.2): + - segment-analytics-react-native (2.21.3): - React-Core - sovran-react-native - Sentry/HybridSDK (8.53.2) @@ -2520,7 +2520,7 @@ SPEC CHECKSUMS: react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7 + react-native-nfc-manager: e5e91b4e9af0551755cdb6eaec55a8ff820ccdc6 react-native-safe-area-context: 90a89cb349c7f8168a707e6452288c2f665b9fd1 react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed React-nativeconfig: 415626a63057638759bcc75e0a96e2e07771a479 @@ -2552,7 +2552,7 @@ SPEC CHECKSUMS: ReactCommon: b2eb96a61b826ff327a773a74357b302cf6da678 RNCAsyncStorage: 0003b916f1a69fe2d20b7910e0d08da3d32c7bd6 RNCClipboard: a4827e134e4774e97fa86f7f986694dd89320f13 - RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 + RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c RNFBApp: 4097f75673f8b42a7cd1ba17e6ea85a94b45e4d1 RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88 RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b @@ -2563,7 +2563,7 @@ SPEC CHECKSUMS: RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8 RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0 RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb - segment-analytics-react-native: bad4c2c7b63818bd493caa2b5759fca59e4ae9a7 + segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594 diff --git a/app/package.json b/app/package.json index f12740464..8a30dda4a 100644 --- a/app/package.json +++ b/app/package.json @@ -145,7 +145,7 @@ "react-native-safe-area-context": "^5.6.1", "react-native-screens": "4.15.3", "react-native-sqlite-storage": "^6.0.1", - "react-native-svg": "^15.12.1", + "react-native-svg": "15.12.1", "react-native-svg-circle-country-flags": "^0.2.2", "react-native-svg-web": "^1.0.9", "react-native-web": "^0.19.0", diff --git a/app/scripts/mobile-ci-build-android.sh b/app/scripts/mobile-ci-build-android.sh index 09be650ab..ba7fdc938 100755 --- a/app/scripts/mobile-ci-build-android.sh +++ b/app/scripts/mobile-ci-build-android.sh @@ -232,9 +232,19 @@ fi # Restore original package files log "Restoring original package files..." if [[ -f "package.json.backup" ]] && [[ -f "../yarn.lock.backup" ]]; then - mv package.json.backup package.json - mv ../yarn.lock.backup ../yarn.lock - log "✅ Package files restored successfully" + if mv package.json.backup package.json && mv ../yarn.lock.backup ../yarn.lock; then + log "✅ Package files restored successfully" + + # Verify restoration by checking yarn.lock doesn't contain tarball references + if grep -q "file:/tmp/mobile-sdk-alpha-ci.tgz" ../yarn.lock 2>/dev/null; then + log "WARNING: yarn.lock still contains tarball references after restoration" + log "This may cause 'yarn.lock is out of date' errors in CI" + fi + else + log "ERROR: Failed to restore package files" + log "This may cause 'yarn.lock is out of date' errors in CI" + exit 1 + fi else log "WARNING: Backup files not found - package.json may still reference tarball" log "Please run 'yarn add @selfxyz/mobile-sdk-alpha@workspace:^' manually" diff --git a/common/index.ts b/common/index.ts index 0546c71d9..efb066495 100644 --- a/common/index.ts +++ b/common/index.ts @@ -18,6 +18,9 @@ export type { UserIdType, } from './src/utils/index.js'; +// Additional type exports +export type { Environment } from './src/utils/types.js'; + // Constants exports export type { Country3LetterCode } from './src/constants/index.js'; diff --git a/common/src/polyfills/crypto.ts b/common/src/polyfills/crypto.ts index ceea9fc57..00bd8429c 100644 --- a/common/src/polyfills/crypto.ts +++ b/common/src/polyfills/crypto.ts @@ -47,7 +47,14 @@ function createHash(algorithm: string) { if (typeof data === 'string') { hasher.update(new TextEncoder().encode(data)); } else { - hasher.update(data); + // Convert Buffer to pure Uint8Array if needed + // Buffer is a subclass of Uint8Array but noble/hashes expects pure Uint8Array + const bytes = + ArrayBuffer.isView(data) && + !(data instanceof Uint8Array && data.constructor === Uint8Array) + ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + : data; + hasher.update(bytes); } return this; }, @@ -96,7 +103,18 @@ function createHmac(algorithm: string, key: string | Uint8Array) { if (finalized) { throw new Error('Cannot update after calling digest(). Hash instance has been finalized.'); } - const dataBytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; + let dataBytes: Uint8Array; + if (typeof data === 'string') { + dataBytes = new TextEncoder().encode(data); + } else { + // Convert Buffer to pure Uint8Array if needed + // Buffer is a subclass of Uint8Array but noble/hashes expects pure Uint8Array + dataBytes = + ArrayBuffer.isView(data) && + !(data instanceof Uint8Array && data.constructor === Uint8Array) + ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) + : data; + } hmacState.update(dataBytes); return this; }, diff --git a/package.json b/package.json index 5d4f3e921..f8b6d553e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "resolutions": { "@babel/core": "^7.28.4", "@babel/runtime": "^7.28.4", + "@swc/core": "1.7.36", "@tamagui/animations-react-native": "1.126.14", "@tamagui/toast": "1.126.14", "@types/node": "^22.18.3", diff --git a/packages/mobile-sdk-alpha/ios/SelfSDK/PassportReader.swift b/packages/mobile-sdk-alpha/ios/SelfSDK/PassportReader.swift index 4a8a9f291..82ae0ac67 100644 --- a/packages/mobile-sdk-alpha/ios/SelfSDK/PassportReader.swift +++ b/packages/mobile-sdk-alpha/ios/SelfSDK/PassportReader.swift @@ -151,7 +151,6 @@ class PassportReader: NSObject { skipCA: skipCABool, skipPACE: skipPACEBool, useExtendedMode: extendedModeBool, - usePacePolling: usePacePollingBool, customDisplayMessage: customMessageHandler ) diff --git a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScannerModule.swift b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScannerModule.swift index 1fe4f092c..42d267c30 100644 --- a/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScannerModule.swift +++ b/packages/mobile-sdk-alpha/ios/SelfSDK/SelfMRZScannerModule.swift @@ -25,7 +25,8 @@ class SelfMRZScannerModule: NSObject, RCTBridgeModule { @objc func startScanning(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { DispatchQueue.main.async { - guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController else { reject("error", "Unable to find root view controller", nil) return } diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index accbe369f..1b5dd5641 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -55,7 +55,7 @@ export { createListenersMap, createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; /** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ -export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; // Core functions diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index a3aea7396..81f480073 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -87,9 +87,7 @@ export { createListenersMap, createSelfClient } from './client'; /** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ export { defaultConfig } from './config/defaults'; -export { extractMRZInfo } from './mrz'; - -export { formatDateToYYMMDD, scanMRZ } from './mrz'; +export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; diff --git a/packages/mobile-sdk-alpha/src/mock/generator.ts b/packages/mobile-sdk-alpha/src/mock/generator.ts index b6aedc44d..ba02d7ac9 100644 --- a/packages/mobile-sdk-alpha/src/mock/generator.ts +++ b/packages/mobile-sdk-alpha/src/mock/generator.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import type { IdDocInput, PassportData } from '@selfxyz/common'; +import type { AadhaarData, IdDocInput, PassportData } from '@selfxyz/common'; import { generateMockDSC, genMockIdDoc, getSKIPEM, initPassportDataParsing } from '@selfxyz/common'; export interface GenerateMockDocumentOptions { @@ -12,6 +12,8 @@ export interface GenerateMockDocumentOptions { selectedAlgorithm: string; selectedCountry: string; selectedDocumentType: 'mock_passport' | 'mock_id_card' | 'mock_aadhaar'; + firstName?: string; + lastName?: string; } const formatDateToYYMMDD = (date: Date): string => { @@ -48,7 +50,10 @@ export async function generateMockDocument({ selectedAlgorithm, selectedCountry, selectedDocumentType, -}: GenerateMockDocumentOptions) { + firstName, + lastName, +}: GenerateMockDocumentOptions): Promise { + console.log('generateMockDocument received names:', { firstName, lastName, isInOfacList }); const randomPassportNumber = Math.random() .toString(36) .substring(2, 11) @@ -67,15 +72,19 @@ export async function generateMockDocument({ signatureType: signatureTypeForGeneration as IdDocInput['signatureType'], expiryDate: getExpiryDateFromYears(expiryYears), passportNumber: randomPassportNumber, + sex: 'M', // Default value }; if (selectedDocumentType === 'mock_aadhaar') { idDocInput.birthDate = getBirthDateFromAge(age, 'DDMMYYYY'); if (isInOfacList) { - idDocInput.lastName = 'HENAO MONTOYA'; - idDocInput.firstName = 'ARCANGEL DE JESUS'; + idDocInput.lastName = lastName || 'HENAO MONTOYA'; + idDocInput.firstName = firstName || 'ARCANGEL DE JESUS'; idDocInput.birthDate = '07-10-1954'; + } else { + if (firstName) idDocInput.firstName = firstName; + if (lastName) idDocInput.lastName = lastName; } const result = genMockIdDoc(idDocInput); @@ -89,10 +98,12 @@ export async function generateMockDocument({ let dobForGeneration: string; if (isInOfacList) { dobForGeneration = '541007'; - idDocInput.lastName = 'HENAO MONTOYA'; - idDocInput.firstName = 'ARCANGEL DE JESUS'; + idDocInput.lastName = lastName || 'HENAO MONTOYA'; + idDocInput.firstName = firstName || 'ARCANGEL DE JESUS'; } else { dobForGeneration = getBirthDateFromAge(age); + if (firstName) idDocInput.firstName = firstName; + if (lastName) idDocInput.lastName = lastName; } idDocInput.birthDate = dobForGeneration; diff --git a/packages/mobile-sdk-alpha/src/mrz/index.ts b/packages/mobile-sdk-alpha/src/mrz/index.ts index 754111b04..62cf65093 100644 --- a/packages/mobile-sdk-alpha/src/mrz/index.ts +++ b/packages/mobile-sdk-alpha/src/mrz/index.ts @@ -12,7 +12,7 @@ import type { ScanResult } from '../types/public'; export type MRZScanOptions = Record; // Re-export processing functions -export { extractMRZInfo, formatDateToYYMMDD } from '../processing/mrz'; +export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from '../processing/mrz'; /** * Scan MRZ (Machine Readable Zone) on a passport or ID card. diff --git a/packages/mobile-sdk-alpha/src/processing/mrz.ts b/packages/mobile-sdk-alpha/src/processing/mrz.ts index a6473e722..63c1e521a 100644 --- a/packages/mobile-sdk-alpha/src/processing/mrz.ts +++ b/packages/mobile-sdk-alpha/src/processing/mrz.ts @@ -238,6 +238,87 @@ export function extractMRZInfo(mrzString: string): MRZInfo { }; } +/** + * Extract name from MRZ string + * Supports TD3 (passport) and TD1 (ID card) formats + * + * @param mrzString - The MRZ data as a string + * @returns Object with firstName and lastName, or null if parsing fails + * + * @example + * ```ts + * const name = extractNameFromMRZ("P line.trim()) + .filter(Boolean); + + // Handle single-line MRZ strings (common for stored data) + // TD3 format: 88 or 90 characters total (2 lines of 44 or 45 chars each) + if (lines.length === 1) { + const mrzLength = lines[0].length; + if (mrzLength === 88 || mrzLength === 90) { + const lineLength = mrzLength === 88 ? 44 : 45; + lines = [lines[0].slice(0, lineLength), lines[0].slice(lineLength)]; + } + } + + if (lines.length === 0) { + return null; + } + + // TD3 format (passport): Name is in line 1 after country code + // Format: P= 2) { + const lastName = parts[0].replace(/<+$/, '').replace(/= 2) { + const lastName = parts[0].replace(/<+$/, '').replace(/ { it('parses valid TD3 MRZ', () => { @@ -109,3 +111,180 @@ describe('formatDateToYYMMDD', () => { expect(() => formatDateToYYMMDD('invalid')).toThrowError(MrzParseError); }); }); + +describe('extractNameFromMRZ', () => { + describe('TD3 format (passports)', () => { + it('extracts first and last name from standard TD3 MRZ', () => { + const mrz = `P { + const mrz = `P { + const mrz = `P { + const mrz = `P { + const mrz = `P { + const name = extractNameFromMRZ(sample); + expect(name).toEqual({ + firstName: 'ANNA MARIA', + lastName: 'ERIKSSON', + }); + }); + + it('extracts name from single-line 88-character MRZ string', () => { + const singleLine = 'P { + const singleLine = "P { + const singleLine = 'P { + it('extracts first and last name from TD1 MRZ', () => { + const mrz = `IDFRAD9202541<<<<<<<<<<<<<<<<< +9007138M3002119FRA<<<<<<<<<<<6 +DUPONT< { + const name = extractNameFromMRZ(sampleTD1); + expect(name).toEqual({ + firstName: 'DUMMY', + lastName: 'DUMMY', + }); + }); + + it('extracts name with multiple first names from TD1', () => { + const mrz = `IDESPY123456789<<<<<<<<<<<<<< +9501011M3012319ESP<<<<<<<<<<<8 +GARCIA< { + it('returns null for empty string', () => { + expect(extractNameFromMRZ('')).toBeNull(); + }); + + it('returns null for whitespace only', () => { + expect(extractNameFromMRZ(' \n ')).toBeNull(); + }); + + it('returns null for invalid MRZ format', () => { + const invalid = 'INVALID MRZ DATA'; + expect(extractNameFromMRZ(invalid)).toBeNull(); + }); + + it('returns null for wrong line count', () => { + const invalid = 'P { + // Even with short lines, if format is recognizable, it should extract + const mrz = `P { + expect(extractNameFromMRZ(null as any)).toBeNull(); + expect(extractNameFromMRZ(undefined as any)).toBeNull(); + }); + + it('handles MRZ with extra whitespace', () => { + const mrz = ` P { + const mrz = 'P('home'); - const [mockDocument, setMockDocument] = useState(null); - - const navigate = (next: Screen) => setScreen(next); - - if (screen === 'generate') { - const GenerateMock = require('./src/GenerateMock').default as GenerateMockCmp; - return navigate('home')} />; - } - - if (screen === 'register') { - const RegisterDocument = require('./src/RegisterDocument').default as RegisterDocumentCmp; - return navigate('home')} />; - } - - if (screen === 'prove') { - const ProveQRCode = require('./src/ProveQRCode').default as ProveQRCodeCmp; - return navigate('home')} />; +import React, { useCallback, useEffect, useState } from 'react'; + +import type { DocumentCatalog, DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js'; +import type { IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js'; +import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + +import HomeScreen from './src/screens/HomeScreen'; +import { screenMap, type ScreenContext, type ScreenRoute } from './src/screens'; +import SelfClientProvider from './src/providers/SelfClientProvider'; + +type SelectedDocumentState = { + data: IDDocument; + metadata: DocumentMetadata; +}; + +function DemoApp() { + const selfClient = useSelfClient(); + + const [screen, setScreen] = useState('home'); + const [catalog, setCatalog] = useState({ documents: [] }); + const [selectedDocument, setSelectedDocument] = useState(null); + + const refreshDocuments = useCallback(async () => { + try { + const selected = await loadSelectedDocument(selfClient); + const nextCatalog = await selfClient.loadDocumentCatalog(); + setCatalog(nextCatalog); + setSelectedDocument(selected); + } catch (error) { + console.warn('Failed to refresh documents', error); + setCatalog({ documents: [] }); + setSelectedDocument(null); + } + }, [selfClient]); + + const navigate = (next: ScreenRoute) => setScreen(next); + + const screenContext: ScreenContext = { + navigate, + goHome: () => setScreen('home'), + documentCatalog: catalog, + selectedDocument, + refreshDocuments, + }; + + useEffect(() => { + if (screen !== 'home' && !screenMap[screen]) { + setScreen('home'); + } + }, [screen]); + + useEffect(() => { + refreshDocuments(); + }, [refreshDocuments]); + + if (screen === 'home') { + return ; } - if (screen === 'camera') { - const DocumentCamera = require('./src/DocumentCamera').default; - return navigate('home')} />; - } + const descriptor = screenMap[screen]; - if (screen === 'nfc') { - const DocumentNFCScan = require('./src/DocumentNFCScan').default; - return navigate('home')} />; + if (!descriptor) { + return null; } - if (screen === 'onboarding') { - const DocumentOnboarding = require('./src/DocumentOnboarding').default; - return navigate('home')} />; - } - - if (screen === 'qr') { - const QRCodeViewFinder = require('./src/QRCodeViewFinder').default; - return navigate('home')} />; - } + const ScreenComponent = descriptor.load(); + const props = descriptor.getProps?.(screenContext) ?? {}; - const MenuButton = ({ - title, - onPress, - isWorking = false, - }: { - title: string; - onPress: () => void; - isWorking?: boolean; - }) => ( - - - {title} - - - ); + return ; +} +function App() { return ( - - - Self Demo App - Mobile SDK Alpha - Available Screens - - - - 🎯 Core Features - navigate('generate')} isWorking={true} /> - navigate('register')} - isWorking={Boolean(mockDocument)} - /> - navigate('prove')} isWorking={Boolean(mockDocument)} /> - - - - 📷 Document Scanning - navigate('camera')} /> - navigate('nfc')} /> - navigate('onboarding')} /> - - - - 📱 QR Code Features - navigate('qr')} /> - - - - ✅ Working | ⏳ Placeholder (Not Implemented) - Tap any screen to explore the demo interface - - + + + ); } -const styles = StyleSheet.create({ - container: { - flexGrow: 1, - backgroundColor: '#f8f9fa', - padding: 20, - }, - header: { - alignItems: 'center', - marginBottom: 32, - paddingTop: 20, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - textAlign: 'center', - color: '#1a1a1a', - marginBottom: 8, - }, - subtitle: { - fontSize: 16, - color: '#666', - textAlign: 'center', - }, - section: { - marginBottom: 32, - }, - sectionTitle: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 16, - color: '#333', - textAlign: 'center', - }, - menuButton: { - width: '100%', - paddingVertical: 16, - paddingHorizontal: 20, - borderRadius: 12, - marginBottom: 12, - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.1, - shadowRadius: 3.84, - elevation: 5, - }, - workingButton: { - backgroundColor: '#007AFF', - }, - placeholderButton: { - backgroundColor: '#fff', - borderWidth: 1, - borderColor: '#e1e5e9', - }, - menuButtonText: { - fontSize: 16, - fontWeight: '600', - textAlign: 'center', - }, - workingButtonText: { - color: '#fff', - }, - placeholderButtonText: { - color: '#666', - }, - footer: { - marginTop: 20, - padding: 20, - backgroundColor: '#fff', - borderRadius: 12, - borderWidth: 1, - borderColor: '#e1e5e9', - }, - footerText: { - textAlign: 'center', - color: '#666', - fontSize: 14, - fontWeight: '500', - marginBottom: 8, - }, - footerSubtext: { - textAlign: 'center', - color: '#999', - fontSize: 12, - }, -}); - export default App; diff --git a/packages/mobile-sdk-demo/__tests__/App.test.tsx b/packages/mobile-sdk-demo/__tests__/App.test.tsx deleted file mode 100644 index 2f65bbb4f..000000000 --- a/packages/mobile-sdk-demo/__tests__/App.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import React from 'react'; -import { Text } from 'react-native'; -import renderer from 'react-test-renderer'; - -import App from '../App'; - -test('renders menu buttons', () => { - const rendered = renderer.create(); - const textNodes = rendered.root.findAllByType(Text); - - expect(textNodes.some(node => node.props.children === 'Self Demo App')).toBe(true); - - ['✅ Generate Mock Data', '⏳ Register Document', '⏳ Prove QR Code'].forEach(label => { - expect(textNodes.some(node => node.props.children === label)).toBe(true); - }); - - rendered.unmount(); -}); diff --git a/packages/mobile-sdk-demo/__tests__/cryptoPolyfills.test.ts b/packages/mobile-sdk-demo/__tests__/cryptoPolyfills.test.ts index 2318401ef..a5d05a59e 100644 --- a/packages/mobile-sdk-demo/__tests__/cryptoPolyfills.test.ts +++ b/packages/mobile-sdk-demo/__tests__/cryptoPolyfills.test.ts @@ -2,6 +2,8 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +import { describe, expect, it } from 'vitest'; + import { computeHmac, pbkdf2, randomBytes, sha256, sha512 } from '../src/utils/ethers'; describe('Crypto Polyfills', () => { @@ -177,25 +179,5 @@ describe('Crypto Polyfills', () => { expect(bytes).toBeInstanceOf(Uint8Array); expect(bytes.length).toBe(16); }); - - it('should have ethers.sha256 registered', () => { - const { ethers } = require('ethers'); - expect(typeof ethers.sha256).toBe('function'); - - const data = new Uint8Array([1, 2, 3, 4]); - const hash = ethers.sha256(data); - expect(typeof hash).toBe('string'); - expect(hash).toMatch(/^0x[a-f0-9]{64}$/); // 32 bytes = 64 hex chars - }); - - it('should have ethers.sha512 registered', () => { - const { ethers } = require('ethers'); - expect(typeof ethers.sha512).toBe('function'); - - const data = new Uint8Array([1, 2, 3, 4]); - const hash = ethers.sha512(data); - expect(typeof hash).toBe('string'); - expect(hash).toMatch(/^0x[a-f0-9]{128}$/); // 64 bytes = 128 hex chars - }); }); }); diff --git a/packages/mobile-sdk-demo/__tests__/documentStore.simple.test.ts b/packages/mobile-sdk-demo/__tests__/documentStore.simple.test.ts new file mode 100644 index 000000000..de60182e7 --- /dev/null +++ b/packages/mobile-sdk-demo/__tests__/documentStore.simple.test.ts @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +/** + * Simplified tests for documentStore BigInt serialization fix + * + * These tests verify that when PassportData with parsed certificates + * (containing BigInt values) is saved and loaded from storage, the + * BigInt values remain intact and don't get corrupted. + */ + +import { describe, expect, it } from 'vitest'; + +import type { PassportData } from '@selfxyz/common/dist/esm/src/utils/types.js'; + +describe('documentStore - BigInt serialization (simplified)', () => { + it('should demonstrate the BigInt serialization problem with JSON.stringify/parse', () => { + // Create a simple PassportData-like object with number arrays + const passportData: Partial = { + mrz: 'P BigInt(passportData.eContent![0])).not.toThrow(); + expect(() => BigInt(passportData.signedAttr![0])).not.toThrow(); + expect(() => BigInt(passportData.encryptedDigest![0])).not.toThrow(); + + // Simulate storage: JSON.stringify then JSON.parse + const serialized = JSON.stringify(passportData); + const deserialized = JSON.parse(serialized) as Partial; + + // Verify arrays are still number arrays after deserialization + expect(typeof deserialized.eContent![0]).toBe('number'); + expect(typeof deserialized.signedAttr![0]).toBe('number'); + expect(typeof deserialized.encryptedDigest![0]).toBe('number'); + + // These should still work with BigInt after serialization + expect(() => BigInt(deserialized.eContent![0])).not.toThrow(); + expect(() => BigInt(deserialized.signedAttr![0])).not.toThrow(); + expect(() => BigInt(deserialized.encryptedDigest![0])).not.toThrow(); + }); + + it('should show that BigInt works with array elements that are numbers', () => { + const numberArray = [48, 130, 1, 51, 2, 1, 0]; + + // This should work fine + numberArray.forEach(num => { + expect(() => BigInt(num)).not.toThrow(); + expect(typeof BigInt(num)).toBe('bigint'); + }); + }); + + it('should demonstrate the problem if array elements become strings', () => { + // This would be the problem scenario if somehow numbers became strings + const stringArray = ['48', '130', '1', '51']; + + // BigInt CAN handle string representations of numbers + stringArray.forEach(str => { + expect(() => BigInt(str)).not.toThrow(); + expect(typeof BigInt(str)).toBe('bigint'); + }); + + // But if there was any corruption to non-numeric strings, it would fail + expect(() => BigInt('not-a-number')).toThrow('Cannot convert not-a-number to a BigInt'); + }); + + it('should verify cloning through JSON preserves number arrays', () => { + const original = { + eContent: [48, 130, 1, -128, 127], + signedAttr: [49, -97, 48, 36], + }; + + // Clone using JSON (what cloneDocument does) + const cloned = JSON.parse(JSON.stringify(original)); + + // Verify types match + expect(Array.isArray(cloned.eContent)).toBe(true); + expect(Array.isArray(cloned.signedAttr)).toBe(true); + expect(typeof cloned.eContent[0]).toBe('number'); + expect(typeof cloned.signedAttr[0]).toBe('number'); + + // Verify values match + expect(cloned.eContent).toEqual(original.eContent); + expect(cloned.signedAttr).toEqual(original.signedAttr); + + // Verify BigInt operations work on cloned data + cloned.eContent.forEach((byte: number) => { + expect(() => BigInt(byte)).not.toThrow(); + }); + }); + + it('should explain the real problem: missing dsc_parsed and passportMetadata', () => { + // The REAL issue is not with the number arrays (eContent, signedAttr, encryptedDigest) + // Those survive JSON serialization fine. + + // The issue is that initPassportDataParsing adds these fields: + // - passportMetadata (contains BigInt values in certificate parsing) + // - dsc_parsed (CertificateData with BigInt values) + // - csca_parsed (CertificateData with BigInt values) + + // When these complex objects go through JSON.stringify/parse, + // BigInt values get corrupted or lost. + + // Our fix: Re-parse the document after loading to restore these fields + + const passportDataBeforeParsing: Partial = { + mrz: 'P { + // Fill with deterministic values for testing + for (let i = 0; i < array.length; i++) { + array[i] = i % 256; + } + return array; +}); + +Object.defineProperty(global, 'crypto', { + value: { + getRandomValues: mockRandomValues, + }, + writable: true, +}); + +describe('secureStorage', () => { + beforeEach(async () => { + mockRandomValues.mockClear(); + vi.clearAllMocks(); + // Clear any existing secrets from previous tests + await clearSecret(); + }); + + afterEach(async () => { + // Clean up after each test + await clearSecret(); + }); + + describe('generateSecret', () => { + it('should generate a 64-character hex string', () => { + const secret = generateSecret(); + expect(secret).toHaveLength(64); + expect(secret).toMatch(/^[0-9a-f]{64}$/i); + }); + + it('should call crypto.getRandomValues with 32 bytes', () => { + generateSecret(); + expect(mockRandomValues).toHaveBeenCalledTimes(1); + expect(mockRandomValues.mock.calls[0][0]).toHaveLength(32); + }); + + it('should generate different secrets on subsequent calls with real crypto', () => { + // Use real crypto for this test + const originalGetRandomValues = mockRandomValues.getMockImplementation(); + + mockRandomValues.mockImplementation((array: Uint8Array) => { + // Simulate real randomness + for (let i = 0; i < array.length; i++) { + array[i] = Math.floor(Math.random() * 256); + } + return array; + }); + + const secret1 = generateSecret(); + const secret2 = generateSecret(); + + expect(secret1).not.toBe(secret2); + + // Restore mock + if (originalGetRandomValues) { + mockRandomValues.mockImplementation(originalGetRandomValues); + } + }); + }); + + describe('isValidSecret', () => { + it('should return true for valid 64-char hex string', () => { + const validSecret = '0'.repeat(64); + expect(isValidSecret(validSecret)).toBe(true); + }); + + it('should return true for valid hex with mixed case', () => { + const validSecret = 'abcdef0123456789'.repeat(4); // gitleaks:allow + expect(isValidSecret(validSecret)).toBe(true); + }); + + it('should return false for short string', () => { + expect(isValidSecret('abc')).toBe(false); + }); + + it('should return false for long string', () => { + expect(isValidSecret('0'.repeat(65))).toBe(false); + }); + + it('should return false for non-hex characters', () => { + const invalidSecret = 'g'.repeat(64); + expect(isValidSecret(invalidSecret)).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isValidSecret('')).toBe(false); + }); + }); + + describe('getOrCreateSecret', () => { + it('should create a new secret if none exists', async () => { + expect(await hasSecret()).toBe(false); + + const secret = await getOrCreateSecret(); + + expect(secret).toHaveLength(64); + expect(isValidSecret(secret)).toBe(true); + expect(await hasSecret()).toBe(true); + }); + + it('should return the same secret on subsequent calls', async () => { + const secret1 = await getOrCreateSecret(); + const secret2 = await getOrCreateSecret(); + + expect(secret1).toBe(secret2); + }); + }); + + describe('hasSecret', () => { + it('should return false when no secret exists', async () => { + expect(await hasSecret()).toBe(false); + }); + + it('should return true when secret exists', async () => { + await getOrCreateSecret(); + expect(await hasSecret()).toBe(true); + }); + + it('should return false after clearing secret', async () => { + await getOrCreateSecret(); + expect(await hasSecret()).toBe(true); + + await clearSecret(); + expect(await hasSecret()).toBe(false); + }); + }); + + describe('getSecretMetadata', () => { + it('should return null when no metadata exists', async () => { + expect(await getSecretMetadata()).toBeNull(); + }); + + it('should return null on native platforms (metadata not supported)', async () => { + await getOrCreateSecret(); + + // Native implementation doesn't store metadata + const metadata = await getSecretMetadata(); + expect(metadata).toBeNull(); + }); + }); + + describe('clearSecret', () => { + it('should remove secret from storage', async () => { + await getOrCreateSecret(); + expect(await hasSecret()).toBe(true); + + await clearSecret(); + expect(await hasSecret()).toBe(false); + }); + + it('should not throw if called when no secret exists', async () => { + await expect(clearSecret()).resolves.not.toThrow(); + }); + }); + + describe('security considerations', () => { + it('should use exactly 32 bytes (256 bits) for security', () => { + generateSecret(); + + const callArgs = mockRandomValues.mock.calls[0][0]; + expect(callArgs).toHaveLength(32); + expect(callArgs).toBeInstanceOf(Uint8Array); + }); + }); + + describe('integration scenarios', () => { + it('should handle complete lifecycle: create → retrieve → clear → create new', async () => { + // Create + const secret1 = await getOrCreateSecret(); + expect(isValidSecret(secret1)).toBe(true); + + // Retrieve (should be same) + const secret2 = await getOrCreateSecret(); + expect(secret2).toBe(secret1); + + // Clear + await clearSecret(); + expect(await hasSecret()).toBe(false); + + // Create new (should be different since we use different values) + mockRandomValues.mockImplementation((array: Uint8Array) => { + for (let i = 0; i < array.length; i++) { + array[i] = (i + 100) % 256; // Different values + } + return array; + }); + + const secret3 = await getOrCreateSecret(); + expect(isValidSecret(secret3)).toBe(true); + expect(secret3).not.toBe(secret1); + }); + + it('should maintain consistency across storage retrievals', async () => { + // First call - create secret + const secret1 = await getOrCreateSecret(); + + // Second call - should retrieve same secret from storage + const secret2 = await getOrCreateSecret(); + expect(secret2).toBe(secret1); + }); + }); +}); diff --git a/packages/mobile-sdk-demo/android/app/build.gradle b/packages/mobile-sdk-demo/android/app/build.gradle index 2ad7aae31..0e57fd1ad 100644 --- a/packages/mobile-sdk-demo/android/app/build.gradle +++ b/packages/mobile-sdk-demo/android/app/build.gradle @@ -44,6 +44,8 @@ android { } } +apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") + dependencies { implementation("com.facebook.react:react-android:0.76.9") implementation("com.facebook.react:hermes-android:0.76.9") diff --git a/packages/mobile-sdk-demo/crypto-polyfill.js b/packages/mobile-sdk-demo/crypto-polyfill.js deleted file mode 100644 index 1c69bd729..000000000 --- a/packages/mobile-sdk-demo/crypto-polyfill.js +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -// Crypto polyfill using @noble/hashes for React Native compatibility -const { sha256 } = require('@noble/hashes/sha256'); -const { sha1 } = require('@noble/hashes/sha1'); -const { sha512 } = require('@noble/hashes/sha512'); -const { Buffer } = require('buffer'); -require('react-native-get-random-values'); // installs globalThis.crypto.getRandomValues - -// Create a crypto polyfill that provides the Node.js crypto API -const crypto = { - createHash: algorithm => { - const algorithms = { - sha256: sha256, - sha1: sha1, - sha512: sha512, - }; - - const hashFunction = algorithms[algorithm.toLowerCase()]; - if (!hashFunction) { - throw new Error(`Unsupported hash algorithm: ${algorithm}`); - } - - let data = Buffer.alloc(0); - - const api = { - update: inputData => { - // Accumulate data - data = Buffer.concat([data, Buffer.from(inputData)]); - return api; - }, - digest: encoding => { - const hash = hashFunction(data); - if (encoding === 'hex') { - return Buffer.from(hash).toString('hex'); - } - return Buffer.from(hash); - }, - }; - return api; - }, - - // Add other commonly used crypto methods as needed - randomBytes: size => { - const array = new Uint8Array(size); - if (typeof globalThis.crypto?.getRandomValues !== 'function') { - throw new Error('crypto.getRandomValues not available; ensure polyfill is loaded'); - } - globalThis.crypto.getRandomValues(array); - return Buffer.from(array); - }, -}; - -module.exports = crypto; diff --git a/packages/mobile-sdk-demo/index.js b/packages/mobile-sdk-demo/index.js index b4975f5b9..dc96910df 100644 --- a/packages/mobile-sdk-demo/index.js +++ b/packages/mobile-sdk-demo/index.js @@ -10,8 +10,10 @@ // eslint-disable-next-line simple-import-sort/imports import 'react-native-get-random-values'; +import React from 'react'; import { Buffer } from 'buffer'; import { AppRegistry } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; import App from './App'; import { name as appName } from './app.json'; @@ -21,4 +23,10 @@ import './src/utils/ethers'; // Set global Buffer before any other imports global.Buffer = Buffer; -AppRegistry.registerComponent(appName, () => App); +const Root = () => ( + + + +); + +AppRegistry.registerComponent(appName, () => Root); diff --git a/packages/mobile-sdk-demo/ios/Podfile.lock b/packages/mobile-sdk-demo/ios/Podfile.lock index 55d6ef350..884c3394b 100644 --- a/packages/mobile-sdk-demo/ios/Podfile.lock +++ b/packages/mobile-sdk-demo/ios/Podfile.lock @@ -1300,6 +1300,72 @@ PODS: - Yoga - react-native-get-random-values (1.11.0): - React-Core + - react-native-safe-area-context (5.6.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common (= 5.6.1) + - react-native-safe-area-context/fabric (= 5.6.1) + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/common (5.6.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - react-native-safe-area-context/fabric (5.6.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - React-nativeconfig (0.76.9) - React-NativeModulesApple (0.76.9): - glog @@ -1572,7 +1638,92 @@ PODS: - React-logger - React-perflogger - React-utils (= 0.76.9) - - RNCPicker (2.11.1): + - RNCAsyncStorage (2.2.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNCPicker (2.11.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNSVG (15.13.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNSVG/common (= 15.13.0) + - Yoga + - RNSVG/common (15.13.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNVectorIcons (10.3.0): - DoubleConversion - glog - hermes-engine @@ -1638,6 +1789,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) @@ -1665,7 +1817,10 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" + - RNSVG (from `../node_modules/react-native-svg`) + - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1755,6 +1910,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" React-nativeconfig: :path: "../node_modules/react-native/ReactCommon" React-NativeModulesApple: @@ -1809,8 +1966,14 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" RNCPicker: :path: "../node_modules/@react-native-picker/picker" + RNSVG: + :path: "../node_modules/react-native-svg" + RNVectorIcons: + :path: "../node_modules/react-native-vector-icons" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1828,7 +1991,7 @@ SPEC CHECKSUMS: glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 9e868dc7be781364296d6ee2f56d0c1a9ef0bb11 Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0 - mobile-sdk-alpha: 96949ad8c8b61a9fa6b918a4202f9cebb9c678cc + mobile-sdk-alpha: 126edf71b65b5a9e294725e4353c2705fa0fd20d NFCPassportReader: 48873f856f91215dbfa1eaaec20eae639672862e OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346 QKMRZParser: 6b419b6f07d6bff6b50429b97de10846dc902c29 @@ -1862,6 +2025,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 33546a3ebefbccb8770c33a1f8a5554fa96a54de React-microtasksnativemodule: d80ff86c8902872d397d9622f1a97aadcc12cead react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba + react-native-safe-area-context: 76bd6904253fc0f68fbc3d7f594b6a394d0ac34c React-nativeconfig: 8efdb1ef1e9158c77098a93085438f7e7b463678 React-NativeModulesApple: cebca2e5320a3d66e123cade23bd90a167ffce5e React-perflogger: 72e653eb3aba9122f9e57cf012d22d2486f33358 @@ -1889,10 +2053,13 @@ SPEC CHECKSUMS: React-utils: ed818f19ab445000d6b5c4efa9d462449326cc9f ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9 - RNCPicker: 3549e7ab9a00047753e9fa852a1858a154cc4275 + RNCAsyncStorage: 87a74d13ba0128f853817e45e21c4051e1f2cd45 + RNCPicker: 31b0c81be6b949dbd8d0c8802e9c6b9615de880a + RNSVG: c22ddda11213ee91192ab2f70b50c78a8bbc30d8 + RNVectorIcons: c95fdae217b0ed388f2b4d7ed7a4edc457c1df47 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a -PODFILE CHECKSUM: 7db6890d140dc2f697c16380d1412b8861ebcff7 +PODFILE CHECKSUM: 22f8edb659097ec6a47366d55dcd021f5b88ccdb COCOAPODS: 1.16.2 diff --git a/packages/mobile-sdk-demo/ios/SelfDemoApp/Info.plist b/packages/mobile-sdk-demo/ios/SelfDemoApp/Info.plist index 75230790d..5a5f77f65 100644 --- a/packages/mobile-sdk-demo/ios/SelfDemoApp/Info.plist +++ b/packages/mobile-sdk-demo/ios/SelfDemoApp/Info.plist @@ -44,5 +44,9 @@ UIViewControllerBasedStatusBarAppearance + UIAppFonts + + Ionicons.ttf + diff --git a/packages/mobile-sdk-demo/ios/SelfDemoApp/LaunchScreen.storyboard b/packages/mobile-sdk-demo/ios/SelfDemoApp/LaunchScreen.storyboard index 6350277bb..783855c1d 100644 --- a/packages/mobile-sdk-demo/ios/SelfDemoApp/LaunchScreen.storyboard +++ b/packages/mobile-sdk-demo/ios/SelfDemoApp/LaunchScreen.storyboard @@ -21,11 +21,6 @@ - diff --git a/packages/mobile-sdk-demo/jest.config.cjs b/packages/mobile-sdk-demo/jest.config.cjs deleted file mode 100644 index 8554cd893..000000000 --- a/packages/mobile-sdk-demo/jest.config.cjs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -module.exports = { - preset: 'react-native', - setupFilesAfterEnv: ['/jest.setup.js'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - transformIgnorePatterns: ['node_modules/(?!(react-native|@react-native|@selfxyz)/)'], - moduleDirectories: ['node_modules', '/../../../node_modules'], - moduleNameMapper: { - '^@selfxyz/common$': '/../../common/dist/cjs/index.cjs', - '^@selfxyz/mobile-sdk-alpha$': '/../mobile-sdk-alpha/dist/cjs/index.cjs', - }, -}; diff --git a/packages/mobile-sdk-demo/jest.setup.js b/packages/mobile-sdk-demo/jest.setup.js deleted file mode 100644 index 9e50c5605..000000000 --- a/packages/mobile-sdk-demo/jest.setup.js +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -/** @jest-environment jsdom */ - -// Mock the native bridge configuration FIRST -global.__fbBatchedBridgeConfig = { - remoteModuleConfig: [], - localModulesConfig: {}, -}; - -// Mock React Native's native modules -const { NativeModules } = require('react-native'); - -// Mock NativeModules -NativeModules.PlatformConstants = { - getConstants: () => ({ - isTesting: true, - reactNativeVersion: { - major: 0, - minor: 76, - patch: 9, - }, - }), -}; - -// Mock DeviceInfo native module -NativeModules.DeviceInfo = { - getConstants: () => ({ - Dimensions: { - window: { width: 375, height: 812 }, - screen: { width: 375, height: 812 }, - }, - PixelRatio: 2, - }), -}; - -// Mock other common native modules -NativeModules.StatusBarManager = { - getConstants: () => ({}), -}; - -NativeModules.Appearance = { - getConstants: () => ({}), -}; - -NativeModules.SourceCode = { - getConstants: () => ({ - scriptURL: 'http://localhost:8081/index.bundle?platform=ios&dev=true', - }), -}; - -NativeModules.UIManager = { - getConstants: () => ({}), - measure: jest.fn(), - measureInWindow: jest.fn(), - measureLayout: jest.fn(), - findSubviewIn: jest.fn(), - dispatchViewManagerCommand: jest.fn(), - setLayoutAnimationEnabledExperimental: jest.fn(), - configureNextLayoutAnimation: jest.fn(), - removeSubviewsFromContainerWithID: jest.fn(), - replaceExistingNonRootView: jest.fn(), - setChildren: jest.fn(), - manageChildren: jest.fn(), - setJSResponder: jest.fn(), - clearJSResponder: jest.fn(), - createView: jest.fn(), - updateView: jest.fn(), - removeRootView: jest.fn(), - addRootView: jest.fn(), - updateRootView: jest.fn(), -}; - -NativeModules.KeyboardObserver = { - addListener: jest.fn(), - removeListeners: jest.fn(), -}; - -// Mock react-native-get-random-values -jest.mock('react-native-get-random-values', () => ({ - polyfillGlobal: jest.fn(), -})); - -// Mock @react-native-picker/picker -jest.mock('@react-native-picker/picker', () => ({ - Picker: 'Picker', - PickerIOS: 'PickerIOS', -})); - -// Mock ethers -jest.mock('ethers', () => { - const mockRandomBytes = jest.fn().mockImplementation(length => new Uint8Array(length)); - mockRandomBytes.register = jest.fn(); - - const mockHashFunction = jest.fn().mockImplementation(() => '0x' + 'a'.repeat(64)); - mockHashFunction.register = jest.fn(); - - const mockSha512Function = jest.fn().mockImplementation(() => '0x' + 'a'.repeat(128)); - mockSha512Function.register = jest.fn(); - - return { - ethers: { - Wallet: jest.fn().mockImplementation(() => ({ - address: '0x1234567890123456789012345678901234567890', - signMessage: jest.fn().mockResolvedValue('0xsignature'), - })), - JsonRpcProvider: jest.fn().mockImplementation(() => ({ - getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }), - })), - randomBytes: mockRandomBytes, - computeHmac: mockHashFunction, - pbkdf2: mockHashFunction, - sha256: mockHashFunction, - sha512: mockSha512Function, - ripemd160: mockHashFunction, - scrypt: mockHashFunction, - }, - }; -}); - -// Mock @selfxyz/common -jest.mock('@selfxyz/common', () => ({ - generateMockPassportData: jest.fn().mockReturnValue({ - documentNumber: '123456789', - dateOfBirth: '1990-01-01', - dateOfExpiry: '2030-01-01', - firstName: 'John', - lastName: 'Doe', - }), - cryptoPolyfill: { - createHash: jest.fn().mockReturnValue({ - update: jest.fn().mockReturnThis(), - digest: jest.fn().mockReturnValue('mocked-hash'), - }), - createHmac: jest.fn().mockReturnValue({ - update: jest.fn().mockReturnThis(), - digest: jest.fn().mockReturnValue('mocked-hmac'), - }), - randomBytes: jest.fn().mockImplementation(size => new Uint8Array(size)), - pbkdf2Sync: jest.fn().mockImplementation(() => new Uint8Array(32)), - }, -})); - -// Mock @selfxyz/mobile-sdk-alpha -jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ - SelfSDK: { - initialize: jest.fn().mockResolvedValue(undefined), - generateProof: jest.fn().mockResolvedValue('mock-proof'), - registerDocument: jest.fn().mockResolvedValue('mock-registration'), - }, -})); - -// Mock console methods to avoid test output clutter -global.console = { - ...console, - log: jest.fn(), - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), -}; diff --git a/packages/mobile-sdk-demo/metro.config.cjs b/packages/mobile-sdk-demo/metro.config.cjs index 6ad3d83c5..76e4d26b1 100644 --- a/packages/mobile-sdk-demo/metro.config.cjs +++ b/packages/mobile-sdk-demo/metro.config.cjs @@ -7,6 +7,7 @@ const path = require('node:path'); const findYarnWorkspaceRoot = require('find-yarn-workspace-root'); const defaultConfig = getDefaultConfig(__dirname); +const { assetExts, sourceExts } = defaultConfig.resolver; const projectRoot = __dirname; const workspaceRoot = findYarnWorkspaceRoot(__dirname) || path.resolve(__dirname, '../..'); @@ -14,6 +15,7 @@ const workspaceRoot = findYarnWorkspaceRoot(__dirname) || path.resolve(__dirname /** * Modern Metro configuration for demo app using native workspace capabilities * Based on the working main app configuration + * @type {import('metro-config').MetroConfig} */ const config = { projectRoot, @@ -22,37 +24,55 @@ const config = { workspaceRoot, // Watch entire workspace root path.resolve(workspaceRoot, 'common'), path.resolve(workspaceRoot, 'packages/mobile-sdk-alpha'), + path.resolve(projectRoot, 'node_modules'), // Watch app's node_modules for custom resolved modules ], + transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer'), + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, + resolver: { // Prevent Haste module naming collisions from duplicate package.json files blockList: [ // Ignore built package.json files to prevent Haste collisions /.*\/dist\/package\.json$/, + /.*\/dist\/esm\/package\.json$/, + /.*\/dist\/cjs\/package\.json$/, /.*\/build\/package\.json$/, + // Prevent duplicate React/React Native - block workspace root versions and use app's versions + new RegExp(`^${workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/node_modules/react(/|$)`), + new RegExp(`^${workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/node_modules/react-dom(/|$)`), + new RegExp(`^${workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/node_modules/react-native(/|$)`), + new RegExp(`^${workspaceRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/node_modules/scheduler(/|$)`), + new RegExp('packages/mobile-sdk-alpha/node_modules/react(/|$)'), + new RegExp('packages/mobile-sdk-alpha/node_modules/react-dom(/|$)'), + new RegExp('packages/mobile-sdk-alpha/node_modules/react-native(/|$)'), + new RegExp('packages/mobile-sdk-alpha/node_modules/scheduler(/|$)'), + // Block the main app's node_modules to avoid collisions + new RegExp('app/node_modules/react(/|$)'), + new RegExp('app/node_modules/react-dom(/|$)'), + new RegExp('app/node_modules/react-native(/|$)'), + new RegExp('app/node_modules/scheduler(/|$)'), ], - // Let workspace packages resolve naturally to their built exports (override where needed) - alias: { - '@selfxyz/mobile-sdk-alpha': path.resolve(workspaceRoot, 'packages/mobile-sdk-alpha/src'), - }, // Enable workspace-aware resolution enableGlobalPackages: true, unstable_enablePackageExports: true, // Prefer React Native-specific exports when available to avoid Node-only deps - unstable_conditionNames: ['require', 'react-native'], + unstable_conditionNames: ['react-native', 'import', 'require'], unstable_enableSymlinks: true, nodeModulesPaths: [path.resolve(projectRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules')], + assetExts: assetExts.filter(ext => ext !== 'svg'), + sourceExts: [...sourceExts, 'svg'], extraNodeModules: { - '@babel/runtime': path.resolve(__dirname, '../../node_modules/@babel/runtime'), - // Pin React and React Native to monorepo root - react: path.resolve(__dirname, '../../node_modules/react'), - 'react-native': path.resolve(__dirname, '../../node_modules/react-native'), // Add workspace packages for proper resolution '@selfxyz/common': path.resolve(workspaceRoot, 'common'), - // Fix snarkjs resolution for @anon-aadhaar/core - snarkjs: path.resolve(__dirname, '../../node_modules/snarkjs/build/main.cjs'), - // Fix ffjavascript resolution for snarkjs dependencies - ffjavascript: path.resolve(__dirname, '../../node_modules/ffjavascript/build/main.cjs'), + '@selfxyz/mobile-sdk-alpha': path.resolve(workspaceRoot, 'packages/mobile-sdk-alpha'), // Crypto polyfills - use custom polyfill with @noble/hashes crypto: path.resolve(__dirname, 'src/polyfills/cryptoPolyfill.js'), stream: require.resolve('stream-browserify'), @@ -63,6 +83,105 @@ const config = { }, // Prefer source files for @selfxyz/common so stack traces reference real filenames resolveRequest: (context, moduleName, platform) => { + // Fix @noble/hashes subpath export resolution + if (moduleName.startsWith('@noble/hashes/')) { + try { + // Extract the subpath (e.g., 'crypto.js', 'sha256', 'hmac') + const subpath = moduleName.replace('@noble/hashes/', ''); + const basePath = require.resolve('@noble/hashes'); + + // For .js files, look in the package directory + if (subpath.endsWith('.js')) { + const subpathFile = path.join(path.dirname(basePath), subpath); + return { + type: 'sourceFile', + filePath: subpathFile, + }; + } else { + // For other imports like 'sha256', 'hmac', etc., try the main directory + const subpathFile = path.join(path.dirname(basePath), `${subpath}.js`); + return { + type: 'sourceFile', + filePath: subpathFile, + }; + } + } catch { + // Fallback to main package if subpath doesn't exist + return { + type: 'sourceFile', + filePath: require.resolve('@noble/hashes'), + }; + } + } + + // Fix snarkjs and ffjavascript platform exports for Android + if (platform === 'android') { + // Handle snarkjs and its nested dependencies that have platform export issues + if ( + moduleName.includes('/snarkjs') && + (moduleName.endsWith('/snarkjs') || moduleName.includes('/snarkjs/node_modules')) + ) { + try { + // Try to resolve the main package file + const packagePath = moduleName.split('/node_modules/').pop(); + const resolved = require.resolve(packagePath || 'snarkjs'); + return { + type: 'sourceFile', + filePath: resolved, + }; + } catch { + // Fallback to basic snarkjs resolution + try { + return { + type: 'sourceFile', + filePath: require.resolve('snarkjs'), + }; + } catch { + // Continue to next check + } + } + } + + // Handle ffjavascript from any nested location + if (moduleName.includes('/ffjavascript') && moduleName.endsWith('/ffjavascript')) { + try { + // Try to resolve ffjavascript from the specific nested location first + const resolved = require.resolve(moduleName); + return { + type: 'sourceFile', + filePath: resolved, + }; + } catch { + // Fallback to resolving ffjavascript from the closest available location + try { + const resolved = require.resolve('ffjavascript'); + return { + type: 'sourceFile', + filePath: resolved, + }; + } catch { + // Continue to next check + } + } + } + + // Handle direct package imports for known problematic packages + const platformProblematicPackages = ['snarkjs', 'ffjavascript']; + for (const pkg of platformProblematicPackages) { + if (moduleName === pkg || moduleName.startsWith(`${pkg}/`)) { + try { + return { + type: 'sourceFile', + filePath: require.resolve(pkg), + }; + } catch { + // Continue to next check + continue; + } + } + } + } + // Handle problematic Node.js modules that don't work in React Native const nodeModuleRedirects = { crypto: path.resolve(__dirname, 'src/polyfills/cryptoPolyfill.js'), @@ -84,8 +203,7 @@ const config = { }; } - // Let @selfxyz/common resolve through its package.json exports - // Remove custom resolution to let Metro handle it naturally + // Fallback to default Metro resolver return context.resolveRequest(context, moduleName, platform); }, }, diff --git a/packages/mobile-sdk-demo/package.json b/packages/mobile-sdk-demo/package.json index 7069195b3..28285d3cb 100644 --- a/packages/mobile-sdk-demo/package.json +++ b/packages/mobile-sdk-demo/package.json @@ -11,19 +11,25 @@ "prebuild": "yarn workspace @selfxyz/mobile-sdk-alpha build", "build": "tsc -p tsconfig.json --noEmit --pretty false", "clean": "rm -rf ios/build android/app/build android/build && cd android && ./gradlew clean && cd ..", - "fmt": "prettier --check .", - "fmt:fix": "prettier --write .", + "format": "prettier --write .", + "ia": "yarn install-app", + "install-app": "yarn install && yarn prebuild && cd ios && pod install && cd ..", "preios": "yarn prebuild", "ios": "react-native run-ios", "lint": "eslint .", "lint:fix": "eslint --fix .", - "nice": "yarn lint:fix && yarn fmt:fix", + "nice": "yarn lint:fix && yarn format", + "reinstall": "yarn clean && yarn install && yarn prebuild && cd ios && pod install && cd ..", "start": "react-native start", - "test": "jest" + "test": "vitest run", + "test:watch": "vitest", + "types": "tsc --noEmit" }, "dependencies": { "@babel/runtime": "^7.28.3", + "@faker-js/faker": "^10.0.0", "@noble/hashes": "^1.5.0", + "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-picker/picker": "^2.11.1", "@react-native/gradle-plugin": "0.76.9", "@selfxyz/common": "workspace:*", @@ -36,8 +42,11 @@ "react": "^18.3.1", "react-native": "0.76.9", "react-native-get-random-values": "^1.11.0", + "react-native-keychain": "^10.0.0", + "react-native-safe-area-context": "^5.6.1", + "react-native-svg": "^15.13.0", + "react-native-vector-icons": "^10.3.0", "stream-browserify": "^3.0.0", - "tamagui": "1.126.14", "util": "^0.12.5" }, "devDependencies": { @@ -45,11 +54,11 @@ "@react-native-community/cli": "^16.0.3", "@react-native/metro-config": "0.76.9", "@tsconfig/react-native": "^3.0.6", - "@types/jest": "^29.5.14", "@types/react": "^18.3.4", + "@types/react-native-vector-icons": "^6.4.18", "@typescript-eslint/eslint-plugin": "^8.44.0", "@typescript-eslint/parser": "^8.44.0", - "babel-jest": "^29.6.3", + "@vitest/ui": "^2.1.8", "eslint": "^8.57.0", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-typescript": "^3.6.1", @@ -57,10 +66,11 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-sort-exports": "^0.9.1", - "jest": "^29.6.3", + "jsdom": "^25.0.1", "metro-react-native-babel-preset": "0.76.9", "prettier": "^3.6.2", - "react-test-renderer": "^18.3.1", - "typescript": "^5.9.2" + "react-native-svg-transformer": "^1.5.1", + "typescript": "^5.9.2", + "vitest": "^2.1.8" } } diff --git a/packages/mobile-sdk-demo/src/DocumentOnboarding.tsx b/packages/mobile-sdk-demo/src/DocumentOnboarding.tsx deleted file mode 100644 index cd3c74533..000000000 --- a/packages/mobile-sdk-demo/src/DocumentOnboarding.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import React from 'react'; -import { Button, ScrollView, StyleSheet, Text, View } from 'react-native'; - -type Props = { - onBack: () => void; -}; - -export default function DocumentOnboarding({ onBack }: Props) { - return ( - - Document Onboarding - Camera Setup & Instructions - - - - This screen would provide onboarding instructions and camera setup for document scanning. - - - - Features (Not Implemented): - • Camera permission requests - • Document positioning guidance - • Animation and visual instructions - • Privacy and security information - • Step-by-step scanning tutorial - - - -