mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 22:58:20 -05:00
feat: improve mixpanel flush strategy (#960)
* feat: improve mixpanel flush strategy * fixes * fix build * update lock * refactor methods * conslidate calls * update package and lock
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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<Props, State> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
13
app/src/mocks/react-native-passport-reader.ts
Normal file
13
app/src/mocks/react-native-passport-reader.ts
Normal file
@@ -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');
|
||||
};
|
||||
@@ -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<PassportNFCScanRoute>();
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ConfirmBelongingScreenProps> = () => {
|
||||
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<ConfirmBelongingScreenProps> = () => {
|
||||
trackEvent(ProofEvents.PROVING_PROCESS_ERROR, {
|
||||
error: message,
|
||||
});
|
||||
trackNfcEvent(ProofEvents.PROVING_PROCESS_ERROR, {
|
||||
error: message,
|
||||
});
|
||||
|
||||
flushAllAnalytics();
|
||||
} finally {
|
||||
setRequestingPermission(false);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
35
app/src/types/react-native-passport-reader.d.ts
vendored
35
app/src/types/react-native-passport-reader.d.ts
vendored
@@ -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;
|
||||
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;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
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<string, unknown>,
|
||||
) => {
|
||||
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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
10
yarn.lock
10
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:^"
|
||||
|
||||
Reference in New Issue
Block a user