Mobile SDK: move provingMachine from the app (#1052)

* Mobile SDK: move provingMachine from the app

* lint, fixes

* fix web build?

* lint

* fix metro build, add deps

* update lock files

* move the status handlers and proving machine tests

* may it be

* fix up

* yolo

---------

Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
This commit is contained in:
Leszek Stachowski
2025-09-17 15:48:36 +02:00
committed by GitHub
parent 30cc43e242
commit 8983ac2268
40 changed files with 1125 additions and 1158 deletions

View File

@@ -15,13 +15,11 @@ import {
wrap,
} from '@sentry/react-native';
interface BaseContext {
sessionId: string;
userId?: string;
platform: 'ios' | 'android';
stage: string;
}
import type {
BaseContext,
NFCScanContext,
ProofContext,
} from '@selfxyz/mobile-sdk-alpha';
// Security: Whitelist of allowed tag keys to prevent XSS
const ALLOWED_TAG_KEYS = new Set([
'session_id',
@@ -89,15 +87,6 @@ const sanitizeTagKey = (key: string): string | null => {
return key;
};
export interface NFCScanContext extends BaseContext, Record<string, unknown> {
scanType: 'mrz' | 'can';
}
export interface ProofContext extends BaseContext, Record<string, unknown> {
circuitType: 'register' | 'dsc' | 'disclose' | null;
currentState: string;
}
export const captureException = (
error: Error,
context?: Record<string, unknown>,

View File

@@ -19,6 +19,7 @@ import {
import { navigationRef } from '@/navigation';
import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider';
import { logNFCEvent, logProofEvent } from '@/Sentry';
import analytics from '@/utils/analytics';
type GlobalCrypto = { crypto?: { subtle?: Crypto['subtle'] } };
@@ -94,6 +95,15 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
auth: {
getPrivateKey: () => unsafe_getPrivateKey(),
},
notification: {
registerDeviceToken: async (sessionId, deviceToken, isMock) => {
// Forward to our app-level function which handles staging vs production
// and also fetches the token if not provided
const { registerDeviceToken: registerFirebaseDeviceToken } =
await import('@/utils/notifications/notificationService');
return registerFirebaseDeviceToken(sessionId, deviceToken, isMock);
},
},
}),
[],
);
@@ -148,6 +158,16 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
}
});
addListener(SdkEvents.PROOF_EVENT, ({ level, context, event, details }) => {
// Log proof events for monitoring/debugging
logProofEvent(level, event, context, details);
});
addListener(SdkEvents.NFC_EVENT, ({ level, context, event, details }) => {
// Log nfc events for monitoring/debugging
logNFCEvent(level, event, context, details);
});
return map;
}, []);

View File

@@ -8,7 +8,7 @@ import { ActivityIndicator, View } from 'react-native';
import type { StaticScreenProps } from '@react-navigation/native';
import { usePreventRemove } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { useProvingStore, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
PassportEvents,
ProofEvents,
@@ -28,7 +28,6 @@ import {
getFCMToken,
requestNotificationPermission,
} from '@/utils/notifications/notificationService';
import { useProvingStore } from '@/utils/proving/provingMachine';
type ConfirmBelongingScreenProps = StaticScreenProps<Record<string, never>>;

View File

@@ -9,7 +9,7 @@ import { SystemBars } from 'react-native-edge-to-edge';
import { ScrollView, Spinner } from 'tamagui';
import { useIsFocused } from '@react-navigation/native';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { useProvingStore, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores';
@@ -31,7 +31,6 @@ import {
notificationError,
notificationSuccess,
} from '@/utils/haptic';
import { useProvingStore } from '@/utils/proving/provingMachine';
const SuccessScreen: React.FC = () => {
const { trackEvent } = useSelfClient();

View File

@@ -22,7 +22,7 @@ import { Eye, EyeOff } from '@tamagui/lucide-icons';
import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils/appType';
import { formatEndpoint } from '@selfxyz/common/utils/scope';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { useProvingStore, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores';
@@ -41,7 +41,6 @@ import { useProofHistoryStore } from '@/stores/proofHistoryStore';
import { black, slate300, white } from '@/utils/colors';
import { formatUserId } from '@/utils/formatUserId';
import { buttonTap } from '@/utils/haptic';
import { useProvingStore } from '@/utils/proving/provingMachine';
const ProveScreen: React.FC = () => {
const selfClient = useSelfClient();

View File

@@ -11,6 +11,10 @@ import type { StaticScreenProps } from '@react-navigation/native';
import { useIsFocused } from '@react-navigation/native';
import type { PassportData } from '@selfxyz/common/types';
import {
type ProvingStateType,
useProvingStore,
} from '@selfxyz/mobile-sdk-alpha';
import failAnimation from '@/assets/animations/loading/fail.json';
import proveLoadingAnimation from '@/assets/animations/loading/prove.json';
@@ -23,8 +27,6 @@ import { advercase, dinot } from '@/utils/fonts';
import { loadingScreenProgress } from '@/utils/haptic';
import { setupNotifications } from '@/utils/notifications/notificationService';
import { getLoadingScreenText } from '@/utils/proving/loadingScreenStateText';
import type { ProvingStateType } from '@/utils/proving/provingMachine';
import { useProvingStore } from '@/utils/proving/provingMachine';
type LoadingScreenProps = StaticScreenProps<Record<string, never>>;

View File

@@ -13,7 +13,8 @@ export const loadCryptoUtils = async () => {
export const loadProvingUtils = async () => {
return Promise.all([
import('@/utils/proving/provingMachine'),
// TODO: can it be safely removed?
// import('@/utils/proving/provingMachine'),
import('@/utils/proving/validateDocument'),
]);
};

View File

@@ -6,8 +6,9 @@ import { Buffer } from 'buffer';
import { Platform } from 'react-native';
import type { PassportData } from '@selfxyz/common/types';
import type { NFCScanContext } from '@selfxyz/mobile-sdk-alpha';
import { logNFCEvent, type NFCScanContext } from '@/Sentry';
import { logNFCEvent } from '@/Sentry';
import { configureNfcAnalytics } from '@/utils/analytics';
import {
PassportReader,

View File

@@ -5,7 +5,7 @@
// Only export what's actually used elsewhere to enable proper tree shaking
// From provingMachine - used in screens and tests
export type { ProvingStateType } from '@/utils/proving/provingMachine';
export type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha';
// From provingUtils - used in tests (keeping these for testing purposes)
export {
encryptAES256GCM,
@@ -16,4 +16,4 @@ export {
// From loadingScreenStateText - used in loading screen
export { getLoadingScreenText } from '@/utils/proving/loadingScreenStateText';
export { useProvingStore } from '@/utils/proving/provingMachine';
export { useProvingStore } from '@selfxyz/mobile-sdk-alpha';

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import type { ProvingStateType } from '@/utils/proving/provingMachine';
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha';
interface LoadingScreenText {
actionText: string;

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +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.
export interface StatusHandlerResult {
shouldDisconnect: boolean;
stateUpdate?: {
error_code?: string;
reason?: string;
socketConnection?: null;
};
actorEvent?: {
type: 'PROVE_FAILURE' | 'PROVE_SUCCESS';
};
analytics?: Array<{
event: string;
data?: Record<string, unknown>;
}>;
}
/**
* Pure functions for handling Socket.IO status messages
* These can be tested independently without mocking complex dependencies
*/
export interface StatusMessage {
status: number;
error_code?: string;
reason?: string;
}
/**
* Determine actions to take based on status code
*/
export function handleStatusCode(
data: StatusMessage,
circuitType: string,
): StatusHandlerResult {
const result: StatusHandlerResult = {
shouldDisconnect: false,
analytics: [],
};
// Failure statuses (3 or 5)
if (data.status === 3 || data.status === 5) {
result.shouldDisconnect = true;
result.stateUpdate = {
error_code: data.error_code,
reason: data.reason,
socketConnection: null,
};
result.actorEvent = { type: 'PROVE_FAILURE' };
result.analytics = [
{
event: 'SOCKETIO_PROOF_FAILURE',
data: {
error_code: data.error_code,
reason: data.reason,
},
},
];
return result;
}
// Success status (4)
if (data.status === 4) {
result.shouldDisconnect = true;
result.stateUpdate = {
socketConnection: null,
};
result.actorEvent = { type: 'PROVE_SUCCESS' };
result.analytics = [
{
event: 'SOCKETIO_PROOF_SUCCESS',
},
];
// Additional tracking for register circuit
if (circuitType === 'register') {
result.analytics.push({
event: 'REGISTER_COMPLETED',
});
}
return result;
}
// Other statuses - no action needed
return result;
}
/**
* Parse incoming socket message into structured data
*/
export function parseStatusMessage(message: unknown): StatusMessage {
if (typeof message === 'string') {
try {
return JSON.parse(message) as StatusMessage;
} catch {
throw new Error('Invalid JSON message received');
}
}
if (typeof message === 'object' && message !== null) {
return message as StatusMessage;
}
throw new Error('Invalid message format');
}