mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 15:18:18 -05:00
Move self app store to mobile sdk (#1040)
This commit is contained in:
@@ -52,8 +52,8 @@
|
||||
"demo:ios": "yarn workspace demo-app ios",
|
||||
"demo:start": "yarn workspace demo-app start",
|
||||
"demo:test": "yarn workspace demo-app test",
|
||||
"fmt": "prettier --check .",
|
||||
"fmt:fix": "prettier --write .",
|
||||
"fmt": "yarn prettier --check .",
|
||||
"fmt:fix": "yarn prettier --write .",
|
||||
"format": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn nice; else yarn fmt:fix; fi'",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
@@ -70,6 +70,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
||||
@@ -63,6 +63,8 @@ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz';
|
||||
|
||||
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
|
||||
|
||||
export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs';
|
||||
|
||||
// Core functions
|
||||
export { isPassportDataValid } from './validation/document';
|
||||
|
||||
@@ -73,5 +75,4 @@ export { parseNFCResponse, scanNFC } from './nfc';
|
||||
export { reactNativeScannerAdapter } from './adapters/react-native/scanner';
|
||||
|
||||
export { scanQRProof } from './qr';
|
||||
|
||||
export { webScannerShim } from './adapters/web/shims';
|
||||
|
||||
@@ -98,6 +98,10 @@ export { formatDateToYYMMDD, scanMRZ } from './mrz';
|
||||
|
||||
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
|
||||
|
||||
export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs';
|
||||
|
||||
// Documents utils
|
||||
|
||||
// Core functions
|
||||
export { isPassportDataValid } from './validation/document';
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.
|
||||
|
||||
import type { DocumentCategory, PassportData } from '@selfxyz/common/types';
|
||||
import type { SelfApp } from '@selfxyz/common/utils';
|
||||
import { generateTEEInputsDiscloseStateless } from '@selfxyz/common/utils/circuits/registerInputs';
|
||||
|
||||
import { useProtocolStore } from '../stores/protocolStore';
|
||||
|
||||
export function generateTEEInputsDisclose(secret: string, passportData: PassportData, selfApp: SelfApp) {
|
||||
return generateTEEInputsDiscloseStateless(secret, passportData, selfApp, (document: DocumentCategory, tree) => {
|
||||
const protocolStore = useProtocolStore.getState();
|
||||
const docStore = (protocolStore as any)[document];
|
||||
if (!docStore) {
|
||||
throw new Error(`Unknown or unloaded document category in protocol store: ${document}`);
|
||||
}
|
||||
switch (tree) {
|
||||
case 'ofac':
|
||||
return docStore.ofac_trees;
|
||||
case 'commitment':
|
||||
if (!docStore.commitment_tree) {
|
||||
throw new Error('Commitment tree not loaded');
|
||||
}
|
||||
return docStore.commitment_tree;
|
||||
default:
|
||||
throw new Error('Unknown tree type');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3,3 +3,4 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export { useProtocolStore } from './protocolStore';
|
||||
export { useSelfAppStore } from './selfAppStore';
|
||||
|
||||
147
packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx
Normal file
147
packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
// 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.
|
||||
|
||||
import type { Socket } from 'socket.io-client';
|
||||
import socketIo from 'socket.io-client';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { WS_DB_RELAYER } from '@selfxyz/common/constants';
|
||||
import type { SelfApp } from '@selfxyz/common/utils/appType';
|
||||
|
||||
interface SelfAppState {
|
||||
selfApp: SelfApp | null;
|
||||
sessionId: string | null;
|
||||
socket: Socket | null;
|
||||
startAppListener: (sessionId: string) => void;
|
||||
cleanSelfApp: () => void;
|
||||
setSelfApp: (selfApp: SelfApp | null) => void;
|
||||
_initSocket: (sessionId: string) => Socket;
|
||||
handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => void;
|
||||
}
|
||||
|
||||
export const useSelfAppStore = create<SelfAppState>((set, get) => ({
|
||||
selfApp: null,
|
||||
sessionId: null,
|
||||
socket: null,
|
||||
|
||||
_initSocket: (sessionId: string): Socket => {
|
||||
const connectionUrl = WS_DB_RELAYER.startsWith('https') ? WS_DB_RELAYER.replace(/^https/, 'wss') : WS_DB_RELAYER;
|
||||
const socketUrl = `${connectionUrl}/websocket`;
|
||||
|
||||
// Create a new socket connection using the updated URL.
|
||||
const socket = socketIo(socketUrl, {
|
||||
path: '/',
|
||||
transports: ['websocket'],
|
||||
forceNew: true, // Ensure a new connection is established
|
||||
query: {
|
||||
sessionId,
|
||||
clientType: 'mobile',
|
||||
},
|
||||
});
|
||||
return socket;
|
||||
},
|
||||
|
||||
setSelfApp: (selfApp: SelfApp | null) => {
|
||||
set({ selfApp });
|
||||
},
|
||||
|
||||
startAppListener: (sessionId: string) => {
|
||||
const currentSocket = get().socket;
|
||||
|
||||
// If a socket connection exists for a different session, disconnect it.
|
||||
if (currentSocket && get().sessionId !== sessionId) {
|
||||
currentSocket.disconnect();
|
||||
set({ socket: null, sessionId: null, selfApp: null });
|
||||
} else if (currentSocket && get().sessionId === sessionId) {
|
||||
return; // Avoid reconnecting if already connected with the same session
|
||||
}
|
||||
|
||||
try {
|
||||
const socket = get()._initSocket(sessionId);
|
||||
set({ socket, sessionId });
|
||||
|
||||
socket.on('connect', () => {});
|
||||
|
||||
// Listen for the event only once per connection attempt
|
||||
socket.once('self_app', (data: unknown) => {
|
||||
try {
|
||||
const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : (data as SelfApp);
|
||||
|
||||
// Basic validation
|
||||
if (!appData || typeof appData !== 'object' || !appData.sessionId) {
|
||||
console.error('[SelfAppStore] Invalid app data received:', appData);
|
||||
// Optionally clear the app data or handle the error appropriately
|
||||
set({ selfApp: null });
|
||||
return;
|
||||
}
|
||||
if (appData.sessionId !== get().sessionId) {
|
||||
console.warn(
|
||||
`[SelfAppStore] Received SelfApp for session ${
|
||||
appData.sessionId
|
||||
}, but current session is ${get().sessionId}. Ignoring.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
set({ selfApp: appData });
|
||||
} catch (error) {
|
||||
console.error('[SelfAppStore] Error processing app data:', error);
|
||||
set({ selfApp: null }); // Clear app data on parsing error
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect_error', error => {
|
||||
console.error('[SelfAppStore] Mobile WS connection error:', error);
|
||||
// Clean up on connection error
|
||||
get().cleanSelfApp();
|
||||
});
|
||||
|
||||
socket.on('error', error => {
|
||||
console.error('[SelfAppStore] Mobile WS error:', error);
|
||||
// Consider if cleanup is needed here as well
|
||||
});
|
||||
|
||||
socket.on('disconnect', (_reason: string) => {
|
||||
// Prevent cleaning up if disconnect was initiated by cleanSelfApp
|
||||
if (get().socket === socket) {
|
||||
set({ socket: null, sessionId: null, selfApp: null });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[SelfAppStore] Exception in startAppListener:', error);
|
||||
get().cleanSelfApp(); // Clean up on exception
|
||||
}
|
||||
},
|
||||
|
||||
cleanSelfApp: () => {
|
||||
const socket = get().socket;
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
}
|
||||
// Reset state
|
||||
set({ selfApp: null, sessionId: null, socket: null });
|
||||
},
|
||||
|
||||
handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => {
|
||||
const socket = get().socket;
|
||||
const sessionId = get().sessionId;
|
||||
|
||||
if (!socket || !sessionId) {
|
||||
console.error('[SelfAppStore] Cannot handleProofResult: Socket or SessionId missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (proof_verified) {
|
||||
socket.emit('proof_verified', {
|
||||
session_id: sessionId,
|
||||
});
|
||||
} else {
|
||||
socket.emit('proof_generation_failed', {
|
||||
session_id: sessionId,
|
||||
error_code,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { PassportData, SelfApp } from '@selfxyz/common';
|
||||
|
||||
import { generateTEEInputsDisclose } from '../../src/processing/generate-disclosure-inputs';
|
||||
import { useProtocolStore } from '../../src/stores/protocolStore';
|
||||
// Mocks for dependencies
|
||||
const mockSecret = '0x' + '00'.repeat(30) + 'a4ec'; // 32-byte hex string
|
||||
const mockPassportData: PassportData = {
|
||||
mrz: 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<L898902C36UTO7408122F1204159ZE184226B<<<<',
|
||||
dsc: '',
|
||||
eContent: [],
|
||||
signedAttr: [],
|
||||
encryptedDigest: [],
|
||||
passportMetadata: {
|
||||
dataGroups: 'dg1',
|
||||
dg1Size: 100,
|
||||
dg1HashSize: 32,
|
||||
dg1HashFunction: 'sha256',
|
||||
dg1HashOffset: 0,
|
||||
dgPaddingBytes: 0,
|
||||
eContentSize: 100,
|
||||
eContentHashFunction: 'sha256',
|
||||
eContentHashOffset: 0,
|
||||
signedAttrSize: 100,
|
||||
signedAttrHashFunction: 'sha256',
|
||||
signatureAlgorithm: 'rsa',
|
||||
saltLength: 32,
|
||||
curveOrExponent: '65537',
|
||||
signatureAlgorithmBits: 0,
|
||||
countryCode: '',
|
||||
cscaFound: false,
|
||||
cscaHashFunction: '',
|
||||
cscaSignatureAlgorithm: '',
|
||||
cscaSaltLength: 0,
|
||||
cscaCurveOrExponent: '',
|
||||
cscaSignatureAlgorithmBits: 0,
|
||||
dsc: '',
|
||||
csca: '',
|
||||
},
|
||||
dsc_parsed: {
|
||||
tbsBytes: new Array(100).fill(1),
|
||||
signatureAlgorithm: 'rsa',
|
||||
publicKeyAlgorithm: 'rsa',
|
||||
publicKeyDetails: {
|
||||
modulus: '12345',
|
||||
exponent: '65537',
|
||||
},
|
||||
signature: new Array(100).fill(1),
|
||||
} as any,
|
||||
csca_parsed: {
|
||||
tbsBytes: new Array(100).fill(1),
|
||||
signatureAlgorithm: 'rsa',
|
||||
publicKeyAlgorithm: 'rsa',
|
||||
publicKeyDetails: {
|
||||
modulus: '12345',
|
||||
exponent: '65537',
|
||||
},
|
||||
signature: new Array(100).fill(1),
|
||||
} as any,
|
||||
documentType: 'passport',
|
||||
documentCategory: 'passport',
|
||||
mock: true,
|
||||
};
|
||||
const mockSelfApp: SelfApp = {
|
||||
userId: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
appName: 'TestSelfApp',
|
||||
logoBase64: '',
|
||||
endpointType: 'https',
|
||||
endpoint: 'https://test.example.com',
|
||||
deeplinkCallback: '',
|
||||
header: '',
|
||||
scope: 'test',
|
||||
sessionId: '',
|
||||
userIdType: 'hex',
|
||||
devMode: false,
|
||||
disclosures: {},
|
||||
version: 0,
|
||||
chainID: 42220,
|
||||
userDefinedData: '',
|
||||
};
|
||||
|
||||
vi.mock('../../src/stores/protocolStore', () => ({
|
||||
useProtocolStore: {
|
||||
getState: () => ({
|
||||
passport: {
|
||||
ofac_trees: {
|
||||
nameAndDob: '{"root":["0"]}',
|
||||
nameAndYob: '{"root":["0"]}',
|
||||
passportNoAndNationality: '{"root":["0"]}',
|
||||
},
|
||||
commitment_tree: '[[]]',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('generateTEEInputsDisclose', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
it('throws error for unknown document category', () => {
|
||||
// Mock the store to return an unknown document category
|
||||
vi.spyOn(useProtocolStore, 'getState').mockReturnValue({
|
||||
unknown: undefined,
|
||||
} as any);
|
||||
|
||||
expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError(
|
||||
`Unknown or unloaded document category in protocol store: passport`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for unknown tree type', () => {
|
||||
// This test doesn't make sense as written since tree type is determined internally
|
||||
// Let's test the commitment tree validation instead
|
||||
vi.spyOn(useProtocolStore, 'getState').mockReturnValue({
|
||||
passport: {
|
||||
ofac_trees: 'ofac-tree-data',
|
||||
commitment_tree: undefined,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError(
|
||||
`Invalid OFAC tree structure: missing required fields`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if commitment tree not loaded', () => {
|
||||
vi.spyOn(useProtocolStore, 'getState').mockReturnValue({
|
||||
passport: {
|
||||
ofac_trees: 'ofac-tree-data',
|
||||
commitment_tree: undefined,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError(
|
||||
`Invalid OFAC tree structure: missing required fields`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user