From 57acda6680371b06f8e109893f828752288783f8 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Mon, 12 Jan 2026 15:03:07 -0800 Subject: [PATCH 1/9] bump build (#1595) --- app/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version.json b/app/version.json index 1b9ef12ac..903c5458e 100644 --- a/app/version.json +++ b/app/version.json @@ -1,6 +1,6 @@ { "ios": { - "build": 203, + "build": 204, "lastDeployed": "2026-01-12T16:10:12.854Z" }, "android": { From 8f0696149dc26a933b6e9753ebff0b506cea29cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:34:07 -0800 Subject: [PATCH 2/9] chore: bump mobile app version to 2.9.10 (#1596) Update build numbers and deployment timestamps after successful deployment. Co-authored-by: github-actions[bot] --- app/version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.json b/app/version.json index 903c5458e..12d4ce934 100644 --- a/app/version.json +++ b/app/version.json @@ -1,7 +1,7 @@ { "ios": { - "build": 204, - "lastDeployed": "2026-01-12T16:10:12.854Z" + "build": 205, + "lastDeployed": "2026-01-12T23:27:08.229Z" }, "android": { "build": 134, From 2939926c825daca60160d05d5526cbfa792057f5 Mon Sep 17 00:00:00 2001 From: Leszek Stachowski Date: Tue, 13 Jan 2026 20:02:33 +0100 Subject: [PATCH 3/9] fix: register document from ManageDocuments screen (#1597) * fix: register document from ManageDocuments screen * coderabbit suggestion --- .../management/ManageDocumentsScreen.tsx | 156 +++++++++++++----- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/app/src/screens/documents/management/ManageDocumentsScreen.tsx b/app/src/screens/documents/management/ManageDocumentsScreen.tsx index 56785056f..260662c3a 100644 --- a/app/src/screens/documents/management/ManageDocumentsScreen.tsx +++ b/app/src/screens/documents/management/ManageDocumentsScreen.tsx @@ -8,7 +8,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Button, ScrollView, Spinner, Text, XStack, YStack } from 'tamagui'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { Check, Eraser } from '@tamagui/lucide-icons'; +import { Check, Eraser, HousePlus } from '@tamagui/lucide-icons'; import type { DocumentCatalog, @@ -33,6 +33,8 @@ import { usePassport } from '@/providers/passportDataProvider'; import { extraYPadding } from '@/utils/styleUtils'; const PassportDataSelector = () => { + const navigation = + useNavigation>(); const selfClient = useSelfClient(); const { loadDocumentCatalog, @@ -73,7 +75,20 @@ const PassportDataSelector = () => { loadPassportDataInfo(); }, [loadPassportDataInfo]); - const handleDocumentSelection = async (documentId: string) => { + const handleDocumentSelection = async ( + documentId: string, + isRegistered: boolean | undefined, + ) => { + if (!isRegistered) { + Alert.alert( + 'Document not registered', + 'This document cannot be selected as active, because it is not registered. Click the add button next to it to register it first.', + [{ text: 'OK', style: 'cancel' }], + ); + + return; + } + await setSelectedDocument(documentId); // Reload to update UI without loading state for quick operations const catalog = await loadDocumentCatalog(); @@ -90,24 +105,40 @@ const PassportDataSelector = () => { await loadPassportDataInfo(); }; - const handleDeleteButtonPress = (documentId: string) => { - Alert.alert( - '⚠️ Delete Document ⚠️', - 'Are you sure you want to delete this document?\n\nThis document is already linked to your identity in Self Protocol and cannot be linked by another person.', - [ - { - text: 'Cancel', - style: 'cancel', + const handleRegisterDocument = async (documentId: string) => { + try { + await setSelectedDocument(documentId); + navigation.navigate('ConfirmBelonging', {}); + } catch (error) { + Alert.alert( + 'Registration Error', + 'Failed to prepare document for registration. Please try again.', + [{ text: 'OK', style: 'cancel' }], + ); + } + }; + + const handleDeleteButtonPress = ( + documentId: string, + isRegistered: boolean | undefined, + ) => { + const message = isRegistered + ? 'Are you sure you want to delete this document?\n\nThis document is already linked to your identity in Self Protocol and cannot be linked by another person.' + : 'Are you sure you want to delete this document?'; + + Alert.alert('⚠️ Delete Document ⚠️', message, [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Delete', + style: 'destructive', + onPress: async () => { + await handleDeleteSpecific(documentId); }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - await handleDeleteSpecific(documentId); - }, - }, - ], - ); + }, + ]); }; const getDisplayName = (documentType: string): string => { @@ -156,6 +187,16 @@ const PassportDataSelector = () => { } }; + const getDocumentBackgroundColor = ( + isSelected: boolean, + isRegistered: boolean | undefined, + ): string => { + if (!isRegistered) { + return '#ffebee'; // Light red for unregistered documents + } + return isSelected ? '$gray2' : 'white'; + }; + if (loading) { return ( @@ -196,6 +237,10 @@ const PassportDataSelector = () => { ); } + const hasUnregisteredDocuments = documentCatalog.documents.some( + doc => !doc.isRegistered, + ); + return ( { > Available Documents + {hasUnregisteredDocuments && ( + + + ⚠️ We've detected some documents that are not registered. In order + to use them, you'll have to register them first by clicking the plus + icon next to them. + + + )} {documentCatalog.documents.map((metadata: DocumentMetadata) => ( { : borderColor } borderRadius="$3" - backgroundColor={ - documentCatalog.selectedDocumentId === metadata.id - ? '$gray2' - : 'white' + backgroundColor={getDocumentBackgroundColor( + documentCatalog.selectedDocumentId === metadata.id, + metadata.isRegistered, + )} + onPress={() => + handleDocumentSelection(metadata.id, metadata.isRegistered) } - onPress={() => handleDocumentSelection(metadata.id)} pressStyle={{ opacity: 0.8 }} > { } borderColor={textBlack} borderWidth={1} - onPress={() => handleDocumentSelection(metadata.id)} + onPress={() => + handleDocumentSelection(metadata.id, metadata.isRegistered) + } > {documentCatalog.selectedDocumentId === metadata.id && ( @@ -256,19 +319,36 @@ const PassportDataSelector = () => { - + + {metadata.isRegistered !== true && ( + + )} + + ))} From 27672e52f6b72b1d47724105d336b25877a49485 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 13 Jan 2026 11:16:17 -0800 Subject: [PATCH 4/9] Add document processor behavior tests (#1599) * Add document processor integration tests * fix types --- packages/mobile-sdk-alpha/package.json | 2 + .../provingMachine.documentProcessor.test.ts | 920 ++++++++++++++++++ yarn.lock | 2 + 3 files changed, 924 insertions(+) create mode 100644 packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 2262c3301..a5a77be84 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -162,6 +162,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@openpassport/zk-kit-lean-imt": "^0.0.6", "@testing-library/react": "^14.1.2", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", @@ -177,6 +178,7 @@ "eslint-plugin-sort-exports": "^0.9.1", "jsdom": "^25.0.1", "lottie-react-native": "7.2.2", + "poseidon-lite": "^0.3.0", "prettier": "^3.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts new file mode 100644 index 000000000..28b7b77d5 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/proving/provingMachine.documentProcessor.test.ts @@ -0,0 +1,920 @@ +// 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 { poseidon2 } from 'poseidon-lite'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { AadhaarData, PassportData } from '@selfxyz/common'; +import { + generateCommitment, + genMockIdDoc, + getCircuitNameFromPassportData, + getLeafDscTree, + isMRZDocument, +} from '@selfxyz/common/utils'; +import * as commonUtils from '@selfxyz/common/utils'; +import { generateCommitmentInAppAadhaar } from '@selfxyz/common/utils/passports/validate'; +import { AttestationIdHex } from '@selfxyz/common/utils/types'; + +import { PassportEvents, ProofEvents } from '../../src/constants/analytics'; +import * as documentUtils from '../../src/documents/utils'; +import { useProvingStore } from '../../src/proving/provingMachine'; +import { fetchAllTreesAndCircuits } from '../../src/stores'; +import type { SelfClient } from '../../src/types/public'; +import { actorMock } from './actorMock'; + +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; + +vi.mock('xstate', async () => { + const actual = await vi.importActual('xstate'); + return { + ...actual, + createActor: vi.fn(() => actorMock), + }; +}); + +vi.mock('../../src/documents/utils', async () => { + const actual = await vi.importActual('../../src/documents/utils'); + return { + ...actual, + loadSelectedDocument: vi.fn(), + storePassportData: vi.fn(), + clearPassportData: vi.fn(), + reStorePassportDataWithRightCSCA: vi.fn(), + markCurrentDocumentAsRegistered: vi.fn(), + }; +}); + +vi.mock('../../src/stores', async () => { + const actual = await vi.importActual('../../src/stores'); + return { + ...actual, + fetchAllTreesAndCircuits: vi.fn(), + }; +}); + +const createCommitmentTree = (commitments: string[]) => { + const tree = new LeanIMT((a, b) => poseidon2([a, b])); + if (commitments.length > 0) { + tree.insertMany(commitments.map(commitment => BigInt(commitment))); + } + return tree.export(); +}; + +const createDscTree = (leaves: string[]) => createCommitmentTree(leaves); + +const buildPassportFixture = (): PassportData => + ({ + mrz: 'P; + publicKeys?: string[]; +}) => ({ + passport: { + commitment_tree: commitmentTree, + dsc_tree: dscTree, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + alternative_csca: alternativeCsca ?? {}, + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, + id_card: { + commitment_tree: commitmentTree, + dsc_tree: dscTree, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + alternative_csca: alternativeCsca ?? {}, + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, + aadhaar: { + commitment_tree: commitmentTree, + dsc_tree: null, + csca_tree: null, + deployed_circuits: deployedCircuits, + circuits_dns_mapping: null, + public_keys: publicKeys ?? [], + ofac_trees: null, + fetch_all: vi.fn(), + fetch_deployed_circuits: vi.fn(), + fetch_circuits_dns_mapping: vi.fn(), + fetch_csca_tree: vi.fn(), + fetch_dsc_tree: vi.fn(), + fetch_identity_tree: vi.fn(), + fetch_alternative_csca: vi.fn(), + fetch_ofac_trees: vi.fn(), + }, +}); + +const createSelfClient = (protocolState: ReturnType) => + ({ + trackEvent: vi.fn(), + logProofEvent: vi.fn(), + emit: vi.fn(), + getPrivateKey: vi.fn().mockResolvedValue('123456789'), + getProvingState: () => useProvingStore.getState(), + getSelfAppState: () => ({ selfApp: null }), + getProtocolState: () => protocolState, + }) as unknown as SelfClient; + +describe('parseIDDocument', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const storePassportDataMock = vi.mocked(documentUtils.storePassportData); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('parses passport data successfully and updates state with parsed result', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const getSKIPEMSpy = vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + const state = useProvingStore.getState(); + expect(getSKIPEMSpy).toHaveBeenCalledWith('staging'); + expect(storePassportDataMock).toHaveBeenCalledWith(selfClient, state.passportData); + if (state.passportData && isMRZDocument(state.passportData)) { + expect(state.passportData.passportMetadata).toBeDefined(); + } + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' }); + if (state.passportData && isMRZDocument(state.passportData)) { + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.PASSPORT_PARSED, + expect.objectContaining({ + success: true, + country_code: state.passportData.passportMetadata?.countryCode, + }), + ); + } + }); + + it('handles missing passport data with PARSE_ERROR and analytics event', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, { + error: 'PassportData is not available', + }); + }); + + it('surfaces parsing failures when the DSC cannot be parsed', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc: 'invalid-certificate', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.PASSPORT_PARSE_FAILED, + expect.objectContaining({ + error: expect.stringMatching(/asn\\.1|parsing/i), + }), + ); + }); + + it('continues when DSC metadata cannot be read and logs empty dsc payload', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + let metadataProxy: PassportData['passportMetadata']; + Object.defineProperty(passportData, 'passportMetadata', { + get() { + return metadataProxy; + }, + set(value) { + metadataProxy = new Proxy(value, { + get(target, prop) { + if (prop === 'dsc') { + throw new Error('dsc parse failed'); + } + return target[prop as keyof typeof target]; + }, + }); + }, + configurable: true, + }); + + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + const parsedEvent = vi + .mocked(selfClient.trackEvent) + .mock.calls.find(([event]) => event === PassportEvents.PASSPORT_PARSED)?.[1]; + + expect(parsedEvent).toEqual( + expect.objectContaining({ + dsc: {}, + }), + ); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_SUCCESS' }); + }); + + it('emits PARSE_ERROR when storing parsed passport data fails', async () => { + const passportData = genMockIdDoc({ idType: 'mock_passport' }) as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + vi.spyOn(commonUtils, 'getSKIPEM').mockResolvedValue({}); + + storePassportDataMock.mockRejectedValue(new Error('storage unavailable')); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + await useProvingStore.getState().parseIDDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PARSE_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(PassportEvents.PASSPORT_PARSE_FAILED, { + error: 'storage unavailable', + }); + }); +}); + +describe('startFetchingData', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const fetchAllTreesMock = vi.mocked(fetchAllTreesAndCircuits); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('fetches trees and circuits for passport documents', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'passport', 'prod', 'KEY123'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_SUCCESS); + }); + + it('fetches trees and circuits for id cards', async () => { + const idCardData = { + ...(genMockIdDoc({ idType: 'mock_id_card' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'IDKEY' } as any, + documentCategory: 'id_card', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: idCardData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: idCardData, env: 'stg' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(fetchAllTreesMock).toHaveBeenCalledWith(selfClient, 'id_card', 'stg', 'IDKEY'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('fetches aadhaar protocol data via aadhaar fetcher', async () => { + const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: aadhaarData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(protocolState.aadhaar.fetch_all).toHaveBeenCalledWith('prod'); + expect(fetchAllTreesMock).not.toHaveBeenCalled(); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_SUCCESS' }); + }); + + it('emits FETCH_ERROR when passport data is missing', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: genMockIdDoc({ idType: 'mock_passport' }) } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'PassportData is not available', + }); + }); + + it('emits FETCH_ERROR when DSC data is missing for passports', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: undefined, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'stg' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'Missing parsed DSC in passport data', + }); + }); + + it('emits FETCH_ERROR when protocol fetch fails', async () => { + const passportData = { + ...(genMockIdDoc({ idType: 'mock_passport' }) as PassportData), + dsc_parsed: { authorityKeyIdentifier: 'KEY123' } as any, + documentCategory: 'passport', + } as PassportData; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + fetchAllTreesMock.mockRejectedValue(new Error('network down')); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, env: 'prod' }); + + await useProvingStore.getState().startFetchingData(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'FETCH_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.FETCH_DATA_FAILED, { + message: 'network down', + }); + }); +}); + +describe('validatingDocument', () => { + const loadSelectedDocumentMock = vi.mocked(documentUtils.loadSelectedDocument); + const clearPassportDataMock = vi.mocked(documentUtils.clearPassportData); + const reStorePassportDataWithRightCSCMock = vi.mocked(documentUtils.reStorePassportDataWithRightCSCA); + const markCurrentDocumentAsRegisteredMock = vi.mocked(documentUtils.markCurrentDocumentAsRegistered); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('clears data and emits PASSPORT_NOT_SUPPORTED when document is unsupported', async () => { + const passportData = buildPassportFixture(); + const unsupportedCircuits = { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }; + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: unsupportedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret: '123456789', circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(clearPassportDataMock).toHaveBeenCalledWith(selfClient); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_NOT_SUPPORTED' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith( + PassportEvents.COMING_SOON, + expect.objectContaining({ status: 'registration_circuit_not_supported' }), + ); + }); + + it('validates disclose when the user is registered', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const commitment = generateCommitment(secret, AttestationIdHex.passport, passportData); + const commitmentTree = createCommitmentTree([commitment]); + + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'disclose'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'disclose' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_SUCCESS); + }); + + it('emits PASSPORT_DATA_NOT_FOUND when disclose document is not registered', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const commitmentTree = createCommitmentTree([]); + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + await useProvingStore.getState().init(selfClient, 'disclose'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'disclose' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'PASSPORT_DATA_NOT_FOUND' }); + }); + + it('restores data when aadhaar is already registered with alternative keys', async () => { + const aadhaarData = genMockIdDoc({ idType: 'mock_aadhaar' }) as AadhaarData; + const secret = '123456789'; + const { commitment_list: commitmentList } = generateCommitmentInAppAadhaar( + secret, + AttestationIdHex.aadhaar, + aadhaarData, + { + public_key_0: aadhaarData.publicKey, + }, + ); + const commitmentTree = createCommitmentTree([commitmentList[0]]); + const deployedCircuits = { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree, + dscTree: null, + deployedCircuits, + publicKeys: [aadhaarData.publicKey], + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: aadhaarData } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: aadhaarData, secret, circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(reStorePassportDataWithRightCSCMock).toHaveBeenCalledWith(selfClient, aadhaarData, aadhaarData.publicKey); + expect(markCurrentDocumentAsRegisteredMock).toHaveBeenCalledWith(selfClient); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ALREADY_REGISTERED' }); + }); + + it('routes to account recovery when nullifier is on chain', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + + const protocolState = buildProtocolState({ + commitmentTree: createCommitmentTree([]), + dscTree: null, + deployedCircuits, + alternativeCsca: {}, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ data: true }), + } as Response), + ); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'register' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'ACCOUNT_RECOVERY_CHOICE' }); + + globalThis.fetch = originalFetch; + }); + + it('switches to register circuit when DSC is already in the tree', async () => { + const passportData = buildPassportFixture(); + const secret = '123456789'; + const registerCircuit = getCircuitNameFromPassportData(passportData, 'register'); + const dscCircuit = getCircuitNameFromPassportData(passportData, 'dsc'); + const deployedCircuits = { + REGISTER: [registerCircuit], + REGISTER_ID: [], + REGISTER_AADHAAR: ['register_aadhaar'], + DSC: [dscCircuit], + DSC_ID: [], + }; + const dscLeaf = getLeafDscTree(passportData.dsc_parsed!, passportData.csca_parsed!); + const dscTree = createDscTree([dscLeaf]); + + const protocolState = buildProtocolState({ + commitmentTree: createCommitmentTree([]), + dscTree, + deployedCircuits, + alternativeCsca: {}, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: passportData } as any); + + const originalFetch = globalThis.fetch; + globalThis.fetch = vi.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ data: false }), + } as Response), + ); + + await useProvingStore.getState().init(selfClient, 'dsc'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData, secret, circuitType: 'dsc' }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(useProvingStore.getState().circuitType).toBe('register'); + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_SUCCESS' }); + + globalThis.fetch = originalFetch; + }); + + it('emits VALIDATION_ERROR when validation throws', async () => { + const protocolState = buildProtocolState({ + commitmentTree: null, + dscTree: null, + deployedCircuits: { + REGISTER: [], + REGISTER_ID: [], + REGISTER_AADHAAR: [], + DSC: [], + DSC_ID: [], + }, + }); + const selfClient = createSelfClient(protocolState); + + loadSelectedDocumentMock.mockResolvedValue({ data: buildPassportFixture() } as any); + + await useProvingStore.getState().init(selfClient, 'register'); + actorMock.send.mockClear(); + vi.mocked(selfClient.trackEvent).mockClear(); + + useProvingStore.setState({ passportData: null }); + + await useProvingStore.getState().validatingDocument(selfClient); + + expect(actorMock.send).toHaveBeenCalledWith({ type: 'VALIDATION_ERROR' }); + expect(selfClient.trackEvent).toHaveBeenCalledWith(ProofEvents.VALIDATION_FAILED, { + message: 'PassportData is not available', + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index d9e270a75..85f78edea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8700,6 +8700,7 @@ __metadata: resolution: "@selfxyz/mobile-sdk-alpha@workspace:packages/mobile-sdk-alpha" dependencies: "@babel/runtime": "npm:^7.28.3" + "@openpassport/zk-kit-lean-imt": "npm:^0.0.6" "@selfxyz/common": "workspace:^" "@selfxyz/euclid": "npm:^0.6.1" "@testing-library/react": "npm:^14.1.2" @@ -8719,6 +8720,7 @@ __metadata: jsdom: "npm:^25.0.1" lottie-react-native: "npm:7.2.2" node-forge: "npm:^1.3.1" + poseidon-lite: "npm:^0.3.0" prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" From bcbb8affd4eb27b0e315de88ab45ebcb4deb5c11 Mon Sep 17 00:00:00 2001 From: "Seshanth.S" <35675963+seshanthS@users.noreply.github.com> Date: Wed, 14 Jan 2026 03:11:48 +0530 Subject: [PATCH 5/9] SELF-1768: fix deeplink navigation (#1598) * fix deeplink navigation * fix tests * fix typing --------- Co-authored-by: Justin Hernandez --- app/src/navigation/deeplinks.ts | 46 ++++++++++++++++------ app/tests/src/navigation/deeplinks.test.ts | 12 ++++-- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/app/src/navigation/deeplinks.ts b/app/src/navigation/deeplinks.ts index af4cd79ce..b2d803644 100644 --- a/app/src/navigation/deeplinks.ts +++ b/app/src/navigation/deeplinks.ts @@ -9,6 +9,7 @@ import { countries } from '@selfxyz/common/constants/countries'; import type { IdDocInput } from '@selfxyz/common/utils'; import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; +import type { RootStackParamList } from '@/navigation'; import { navigationRef } from '@/navigation'; import useUserStore from '@/stores/userStore'; import { IS_DEV_MODE } from '@/utils/devUtils'; @@ -108,6 +109,28 @@ export const getAndClearQueuedUrl = (): string | null => { return url; }; +const safeNavigate = ( + navigationState: ReturnType, +): void => { + const targetScreen = navigationState.routes[1]?.name as + | keyof RootStackParamList + | undefined; + + const currentRoute = navigationRef.getCurrentRoute(); + const isColdLaunch = currentRoute?.name === 'Splash'; + + if (!isColdLaunch && targetScreen) { + // Use object syntax to satisfy TypeScript's strict typing for navigate + // The params will be undefined for screens that don't require them + navigationRef.navigate({ + name: targetScreen, + params: undefined, + } as Parameters[0]); + } else { + navigationRef.reset(navigationState); + } +}; + export const handleUrl = (selfClient: SelfClient, uri: string) => { const validatedParams = parseAndValidateUrlParams(uri); const { @@ -125,7 +148,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { selfClient.getSelfAppState().setSelfApp(selfAppJson); selfClient.getSelfAppState().startAppListener(selfAppJson.sessionId); - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState( 'ProvingScreenRouter', correctParentScreen, @@ -137,7 +160,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { if (IS_DEV_MODE) { console.error('Error parsing selfApp:', error); } - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen), ); } @@ -145,7 +168,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { selfClient.getSelfAppState().cleanSelfApp(); selfClient.getSelfAppState().startAppListener(sessionId); - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState('ProvingScreenRouter', correctParentScreen), ); } else if (mock_passport) { @@ -175,25 +198,26 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { }); // Reset navigation stack with correct parent -> MockDataDeepLink - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState('MockDataDeepLink', correctParentScreen), ); } catch (error) { if (IS_DEV_MODE) { console.error('Error parsing mock_passport data or navigating:', error); } - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen), ); } } else if (referrer && typeof referrer === 'string') { useUserStore.getState().setDeepLinkReferrer(referrer); - // Navigate to HomeScreen - it will show confirmation modal and then navigate to GratificationScreen - navigationRef.reset({ - index: 0, - routes: [{ name: 'Home' }], - }); + const currentRoute = navigationRef.getCurrentRoute(); + if (currentRoute?.name === 'Home') { + // Already on Home, no navigation needed - the modal will show automatically + } else { + safeNavigate(createDeeplinkNavigationState('Home', 'Home')); + } } else if (Platform.OS === 'web') { // TODO: web handle links if we need to idk if we do // For web, we can handle the URL some other way if we dont do this loading app in web always navigates to QRCodeTrouble @@ -211,7 +235,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { 'No sessionId, selfApp or valid OAuth parameters found in the data', ); } - navigationRef.reset( + safeNavigate( createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen), ); } diff --git a/app/tests/src/navigation/deeplinks.test.ts b/app/tests/src/navigation/deeplinks.test.ts index 7ce0027e1..b73f87b6f 100644 --- a/app/tests/src/navigation/deeplinks.test.ts +++ b/app/tests/src/navigation/deeplinks.test.ts @@ -36,6 +36,7 @@ jest.mock('@/navigation', () => ({ navigate: jest.fn(), isReady: jest.fn(() => true), reset: jest.fn(), + getCurrentRoute: jest.fn(), }, })); @@ -66,6 +67,10 @@ describe('deeplinks', () => { setDeepLinkUserDetails, }); mockPlatform.OS = 'ios'; + + // Setup default getCurrentRoute mock to return Splash (cold launch scenario) + const { navigationRef } = require('@/navigation'); + navigationRef.getCurrentRoute.mockReturnValue({ name: 'Splash' }); }); describe('handleUrl', () => { @@ -156,9 +161,10 @@ describe('deeplinks', () => { const { navigationRef } = require('@/navigation'); // Should navigate to HomeScreen, which will show confirmation modal + // During cold launch (Splash screen), reset is called with full navigation state expect(navigationRef.reset).toHaveBeenCalledWith({ - index: 0, - routes: [{ name: 'Home' }], + index: 1, + routes: [{ name: 'Home' }, { name: 'Home' }], }); }); @@ -598,7 +604,7 @@ describe('deeplinks', () => { mockLinking.getInitialURL.mockResolvedValue(undefined as any); mockLinking.addEventListener.mockReturnValue({ remove }); - const cleanup = setupUniversalLinkListenerInNavigation(); + const cleanup = setupUniversalLinkListenerInNavigation({} as SelfClient); expect(mockLinking.addEventListener).toHaveBeenCalled(); cleanup(); expect(remove).toHaveBeenCalled(); From 2c0a03ac4b73d7d6e98606f8e45764678d6d24b4 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 13 Jan 2026 13:51:01 -0800 Subject: [PATCH 6/9] update text (#1600) --- .../account/settings/ShowRecoveryPhraseScreen.tsx | 6 ++---- .../screens/onboarding/SaveRecoveryPhraseScreen.tsx | 4 ++-- app/src/utils/crypto/mnemonic.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx b/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx index 54b6adef8..33a0ae2bf 100644 --- a/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx +++ b/app/src/screens/account/settings/ShowRecoveryPhraseScreen.tsx @@ -15,6 +15,7 @@ import Mnemonic from '@/components/Mnemonic'; import useMnemonic from '@/hooks/useMnemonic'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { useSettingStore } from '@/stores/settingStore'; +import { getRecoveryPhraseWarningMessage } from '@/utils/crypto/mnemonic'; import { IS_EUCLID_ENABLED } from '@/utils/devUtils'; function useCopyRecoveryPhrase(mnemonic: string[] | undefined) { @@ -89,10 +90,7 @@ const ShowRecoveryPhraseScreen: React.FC & { gap={20} > - - This phrase is the only way to recover your account. Keep it secret, - keep it safe. - + {getRecoveryPhraseWarningMessage()} ); diff --git a/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx b/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx index de62cca5e..6a732bb45 100644 --- a/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx +++ b/app/src/screens/onboarding/SaveRecoveryPhraseScreen.tsx @@ -23,6 +23,7 @@ import useMnemonic from '@/hooks/useMnemonic'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { STORAGE_NAME } from '@/services/cloud-backup'; import { useSettingStore } from '@/stores/settingStore'; +import { getRecoveryPhraseWarningMessage } from '@/utils/crypto/mnemonic'; const SaveRecoveryPhraseScreen: React.FC = () => { const [userHasSeenMnemonic, setUserHasSeenMnemonic] = useState(false); @@ -55,8 +56,7 @@ const SaveRecoveryPhraseScreen: React.FC = () => { Save your recovery phrase - This phrase is the only way to recover your account. Keep it secret, - keep it safe. + {getRecoveryPhraseWarningMessage()} Date: Tue, 13 Jan 2026 14:08:50 -0800 Subject: [PATCH 7/9] chore: bump version to 2.9.11 (#1601) * chore: bump version to 2.9.11 * fix linting --- app/android/app/build.gradle | 2 +- app/ios/OpenPassport/Info.plist | 2 +- app/ios/Self.xcodeproj/project.pbxproj | 4 ++-- app/package.json | 2 +- .../screens/documents/management/ManageDocumentsScreen.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index f1c4e75c6..5ccd0a476 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -135,7 +135,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 121 - versionName "2.9.10" + versionName "2.9.11" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { cmake { diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist index 0aee45559..cb975feea 100644 --- a/app/ios/OpenPassport/Info.plist +++ b/app/ios/OpenPassport/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.9.10 + 2.9.11 CFBundleSignature ???? CFBundleURLTypes diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 6ee7b5d90..1917c7aac 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -546,7 +546,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.10; + MARKETING_VERSION = 2.9.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -686,7 +686,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.10; + MARKETING_VERSION = 2.9.11; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/app/package.json b/app/package.json index 524d5c329..e1f3bdc3e 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@selfxyz/mobile-app", - "version": "2.9.10", + "version": "2.9.11", "private": true, "type": "module", "scripts": { diff --git a/app/src/screens/documents/management/ManageDocumentsScreen.tsx b/app/src/screens/documents/management/ManageDocumentsScreen.tsx index 260662c3a..f1aef3eb1 100644 --- a/app/src/screens/documents/management/ManageDocumentsScreen.tsx +++ b/app/src/screens/documents/management/ManageDocumentsScreen.tsx @@ -109,7 +109,7 @@ const PassportDataSelector = () => { try { await setSelectedDocument(documentId); navigation.navigate('ConfirmBelonging', {}); - } catch (error) { + } catch { Alert.alert( 'Registration Error', 'Failed to prepare document for registration. Please try again.', From ac4921a458df925925f791337165248f86031cbf Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 13 Jan 2026 14:31:47 -0800 Subject: [PATCH 8/9] Require real document for settings options (#1603) --- app/src/screens/account/settings/SettingsScreen.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/app/src/screens/account/settings/SettingsScreen.tsx b/app/src/screens/account/settings/SettingsScreen.tsx index f9ea00ce2..cb53ccce3 100644 --- a/app/src/screens/account/settings/SettingsScreen.tsx +++ b/app/src/screens/account/settings/SettingsScreen.tsx @@ -187,22 +187,16 @@ const SettingsScreen: React.FC = () => { const screenRoutes = useMemo(() => { const baseRoutes = isDevMode ? [...routes, ...DEBUG_MENU] : routes; - - // Show all routes while loading or if user has a real document - if (hasRealDocument === null || hasRealDocument === true) { - return baseRoutes; - } - const shouldHideCloudBackup = Platform.OS === 'android'; + const hasConfirmedRealDocument = hasRealDocument === true; - // Only filter out document-related routes if we've confirmed user has no real documents return baseRoutes.filter(([, , route]) => { if (DOCUMENT_DEPENDENT_ROUTES.includes(route)) { - return false; + return hasConfirmedRealDocument; } if (shouldHideCloudBackup && route === CLOUD_BACKUP_ROUTE) { - return false; + return hasConfirmedRealDocument; } return true; From 19735fb02b99a5cd21ed85f199e2a56f326408cd Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 13 Jan 2026 14:32:22 -0800 Subject: [PATCH 9/9] clean up dev settings (#1604) --- app/src/screens/dev/DevSettingsScreen.tsx | 192 ++++++++++++---------- 1 file changed, 108 insertions(+), 84 deletions(-) diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index d40e48c4f..8eba89e13 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -33,7 +33,6 @@ import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks'; import BugIcon from '@/assets/icons/bug_icon.svg'; -import IdIcon from '@/assets/icons/id_icon.svg'; import WarningIcon from '@/assets/icons/warning.svg'; import type { RootStackParamList } from '@/navigation'; import { navigationScreens } from '@/navigation'; @@ -287,6 +286,110 @@ const ScreenSelector = ({}) => { ); }; +const LogLevelSelector = ({ + currentLevel, + onSelect, +}: { + currentLevel: string; + onSelect: (level: 'debug' | 'info' | 'warn' | 'error') => void; +}) => { + const [open, setOpen] = useState(false); + + const logLevels = ['debug', 'info', 'warn', 'error'] as const; + + return ( + <> + + + + + + + + + Select log level + + + + + {logLevels.map(level => ( + { + setOpen(false); + onSelect(level); + }} + > + + + {level.toUpperCase()} + + {currentLevel === level && ( + + )} + + + ))} + + + + + + ); +}; + const DevSettingsScreen: React.FC = ({}) => { const { clearDocumentCatalogForMigrationTesting } = usePassport(); const clearPointEvents = usePointEventStore(state => state.clearEvents); @@ -547,57 +650,6 @@ const DevSettingsScreen: React.FC = ({}) => { paddingTop="$4" paddingBottom={paddingBottom} > - } - title="Manage ID Documents" - description="Register new IDs and generate test IDs" - > - {[ - { - label: 'Manage available IDs', - onPress: () => { - navigation.navigate('ManageDocuments'); - }, - }, - { - label: 'Generate Test ID', - onPress: () => { - navigation.navigate('CreateMock'); - }, - }, - { - label: 'Scan new ID Document', - onPress: () => { - navigation.navigate('DocumentOnboarding'); - }, - }, - ].map(({ label, onPress }) => ( - - - - ))} - - } title="Debug Shortcuts" @@ -696,38 +748,10 @@ const DevSettingsScreen: React.FC = ({}) => { title="Log Level" description="Configure logging verbosity" > - - {(['debug', 'info', 'warn', 'error'] as const).map(level => ( - - ))} - +