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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions app/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,28 @@ jest.mock('react-native-nfc-manager', () => ({
}));

// Mock react-native-passport-reader
jest.mock('react-native-passport-reader', () => ({
default: {
jest.mock('react-native-passport-reader', () => {
const mockScanPassport = jest.fn();
// Mock the parameter count for scanPassport (iOS native method takes 9 parameters)
Object.defineProperty(mockScanPassport, 'length', { value: 9 });

const mockPassportReader = {
configure: jest.fn(),
scanPassport: jest.fn(),
scanPassport: mockScanPassport,
readPassport: jest.fn(),
cancelPassportRead: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
}));
};

return {
PassportReader: mockPassportReader,
default: mockPassportReader,
reset: jest.fn(),
scan: jest.fn(),
};
});

const { NativeModules } = require('react-native');

Expand Down
14 changes: 2 additions & 12 deletions app/src/mocks/react-native-passport-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,7 @@

// Mock PassportReader object with analytics methods
export const PassportReader = {
configure: (
Copy link
Member Author

Choose a reason for hiding this comment

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

missed this ai hallucination....created an unusable typing that doesn't exist anywhere that broke nfc scanning

@seshanthS

token: string,
enableDebug?: boolean,
flushPolicies?: {
flushInterval?: number;
flushCount?: number;
flushOnBackground?: boolean;
flushOnForeground?: boolean;
flushOnNetworkChange?: boolean;
},
) => {
configure: (token: string, enableDebug?: boolean) => {
// No-op for web
return Promise.resolve();
},
Expand All @@ -32,7 +22,7 @@ export const PassportReader = {
// No-op for web
return Promise.resolve();
},
scan: async () => {
scanPassport: async () => {
throw new Error('NFC scanning is not supported on web');
},
};
Expand Down
3 changes: 1 addition & 2 deletions app/src/providers/selfClientProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import {
} from '@selfxyz/mobile-sdk-alpha';
import { TrackEventParams } from '@selfxyz/mobile-sdk-alpha';

import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider';
import analytics from '@/utils/analytics';

import { unsafe_getPrivateKey } from './authProvider';

/**
* Provides a configured Self SDK client instance to all descendants.
*
Expand Down
24 changes: 12 additions & 12 deletions app/src/types/react-native-passport-reader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ declare module 'react-native-passport-reader' {
}

interface PassportReader {
configure(
Copy link
Member Author

Choose a reason for hiding this comment

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

remove unused type

token: string,
enableDebug?: boolean,
flushPolicies?: {
flushInterval?: number;
flushCount?: number;
flushOnBackground?: boolean;
flushOnForeground?: boolean;
flushOnNetworkChange?: boolean;
},
): void;
configure?(token: string, enableDebug?: boolean): void;
trackEvent?(name: string, properties?: Record<string, unknown>): void;
flush?(): void;
reset(): void;
scan(options: ScanOptions): Promise<{
scanPassport(
passportNumber: string,
dateOfBirth: string,
dateOfExpiry: string,
canNumber: string,
useCan: boolean,
skipPACE: boolean,
skipCA: boolean,
extendedMode: boolean,
usePacePolling: boolean,
): Promise<{
mrz: string;
eContent: string;
encryptedDigest: string;
Expand Down
28 changes: 17 additions & 11 deletions app/src/utils/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,18 +200,24 @@ const flushMixpanelEvents = async () => {
// --- Mixpanel NFC Analytics ---
export const configureNfcAnalytics = async () => {
if (!MIXPANEL_NFC_PROJECT_TOKEN || mixpanelConfigured) return;
const enableDebugLogs = JSON.parse(String(ENABLE_DEBUG_LOGS));
if (PassportReader.configure) {
await Promise.resolve(
PassportReader.configure(MIXPANEL_NFC_PROJECT_TOKEN, enableDebugLogs, {
Copy link
Member Author

Choose a reason for hiding this comment

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

hallucination

flushInterval: 20,
flushCount: 5,
flushOnBackground: true,
flushOnForeground: true,
flushOnNetworkChange: true,
}),
);
const enableDebugLogs =
String(ENABLE_DEBUG_LOGS ?? '')
.trim()
.toLowerCase() === 'true';

// Check if PassportReader and configure method exist (Android doesn't have configure)
if (PassportReader && typeof PassportReader.configure === 'function') {
try {
// iOS configure method only accepts token and enableDebugLogs
// Android doesn't have this method at all
await Promise.resolve(
PassportReader.configure(MIXPANEL_NFC_PROJECT_TOKEN, enableDebugLogs),
);
} catch (error) {
console.warn('Failed to configure NFC analytics:', error);
}
}

setupFlushPolicies();
mixpanelConfigured = true;
};
Expand Down
18 changes: 11 additions & 7 deletions app/src/utils/nfcScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,17 @@ const scanAndroid = async (inputs: Inputs) => {

const scanIOS = async (inputs: Inputs) => {
return await Promise.resolve(
PassportReader.scan({
documentNumber: inputs.passportNumber,
dateOfBirth: inputs.dateOfBirth,
dateOfExpiry: inputs.dateOfExpiry,
canNumber: inputs.canNumber ?? '',
useCan: inputs.useCan ?? false,
}),
PassportReader.scanPassport(
inputs.passportNumber,
inputs.dateOfBirth,
inputs.dateOfExpiry,
inputs.canNumber ?? '',
inputs.useCan ?? false,
inputs.skipPACE ?? false,
inputs.skipCA ?? false,
inputs.extendedMode ?? false,
inputs.usePacePolling ?? false,
),
);
};

Expand Down
5 changes: 4 additions & 1 deletion app/src/utils/proving/provingMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ interface ProvingState {
endpointType: EndpointType | null;
fcmToken: string | null;
env: 'prod' | 'stg' | null;
selfClient: SelfClient | null;
setFcmToken: (token: string) => void;
init: (
selfClient: SelfClient,
Expand Down Expand Up @@ -330,6 +331,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
reason: null,
endpointType: null,
fcmToken: null,
selfClient: null,
setFcmToken: (token: string) => {
set({ fcmToken: token });
trackEvent(ProofEvents.FCM_TOKEN_STORED);
Expand Down Expand Up @@ -632,6 +634,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
circuitType,
endpointType: null,
env: null,
selfClient,
});

actor = createActor(provingMachine);
Expand All @@ -649,7 +652,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {

const { data: passportData } = selectedDocument;

const secret = await selfClient.getPrivateKey();
const secret = await get().selfClient?.getPrivateKey();
Copy link
Member Author

Choose a reason for hiding this comment

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

fix test

if (!secret) {
console.error('Could not load secret');
trackEvent(ProofEvents.LOAD_SECRET_FAILED);
Expand Down
129 changes: 129 additions & 0 deletions app/tests/src/nativeModules/passportReader.simple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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.

/**
* Simple contract tests for PassportReader native module
* These tests verify critical interface requirements without conditional expects
*/

import { PassportReader } from 'react-native-passport-reader';

describe('PassportReader Simple Contract Tests', () => {
describe('Critical Interface Requirements', () => {
it('should have scanPassport method (not scan)', () => {
// This prevents the iOS "scan is undefined" bug
expect(PassportReader.scanPassport).toBeDefined();
expect(typeof PassportReader.scanPassport).toBe('function');
});

it('should NOT have scan method', () => {
// This was the source of the iOS bug
expect((PassportReader as any).scan).toBeUndefined();
});

it('should have reset method', () => {
// This should always exist
expect(PassportReader.reset).toBeDefined();
expect(typeof PassportReader.reset).toBe('function');
});

it('should have scanPassport with correct parameter count', () => {
// scanPassport should take exactly 9 parameters
expect(PassportReader.scanPassport.length).toBe(9);
});

it('should allow configure to be optional', () => {
// configure might not exist on Android - this should not crash
const configureType = typeof PassportReader.configure;
expect(['function', 'undefined']).toContain(configureType);
});

it('should allow trackEvent to be optional', () => {
// trackEvent might not exist on all platforms
const trackEventType = typeof PassportReader.trackEvent;
expect(['function', 'undefined']).toContain(trackEventType);
});

it('should allow flush to be optional', () => {
// flush might not exist on all platforms
const flushType = typeof PassportReader.flush;
expect(['function', 'undefined']).toContain(flushType);
});
});

describe('Safe Method Calling Patterns', () => {
it('should be safe to check configure existence', () => {
// This pattern should never crash
expect(() => {
const hasConfigured = Boolean(PassportReader.configure);
return hasConfigured;
}).not.toThrow();
});

it('should be safe to check trackEvent existence', () => {
// This pattern should never crash
expect(() => {
const hasTrackEvent = Boolean(PassportReader.trackEvent);
return hasTrackEvent;
}).not.toThrow();
});

it('should be safe to check flush existence', () => {
// This pattern should never crash
expect(() => {
const hasFlush = Boolean(PassportReader.flush);
return hasFlush;
}).not.toThrow();
});
});

describe('Method Invocation Safety', () => {
it('should not crash when calling scanPassport', () => {
// Should be callable (might fail due to missing NFC, but should not crash due to undefined)
expect(() => {
PassportReader.scanPassport(
'test',
'test',
'test',
'test',
false,
false,
false,
false,
false,
);
}).not.toThrow(TypeError);
});

it('should not crash when calling reset', () => {
// Should be callable
expect(() => {
PassportReader.reset();
}).not.toThrow(TypeError);
});
});

describe('Interface Consistency', () => {
it('should have consistent method naming', () => {
// Ensure we use the correct method names
expect(PassportReader.scanPassport).toBeDefined(); // ✅ Correct
expect((PassportReader as any).scan).toBeUndefined(); // ❌ Wrong (causes iOS crash)
});

it('should have proper method types', () => {
// All defined methods should be functions
expect(typeof PassportReader.reset).toBe('function');
expect(typeof PassportReader.scanPassport).toBe('function');

// Optional methods should be function or undefined
expect(['function', 'undefined']).toContain(
typeof PassportReader.configure,
);
expect(['function', 'undefined']).toContain(
typeof PassportReader.trackEvent,
);
expect(['function', 'undefined']).toContain(typeof PassportReader.flush);
});
});
});
Loading
Loading