Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
57 changes: 57 additions & 0 deletions packages/mobile-sdk-alpha/src/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
*
Expand Down Expand Up @@ -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}`);
Expand Down
3 changes: 3 additions & 0 deletions packages/mobile-sdk-alpha/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions packages/mobile-sdk-alpha/src/processing/mrz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,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)];
}
Expand All @@ -281,7 +287,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];
Expand Down
1 change: 1 addition & 0 deletions packages/mobile-sdk-alpha/src/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
33 changes: 29 additions & 4 deletions packages/mobile-sdk-demo/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1680,7 +1680,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSVG (15.13.0):
- RNKeychain (10.0.0):
- DoubleConversion
- glog
- hermes-engine
Expand All @@ -1700,9 +1700,30 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.13.0)
- Yoga
- RNSVG/common (15.13.0):
- RNSVG (15.12.1):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.10.14.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.12.1)
- Yoga
- RNSVG/common (15.12.1):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1819,6 +1840,7 @@ DEPENDENCIES:
- 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`)
Expand Down Expand Up @@ -1970,6 +1992,8 @@ EXTERNAL SOURCES:
: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:
Expand Down Expand Up @@ -2055,7 +2079,8 @@ SPEC CHECKSUMS:
ReactCommon: 300d8d9c5cb1a6cd79a67cf5d8f91e4d477195f9
RNCAsyncStorage: 87a74d13ba0128f853817e45e21c4051e1f2cd45
RNCPicker: 31b0c81be6b949dbd8d0c8802e9c6b9615de880a
RNSVG: c22ddda11213ee91192ab2f70b50c78a8bbc30d8
RNKeychain: 850638785745df5f70c37251130617a66ec82102
RNSVG: 8dd938fb169dd81009b74c2334780d7d2a04a373
RNVectorIcons: c95fdae217b0ed388f2b4d7ed7a4edc457c1df47
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: feb4910aba9742cfedc059e2b2902e22ffe9954a
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile-sdk-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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"
Expand Down
46 changes: 46 additions & 0 deletions packages/mobile-sdk-demo/src/components/AlgorithmCountryFields.tsx
Original file line number Diff line number Diff line change
@@ -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,
}))}
/>
</>
);
}
85 changes: 85 additions & 0 deletions packages/mobile-sdk-demo/src/components/PickerField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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 { Platform, StyleSheet, Text, View } from 'react-native';
import { Picker } from '@react-native-picker/picker';
import Icon from 'react-native-vector-icons/Ionicons';

export type PickerItem = { label: string; value: string };

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>
<View style={styles.pickerContainer}>
<Picker
enabled={enabled}
selectedValue={selectedValue}
onValueChange={(itemValue: string) => onValueChange(itemValue)}
style={styles.picker}
>
{items.map(({ label: itemLabel, value }) => (
<Picker.Item label={itemLabel} value={value} key={value} />
))}
</Picker>
{Platform.OS === 'ios' && <Icon name="chevron-down-outline" size={20} color="#000" style={styles.pickerIcon} />}
</View>
</View>
);
}

const styles = StyleSheet.create({
inputContainer: {
marginBottom: 10,
},
label: {
marginBottom: 4,
fontWeight: '600',
color: '#333',
fontSize: 14,
},
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,
},
}),
},
});
5 changes: 3 additions & 2 deletions packages/mobile-sdk-demo/src/components/ScreenLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Loading
Loading