diff --git a/app/Gemfile.lock b/app/Gemfile.lock index e5fd872d4..ece09b4eb 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -25,8 +25,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1151.0) - aws-sdk-core (3.230.0) + aws-partitions (1.1152.0) + aws-sdk-core (3.231.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -34,11 +34,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.110.0) - aws-sdk-core (~> 3, >= 3.228.0) + aws-sdk-kms (1.112.0) + aws-sdk-core (~> 3, >= 3.231.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.197.0) - aws-sdk-core (~> 3, >= 3.228.0) + aws-sdk-s3 (1.198.0) + aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) diff --git a/app/android/react-native-passport-reader/index.android.js b/app/android/react-native-passport-reader/index.android.js index a60029b23..f6c78652c 100644 --- a/app/android/react-native-passport-reader/index.android.js +++ b/app/android/react-native-passport-reader/index.android.js @@ -7,7 +7,8 @@ const DATE_REGEX = /^\d{6}$/ module.exports = { ...RNPassportReader, - scan + scan, + reset: RNPassportReader.reset } function scan({ documentNumber, dateOfBirth, dateOfExpiry, canNumber, useCan, quality=1 }) { diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index debda0138..d2a604534 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -2089,7 +2089,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - segment-analytics-react-native (2.21.1): + - segment-analytics-react-native (2.21.2): - React-Core - sovran-react-native - Sentry/HybridSDK (8.52.1) @@ -2531,7 +2531,7 @@ SPEC CHECKSUMS: RNScreens: 224dba0e9e7674d911ebf3931eddca686f133e8a RNSentry: d240d406990e08d9b1fa967aaac67b7cb61b32e2 RNSVG: e1a716d635c65297c86e874eeb6adf3704a2e50a - segment-analytics-react-native: 5c3e8a4ee6d7532a011ed862d7c7d4fb5e5303e2 + segment-analytics-react-native: bad4c2c7b63818bd493caa2b5759fca59e4ae9a7 Sentry: 2cbbe3592f30050c60e916c63c7f5a2fa584005e SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594 diff --git a/app/jest.setup.js b/app/jest.setup.js index 9762264f1..e0e1bd94a 100644 --- a/app/jest.setup.js +++ b/app/jest.setup.js @@ -13,6 +13,11 @@ jest.mock( { virtual: true }, ); +jest.mock('@env', () => ({ + ENABLE_DEBUG_LOGS: 'false', + MIXPANEL_NFC_PROJECT_TOKEN: 'test-token', +})); + global.FileReader = class { constructor() { this.onload = null; @@ -202,13 +207,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(), }, })); +const { NativeModules } = require('react-native'); + +NativeModules.PassportReader = { + configure: jest.fn(), + scanPassport: jest.fn(), + trackEvent: jest.fn(), + flush: jest.fn(), +}; + +jest.mock('@react-native-community/netinfo', () => ({ + addEventListener: jest.fn(() => jest.fn()), + fetch: jest.fn(() => Promise.resolve({ isConnected: true })), +})); + // Mock @stablelib packages jest.mock('@stablelib/cbor', () => ({ encode: jest.fn(), diff --git a/app/package.json b/app/package.json index b25a3dd0c..088cf8f27 100644 --- a/app/package.json +++ b/app/package.json @@ -42,7 +42,7 @@ "mobile-local-deploy": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs both", "mobile-local-deploy:android": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs android", "mobile-local-deploy:ios": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs ios", - "nice": "yarn imports:fix && yarn lint:fix && yarn fmt:fix", + "nice": "yarn build:deps && yarn imports:fix && yarn lint:fix && yarn fmt:fix", "reinstall": "yarn clean && yarn install && yarn install-app", "release": "./scripts/release.sh", "release:major": "./scripts/release.sh major", @@ -82,7 +82,7 @@ "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "@robinbobin/react-native-google-drive-api-wrapper": "^2.2.3", - "@segment/analytics-react-native": "^2.21.0", + "@segment/analytics-react-native": "^2.21.2", "@segment/sovran-react-native": "^1.1.3", "@selfxyz/common": "workspace:^", "@selfxyz/mobile-sdk-alpha": "workspace:^", diff --git a/app/src/Segment.ts b/app/src/Segment.ts index efc31fb4b..763084225 100644 --- a/app/src/Segment.ts +++ b/app/src/Segment.ts @@ -9,6 +9,7 @@ import { createClient, EventPlugin, PluginType, + StartupFlushPolicy, } from '@segment/analytics-react-native'; import '@ethersproject/shims'; @@ -48,7 +49,7 @@ export const createSegmentClient = () => { return segmentClient; } - const flushPolicies = [new BackgroundFlushPolicy()]; + const flushPolicies = [new BackgroundFlushPolicy(), new StartupFlushPolicy()]; const client = createClient({ writeKey: SEGMENT_KEY, @@ -56,6 +57,8 @@ export const createSegmentClient = () => { trackDeepLinks: true, debug: __DEV__, collectDeviceId: false, + flushAt: 20, // Flush every 20 events + flushInterval: 20000, // Flush every 20 seconds defaultSettings: { integrations: { 'Segment.io': { diff --git a/app/src/components/ErrorBoundary.tsx b/app/src/components/ErrorBoundary.tsx index 1862fecf9..c7a08c5ff 100644 --- a/app/src/components/ErrorBoundary.tsx +++ b/app/src/components/ErrorBoundary.tsx @@ -7,9 +7,7 @@ import React, { Component } from 'react'; import { Text, View } from 'react-native'; import { captureException } from '@/Sentry'; -import analytics from '@/utils/analytics'; - -const { flush: flushAnalytics } = analytics(); +import { flushAllAnalytics, trackNfcEvent } from '@/utils/analytics'; interface Props { children: React.ReactNode; @@ -30,8 +28,12 @@ class ErrorBoundary extends Component { } componentDidCatch(error: Error, info: ErrorInfo) { - // Flush analytics before the app crashes - flushAnalytics(); + trackNfcEvent('error_boundary', { + message: error.message, + stack: info.componentStack, + }); + // Flush all analytics before the app crashes + flushAllAnalytics(); captureException(error, { componentStack: info.componentStack, errorBoundary: true, diff --git a/app/src/mocks/react-native-passport-reader.ts b/app/src/mocks/react-native-passport-reader.ts new file mode 100644 index 000000000..0461330ff --- /dev/null +++ b/app/src/mocks/react-native-passport-reader.ts @@ -0,0 +1,13 @@ +// 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. + +// Web mock for react-native-passport-reader +export const reset = async () => { + // No-op for web + return Promise.resolve(); +}; + +export const scan = async () => { + throw new Error('NFC scanning is not supported on web'); +}; diff --git a/app/src/screens/passport/PassportNFCScanScreen.tsx b/app/src/screens/passport/PassportNFCScanScreen.tsx index 747acf303..abbd29dc3 100644 --- a/app/src/screens/passport/PassportNFCScanScreen.tsx +++ b/app/src/screens/passport/PassportNFCScanScreen.tsx @@ -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 { flushAllAnalytics, trackNfcEvent } from '@/utils/analytics'; import { black, slate100, slate400, slate500, white } from '@/utils/colors'; import { sendFeedbackEmail } from '@/utils/email'; import { dinot } from '@/utils/fonts'; @@ -79,6 +80,7 @@ type PassportNFCScanRoute = RouteProp< const PassportNFCScanScreen: React.FC = () => { const selfClient = useSelfClient(); const { trackEvent } = selfClient; + const navigation = useNavigation(); const route = useRoute(); const { showModal } = useFeedback(); @@ -137,6 +139,7 @@ const PassportNFCScanScreen: React.FC = () => { const openErrorModal = useCallback( (message: string) => { + flushAllAnalytics(); showModal({ titleText: 'NFC Scan Error', bodyText: message, @@ -206,6 +209,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); @@ -249,10 +255,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; } @@ -317,10 +327,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; } @@ -335,8 +349,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), }); openErrorModal(message); @@ -350,6 +369,7 @@ const PassportNFCScanScreen: React.FC = () => { setIsNfcSheetOpen(false); } } else if (isNfcSupported) { + flushAllAnalytics(); if (Platform.OS === 'ios') { Linking.openURL('App-Prefs:root=General&path=About'); } else { @@ -376,6 +396,7 @@ const PassportNFCScanScreen: React.FC = () => { }); const onCancelPress = async () => { + flushAllAnalytics(); const hasValidDocument = await hasAnyValidRegisteredDocument(selfClient); if (hasValidDocument) { navigateToHome(); diff --git a/app/src/screens/passport/PassportNFCTroubleScreen.tsx b/app/src/screens/passport/PassportNFCTroubleScreen.tsx index d5fac3b55..07aa41053 100644 --- a/app/src/screens/passport/PassportNFCTroubleScreen.tsx +++ b/app/src/screens/passport/PassportNFCTroubleScreen.tsx @@ -13,12 +13,10 @@ import { Caption } from '@/components/typography/Caption'; import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; -import analytics from '@/utils/analytics'; +import analytics, { flushAllAnalytics } from '@/utils/analytics'; import { slate500 } from '@/utils/colors'; import { sendFeedbackEmail } from '@/utils/email'; -const { flush: flushAnalytics } = analytics(); - const tips: TipProps[] = [ { title: 'Know Your Chip Location', @@ -55,7 +53,7 @@ const PassportNFCTrouble: React.FC = () => { // error screen, flush analytics useEffect(() => { - flushAnalytics(); + flushAllAnalytics(); }, []); // 5-taps with a single finger diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index 34ef884fa..7e49ef1e2 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -21,6 +21,7 @@ import { Title } from '@/components/typography/Title'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { styles } from '@/screens/prove/ProofRequestStatusScreen'; +import analytics, { flushAllAnalytics, trackNfcEvent } from '@/utils/analytics'; import { black, white } from '@/utils/colors'; import { notificationSuccess } from '@/utils/haptic'; import { @@ -52,6 +53,7 @@ const ConfirmBelongingScreen: React.FC = () => { try { setRequestingPermission(true); trackEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED); + trackNfcEvent(ProofEvents.NOTIFICATION_PERMISSION_REQUESTED); // Request notification permission const permissionGranted = await requestNotificationPermission(); @@ -74,6 +76,11 @@ const ConfirmBelongingScreen: React.FC = () => { trackEvent(ProofEvents.PROVING_PROCESS_ERROR, { error: message, }); + trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, { + error: message, + }); + + flushAllAnalytics(); } finally { setRequestingPermission(false); } diff --git a/app/src/screens/prove/QRCodeTroubleScreen.tsx b/app/src/screens/prove/QRCodeTroubleScreen.tsx index b35e46a08..d59b3e210 100644 --- a/app/src/screens/prove/QRCodeTroubleScreen.tsx +++ b/app/src/screens/prove/QRCodeTroubleScreen.tsx @@ -9,11 +9,9 @@ import Tips from '@/components/Tips'; import { Caption } from '@/components/typography/Caption'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout'; -import analytics from '@/utils/analytics'; +import analytics, { flushAllAnalytics } from '@/utils/analytics'; import { slate500 } from '@/utils/colors'; -const { flush: flushAnalytics } = analytics(); - const tips: TipProps[] = [ { title: 'Ensure Valid QR Code', @@ -49,7 +47,7 @@ const QRCodeTrouble: React.FC = () => { // error screen, flush analytics useEffect(() => { - flushAnalytics(); + flushAllAnalytics(); }, []); return ( diff --git a/app/src/types/react-native-passport-reader.d.ts b/app/src/types/react-native-passport-reader.d.ts index 1c5b0166a..cca693342 100644 --- a/app/src/types/react-native-passport-reader.d.ts +++ b/app/src/types/react-native-passport-reader.d.ts @@ -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): void; + flush?(): void; reset(): void; scan(options: ScanOptions): Promise<{ mrz: string; @@ -33,6 +45,23 @@ declare module 'react-native-passport-reader' { }>; } - const PassportReader: PassportReader; - export default PassportReader; + export const PassportReader: PassportReader; + export function configure(token: string): void; + export function reset(): void; + export function scan(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; + }>; } diff --git a/app/src/utils/analytics.ts b/app/src/utils/analytics.ts index 85ebb2d16..239590c2d 100644 --- a/app/src/utils/analytics.ts +++ b/app/src/utils/analytics.ts @@ -2,6 +2,10 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +import { AppState, type AppStateStatus } from 'react-native'; +import { NativeModules } from 'react-native'; +import { ENABLE_DEBUG_LOGS, MIXPANEL_NFC_PROJECT_TOKEN } from '@env'; +import NetInfo from '@react-native-community/netinfo'; import type { JsonMap, JsonValue } from '@segment/analytics-react-native'; import { TrackEventParams } from '@selfxyz/mobile-sdk-alpha'; @@ -10,6 +14,15 @@ import { createSegmentClient } from '@/Segment'; const segmentClient = createSegmentClient(); +// --- Analytics flush strategy --- +let mixpanelConfigured = false; +let eventCount = 0; +let isConnected = true; +const eventQueue: Array<{ + name: string; + properties?: Record; +}> = []; + function coerceToJsonValue( value: unknown, seen = new WeakSet(), @@ -136,3 +149,101 @@ const analytics = () => { }; export default analytics; + +/** + * Cleanup function to clear event queues + */ +export const cleanupAnalytics = () => { + eventQueue.length = 0; + eventCount = 0; +}; + +const setupFlushPolicies = () => { + AppState.addEventListener('change', (state: AppStateStatus) => { + if (state === 'background' || state === 'active') { + flushMixpanelEvents(); + } + }); + + NetInfo.addEventListener(state => { + isConnected = state.isConnected ?? true; + if (isConnected) { + flushMixpanelEvents(); + } + }); +}; + +const flushMixpanelEvents = () => { + if (!MIXPANEL_NFC_PROJECT_TOKEN) return; + try { + if (__DEV__) console.log('[Mixpanel] flush'); + // Send any queued events before flushing + while (eventQueue.length > 0) { + const evt = eventQueue.shift()!; + NativeModules.PassportReader?.trackEvent?.(evt.name, evt.properties); + } + NativeModules.PassportReader?.flush?.(); + eventCount = 0; + } catch (err) { + if (__DEV__) console.warn('Mixpanel flush failed', err); + // re-queue on failure + if (typeof err !== 'undefined') { + // no-op, events are already queued if failure happened before flush + } + } +}; + +// --- Mixpanel NFC Analytics --- +export const configureNfcAnalytics = () => { + if (!MIXPANEL_NFC_PROJECT_TOKEN || mixpanelConfigured) return; + const enableDebugLogs = JSON.parse(String(ENABLE_DEBUG_LOGS)); + NativeModules.PassportReader.configure( + MIXPANEL_NFC_PROJECT_TOKEN, + enableDebugLogs, + { + flushInterval: 20, + flushCount: 5, + flushOnBackground: true, + flushOnForeground: true, + flushOnNetworkChange: true, + }, + ); + setupFlushPolicies(); + mixpanelConfigured = true; +}; + +/** + * Consolidated analytics flush function that flushes both Segment and Mixpanel events + * This should be called when you want to ensure all analytics events are sent immediately + */ +export const flushAllAnalytics = () => { + // Flush Segment analytics + const { flush: flushAnalytics } = analytics(); + flushAnalytics(); + + // Flush Mixpanel events + flushMixpanelEvents(); +}; + +export const trackNfcEvent = ( + name: string, + properties?: Record, +) => { + if (!MIXPANEL_NFC_PROJECT_TOKEN) return; + if (!mixpanelConfigured) configureNfcAnalytics(); + + if (!isConnected) { + eventQueue.push({ name, properties }); + return; + } + + try { + NativeModules.PassportReader?.trackEvent?.(name, properties); + eventCount++; + if (eventCount >= 5) { + flushMixpanelEvents(); + } + } catch (err) { + eventQueue.push({ name, properties }); + } +}; diff --git a/app/src/utils/nfcScanner.ts b/app/src/utils/nfcScanner.ts index 9c025b3f3..0ea3af4be 100644 --- a/app/src/utils/nfcScanner.ts +++ b/app/src/utils/nfcScanner.ts @@ -2,14 +2,14 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Buffer } from 'buffer'; import { NativeModules, Platform } from 'react-native'; -import PassportReader from 'react-native-passport-reader'; -import { ENABLE_DEBUG_LOGS, MIXPANEL_NFC_PROJECT_TOKEN } from '@env'; +import { reset, scan as scanDocument } from 'react-native-passport-reader'; import type { PassportData } from '@selfxyz/common/types'; +import { configureNfcAnalytics } from '@/utils/analytics'; + interface AndroidScanResponse { mrz: string; eContent: string; @@ -43,9 +43,17 @@ export const parseScanResponse = (response: unknown) => { : handleResponseIOS(response); }; +export const scan = async (inputs: Inputs) => { + configureNfcAnalytics(); + + return Platform.OS === 'android' + ? await scanAndroid(inputs) + : await scanIOS(inputs); +}; + const scanAndroid = async (inputs: Inputs) => { - PassportReader.reset(); - return await PassportReader.scan({ + reset(); + return await scanDocument({ documentNumber: inputs.passportNumber, dateOfBirth: inputs.dateOfBirth, dateOfExpiry: inputs.dateOfExpiry, @@ -55,7 +63,7 @@ const scanAndroid = async (inputs: Inputs) => { }; const scanIOS = async (inputs: Inputs) => { - return await NativeModules.PassportReader.scanPassport( + return await NativeModules.PassportReader.scanDocument( inputs.passportNumber, inputs.dateOfBirth, inputs.dateOfExpiry, @@ -68,23 +76,6 @@ const scanIOS = async (inputs: Inputs) => { ); }; -export const scan = async (inputs: Inputs) => { - if (MIXPANEL_NFC_PROJECT_TOKEN) { - if (Platform.OS === 'ios') { - const enableDebugLogs = JSON.parse(String(ENABLE_DEBUG_LOGS)); - NativeModules.PassportReader.configure( - MIXPANEL_NFC_PROJECT_TOKEN, - enableDebugLogs, - ); - } else { - } - } - - return Platform.OS === 'android' - ? await scanAndroid(inputs) - : await scanIOS(inputs); -}; - const handleResponseIOS = (response: unknown) => { const parsed = JSON.parse(String(response)); const dgHashesObj = JSON.parse(parsed?.dataGroupHashes); diff --git a/app/tests/src/components/ErrorBoundary.test.tsx b/app/tests/src/components/ErrorBoundary.test.tsx index 066d8d3f4..c6490ee49 100644 --- a/app/tests/src/components/ErrorBoundary.test.tsx +++ b/app/tests/src/components/ErrorBoundary.test.tsx @@ -7,11 +7,13 @@ import { Text } from 'react-native'; import { render } from '@testing-library/react-native'; const mockFlush = jest.fn(); -const mockAnalytics = jest.fn(() => ({ - flush: mockFlush, -})); +const mockTrackNfcEvent = jest.fn(); +const mockFlushAllAnalytics = jest.fn(); -jest.doMock('@/utils/analytics', () => mockAnalytics); +jest.doMock('@/utils/analytics', () => ({ + trackNfcEvent: mockTrackNfcEvent, + flushAllAnalytics: mockFlushAllAnalytics, +})); jest.mock('@/Sentry', () => ({ captureException: jest.fn(), })); @@ -81,7 +83,11 @@ describe('ErrorBoundary', () => { ); consoleError.mockRestore(); - expect(mockFlush).toHaveBeenCalled(); + expect(mockTrackNfcEvent).toHaveBeenCalledWith('error_boundary', { + message: 'boom', + stack: expect.any(String), + }); + expect(mockFlushAllAnalytics).toHaveBeenCalled(); }); it('renders children normally when no error occurs', () => { diff --git a/app/vite.config.ts b/app/vite.config.ts index ad7d8c80d..532dc9520 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -42,6 +42,10 @@ export default defineConfig({ __dirname, 'src/mocks/react-native-gesture-handler.ts', ), + 'react-native-passport-reader': path.resolve( + __dirname, + 'src/mocks/react-native-passport-reader.ts', + ), }, }, plugins: [ diff --git a/yarn.lock b/yarn.lock index 9eb4a1569..e11d3a6cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4833,9 +4833,9 @@ __metadata: languageName: node linkType: hard -"@segment/analytics-react-native@npm:^2.21.0": - version: 2.21.1 - resolution: "@segment/analytics-react-native@npm:2.21.1" +"@segment/analytics-react-native@npm:^2.21.2": + version: 2.21.2 + resolution: "@segment/analytics-react-native@npm:2.21.2" dependencies: "@segment/tsub": "npm:2.0.0" "@stdlib/number-float64-base-normalize": "npm:0.0.8" @@ -4851,7 +4851,7 @@ __metadata: peerDependenciesMeta: "@react-native-async-storage/async-storage": optional: true - checksum: 10c0/5d7696f0b295d13bbb9d786db7ecf9c1c679a51525bf3051aef1907708b2808e4627da378998900c870358d54cd7680ad03cbaff2786dcb5faef2c2dfdddd03d + checksum: 10c0/bc87ab176fa4280e200f396bc5ee70e8cb0c0fd484f293e13bd8c5f59d1d295e270c839f1a965da740af75a686bd7294392b57edd21502121816c74fa5905845 languageName: node linkType: hard @@ -5103,7 +5103,7 @@ __metadata: "@react-navigation/native": "npm:^7.0.14" "@react-navigation/native-stack": "npm:^7.2.0" "@robinbobin/react-native-google-drive-api-wrapper": "npm:^2.2.3" - "@segment/analytics-react-native": "npm:^2.21.0" + "@segment/analytics-react-native": "npm:^2.21.2" "@segment/sovran-react-native": "npm:^1.1.3" "@selfxyz/common": "workspace:^" "@selfxyz/mobile-sdk-alpha": "workspace:^"