Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 23 additions & 1 deletion app/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ require('react-native-gesture-handler/jestSetup');

jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

jest.mock('@env', () => ({
ENABLE_DEBUG_LOGS: 'false',
MIXPANEL_NFC_PROJECT_TOKEN: 'test-token',
}));

global.FileReader = class {
constructor() {
this.onload = null;
Expand Down Expand Up @@ -197,13 +202,30 @@ jest.mock('react-native-nfc-manager', () => ({
// Mock react-native-passport-reader
jest.mock('react-native-passport-reader', () => ({
default: {
initialize: jest.fn(),
configure: jest.fn(),
scanPassport: jest.fn(),
readPassport: jest.fn(),
cancelPassportRead: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
}));
Comment on lines 208 to 218
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Mock parity with types: add scan alias to match d.ts and potential callsites

The default export for react-native-passport-reader only exposes scanPassport, while the d.ts (and likely some callsites) reference scan. This will cause test failures or undefined calls in environments that import the default module instead of NativeModules.

Apply this diff to add a scan alias:

 jest.mock('react-native-passport-reader', () => ({
   default: {
     configure: jest.fn(),
-    scanPassport: jest.fn(),
+    scanPassport: jest.fn(),
+    // Alias to align with TS types/callsites
+    scan: jest.fn(),
     readPassport: jest.fn(),
     cancelPassportRead: jest.fn(),
     trackEvent: jest.fn(),
     flush: jest.fn(),
     reset: jest.fn(),
   },
 }));

Run to spot any lingering scanPassport( vs scan( mismatches:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 --type=ts --type=tsx '\bPassportReader\b.*\bscan(Passport)?\s*\('
rg -nP -C2 --type=ts --type=tsx '\bNativeModules\.PassportReader\b.*\bscan(Passport)?\s*\('

Length of output: 228


🏁 Script executed:

#!/bin/bash
# Search for both scanPassport and scan callsites across TS/TSX/JS/JSX files
rg -nP -C2 '\bPassportReader\b.*\bscan(?:Passport)?\s*\(' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx'
rg -nP -C2 '\bNativeModules\.PassportReader\b.*\bscan(?:Passport)?\s*\(' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx'

Length of output: 1200


Add scan alias to the PassportReader mock to cover Android callsites

Your Android scanner helper calls PassportReader.scan(…) (app/src/utils/nfcScanner.ts line 128), but your Jest setup only mocks scanPassport. Without a scan mock, tests hitting scanAndroid will fail with undefined is not a function.

Apply this diff in app/jest.setup.js (around lines 203–213):

 jest.mock('react-native-passport-reader', () => ({
   default: {
     configure: jest.fn(),
     scanPassport: jest.fn(),
+    // Alias to align with TS types/callsites (e.g. scanAndroid)
+    scan: jest.fn(),
     readPassport: jest.fn(),
     cancelPassportRead: jest.fn(),
     trackEvent: jest.fn(),
     flush: jest.fn(),
     reset: jest.fn(),
   },
 }));

— without this, any PassportReader.scan(...) calls will be unmocked and break your Android tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('react-native-passport-reader', () => ({
default: {
initialize: jest.fn(),
configure: jest.fn(),
scanPassport: jest.fn(),
readPassport: jest.fn(),
cancelPassportRead: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
}));
jest.mock('react-native-passport-reader', () => ({
default: {
configure: jest.fn(),
scanPassport: jest.fn(),
// Alias to align with TS types/callsites (e.g. scanAndroid)
scan: jest.fn(),
readPassport: jest.fn(),
cancelPassportRead: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
reset: jest.fn(),
},
}));
🤖 Prompt for AI Agents
In app/jest.setup.js around lines 203 to 213, the PassportReader mock only
provides scanPassport/readPassport/etc. but not the Android alias scan, so tests
calling PassportReader.scan(...) will fail; update the mock to add a scan
property that points to the same jest.fn() used by scanPassport (i.e., add scan:
jest.fn() or set scan: mock.scanPassport) so PassportReader.scan(...) is defined
and uses the existing mock behavior.


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

NativeModules.PassportReader = {
configure: jest.fn(),
scanPassport: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
};

Comment on lines +220 to +228
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Keep NativeModules mock consistent and complete

NativeModules.PassportReader mirrors only some methods. To avoid drift between the default export and NativeModules, provide a scan alias here too (and, ideally, share the same jest.fn where practical).

Apply this diff:

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

 NativeModules.PassportReader = {
   configure: jest.fn(),
-  scanPassport: jest.fn(),
+  scanPassport: jest.fn(),
+  // Alias for parity with d.ts and default export
+  scan: jest.fn(),
   trackEvent: jest.fn(),
   flush: jest.fn(),
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { NativeModules } = require('react-native');
NativeModules.PassportReader = {
configure: jest.fn(),
scanPassport: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
};
const { NativeModules } = require('react-native');
NativeModules.PassportReader = {
configure: jest.fn(),
scanPassport: jest.fn(),
// Alias for parity with d.ts and default export
scan: jest.fn(),
trackEvent: jest.fn(),
flush: jest.fn(),
};
🤖 Prompt for AI Agents
In app/jest.setup.js around lines 215 to 223, NativeModules.PassportReader
defines configure, scanPassport, trackEvent, and flush but is missing the scan
alias; update the mock to include scan and make it reference the same jest.fn()
as scanPassport (i.e., create one jest.fn for the passport scan and assign it to
both scanPassport and scan) so the mock stays consistent and methods remain in
sync.

jest.mock('@react-native-community/netinfo', () => ({
addEventListener: jest.fn(() => jest.fn()),
fetch: jest.fn(() => Promise.resolve({ isConnected: true })),
}));
Comment on lines +229 to +232
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

Duplicate NetInfo mock overrides earlier one (breaks useNetInfo/behavior)

This second jest.mock('@react-native-community/netinfo', ...) overrides the earlier mock (Lines 147–160), dropping useNetInfo and changing method shapes. That can cause test flakiness or crashes.

Apply this diff to remove the duplicate:

-jest.mock('@react-native-community/netinfo', () => ({
-  addEventListener: jest.fn(() => jest.fn()),
-  fetch: jest.fn(() => Promise.resolve({ isConnected: true })),
-}));

Then, update the first NetInfo mock (Lines 147–160) to return an unsubscribe from addEventListener and a resolved value from fetch:

// In the first mock block (existing):
addEventListener: jest.fn(() => jest.fn()), // returns unsubscribe
fetch: jest.fn().mockResolvedValue({ isConnected: true, isInternetReachable: true }),

If you want, I can push a combined single mock that covers useNetInfo, addEventListener, and fetch consistently.

🤖 Prompt for AI Agents
In app/jest.setup.js around lines 224–227 there is a duplicate
jest.mock('@react-native-community/netinfo') that overrides the earlier mock and
drops useNetInfo/changes method shapes; remove this second mock block and
instead update the first mock block (around lines 147–160) so addEventListener
returns an unsubscribe function (e.g., jest.fn(() => jest.fn())) and fetch is a
resolved promise (e.g., jest.fn().mockResolvedValue({ isConnected: true,
isInternetReachable: true })), ensuring the single mock covers useNetInfo,
addEventListener, and fetch consistently.


// Mock @stablelib packages
jest.mock('@stablelib/cbor', () => ({
encode: jest.fn(),
Expand Down
6 changes: 6 additions & 0 deletions app/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Text, View } from 'react-native';

import { captureException } from '@/Sentry';
import analytics from '@/utils/analytics';
import { flushMixpanelEvents, trackNfcEvent } from '@/utils/nfcScanner';
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

Sanitize error payloads before sending to analytics to avoid PII leakage

Forwarding raw error.message and full component stack to Mixpanel can inadvertently leak PII or sensitive details. Please sanitize and truncate before tracking. Also safe-guard analytics calls to be no-throw in this path.

Apply this diff to sanitize and bound the payload:

   componentDidCatch(error: Error, info: ErrorInfo) {
-    trackNfcEvent('error_boundary', {
-      message: error.message,
-      stack: info.componentStack,
-    });
+    try {
+      const safeMessage =
+        typeof error?.message === 'string' ? sanitizeErrorMessage(error.message) : 'unknown';
+      const safeStack =
+        typeof info?.componentStack === 'string'
+          ? info.componentStack.slice(0, 512)
+          : undefined;
+      trackNfcEvent('error_boundary', {
+        message: safeMessage,
+        stack: safeStack,
+      });
+    } catch {
+      // no-op: never let analytics crash the error boundary
+    }
     flushMixpanelEvents();

Add this import at the top of the file:

import { sanitizeErrorMessage } from '@/utils/utils';

Also applies to: 34-39

🤖 Prompt for AI Agents
In app/src/components/ErrorBoundary.tsx around line 11 (and also for the
analytics call area at lines 34-39), add the import "import {
sanitizeErrorMessage } from '@/utils/utils';" then replace direct uses of
error.message and componentStack when calling track/flush Mixpanel functions
with sanitized, truncated versions (e.g. sanitizeErrorMessage(error.message) and
sanitizeErrorMessage(componentStack) or slice to a safe max length), and wrap
the entire analytics call block in a try/catch to ensure any errors in telemetry
do not propagate (log or swallow telemetry errors safely).


const { flush: flushAnalytics } = analytics();

Expand All @@ -30,6 +31,11 @@ class ErrorBoundary extends Component<Props, State> {
}

componentDidCatch(error: Error, info: ErrorInfo) {
trackNfcEvent('error_boundary', {
message: error.message,
stack: info.componentStack,
});
flushMixpanelEvents();
// Flush analytics before the app crashes
flushAnalytics();
captureException(error, {
Expand Down
45 changes: 37 additions & 8 deletions app/src/screens/passport/PassportNFCScanScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { useFeedback } from '@/providers/feedbackProvider';
import { storePassportData } from '@/providers/passportDataProvider';
import useUserStore from '@/stores/userStore';
import analytics from '@/utils/analytics';
import { black, slate100, slate400, slate500, white } from '@/utils/colors';
import { sendFeedbackEmail } from '@/utils/email';
import { dinot } from '@/utils/fonts';
Expand All @@ -54,7 +55,12 @@ import {
feedbackUnsuccessful,
impactLight,
} from '@/utils/haptic';
import { parseScanResponse, scan } from '@/utils/nfcScanner';
import {
flushMixpanelEvents,
parseScanResponse,
scan,
trackNfcEvent,
} from '@/utils/nfcScanner';
import { sanitizeErrorMessage } from '@/utils/utils';

const emitter =
Expand All @@ -79,6 +85,7 @@ type PassportNFCScanRoute = RouteProp<
const PassportNFCScanScreen: React.FC = () => {
const selfClient = useSelfClient();
const { trackEvent } = selfClient;
const { flush: flushAnalytics } = analytics();
const navigation = useNavigation();
const route = useRoute<PassportNFCScanRoute>();
const { showModal } = useFeedback();
Expand Down Expand Up @@ -137,6 +144,8 @@ const PassportNFCScanScreen: React.FC = () => {

const openErrorModal = useCallback(
(message: string) => {
flushAnalytics();
flushMixpanelEvents();
showModal({
titleText: 'NFC Scan Error',
bodyText: message,
Expand Down Expand Up @@ -206,6 +215,9 @@ const PassportNFCScanScreen: React.FC = () => {
trackEvent(PassportEvents.NFC_SCAN_FAILED, {
error: 'timeout',
});
trackNfcEvent(PassportEvents.NFC_SCAN_FAILED, {
error: 'timeout',
});
openErrorModal('Scan timed out. Please try again.');
setIsNfcSheetOpen(false);
}, 30000);
Expand Down Expand Up @@ -249,10 +261,14 @@ const PassportNFCScanScreen: React.FC = () => {
passportData = parseScanResponse(scanResponse);
} catch (e: unknown) {
console.error('Parsing NFC Response Unsuccessful');
const errMsg = sanitizeErrorMessage(
e instanceof Error ? e.message : String(e),
);
trackEvent(PassportEvents.NFC_RESPONSE_PARSE_FAILED, {
error: sanitizeErrorMessage(
e instanceof Error ? e.message : String(e),
),
error: errMsg,
});
trackNfcEvent(PassportEvents.NFC_RESPONSE_PARSE_FAILED, {
error: errMsg,
});
return;
}
Expand Down Expand Up @@ -317,10 +333,14 @@ const PassportNFCScanScreen: React.FC = () => {
return;
}
console.error('Passport Parsed Failed:', e);
const errMsg = sanitizeErrorMessage(
e instanceof Error ? e.message : String(e),
);
trackEvent(PassportEvents.PASSPORT_PARSE_FAILED, {
error: sanitizeErrorMessage(
e instanceof Error ? e.message : String(e),
),
error: errMsg,
});
trackNfcEvent(PassportEvents.PASSPORT_PARSE_FAILED, {
error: errMsg,
});
return;
}
Expand All @@ -335,8 +355,13 @@ const PassportNFCScanScreen: React.FC = () => {
).toFixed(2);
console.error('NFC Scan Unsuccessful:', e);
const message = e instanceof Error ? e.message : String(e);
const sanitized = sanitizeErrorMessage(message);
trackEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitizeErrorMessage(message),
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
trackNfcEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
Comment on lines +352 to 360
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use the sanitized message for the error modal to prevent accidental PII exposure

You're sanitizing the message for analytics but still passing the raw message to the user-facing modal. Surface the sanitized version there too to avoid exposing MRZ or other sensitive values.

Apply this change near the existing code:

-        const message = e instanceof Error ? e.message : String(e);
-        const sanitized = sanitizeErrorMessage(message);
+        const message = e instanceof Error ? e.message : String(e);
+        const sanitized = sanitizeErrorMessage(message);
...
-        openErrorModal(message);
+        openErrorModal(sanitized);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sanitized = sanitizeErrorMessage(message);
trackEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitizeErrorMessage(message),
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
trackNfcEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
const message = e instanceof Error ? e.message : String(e);
const sanitized = sanitizeErrorMessage(message);
trackEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
trackNfcEvent(PassportEvents.NFC_SCAN_FAILED, {
error: sanitized,
duration_seconds: parseFloat(scanDurationSeconds),
});
openErrorModal(sanitized);
🤖 Prompt for AI Agents
In app/src/screens/passport/PassportNFCScanScreen.tsx around lines 358 to 366,
the code logs a sanitized error for analytics but still shows the raw message in
the user-facing modal; replace the raw message with the sanitized variable when
invoking the error modal so the displayed content uses
sanitizeErrorMessage(message) (sanitized) instead of the original message,
ensuring any modal props or state that currently use the raw message are updated
to use sanitized and keeping analytics tracking as-is.

openErrorModal(message);
Expand All @@ -350,6 +375,8 @@ const PassportNFCScanScreen: React.FC = () => {
setIsNfcSheetOpen(false);
}
} else if (isNfcSupported) {
flushAnalytics();
flushMixpanelEvents();
if (Platform.OS === 'ios') {
Linking.openURL('App-Prefs:root=General&path=About');
} else {
Expand All @@ -376,6 +403,8 @@ const PassportNFCScanScreen: React.FC = () => {
});

const onCancelPress = async () => {
flushAnalytics();
flushMixpanelEvents();
const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient);
if (hasValidDocument) {
navigateToHome();
Expand Down
2 changes: 2 additions & 0 deletions app/src/screens/passport/PassportNFCTroubleScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import analytics from '@/utils/analytics';
import { slate500 } from '@/utils/colors';
import { sendFeedbackEmail } from '@/utils/email';
import { flushMixpanelEvents } from '@/utils/nfcScanner';

const { flush: flushAnalytics } = analytics();

Expand Down Expand Up @@ -56,6 +57,7 @@ const PassportNFCTrouble: React.FC = () => {
// error screen, flush analytics
useEffect(() => {
flushAnalytics();
flushMixpanelEvents();
}, []);

// 5-taps with a single finger
Expand Down
9 changes: 9 additions & 0 deletions app/src/screens/prove/ConfirmBelongingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import { Title } from '@/components/typography/Title';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { styles } from '@/screens/prove/ProofRequestStatusScreen';
import analytics from '@/utils/analytics';
import { black, white } from '@/utils/colors';
import { notificationSuccess } from '@/utils/haptic';
import { flushMixpanelEvents, trackNfcEvent } from '@/utils/nfcScanner';
import {
getFCMToken,
requestNotificationPermission,
Expand Down Expand Up @@ -52,6 +54,7 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
try {
setRequestingPermission(true);
trackEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED);
trackNfcEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED);

// Request notification permission
const permissionGranted = await requestNotificationPermission();
Expand All @@ -74,6 +77,12 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
trackEvent(ProofEvents.PROVING_PROCESS_ERROR, {
error: message,
});
trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, {
error: message,
});
const { flush: flushAnalytics } = analytics();
flushAnalytics();
flushMixpanelEvents();
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Sanitize error data before sending to analytics

error.message can contain PII or sensitive details. Shipping raw messages to analytics (Segment + Mixpanel) increases leakage risk. Prefer an error code/category and a bounded, scrubbed message.

Apply this refactor to gate and sanitize payloads:

-      trackEvent(ProofEvents.PROVING_PROCESS_ERROR, {
-        error: message,
-      });
-      trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, {
-        error: message,
-      });
+      const safeError = (() => {
+        // Keep a tiny, bounded footprint: code + coarse category
+        const code =
+          error instanceof Error && 'name' in error ? String(error.name) : 'UnknownError';
+        // Avoid sending raw messages; bucketize instead
+        const category =
+          /network|timeout/i.test(message) ? 'network' :
+          /permission|denied/i.test(message) ? 'permission' :
+          /cancel|abort/i.test(message) ? 'cancellation' :
+          'generic';
+        return { code, category };
+      })();
+      trackEvent(ProofEvents.PROVING_PROCESS_ERROR, safeError);
+      trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, safeError);

Search for other analytics calls that include raw error, stack, or message and replace with sanitized payloads:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 --type=ts --type=tsx '\btrack(Nfc)?Event\(\s*[^,]+,\s*\{[^}]*\b(error|stack|message)\b' 

Length of output: 128


I’ll broaden the search to include both .ts and .tsx files without relying on ripgrep’s built-in types:


🏁 Script executed:

#!/bin/bash
# Search all .ts & .tsx for analytics calls sending raw error, stack, or message
rg -nP -C2 -g '*.ts' -g '*.tsx' '\btrack(Nfc)?Event\(\s*[^,]+,\s*\{[^}]*\b(error|stack|message)\b'

Length of output: 1719


Sanitize error payloads before emitting analytics events

Error objects can contain PII or sensitive internals. We must never ship raw .message, .error, or .stack fields to Segment/Mixpanel. Instead, extract only a safe error code/name plus a coarse-grained category (e.g. “network”, “permission”, “cancellation”, “generic”).

Please apply this refactor everywhere you currently call trackEvent or trackNfcEvent with raw error data:

• app/src/screens/prove/ConfirmBelongingScreen.tsx (lines 80–85)
Replace:

-     trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, {
-       error: message,
-     });
+   const safeError = sanitizeError(error, message);
+   trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, safeError);

• app/src/providers/authProvider.tsx (line 197)
Replace:

-     trackEvent(AuthEvents.BIOMETRIC_LOGIN_FAILED, { error });
+   trackEvent(AuthEvents.BIOMETRIC_LOGIN_FAILED, sanitizeError(error));

• app/src/providers/authProvider.web.tsx (line 210)
Replace:

-     trackEvent(AuthEvents.BIOMETRIC_LOGIN_FAILED, { error });
+   trackEvent(AuthEvents.BIOMETRIC_LOGIN_FAILED, sanitizeError(error));

• app/src/utils/proving/provingMachine.ts (line 403)
Replace:

-     trackEvent(ProofEvents.TEE_WS_ERROR, { error: result.error });
+    trackEvent(ProofEvents.TEE_WS_ERROR, sanitizeError(result.error));

• app/src/utils/proving/provingMachine.ts (line 851)
Replace:

-     trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg });
+    trackEvent(ProofEvents.TEE_CONN_FAILED, sanitizeError(undefined, msg));

You can centralize the logic in a helper:

function sanitizeError(err?: unknown, msg?: string) {
  const code = err instanceof Error ? err.name : 'UnknownError';
  const category = /network|timeout/i.test(msg ?? '') ? 'network'
    : /permission|denied/i.test(msg ?? '') ? 'permission'
    : /cancel|abort/i.test(msg ?? '') ? 'cancellation'
    : 'generic';
  return { code, category };
}

Apply this pattern to any future analytics events to avoid inadvertent PII leakage.

🤖 Prompt for AI Agents
In app/src/screens/prove/ConfirmBelongingScreen.tsx around lines 80–85, the code
currently sends raw error text to analytics; replace the direct passing of
message with a sanitized payload: create or import a sanitizeError(err?:
unknown, msg?: string) helper that returns only a safe code/name and a
coarse-grained category, call sanitizeError(undefined, message) (or with the
actual error if available) and pass that object to trackNfcEvent instead of the
raw message, then keep the existing analytics flush calls unchanged.

} finally {
setRequestingPermission(false);
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/screens/prove/QRCodeTroubleScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useHapticNavigation from '@/hooks/useHapticNavigation';
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import analytics from '@/utils/analytics';
import { slate500 } from '@/utils/colors';
import { flushMixpanelEvents } from '@/utils/nfcScanner';

const { flush: flushAnalytics } = analytics();

Expand Down Expand Up @@ -50,6 +51,7 @@ const QRCodeTrouble: React.FC = () => {
// error screen, flush analytics
useEffect(() => {
flushAnalytics();
flushMixpanelEvents();
}, []);

return (
Expand Down
14 changes: 13 additions & 1 deletion app/src/types/react-native-passport-reader.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@ declare module 'react-native-passport-reader' {
}

interface PassportReader {
configure(token: string): void;
configure(
token: string,
enableDebug?: boolean,
flushPolicies?: {
flushInterval?: number;
flushCount?: number;
flushOnBackground?: boolean;
flushOnForeground?: boolean;
flushOnNetworkChange?: boolean;
},
): void;
trackEvent?(name: string, properties?: Record<string, unknown>): void;
flush?(): void;
Comment on lines +16 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

API alignment: configure looks good; confirm scan vs scanPassport naming across native/mocks

The expanded configure signature and optional trackEvent/flush match the PR intent. However, elsewhere in the repo and mocks we expose scanPassport via NativeModules.PassportReader, while this type defines scan(...). This mismatch can surface as runtime errors or type unsafety depending on which entry point callers use.

  • If the JS wrapper truly exports scan(...), ensure mocks/native also expose scan (alias scanPassport).
  • If the native bridge is the public API, extend the d.ts with an optional scanPassport?(...) to reflect reality.

To cover both code paths without breaking types, add a secondary optional method in the interface:

// Add alongside `scan(...)` in the PassportReader interface
scanPassport?(options: ScanOptions): Promise<{
  mrz: string;
  eContent: string;
  encryptedDigest: string;
  photo: { base64: string };
  digestAlgorithm: string;
  signerInfoDigestAlgorithm: string;
  digestEncryptionAlgorithm: string;
  LDSVersion: string;
  unicodeVersion: string;
  encapContent: string;
  documentSigningCertificate: string;
  dataGroupHashes: string;
}>;

If you prefer keeping a single name, I can generate diffs to switch the mocks and callsites to scan(...) instead—say the word.


🏁 Script executed:

#!/bin/bash
# Find occurrences of scanPassport
rg -n "scanPassport" -C2 || true

# Find occurrences of scan( in PassportReader context
rg -n "\.scan\s*\(" -C2 || true

# Find NativeModules.PassportReader usage
rg -n "NativeModules\.PassportReader" -C2 || true

# List mock files related to passport reader
rg -i "__mocks__.*passport[-_]reader" -l || true

Length of output: 8566


Address scan vs scanPassport naming mismatch

There’s currently a gap between what the native modules and mocks expose (iOS / JS calls use scanPassport) and what the Android bridge and TypeScript definitions expect (scan). Without alignment, callers will hit undefined-method errors at runtime or TypeScript will flag missing properties.

• In app/src/types/react-native-passport-reader.d.ts, the interface lists scan(...) but not scanPassport. Add an optional alias there:

export interface PassportReader {
  // …existing members…
  scan(options: ScanOptions): Promise<ScanResult>;
  /** iOS native bridge uses scanPassport */
  scanPassport?(options: ScanOptions): Promise<ScanResult>;
  // …existing members…
}

• In app/jest.setup.js, the mock only defines scanPassport. Mirror the Android shape so tests covering JS calls to scan will pass:

NativeModules.PassportReader = {
  configure: jest.fn(),
  scanPassport: jest.fn(),
  /** alias for Android-style API */
  scan: jest.fn((opts) => NativeModules.PassportReader.scanPassport(opts)),
  trackEvent: jest.fn(),
  flush: jest.fn(),
};

• Elsewhere in the JS wrapper (app/src/utils/nfcScanner.ts), scanIOS uses scanPassport while scanAndroid uses PassportReader.scan. With both signatures in the TS interface and both methods on the mock, all code paths and tests remain type-safe.

Implementing these changes will harmonize native bridges, mocks, and the TS layer—eliminating brittle ad-hoc aliases in callers and preventing runtime/type errors.

🤖 Prompt for AI Agents
In app/src/types/react-native-passport-reader.d.ts around lines 16–28, the
PassportReader interface exposes scan but not scanPassport which causes a
mismatch with iOS/native mocks; add an optional scanPassport?(options:
ScanOptions): Promise<ScanResult> alias alongside scan; in app/jest.setup.js
update the NativeModules.PassportReader mock to define both scanPassport and
scan (make scan call/forward to scanPassport) plus keep
configure/trackEvent/flush mocks so tests using either method pass; finally
verify app/src/utils/nfcScanner.ts uses the unified signatures (scan or
scanPassport) or call the appropriate alias to ensure both Android and iOS code
paths are covered and type-safe.

reset(): void;
scan(options: ScanOptions): Promise<{
mrz: string;
Expand Down
Loading
Loading