Fix mobile demo app document registration (#1182)

* Enable WebSocket connections in demo client

* save working keychain

* save wip

* save polish tweaks

* downgrade react-native-svg

* abstract components

* onSuccess alert displays only once

* sort by registered first

* add clear all documents button

* formatting and typing

* refresh register document screen after successful registration

* fix double tap on register

* coderabbit feedback

* lock NFCPassportReader to commit

* remove react native picker

* remove lock

* minor fixes
This commit is contained in:
Justin Hernandez
2025-10-02 18:27:11 -07:00
committed by GitHub
parent f2cceb3150
commit 318b83fa57
21 changed files with 741 additions and 269 deletions

View File

@@ -43,6 +43,7 @@ export interface DocumentMetadata {
data: string; // DG1/MRZ data for passports/IDs, relevant data for aadhaar
mock: boolean; // whether this is a mock document
isRegistered?: boolean; // whether the document is registered onChain
registeredAt?: number; // timestamp (epoch ms) when document was registered
}
export type DocumentType =

View File

@@ -13,10 +13,12 @@ import {
brutforceSignatureAlgorithmDsc,
calculateContentHash,
inferDocumentCategory,
isAadhaarDocument,
isMRZDocument,
parseCertificateSimple,
} from '@selfxyz/common';
import { extractNameFromMRZ } from '../processing/mrz';
import { SelfClient } from '../types/public';
export async function clearPassportData(selfClient: SelfClient) {
@@ -35,6 +37,53 @@ export async function clearPassportData(selfClient: SelfClient) {
await selfClient.saveDocumentCatalog({ documents: [] });
}
/**
* Extract name from a document by loading its full data.
* Works for both MRZ documents (passport/ID card) and Aadhaar documents.
*
* @param selfClient - The SelfClient instance
* @param documentId - The document ID to extract name from
* @returns Object with firstName and lastName, or null if extraction fails
*/
export async function extractNameFromDocument(
selfClient: SelfClient,
documentId: string,
): Promise<{ firstName: string; lastName: string } | null> {
try {
const document = await selfClient.loadDocumentById(documentId);
if (!document) {
return null;
}
// For Aadhaar documents, extract name from extractedFields
if (isAadhaarDocument(document)) {
const name = document.extractedFields?.name;
if (name && typeof name === 'string') {
// Aadhaar name is typically "FIRSTNAME LASTNAME" format
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
const firstName = parts[0];
const lastName = parts.slice(1).join(' ');
return { firstName, lastName };
} else if (parts.length === 1) {
return { firstName: parts[0], lastName: '' };
}
}
return null;
}
// For MRZ documents (passport/ID card), extract from MRZ string
if (isMRZDocument(document)) {
return extractNameFromMRZ(document.mrz);
}
return null;
} catch (error) {
console.error('Error extracting name from document:', error);
return null;
}
}
/**
* Gets all documents from the document catalog.
*
@@ -219,6 +268,14 @@ export async function updateDocumentRegistrationState(
if (documentIndex !== -1) {
catalog.documents[documentIndex].isRegistered = isRegistered;
// Set registration timestamp when marking as registered
if (isRegistered) {
catalog.documents[documentIndex].registeredAt = Date.now();
} else {
// Clear timestamp when unregistering
catalog.documents[documentIndex].registeredAt = undefined;
}
await selfClient.saveDocumentCatalog(catalog);
console.log(`Updated registration state for document ${documentId}: ${isRegistered}`);

View File

@@ -90,6 +90,9 @@ export { defaultConfig } from './config/defaults';
export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD, scanMRZ } from './mrz';
// Document utils
export { extractNameFromDocument } from './documents/utils';
export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator';
// Core functions

View File

@@ -276,10 +276,16 @@ export function extractNameFromMRZ(mrzString: string): { firstName: string; last
.filter(Boolean);
// Handle single-line MRZ strings (common for stored data)
// TD3 format: 88 or 90 characters total (2 lines of 44 or 45 chars each)
if (lines.length === 1) {
const mrzLength = lines[0].length;
if (mrzLength === 88 || mrzLength === 90) {
// TD1 format (ID card): 90 characters = 3 lines × 30 chars
// Detect TD1 by checking if it starts with 'I' (ID card) or 'A' (type A) or 'C' (type C)
if (mrzLength === 90 && /^[IAC][<A-Z]/.test(lines[0])) {
lines = [lines[0].slice(0, 30), lines[0].slice(30, 60), lines[0].slice(60, 90)];
}
// TD3 format (passport): 88 chars (2×44) or 90 chars (2×45)
else if (mrzLength === 88 || mrzLength === 90) {
const lineLength = mrzLength === 88 ? 44 : 45;
lines = [lines[0].slice(0, lineLength), lines[0].slice(lineLength)];
}
@@ -294,7 +300,7 @@ export function extractNameFromMRZ(mrzString: string): { firstName: string; last
// TD3 typically has 2 lines, first line is usually 44 chars but we'll be lenient
if (lines.length === 2) {
const line1 = lines[0];
const nameMatch = line1.match(/^P<[A-Z]{3}(.+)$/);
const nameMatch = line1.match(/^[IPO]<[A-Z]{3}(.+)$/);
if (nameMatch) {
const namePart = nameMatch[1];

View File

@@ -19,6 +19,7 @@ export interface DocumentMetadata {
encryptedBlobRef?: string; // opaque pointer; no plaintext PII
mock: boolean;
isRegistered?: boolean;
registeredAt?: number; // timestamp (epoch ms) when document was registered
}
export interface DocumentData {

View File

@@ -26,7 +26,7 @@ target "SelfDemoApp" do
)
# Use the custom NFCPassportReader fork
pod "NFCPassportReader", :git => "git@github.com:selfxyz/NFCPassportReader.git"
pod "NFCPassportReader", :git => "git@github.com:selfxyz/NFCPassportReader.git", :commit => "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
post_install do |installer|
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202

View File

@@ -1659,7 +1659,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNCPicker (2.11.2):
- RNKeychain (10.0.0):
- DoubleConversion
- glog
- hermes-engine
@@ -1680,7 +1680,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.13.0):
- RNSVG (15.12.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1700,9 +1700,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.13.0)
- RNSVG/common (= 15.12.1)
- Yoga
- RNSVG/common (15.13.0):
- RNSVG/common (15.12.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1756,7 +1756,7 @@ DEPENDENCIES:
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- "mobile-sdk-alpha (from `../node_modules/@selfxyz/mobile-sdk-alpha`)"
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`)"
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@@ -1818,7 +1818,7 @@ DEPENDENCIES:
- ReactCodegen (from `build/generated/ios`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -1849,6 +1849,7 @@ EXTERNAL SOURCES:
mobile-sdk-alpha:
:path: "../node_modules/@selfxyz/mobile-sdk-alpha"
NFCPassportReader:
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
:git: "git@github.com:selfxyz/NFCPassportReader.git"
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
@@ -1968,8 +1969,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCPicker:
:path: "../node_modules/@react-native-picker/picker"
RNKeychain:
:path: "../node_modules/react-native-keychain"
RNSVG:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
@@ -1979,7 +1980,7 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
NFCPassportReader:
:commit: 04ede227cbfd377e2b4bc9b38f9a89eebdcab52f
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
:git: "git@github.com:selfxyz/NFCPassportReader.git"
SPEC CHECKSUMS:
@@ -2054,12 +2055,12 @@ SPEC CHECKSUMS:
ReactCodegen: f853a20cc9125c5521c8766b4b49375fec20648b
ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9
RNCAsyncStorage: 87a74d13ba0128f853817e45e21c4051e1f2cd45
RNCPicker: 31b0c81be6b949dbd8d0c8802e9c6b9615de880a
RNSVG: c22ddda11213ee91192ab2f70b50c78a8bbc30d8
RNKeychain: 850638785745df5f70c37251130617a66ec82102
RNSVG: 8dd938fb169dd81009b74c2334780d7d2a04a373
RNVectorIcons: c95fdae217b0ed388f2b4d7ed7a4edc457c1df47
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
PODFILE CHECKSUM: 22f8edb659097ec6a47366d55dcd021f5b88ccdb
PODFILE CHECKSUM: 7bafdc4607a2a09088e9b68be33648f72b535141
COCOAPODS: 1.16.2

View File

@@ -30,7 +30,6 @@
"@faker-js/faker": "^10.0.0",
"@noble/hashes": "^1.5.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.1",
"@react-native/gradle-plugin": "0.76.9",
"@selfxyz/common": "workspace:*",
"@selfxyz/mobile-sdk-alpha": "workspace:*",
@@ -44,7 +43,7 @@
"react-native-get-random-values": "^1.11.0",
"react-native-keychain": "^10.0.0",
"react-native-safe-area-context": "^5.6.1",
"react-native-svg": "^15.13.0",
"react-native-svg": "15.12.1",
"react-native-vector-icons": "^10.3.0",
"stream-browserify": "^3.0.0",
"util": "^0.12.5"

View File

@@ -0,0 +1,46 @@
// 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 React from 'react';
import { countryCodes } from '@selfxyz/common';
import { signatureAlgorithmToStrictSignatureAlgorithm } from '@selfxyz/mobile-sdk-alpha';
import { PickerField } from './PickerField';
const algorithmOptions = Object.keys(signatureAlgorithmToStrictSignatureAlgorithm);
const countryOptions = Object.keys(countryCodes);
export function AlgorithmCountryFields({
show,
algorithm,
setAlgorithm,
country,
setCountry,
}: {
show: boolean;
algorithm: string;
setAlgorithm: (value: string) => void;
country: string;
setCountry: (value: string) => void;
}) {
if (!show) return null;
return (
<>
<PickerField
label="Algorithm"
selectedValue={algorithm}
onValueChange={setAlgorithm}
items={algorithmOptions.map(alg => ({ label: alg, value: alg }))}
/>
<PickerField
label="Country"
selectedValue={country}
onValueChange={setCountry}
items={countryOptions.map(code => ({
label: `${code} - ${countryCodes[code as keyof typeof countryCodes]}`,
value: code,
}))}
/>
</>
);
}

View File

@@ -0,0 +1,43 @@
// 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 React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { SimplePicker } from './SimplePicker';
import type { PickerItem } from './SimplePicker';
export { type PickerItem };
export function PickerField({
label,
selectedValue,
onValueChange,
items,
enabled = true,
}: {
label: string;
selectedValue: string;
onValueChange: (value: string) => void;
items: PickerItem[];
enabled?: boolean;
}) {
return (
<View style={styles.inputContainer}>
<Text style={styles.label}>{label}</Text>
<SimplePicker enabled={enabled} selectedValue={selectedValue} onValueChange={onValueChange} items={items} />
</View>
);
}
const styles = StyleSheet.create({
inputContainer: {
marginBottom: 10,
},
label: {
marginBottom: 4,
fontWeight: '600',
color: '#333',
fontSize: 14,
},
});

View File

@@ -13,12 +13,13 @@ type Props = {
onBack: () => void;
children: React.ReactNode;
contentStyle?: ViewStyle;
rightAction?: React.ReactNode;
};
export default function ScreenLayout({ title, onBack, children, contentStyle }: Props) {
export default function ScreenLayout({ title, onBack, children, contentStyle, rightAction }: Props) {
return (
<SafeAreaScrollView contentContainerStyle={styles.container} backgroundColor="#fafbfc">
<StandardHeader title={title} onBack={onBack} />
<StandardHeader title={title} onBack={onBack} rightAction={rightAction} />
<View style={[styles.content, contentStyle]}>{children}</View>
</SafeAreaScrollView>
);

View File

@@ -0,0 +1,117 @@
// 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 React, { useState } from 'react';
import { FlatList, Modal, Pressable, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
export type PickerItem = { label: string; value: string };
type SimplePickerProps = {
items: PickerItem[];
selectedValue: string;
onValueChange: (value: string) => void;
enabled?: boolean;
};
export function SimplePicker({ items, selectedValue, onValueChange, enabled = true }: SimplePickerProps) {
const [modalVisible, setModalVisible] = useState(false);
const selectedItem = items.find(item => item.value === selectedValue);
const handleSelect = (value: string) => {
onValueChange(value);
setModalVisible(false);
};
return (
<>
<Pressable
onPress={() => enabled && setModalVisible(true)}
style={[styles.pickerPressable, !enabled && styles.disabled]}
>
<Text style={styles.pickerText}>{selectedItem?.label || 'Select...'}</Text>
<Icon name="chevron-down-outline" size={20} color="#000" />
</Pressable>
<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}
>
<SafeAreaView style={styles.modalContainer}>
<View style={styles.modalContent}>
<FlatList
data={items}
keyExtractor={item => item.value}
renderItem={({ item }) => (
<TouchableOpacity style={styles.optionItem} onPress={() => handleSelect(item.value)}>
<Text style={styles.optionText}>{item.label}</Text>
{item.value === selectedValue && <Icon name="checkmark-outline" size={24} color="#007AFF" />}
</TouchableOpacity>
)}
/>
<TouchableOpacity style={styles.closeButton} onPress={() => setModalVisible(false)}>
<Text style={styles.closeButtonText}>Close</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</Modal>
</>
);
}
const styles = StyleSheet.create({
pickerPressable: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 6,
backgroundColor: '#fff',
paddingHorizontal: 12,
height: 44,
},
pickerText: {
color: '#000',
fontSize: 14,
},
disabled: {
backgroundColor: '#f0f0f0',
},
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalContent: {
backgroundColor: '#fff',
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
maxHeight: '50%',
},
optionItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
optionText: {
fontSize: 16,
},
closeButton: {
padding: 15,
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: '#eee',
},
closeButtonText: {
fontSize: 16,
color: '#007AFF',
fontWeight: '600',
},
});

View File

@@ -9,15 +9,19 @@ import Icon from 'react-native-vector-icons/Ionicons';
type Props = {
title: string;
onBack: () => void;
rightAction?: React.ReactNode;
};
export default function StandardHeader({ title, onBack }: Props) {
export default function StandardHeader({ title, onBack, rightAction }: Props) {
return (
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={onBack}>
<Icon name="chevron-back" size={20} color="#0550ae" />
<Text style={styles.backButtonText}>Back</Text>
</TouchableOpacity>
<View style={styles.topRow}>
<TouchableOpacity style={styles.backButton} onPress={onBack}>
<Icon name="chevron-back" size={20} color="#0550ae" />
<Text style={styles.backButtonText}>Back</Text>
</TouchableOpacity>
{rightAction}
</View>
<Text style={styles.title}>{title}</Text>
</View>
);
@@ -27,13 +31,18 @@ const styles = StyleSheet.create({
header: {
marginBottom: 16,
},
topRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'flex-start',
paddingVertical: 6,
paddingHorizontal: 12,
marginBottom: 8,
marginLeft: -12,
},
backButtonText: {

View File

@@ -4,7 +4,7 @@
import { useCallback, useEffect, useState } from 'react';
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import { getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { updateAfterDelete } from '../lib/catalog';
@@ -20,13 +20,24 @@ export function useDocuments() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const [clearing, setClearing] = useState(false);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const all = await getAllDocuments(selfClient);
setDocuments(Object.values(all));
const sortedDocuments = Object.values(all).sort((a, b) => {
// Registered documents first
if (a.metadata.isRegistered && !b.metadata.isRegistered) {
return -1;
}
if (!a.metadata.isRegistered && b.metadata.isRegistered) {
return 1;
}
return 0;
});
setDocuments(sortedDocuments);
} catch (err) {
setDocuments([]);
setError(err instanceof Error ? err.message : String(err));
@@ -55,5 +66,42 @@ export function useDocuments() {
[selfClient, refresh],
);
return { documents, loading, error, deleting, refresh, deleteDocument } as const;
const clearAllDocuments = useCallback(async () => {
setClearing(true);
setError(null);
let originalCatalog: DocumentCatalog | null = null;
try {
// Read and persist the existing catalog.
originalCatalog = await selfClient.loadDocumentCatalog();
const docIds = originalCatalog.documents.map(d => d.id);
// Write an empty catalog to atomically remove references.
const emptyCatalog = {
documents: [],
selectedDocumentId: undefined,
};
await selfClient.saveDocumentCatalog(emptyCatalog);
try {
// Then perform deletions of document ids from storage.
for (const docId of docIds) {
await selfClient.deleteDocument(docId);
}
} catch (deletionError) {
// If any deletion fails, restore the previous catalog and re-throw.
if (originalCatalog) {
await selfClient.saveDocumentCatalog(originalCatalog);
}
throw deletionError; // Re-throw to be caught by the outer catch block.
}
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setClearing(false);
}
}, [selfClient, refresh]);
return { documents, loading, error, deleting, clearing, refresh, deleteDocument, clearAllDocuments } as const;
}

View File

@@ -23,6 +23,7 @@ export function useRegistration() {
const init = useProvingStore(state => state.init);
const setUserConfirmed = useProvingStore(state => state.setUserConfirmed);
const autoConfirmTimer = useRef<NodeJS.Timeout>();
const onCompleteRef = useRef<null | (() => void)>(null);
const [registering, setRegistering] = useState(false);
const [statusMessage, setStatusMessage] = useState('');
@@ -45,6 +46,24 @@ export function useRegistration() {
return () => unsubscribe();
}, [selfClient, registering, addLog]);
// Also listen for explicit SDK success event as a reliable completion signal
useEffect(() => {
if (!registering) return;
const unsubscribe = selfClient.on(SdkEvents.PROVING_ACCOUNT_VERIFIED_SUCCESS, () => {
setStatusMessage('🎉 Registration completed successfully!');
addLog('Document registered on-chain! (event)', 'info');
if (onCompleteRef.current) {
try {
onCompleteRef.current();
} finally {
onCompleteRef.current = null;
}
}
setRegistering(false);
});
return () => unsubscribe();
}, [selfClient, registering, addLog]);
useEffect(() => {
if (!registering) return;
switch (currentState) {
@@ -88,6 +107,13 @@ export function useRegistration() {
setStatusMessage('🎉 Registration completed successfully!');
addLog('Document registered on-chain!', 'info');
setRegistering(false);
if (onCompleteRef.current) {
try {
onCompleteRef.current();
} finally {
onCompleteRef.current = null; // ensure one-shot
}
}
break;
case 'error':
case 'failure':
@@ -122,12 +148,18 @@ export function useRegistration() {
state: { registering, statusMessage, currentState, logs, showLogs },
actions: {
start,
setOnComplete: (cb: (() => void) | null) => {
onCompleteRef.current = cb;
},
toggleLogs: () => setShowLogs(s => !s),
reset: () => {
setRegistering(false);
setStatusMessage('');
setLogs([]);
setShowLogs(false);
onCompleteRef.current = null;
// Reset the SDK's proving store state to prevent stale 'completed' state
useProvingStore.setState({ currentState: 'idle' });
},
},
} as const;

View File

@@ -29,19 +29,84 @@ const createFetch = () => {
return (input: RequestInfo | URL, init?: RequestInit) => fetchImpl(input, init);
};
const createWsAdapter = () => ({
connect: (_url: string): WsConn => {
const createWsAdapter = () => {
const WebSocketImpl = globalThis.WebSocket;
if (!WebSocketImpl) {
return {
send: () => {
throw new Error('WebSocket send is not implemented in the demo environment.');
connect: () => {
throw new Error('WebSocket is not available in this environment. Provide a WebSocket implementation.');
},
close: () => {},
onMessage: () => {},
onError: () => {},
onClose: () => {},
};
},
});
}
return {
connect: (url: string, opts?: { signal?: AbortSignal; headers?: Record<string, string> }): WsConn => {
const socket = new WebSocketImpl(url);
let abortHandler: (() => void) | null = null;
if (opts?.signal) {
abortHandler = () => {
socket.close();
};
if (typeof opts.signal.addEventListener === 'function') {
opts.signal.addEventListener('abort', abortHandler, { once: true });
}
}
const attach = (event: 'message' | 'error' | 'close', handler: (payload?: any) => void) => {
// Clean up abort listener when socket closes
if (event === 'close' && abortHandler && opts?.signal) {
const originalHandler = handler;
handler = (payload?: any) => {
if (typeof opts.signal!.removeEventListener === 'function') {
opts.signal!.removeEventListener('abort', abortHandler!);
}
originalHandler(payload);
};
}
if (typeof socket.addEventListener === 'function') {
if (event === 'message') {
(socket.addEventListener as any)('message', handler as any);
} else if (event === 'error') {
(socket.addEventListener as any)('error', handler as any);
} else {
(socket.addEventListener as any)('close', handler as any);
}
} else {
if (event === 'message') {
(socket as any).onmessage = handler;
} else if (event === 'error') {
(socket as any).onerror = handler;
} else {
(socket as any).onclose = handler;
}
}
};
return {
send: (data: string | ArrayBufferView | ArrayBuffer) => socket.send(data),
close: () => socket.close(),
onMessage: cb => {
attach('message', event => {
// React Native emits { data }, whereas browsers emit MessageEvent.
const payload = (event as { data?: unknown }).data ?? event;
cb(payload);
});
},
onError: cb => {
attach('error', error => cb(error));
},
onClose: cb => {
attach('close', () => cb());
},
};
},
};
};
const hash = (data: Uint8Array): Uint8Array => sha256(data);
@@ -74,7 +139,9 @@ export function SelfClientProvider({ children }: PropsWithChildren) {
auth: {
async getPrivateKey(): Promise<string | null> {
try {
return await getOrCreateSecret();
const secret = await getOrCreateSecret();
// Ensure the secret is 0x-prefixed for components expecting hex strings
return secret.startsWith('0x') ? secret : `0x${secret}`;
} catch (error) {
console.error('Failed to get/create secret:', error);
return null;

View File

@@ -2,11 +2,11 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
// no direct SDK calls here
import { extractNameFromDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import ScreenLayout from '../components/ScreenLayout';
import { formatDataPreview, humanizeDocumentType, maskId } from '../utils/document';
@@ -22,13 +22,63 @@ type Props = {
// helpers moved to utils/document
export default function DocumentsList({ onBack, catalog }: Props) {
const { documents, loading, error, deleting, deleteDocument, refresh } = useDocuments();
const selfClient = useSelfClient();
const { documents, loading, error, deleting, deleteDocument, refresh, clearing, clearAllDocuments } = useDocuments();
const [documentNames, setDocumentNames] = useState<Record<string, { firstName: string; lastName: string }>>({});
// Refresh when catalog selection changes (e.g., after generation or external updates)
useEffect(() => {
refresh();
}, [catalog.selectedDocumentId, refresh]);
// Load names for all documents
useEffect(() => {
let cancelled = false;
const loadDocumentNames = async () => {
const names: Record<string, { firstName: string; lastName: string }> = {};
await Promise.all(
documents.map(async doc => {
const name = await extractNameFromDocument(selfClient, doc.metadata.id);
if (name) {
names[doc.metadata.id] = name;
}
}),
);
if (!cancelled) {
setDocumentNames(names);
}
};
if (documents.length === 0) {
setDocumentNames({});
return;
}
loadDocumentNames();
return () => {
cancelled = true;
};
}, [documents, selfClient]);
const handleClearAll = () => {
Alert.alert('Clear All Documents', 'Are you sure you want to delete all documents? This action cannot be undone.', [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Clear All',
style: 'destructive',
onPress: async () => {
try {
await clearAllDocuments();
} catch (err) {
Alert.alert('Error', `Failed to clear documents: ${err instanceof Error ? err.message : String(err)}`);
}
},
},
]);
};
const handleDelete = async (documentId: string, documentType: string) => {
Alert.alert('Delete Document', `Are you sure you want to delete this ${humanizeDocumentType(documentType)}?`, [
{ text: 'Cancel', style: 'cancel' },
@@ -68,10 +118,9 @@ export default function DocumentsList({ onBack, catalog }: Props) {
if (documents.length === 0) {
return (
<View style={styles.emptyState}>
<Text style={styles.emptyText}>No documents yet</Text>
<Text style={styles.emptyText}>No documents</Text>
<Text style={styles.emptySubtext}>
Generate a mock document to see it appear here. The demo document store keeps everything locally on your
device.
Generate a mock document or scan a real document to see it appear here.
</Text>
</View>
);
@@ -83,11 +132,16 @@ export default function DocumentsList({ onBack, catalog }: Props) {
const preview = formatDataPreview(metadata);
const documentId = maskId(metadata.id);
const isDeleting = deleting === metadata.id;
const nameData = documentNames[metadata.id];
const fullName = nameData ? `${nameData.firstName} ${nameData.lastName}`.trim() : null;
return (
<View key={metadata.id} style={styles.documentCard}>
<View style={styles.documentHeader}>
<Text style={styles.documentType}>{humanizeDocumentType(metadata.documentType)}</Text>
<View style={styles.documentTitleContainer}>
<Text style={styles.documentType}>{humanizeDocumentType(metadata.documentType)}</Text>
{fullName && <Text style={styles.documentName}>{fullName}</Text>}
</View>
<View style={styles.headerRight}>
<View style={[styles.statusBadge, badgeStyle]}>
<Text style={styles.statusText}>{statusLabel}</Text>
@@ -105,7 +159,6 @@ export default function DocumentsList({ onBack, catalog }: Props) {
</TouchableOpacity>
</View>
</View>
<Text style={styles.documentMeta}>{(metadata.documentCategory ?? 'unknown').toUpperCase()}</Text>
<Text style={styles.documentMeta}>{metadata.mock ? 'Mock data' : 'Live data'}</Text>
<Text style={styles.documentPreview} selectable>
{preview}
@@ -115,10 +168,24 @@ export default function DocumentsList({ onBack, catalog }: Props) {
</View>
);
});
}, [documents, error, loading, deleting]);
}, [documents, error, loading, deleting, documentNames]);
const clearButton = (
<TouchableOpacity
style={[styles.clearButton, (clearing || documents.length === 0) && styles.disabledButton]}
onPress={handleClearAll}
disabled={clearing || documents.length === 0}
>
{clearing ? (
<ActivityIndicator size="small" color="#dc3545" />
) : (
<Text style={styles.clearButtonText}>Clear All</Text>
)}
</TouchableOpacity>
);
return (
<ScreenLayout title="My Documents" onBack={onBack}>
<ScreenLayout title="My Documents" onBack={onBack} rightAction={clearButton}>
{content}
</ScreenLayout>
);
@@ -131,6 +198,33 @@ const styles = StyleSheet.create({
paddingHorizontal: 24,
paddingVertical: 20,
},
headerContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
marginBottom: 10,
},
clearButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
backgroundColor: '#ffeef0',
borderWidth: 1,
borderColor: '#dc3545',
alignItems: 'center',
justifyContent: 'center',
minHeight: 30,
minWidth: 80,
alignSelf: 'flex-end',
},
clearButtonText: {
color: '#dc3545',
fontWeight: '600',
fontSize: 14,
},
disabledButton: {
backgroundColor: '#f8f9fa',
borderColor: '#e1e5e9',
},
content: {
flex: 1,
},
@@ -153,11 +247,20 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
marginBottom: 8,
},
documentTitleContainer: {
flex: 1,
marginRight: 12,
},
documentType: {
fontSize: 18,
fontWeight: '600',
color: '#333',
flex: 1,
},
documentName: {
fontSize: 15,
fontWeight: '500',
color: '#0550ae',
marginTop: 2,
},
headerRight: {
alignItems: 'flex-end',

View File

@@ -3,25 +3,20 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useState } from 'react';
import { ActivityIndicator, Button, Platform, StyleSheet, Switch, Text, TextInput, View } from 'react-native';
import { ActivityIndicator, Alert, Button, Platform, StyleSheet, Switch, Text, TextInput, View } from 'react-native';
import { faker } from '@faker-js/faker';
import { calculateContentHash, countryCodes, inferDocumentCategory, isMRZDocument } from '@selfxyz/common';
import { calculateContentHash, inferDocumentCategory, isMRZDocument } from '@selfxyz/common';
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import {
generateMockDocument,
signatureAlgorithmToStrictSignatureAlgorithm,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import { generateMockDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { Picker } from '@react-native-picker/picker';
import Icon from 'react-native-vector-icons/Ionicons';
import SafeAreaScrollView from '../components/SafeAreaScrollView';
import StandardHeader from '../components/StandardHeader';
import { AlgorithmCountryFields } from '../components/AlgorithmCountryFields';
import { PickerField } from '../components/PickerField';
const algorithmOptions = Object.keys(signatureAlgorithmToStrictSignatureAlgorithm);
const documentTypeOptions = ['mock_passport', 'mock_id_card', 'mock_aadhaar'] as const;
const countryOptions = Object.keys(countryCodes);
const documentTypePickerItems = documentTypeOptions.map(dt => ({ label: dt, value: dt }));
const defaultAge = '21';
const defaultExpiryYears = '5';
@@ -68,6 +63,9 @@ export default function GenerateMock({ onDocumentStored, onNavigate, onBack }: P
const handleGenerate = async () => {
setLoading(true);
setError(null);
// Force React to render the loading state before starting async work
await new Promise(resolve => setTimeout(resolve, 0));
const startTime = Date.now();
try {
const ageNum = Number(age);
const expiryNum = Number(expiryYears);
@@ -111,8 +109,23 @@ export default function GenerateMock({ onDocumentStored, onNavigate, onBack }: P
catalog.selectedDocumentId = documentId;
await selfClient.saveDocumentCatalog(catalog);
await onDocumentStored?.();
// Auto-navigate to register screen after successful generation
onNavigate('register');
// Refresh first and last name with new random values after successful generation
setFirstName(getRandomFirstName());
setLastName(getRandomLastName());
// Ensure minimum loading display time (500ms) for better UX
const elapsed = Date.now() - startTime;
if (elapsed < 500) {
await new Promise(resolve => setTimeout(resolve, 500 - elapsed));
}
// Auto-navigate to register screen only if it's the first document
if (catalog.documents.length === 1) {
onNavigate('register');
} else {
Alert.alert('Success', 'Mock document generated successfully.');
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
@@ -161,65 +174,19 @@ export default function GenerateMock({ onDocumentStored, onNavigate, onBack }: P
ios_backgroundColor="#d1d5db"
/>
</View>
{documentType !== 'mock_aadhaar' && (
<>
<View style={styles.inputContainer}>
<Text style={styles.label}>Algorithm</Text>
<View style={styles.pickerContainer}>
<Picker
selectedValue={algorithm}
onValueChange={(itemValue: string) => setAlgorithm(itemValue)}
style={styles.picker}
>
{algorithmOptions.map(alg => (
<Picker.Item label={alg} value={alg} key={alg} />
))}
</Picker>
{Platform.OS === 'ios' && (
<Icon name="chevron-down-outline" size={20} color="#000" style={styles.pickerIcon} />
)}
</View>
</View>
<View style={styles.inputContainer}>
<Text style={styles.label}>Country</Text>
<View style={styles.pickerContainer}>
<Picker
selectedValue={country}
onValueChange={(itemValue: string) => setCountry(itemValue)}
style={styles.picker}
>
{countryOptions.map(code => (
<Picker.Item
label={`${code} - ${countryCodes[code as keyof typeof countryCodes]}`}
value={code}
key={code}
/>
))}
</Picker>
{Platform.OS === 'ios' && (
<Icon name="chevron-down-outline" size={20} color="#000" style={styles.pickerIcon} />
)}
</View>
</View>
</>
)}
<View style={styles.inputContainer}>
<Text style={styles.label}>Document Type</Text>
<View style={styles.pickerContainer}>
<Picker
selectedValue={documentType}
onValueChange={(itemValue: string) => setDocumentType(itemValue as (typeof documentTypeOptions)[number])}
style={styles.picker}
>
{documentTypeOptions.map(dt => (
<Picker.Item label={dt} value={dt} key={dt} />
))}
</Picker>
{Platform.OS === 'ios' && (
<Icon name="chevron-down-outline" size={20} color="#000" style={styles.pickerIcon} />
)}
</View>
</View>
<AlgorithmCountryFields
show={documentType !== 'mock_aadhaar'}
algorithm={algorithm}
setAlgorithm={setAlgorithm}
country={country}
setCountry={setCountry}
/>
<PickerField
label="Document Type"
selectedValue={documentType}
onValueChange={(itemValue: string) => setDocumentType(itemValue as (typeof documentTypeOptions)[number])}
items={documentTypePickerItems}
/>
<View style={styles.buttonRow}>
<View style={styles.buttonWrapper}>
<Button title="Reset" onPress={reset} color={Platform.OS === 'ios' ? '#007AFF' : undefined} />
@@ -233,7 +200,11 @@ export default function GenerateMock({ onDocumentStored, onNavigate, onBack }: P
/>
</View>
</View>
{loading && <ActivityIndicator style={styles.spinner} size="large" color="#0000ff" />}
{loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0550ae" />
</View>
)}
{error && <Text style={styles.error}>{error}</Text>}
</SafeAreaScrollView>
);
@@ -272,36 +243,6 @@ const styles = StyleSheet.create({
marginVertical: 6,
paddingHorizontal: 4,
},
pickerContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 6,
backgroundColor: '#fff',
},
picker: {
flex: 1,
color: '#000',
...Platform.select({
ios: {
height: 40,
},
android: {
height: 40,
},
}),
},
pickerIcon: {
position: 'absolute',
right: 12,
top: 10,
...Platform.select({
ios: {
top: 10,
},
}),
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
@@ -311,6 +252,15 @@ const styles = StyleSheet.create({
flex: 1,
marginHorizontal: 6,
},
spinner: { marginVertical: 16 },
loadingContainer: {
marginVertical: 20,
alignItems: 'center',
gap: 12,
},
loadingText: {
fontSize: 14,
color: '#0550ae',
fontWeight: '500',
},
error: { color: 'red', marginTop: 12, textAlign: 'center', fontSize: 14 },
});

View File

@@ -6,9 +6,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, Button, ScrollView, StyleSheet, Text, View } from 'react-native';
import type { DocumentCatalog, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
import { extractNameFromMRZ, getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { extractNameFromDocument, getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { PickerField } from '../components/PickerField';
import { Picker } from '@react-native-picker/picker';
import ScreenLayout from '../components/ScreenLayout';
import LogsPanel from '../components/LogsPanel';
import { useRegistration } from '../hooks/useRegistration';
@@ -36,13 +36,12 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
};
}, []);
const [selectedDocumentId, setSelectedDocumentId] = useState<string>(catalog.selectedDocumentId || '');
const [selectedDocumentId, setSelectedDocumentId] = useState<string>('');
const [selectedDocument, setSelectedDocument] = useState<IDDocument | null>(null);
const [loading, setLoading] = useState(false);
const registering = regState.registering;
const statusMessage = regState.statusMessage;
const detailedLogs = regState.logs;
const showLogs = regState.showLogs;
const [documentNames, setDocumentNames] = useState<Record<string, { firstName: string; lastName: string }>>({});
// Refresh catalog helper
const refreshCatalog = useCallback(async () => {
@@ -58,12 +57,52 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
}
}, [selfClient, onSuccess]);
// Update selected document when catalog changes (e.g., after generating a new mock)
// Auto-select first available unregistered document (newest first)
useEffect(() => {
if (catalog.selectedDocumentId && catalog.selectedDocumentId !== selectedDocumentId) {
setSelectedDocumentId(catalog.selectedDocumentId);
const availableDocuments = (catalog.documents || []).filter(doc => !doc.isRegistered).reverse();
const firstUnregisteredDocId = availableDocuments[0]?.id;
if (firstUnregisteredDocId && !selectedDocumentId) {
setSelectedDocumentId(firstUnregisteredDocId);
}
}, [catalog.selectedDocumentId, selectedDocumentId]);
}, [catalog.documents, selectedDocumentId]);
// Auto-select when catalog changes and current selection is no longer available
useEffect(() => {
const availableDocuments = (catalog.documents || []).filter(doc => !doc.isRegistered).reverse();
const isCurrentSelectionAvailable = availableDocuments.some(doc => doc.id === selectedDocumentId);
if (!isCurrentSelectionAvailable && availableDocuments.length > 0) {
setSelectedDocumentId(availableDocuments[0].id);
}
}, [catalog.documents, selectedDocumentId]);
// Load names for all documents in the catalog
useEffect(() => {
let cancelled = false;
const loadDocumentNames = async () => {
const names: Record<string, { firstName: string; lastName: string }> = {};
await Promise.all(
(catalog.documents || []).map(async doc => {
if (doc.isRegistered) return;
const name = await extractNameFromDocument(selfClient, doc.id);
if (name) {
names[doc.id] = name;
}
}),
);
if (!cancelled) {
setDocumentNames(names);
}
};
loadDocumentNames();
return () => {
cancelled = true;
};
}, [catalog.documents, selfClient]);
useEffect(() => {
const loadSelectedDocument = async () => {
@@ -87,40 +126,29 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
loadSelectedDocument();
}, [selectedDocumentId, selfClient]);
// Monitor completion and errors for dialogs
// One-shot completion handler to avoid repeated alerts
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
if (!registering && regState.statusMessage.startsWith('🎉')) {
timeoutId = setTimeout(async () => {
// Guard against updates after unmount or during a new registration attempt
if (mounted.current && !registering && regState.statusMessage.startsWith('🎉')) {
await refreshCatalog();
Alert.alert(
'Success! 🎉',
`Your ${selectedDocument?.mock ? 'mock ' : ''}document has been registered on-chain!`,
[
{
text: 'OK',
onPress: () => {
if (mounted.current) {
setSelectedDocumentId('');
}
},
},
],
);
}
}, 1000);
}
// Cleanup the timeout if the component unmounts or dependencies change
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [registering, regState.statusMessage, selectedDocument, refreshCatalog]);
actions.setOnComplete(async () => {
if (!mounted.current) return;
await refreshCatalog();
Alert.alert(
'Success! 🎉',
`Your ${selectedDocument?.mock ? 'mock ' : ''}document has been registered on-chain!`,
[
{
text: 'OK',
onPress: () => {
if (mounted.current) {
setSelectedDocumentId('');
actions.reset();
}
},
},
],
);
});
return () => actions.setOnComplete(null);
}, [actions, selectedDocument, refreshCatalog]);
const handleRegister = async () => {
if (!selectedDocument || !selectedDocumentId) return;
@@ -138,37 +166,37 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
// Filter to only unregistered documents and sort newest first
const availableDocuments = (catalog.documents || []).filter(doc => !doc.isRegistered).reverse();
const firstAvailableDocId = availableDocuments[0]?.id || '';
const selectedIdForPicker = selectedDocumentId || firstAvailableDocId || '';
return (
<ScreenLayout title="Register Document [WiP]" onBack={onBack}>
<ScreenLayout title="Register Document" onBack={onBack}>
<View style={styles.content}>
<View style={styles.pickerContainer}>
<Text style={styles.label}>Select Document</Text>
<View style={styles.pickerWrapper}>
<Picker
selectedValue={selectedDocumentId}
onValueChange={(itemValue: string) => setSelectedDocumentId(itemValue)}
style={styles.picker}
itemStyle={styles.pickerItem}
enabled={!registering}
>
<Picker.Item label="Select a document..." value="" style={styles.pickerItem} />
{availableDocuments.map(doc => {
const nameData = extractNameFromMRZ(doc.data || '');
const docType = humanizeDocumentType(doc.documentType);
const docId = doc.id.slice(0, 8);
{availableDocuments.length > 0 && (
<PickerField
label="Select Document"
selectedValue={selectedIdForPicker}
onValueChange={setSelectedDocumentId}
enabled={!registering}
items={
!firstAvailableDocId
? [{ label: 'Select a document...', value: '' }]
: availableDocuments.map(doc => {
const nameData = documentNames[doc.id];
const docType = humanizeDocumentType(doc.documentType);
const docId = doc.id.slice(0, 8);
let label = `${docType} - ${docId}...`;
if (nameData) {
const fullName = `${nameData.firstName} ${nameData.lastName}`.trim();
label = fullName ? `${fullName} - ${docType} - ${docId}...` : label;
}
let label = `${docType} - ${docId}...`;
if (nameData) {
const fullName = `${nameData.firstName} ${nameData.lastName}`.trim();
label = fullName ? `${fullName} - ${docType} - ${docId}...` : label;
}
return <Picker.Item key={doc.id} label={label} value={doc.id} style={styles.pickerItem} />;
})}
</Picker>
</View>
</View>
return { label, value: doc.id };
})
}
/>
)}
{loading && (
<View style={styles.loadingContainer}>
@@ -182,11 +210,11 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
<Text style={styles.statusText}>{statusMessage}</Text>
<Text style={styles.statusState}>State: {currentState}</Text>
<LogsPanel logs={detailedLogs} show={showLogs} onToggle={actions.toggleLogs} />
<LogsPanel logs={regState.logs} show={regState.showLogs} onToggle={actions.toggleLogs} />
</View>
)}
{selectedDocument && !loading && (
{selectedDocument && !loading && availableDocuments.length > 0 && (
<>
<View style={styles.documentSection}>
<Text style={styles.documentTitle}>Document Data:</Text>
@@ -213,7 +241,7 @@ export default function RegisterDocument({ catalog, onBack, onSuccess }: Props)
</View>
)}
{!selectedDocumentId && availableDocuments.length === 0 && (
{availableDocuments.length === 0 && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>
No unregistered documents available. Generate a mock document to get started.
@@ -235,28 +263,12 @@ const styles = StyleSheet.create({
content: {
flex: 1,
},
pickerContainer: {
marginBottom: 24,
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
pickerWrapper: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
overflow: 'hidden',
},
picker: {
height: 50,
},
pickerItem: {
fontSize: 13,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',

View File

@@ -53,9 +53,10 @@ export const screenDescriptors: ScreenDescriptor[] = [
sectionTitle: '⭐ Mock Documents',
status: 'working',
load: () => require('./RegisterDocument').default,
getProps: ({ navigate, documentCatalog }) => ({
getProps: ({ navigate, documentCatalog, refreshDocuments }) => ({
catalog: documentCatalog,
onBack: () => navigate('home'),
onSuccess: refreshDocuments,
}),
},
{

View File

@@ -6254,16 +6254,6 @@ __metadata:
languageName: node
linkType: hard
"@react-native-picker/picker@npm:^2.11.1":
version: 2.11.2
resolution: "@react-native-picker/picker@npm:2.11.2"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10c0/f9a6449797311367d52c5a0ef692fd76784ff151696db3702b21e8e94ddbd209494c0362281a55f197c43d5da67d5337046f95c4e79fdb63a493ff3565cb689e
languageName: node
linkType: hard
"@react-native/assets-registry@npm:0.76.9":
version: 0.76.9
resolution: "@react-native/assets-registry@npm:0.76.9"
@@ -23825,7 +23815,6 @@ __metadata:
"@noble/hashes": "npm:^1.5.0"
"@react-native-async-storage/async-storage": "npm:^2.2.0"
"@react-native-community/cli": "npm:^16.0.3"
"@react-native-picker/picker": "npm:^2.11.1"
"@react-native/gradle-plugin": "npm:0.76.9"
"@react-native/metro-config": "npm:0.76.9"
"@selfxyz/common": "workspace:*"
@@ -23857,7 +23846,7 @@ __metadata:
react-native-get-random-values: "npm:^1.11.0"
react-native-keychain: "npm:^10.0.0"
react-native-safe-area-context: "npm:^5.6.1"
react-native-svg: "npm:^15.13.0"
react-native-svg: "npm:15.12.1"
react-native-svg-transformer: "npm:^1.5.1"
react-native-vector-icons: "npm:^10.3.0"
stream-browserify: "npm:^3.0.0"
@@ -26564,20 +26553,6 @@ __metadata:
languageName: node
linkType: hard
"react-native-svg@npm:^15.13.0":
version: 15.13.0
resolution: "react-native-svg@npm:15.13.0"
dependencies:
css-select: "npm:^5.1.0"
css-tree: "npm:^1.1.3"
warn-once: "npm:0.1.1"
peerDependencies:
react: "*"
react-native: "*"
checksum: 10c0/89ba2ed640f78e69d44980473530fdb83e24336a1a43936e19f532e772c41e8532ee1d4f3ae003339e817a3e3b055aa2a8ee3c0da31754cd2c83fa1a81130d6a
languageName: node
linkType: hard
"react-native-vector-icons@npm:^10.3.0":
version: 10.3.0
resolution: "react-native-vector-icons@npm:10.3.0"