mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
SELF-808: Implement document camera MRZ demo screen (#1204)
This commit is contained in:
@@ -117,7 +117,7 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-sort-exports": "^0.9.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -126,7 +126,7 @@
|
||||
"tamagui": "1.126.14",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { DocumentCatalog, DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
|
||||
import { loadSelectedDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import HomeScreen from './src/screens/HomeScreen';
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { computeHmac, pbkdf2, randomBytes, sha256, sha512 } from '../src/utils/ethers';
|
||||
|
||||
describe('Crypto Polyfills', () => {
|
||||
describe('randomBytes', () => {
|
||||
it('should generate random bytes of specified length', () => {
|
||||
const bytes = randomBytes(32);
|
||||
expect(bytes).toBeInstanceOf(Uint8Array);
|
||||
expect(bytes.length).toBe(32);
|
||||
});
|
||||
|
||||
it('should generate different random bytes on each call', () => {
|
||||
const bytes1 = randomBytes(16);
|
||||
const bytes2 = randomBytes(16);
|
||||
expect(bytes1).not.toEqual(bytes2);
|
||||
});
|
||||
|
||||
it('should handle different lengths', () => {
|
||||
const bytes8 = randomBytes(8);
|
||||
const bytes64 = randomBytes(64);
|
||||
expect(bytes8.length).toBe(8);
|
||||
expect(bytes64.length).toBe(64);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sha256', () => {
|
||||
it('should hash data correctly', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash = sha256(data);
|
||||
|
||||
expect(hash).toBeInstanceOf(Uint8Array);
|
||||
expect(hash.length).toBe(32); // SHA-256 produces 32 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent hashes for same input', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash1 = sha256(data);
|
||||
const hash2 = sha256(data);
|
||||
|
||||
expect(hash1).toEqual(hash2);
|
||||
});
|
||||
|
||||
it('should produce different hashes for different inputs', () => {
|
||||
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
|
||||
const hash1 = sha256(data1);
|
||||
const hash2 = sha256(data2);
|
||||
|
||||
expect(hash1).not.toEqual(hash2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sha512', () => {
|
||||
it('should hash data correctly', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash = sha512(data);
|
||||
|
||||
expect(hash).toBeInstanceOf(Uint8Array);
|
||||
expect(hash.length).toBe(64); // SHA-512 produces 64 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent hashes for same input', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash1 = sha512(data);
|
||||
const hash2 = sha512(data);
|
||||
|
||||
expect(hash1).toEqual(hash2);
|
||||
});
|
||||
|
||||
it('should produce different hashes for different inputs', () => {
|
||||
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
|
||||
const hash1 = sha512(data1);
|
||||
const hash2 = sha512(data2);
|
||||
|
||||
expect(hash1).not.toEqual(hash2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeHmac', () => {
|
||||
it('should compute HMAC-SHA256 correctly', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac = computeHmac('sha256', key, data);
|
||||
|
||||
expect(hmac).toBeInstanceOf(Uint8Array);
|
||||
expect(hmac.length).toBe(32); // HMAC-SHA256 produces 32 bytes
|
||||
});
|
||||
|
||||
it('should compute HMAC-SHA512 correctly', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac = computeHmac('sha512', key, data);
|
||||
|
||||
expect(hmac).toBeInstanceOf(Uint8Array);
|
||||
expect(hmac.length).toBe(64); // HMAC-SHA512 produces 64 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent HMAC for same inputs', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac1 = computeHmac('sha256', key, data);
|
||||
const hmac2 = computeHmac('sha256', key, data);
|
||||
|
||||
expect(hmac1).toEqual(hmac2);
|
||||
});
|
||||
|
||||
it('should produce different HMAC for different keys', () => {
|
||||
const key1 = new Uint8Array([1, 2, 3, 4]);
|
||||
const key2 = new Uint8Array([1, 2, 3, 5]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac1 = computeHmac('sha256', key1, data);
|
||||
const hmac2 = computeHmac('sha256', key2, data);
|
||||
|
||||
expect(hmac1).not.toEqual(hmac2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pbkdf2', () => {
|
||||
it('should derive key using PBKDF2-SHA256', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
|
||||
expect(key).toBeInstanceOf(Uint8Array);
|
||||
expect(key.length).toBe(32);
|
||||
});
|
||||
|
||||
it('should derive key using PBKDF2-SHA512', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key = pbkdf2(password, salt, 1000, 64, 'sha512');
|
||||
|
||||
expect(key).toBeInstanceOf(Uint8Array);
|
||||
expect(key.length).toBe(64);
|
||||
});
|
||||
|
||||
it('should produce consistent keys for same inputs', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
|
||||
expect(key1).toEqual(key2);
|
||||
});
|
||||
|
||||
it('should produce different keys for different salts', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt1 = new Uint8Array([5, 6, 7, 8]);
|
||||
const salt2 = new Uint8Array([5, 6, 7, 9]);
|
||||
const key1 = pbkdf2(password, salt1, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt2, 1000, 32, 'sha256');
|
||||
|
||||
expect(key1).not.toEqual(key2);
|
||||
});
|
||||
|
||||
it('should handle different iteration counts', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt, 2000, 32, 'sha256');
|
||||
|
||||
expect(key1).not.toEqual(key2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ethers integration', () => {
|
||||
it('should have ethers.randomBytes registered', () => {
|
||||
// This test verifies that ethers.js is using our polyfill
|
||||
const { ethers } = require('ethers');
|
||||
expect(typeof ethers.randomBytes).toBe('function');
|
||||
|
||||
const bytes = ethers.randomBytes(16);
|
||||
expect(bytes).toBeInstanceOf(Uint8Array);
|
||||
expect(bytes.length).toBe(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "mobile-sdk-demo",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"analyze:bundle:android": "yarn prebuild && node scripts/bundle-analyze-ci.cjs android",
|
||||
@@ -9,7 +10,7 @@
|
||||
"preandroid": "yarn prebuild",
|
||||
"android": "react-native run-android --verbose",
|
||||
"prebuild": "yarn workspace @selfxyz/mobile-sdk-alpha build",
|
||||
"build": "tsc -p tsconfig.json --noEmit --pretty false",
|
||||
"build": "yarn prebuild && tsc -p tsconfig.json --noEmit --pretty false",
|
||||
"clean": "rm -rf ios/build android/app/build android/build && cd android && ./gradlew clean && cd ..",
|
||||
"format": "prettier --write .",
|
||||
"ia": "yarn install-app",
|
||||
@@ -23,7 +24,7 @@
|
||||
"start": "react-native start",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"types": "tsc --noEmit"
|
||||
"types": "yarn prebuild && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { buildValidationRows, humanizeDocumentType, type NormalizedMRZResult } from '../utils/camera';
|
||||
|
||||
interface Props {
|
||||
result: NormalizedMRZResult;
|
||||
}
|
||||
|
||||
export default function DocumentScanResultCard({ result }: Props) {
|
||||
const validationRows = useMemo(() => buildValidationRows(result.info.validation), [result]);
|
||||
|
||||
return (
|
||||
<View style={styles.resultCard} accessible accessibilityRole="summary">
|
||||
<Text style={styles.resultTitle}>Scan summary</Text>
|
||||
|
||||
<View style={styles.resultRow}>
|
||||
<Text style={styles.resultLabel}>Document number</Text>
|
||||
<Text style={styles.resultValue}>{result.info.documentNumber}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.resultRow}>
|
||||
<Text style={styles.resultLabel}>Document type</Text>
|
||||
<Text style={styles.resultValue}>{humanizeDocumentType(result.info.documentType)}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.resultRow}>
|
||||
<Text style={styles.resultLabel}>Issuing country</Text>
|
||||
<Text style={styles.resultValue}>{result.info.issuingCountry || 'Unknown'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.resultRow}>
|
||||
<Text style={styles.resultLabel}>Date of birth</Text>
|
||||
<Text style={styles.resultValue}>{result.readableBirthDate}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.resultRow}>
|
||||
<Text style={styles.resultLabel}>Expiry date</Text>
|
||||
<Text style={styles.resultValue}>{result.readableExpiryDate}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.validationSection}>
|
||||
<Text style={styles.validationTitle}>Validation checks</Text>
|
||||
{validationRows ? (
|
||||
validationRows.map(row => (
|
||||
<View key={row.label} style={styles.validationRow}>
|
||||
<Text style={styles.validationLabel}>{row.label}</Text>
|
||||
<Text
|
||||
style={[styles.validationBadge, row.value ? styles.validationPass : styles.validationFail]}
|
||||
accessibilityRole="text"
|
||||
>
|
||||
{row.value ? '✓ Pass' : '✗ Fail'}
|
||||
</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Text style={styles.validationPlaceholder}>Validation details are not available for this scan yet.</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
resultCard: {
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
marginBottom: 16,
|
||||
shadowColor: '#0f172a',
|
||||
shadowOpacity: 0.08,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 6,
|
||||
elevation: 2,
|
||||
},
|
||||
resultTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: '#0f172a',
|
||||
marginBottom: 12,
|
||||
},
|
||||
resultRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
},
|
||||
resultLabel: {
|
||||
color: '#334155',
|
||||
fontWeight: '500',
|
||||
fontSize: 14,
|
||||
},
|
||||
resultValue: {
|
||||
color: '#0f172a',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
validationSection: {
|
||||
marginTop: 12,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e2e8f0',
|
||||
},
|
||||
validationTitle: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
validationRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
},
|
||||
validationLabel: {
|
||||
color: '#1f2937',
|
||||
fontSize: 14,
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
validationBadge: {
|
||||
minWidth: 90,
|
||||
textAlign: 'center',
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 999,
|
||||
fontWeight: '600',
|
||||
fontSize: 12,
|
||||
color: '#ffffff',
|
||||
},
|
||||
validationPass: {
|
||||
backgroundColor: '#16a34a',
|
||||
},
|
||||
validationFail: {
|
||||
backgroundColor: '#b91c1c',
|
||||
},
|
||||
validationPlaceholder: {
|
||||
color: '#475569',
|
||||
fontSize: 13,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
|
||||
import { getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { updateAfterDelete } from '../lib/catalog';
|
||||
|
||||
182
packages/mobile-sdk-demo/src/hooks/useMRZScanner.ts
Normal file
182
packages/mobile-sdk-demo/src/hooks/useMRZScanner.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AccessibilityInfo, PermissionsAndroid, Platform } from 'react-native';
|
||||
|
||||
import type { MRZInfo } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { useReadMRZ } from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
|
||||
|
||||
import { normalizeMRZPayload, type NormalizedMRZResult } from '../utils/camera';
|
||||
|
||||
type PermissionState = 'loading' | 'granted' | 'denied';
|
||||
type ScanState = 'idle' | 'scanning' | 'success' | 'error';
|
||||
|
||||
function announceForAccessibility(message: string) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
AccessibilityInfo.announceForAccessibility?.(message);
|
||||
} catch {
|
||||
// Intentionally swallow to avoid crashing accessibility users on announce failures.
|
||||
}
|
||||
}
|
||||
|
||||
export interface DocumentScannerCopy {
|
||||
instructions: string;
|
||||
success: string;
|
||||
error: string;
|
||||
permissionDenied: string;
|
||||
resetAnnouncement: string;
|
||||
}
|
||||
|
||||
export interface DocumentScannerState {
|
||||
permissionStatus: PermissionState;
|
||||
scanState: ScanState;
|
||||
mrzResult: NormalizedMRZResult | null;
|
||||
error: string | null;
|
||||
requestPermission: () => Promise<void>;
|
||||
handleMRZDetected: (payload: MRZInfo) => void;
|
||||
handleScannerError: (error: string) => void;
|
||||
handleScanAgain: () => void;
|
||||
}
|
||||
|
||||
export function useMRZScanner(copy: DocumentScannerCopy): DocumentScannerState {
|
||||
const [permissionStatus, setPermissionStatus] = useState<PermissionState>('loading');
|
||||
const [scanState, setScanState] = useState<ScanState>('idle');
|
||||
const [mrzResult, setMrzResult] = useState<NormalizedMRZResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const scanStartTimeRef = useRef<number>(Date.now());
|
||||
const { onPassportRead } = useReadMRZ(scanStartTimeRef);
|
||||
|
||||
const requestPermission = useCallback(async () => {
|
||||
setPermissionStatus('loading');
|
||||
setError(null);
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
try {
|
||||
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA, {
|
||||
title: 'Camera permission',
|
||||
message: 'We need your permission to access the camera for MRZ scanning.',
|
||||
buttonPositive: 'Allow',
|
||||
buttonNegative: 'Cancel',
|
||||
buttonNeutral: 'Ask me later',
|
||||
});
|
||||
|
||||
if (result === PermissionsAndroid.RESULTS.GRANTED) {
|
||||
setPermissionStatus('granted');
|
||||
} else {
|
||||
setPermissionStatus('denied');
|
||||
}
|
||||
} catch {
|
||||
setPermissionStatus('denied');
|
||||
setError('Camera permission request failed. Please try again.');
|
||||
}
|
||||
} else {
|
||||
setPermissionStatus('granted');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestPermission();
|
||||
}, [requestPermission]);
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionStatus === 'granted') {
|
||||
announceForAccessibility(copy.instructions);
|
||||
setScanState(current => {
|
||||
if (current === 'success') {
|
||||
return current;
|
||||
}
|
||||
scanStartTimeRef.current = Date.now();
|
||||
return 'scanning';
|
||||
});
|
||||
} else if (permissionStatus === 'denied') {
|
||||
announceForAccessibility(copy.permissionDenied);
|
||||
setScanState('idle');
|
||||
}
|
||||
}, [copy.instructions, copy.permissionDenied, permissionStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scanState === 'success') {
|
||||
announceForAccessibility(copy.success);
|
||||
} else if (scanState === 'error') {
|
||||
announceForAccessibility(copy.error);
|
||||
}
|
||||
}, [copy.error, copy.success, scanState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
announceForAccessibility(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
const handleMRZDetected = useCallback(
|
||||
(payload: MRZInfo) => {
|
||||
setError(null);
|
||||
|
||||
setScanState(current => {
|
||||
if (current === 'success') {
|
||||
return current;
|
||||
}
|
||||
return 'scanning';
|
||||
});
|
||||
|
||||
try {
|
||||
const normalized = normalizeMRZPayload(payload);
|
||||
setMrzResult(normalized);
|
||||
setScanState('success');
|
||||
onPassportRead(null, normalized.info);
|
||||
} catch {
|
||||
setScanState('error');
|
||||
setError('Unable to validate the MRZ data from the scan.');
|
||||
}
|
||||
},
|
||||
[onPassportRead],
|
||||
);
|
||||
|
||||
const handleScannerError = useCallback((scannerError: string) => {
|
||||
setScanState('error');
|
||||
setError(scannerError || 'An unexpected camera error occurred.');
|
||||
}, []);
|
||||
|
||||
const handleScanAgain = useCallback(() => {
|
||||
if (permissionStatus === 'denied') {
|
||||
requestPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
scanStartTimeRef.current = Date.now();
|
||||
setMrzResult(null);
|
||||
setError(null);
|
||||
setScanState('scanning');
|
||||
announceForAccessibility(copy.resetAnnouncement);
|
||||
}, [copy.resetAnnouncement, permissionStatus, requestPermission]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
permissionStatus,
|
||||
scanState,
|
||||
mrzResult,
|
||||
error,
|
||||
requestPermission,
|
||||
handleMRZDetected,
|
||||
handleScannerError,
|
||||
handleScanAgain,
|
||||
}),
|
||||
[
|
||||
error,
|
||||
handleMRZDetected,
|
||||
handleScanAgain,
|
||||
handleScannerError,
|
||||
mrzResult,
|
||||
permissionStatus,
|
||||
requestPermission,
|
||||
scanState,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { IDDocument } from '@selfxyz/common/utils/types';
|
||||
import { SdkEvents, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
export type RegistrationState = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
|
||||
|
||||
export function updateAfterDelete(catalog: DocumentCatalog, deletedId: string): DocumentCatalog {
|
||||
const remaining = (catalog.documents || []).filter(doc => doc.id !== deletedId);
|
||||
|
||||
@@ -2,26 +2,233 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { ActivityIndicator, Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import PlaceholderScreen from '../components/PlaceholderScreen';
|
||||
import { MRZScannerView } from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
|
||||
|
||||
import ScreenLayout from '../components/ScreenLayout';
|
||||
import DocumentScanResultCard from '../components/DocumentScanResultCard';
|
||||
import { useMRZScanner } from '../hooks/useMRZScanner';
|
||||
|
||||
type Props = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
const instructionsText = 'Align the machine-readable text with the frame and hold steady while we scan.';
|
||||
|
||||
const successMessage = 'Document scan successful. Review the details below.';
|
||||
const errorMessage = 'We could not read your document. Adjust lighting and try again.';
|
||||
const permissionDeniedMessage = 'Camera access was denied. Enable permissions to scan your document.';
|
||||
|
||||
export default function DocumentCamera({ onBack }: Props) {
|
||||
const scannerCopy = {
|
||||
instructions: instructionsText,
|
||||
success: successMessage,
|
||||
error: errorMessage,
|
||||
permissionDenied: permissionDeniedMessage,
|
||||
resetAnnouncement: 'Ready to scan again. Align the document in the viewfinder.',
|
||||
} as const;
|
||||
|
||||
const {
|
||||
permissionStatus,
|
||||
scanState,
|
||||
mrzResult,
|
||||
error,
|
||||
requestPermission,
|
||||
handleMRZDetected,
|
||||
handleScannerError,
|
||||
handleScanAgain,
|
||||
} = useMRZScanner(scannerCopy);
|
||||
|
||||
const handleSaveDocument = useCallback(() => {
|
||||
if (!mrzResult) {
|
||||
Alert.alert('Save Document', 'Scan a document before attempting to save.');
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Save Document',
|
||||
'Document storage will be available in a future release. Your scan is ready when you need it.',
|
||||
);
|
||||
}, [mrzResult]);
|
||||
|
||||
const renderPermissionDenied = () => (
|
||||
<View style={styles.centeredState}>
|
||||
<Text style={styles.permissionText}>{permissionDeniedMessage}</Text>
|
||||
<TouchableOpacity accessibilityRole="button" style={styles.secondaryButton} onPress={requestPermission}>
|
||||
<Text style={styles.secondaryButtonText}>Request Permission</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderLoading = () => (
|
||||
<View style={styles.centeredState}>
|
||||
<ActivityIndicator accessibilityLabel="Loading camera" color="#0f172a" />
|
||||
<Text style={styles.statusText}>Preparing camera…</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<PlaceholderScreen
|
||||
<ScreenLayout
|
||||
title="Document Camera"
|
||||
onBack={onBack}
|
||||
description="This screen would handle camera-based document scanning for passports and ID cards."
|
||||
features={[
|
||||
'Camera integration for document scanning',
|
||||
'MRZ (Machine Readable Zone) detection',
|
||||
'Document validation and parsing',
|
||||
'Real-time feedback and guidance',
|
||||
]}
|
||||
/>
|
||||
onBack={() => {
|
||||
onBack();
|
||||
}}
|
||||
contentStyle={styles.screenContent}
|
||||
rightAction={
|
||||
<TouchableOpacity accessibilityRole="button" onPress={handleSaveDocument}>
|
||||
<Text style={styles.headerAction}>Save Document</Text>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
>
|
||||
{permissionStatus === 'loading' && renderLoading()}
|
||||
{permissionStatus === 'denied' && renderPermissionDenied()}
|
||||
|
||||
{permissionStatus === 'granted' && (
|
||||
<View style={styles.contentWrapper}>
|
||||
<View style={styles.cameraWrapper}>
|
||||
<MRZScannerView style={styles.scanner} onMRZDetected={handleMRZDetected} onError={handleScannerError} />
|
||||
<View style={styles.overlay} accessibilityLiveRegion="polite" pointerEvents="none">
|
||||
<Text style={styles.overlayTitle}>Position your document</Text>
|
||||
<Text style={styles.overlayText}>{instructionsText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.statusContainer}>
|
||||
{scanState === 'scanning' && !error && (
|
||||
<View style={styles.statusRow}>
|
||||
<ActivityIndicator accessibilityLabel="Scanning" color="#2563eb" size="small" />
|
||||
<Text style={styles.statusText}>Scanning for MRZ data…</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{scanState === 'success' && mrzResult && (
|
||||
<Text style={[styles.statusText, styles.successText]}>{successMessage}</Text>
|
||||
)}
|
||||
|
||||
{scanState === 'error' && error && <Text style={[styles.statusText, styles.errorText]}>{error}</Text>}
|
||||
</View>
|
||||
|
||||
{mrzResult && <DocumentScanResultCard result={mrzResult} />}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity accessibilityRole="button" onPress={handleScanAgain} style={styles.secondaryButton}>
|
||||
<Text style={styles.secondaryButtonText}>Scan Again</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity accessibilityRole="button" onPress={handleSaveDocument} style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>Save Document</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screenContent: {
|
||||
gap: 16,
|
||||
},
|
||||
contentWrapper: {
|
||||
flex: 1,
|
||||
},
|
||||
cameraWrapper: {
|
||||
backgroundColor: '#0f172a',
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
minHeight: 260,
|
||||
marginBottom: 16,
|
||||
},
|
||||
scanner: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#0f172a',
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(15, 23, 42, 0.75)',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
overlayTitle: {
|
||||
color: '#f8fafc',
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
marginBottom: 4,
|
||||
},
|
||||
overlayText: {
|
||||
color: '#e2e8f0',
|
||||
fontSize: 14,
|
||||
},
|
||||
statusContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
statusRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
statusText: {
|
||||
color: '#0f172a',
|
||||
fontSize: 14,
|
||||
marginTop: 8,
|
||||
},
|
||||
successText: {
|
||||
color: '#15803d',
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#b91c1c',
|
||||
fontWeight: '600',
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
primaryButton: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0f172a',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
primaryButtonText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
secondaryButton: {
|
||||
flex: 1,
|
||||
backgroundColor: '#e2e8f0',
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: '#0f172a',
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
},
|
||||
headerAction: {
|
||||
color: '#2563eb',
|
||||
fontWeight: '600',
|
||||
},
|
||||
centeredState: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
permissionText: {
|
||||
color: '#0f172a',
|
||||
textAlign: 'center',
|
||||
fontSize: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
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';
|
||||
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
|
||||
import { extractNameFromDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import ScreenLayout from '../components/ScreenLayout';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ActivityIndicator, Alert, Button, Platform, StyleSheet, Switch, Text, T
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { calculateContentHash, inferDocumentCategory, isMRZDocument } from '@selfxyz/common';
|
||||
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
|
||||
import { generateMockDocument, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import SafeAreaScrollView from '../components/SafeAreaScrollView';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
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 type { DocumentCatalog, IDDocument } from '@selfxyz/common/utils/types';
|
||||
import { extractNameFromDocument, getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { PickerField } from '../components/PickerField';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog, DocumentMetadata, IDDocument } from '@selfxyz/common/utils/types';
|
||||
|
||||
export type ScreenId = 'generate' | 'register' | 'prove' | 'camera' | 'nfc' | 'documents';
|
||||
|
||||
|
||||
120
packages/mobile-sdk-demo/src/utils/camera.ts
Normal file
120
packages/mobile-sdk-demo/src/utils/camera.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { type MRZInfo, type MRZValidation, extractMRZInfo } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
export type MRZPayload = MRZInfo | (MRZInfo & { rawMRZ?: string; raw?: string; mrzString?: string }) | string;
|
||||
|
||||
export interface NormalizedMRZResult {
|
||||
info: MRZInfo;
|
||||
readableBirthDate: string;
|
||||
readableExpiryDate: string;
|
||||
}
|
||||
|
||||
export interface ValidationRow {
|
||||
label: string;
|
||||
value: boolean | undefined | null;
|
||||
}
|
||||
|
||||
export function humanizeDocumentType(documentType: string): string {
|
||||
if (documentType === 'P') {
|
||||
return 'Passport';
|
||||
}
|
||||
|
||||
if (documentType === 'I') {
|
||||
return 'ID Card';
|
||||
}
|
||||
|
||||
if (!documentType) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
return documentType.trim().toUpperCase();
|
||||
}
|
||||
|
||||
export function buildValidationRows(validation?: MRZValidation): ValidationRow[] | null {
|
||||
if (!validation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: 'Format', value: validation.format },
|
||||
{ label: 'Document number checksum', value: validation.passportNumberChecksum },
|
||||
{ label: 'Date of birth checksum', value: validation.dateOfBirthChecksum },
|
||||
{ label: 'Expiry date checksum', value: validation.dateOfExpiryChecksum },
|
||||
{ label: 'Composite checksum', value: validation.compositeChecksum },
|
||||
{ label: 'Overall validation', value: validation.overall },
|
||||
];
|
||||
}
|
||||
|
||||
export function formatMRZDate(mrzDate: string, locale: string = 'default'): string {
|
||||
if (!/^\d{6}$/.test(mrzDate)) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const yearPart = parseInt(mrzDate.slice(0, 2), 10);
|
||||
const monthPart = parseInt(mrzDate.slice(2, 4), 10);
|
||||
const dayPart = parseInt(mrzDate.slice(4, 6), 10);
|
||||
|
||||
if (Number.isNaN(yearPart) || monthPart < 1 || monthPart > 12 || dayPart < 1 || dayPart > 31) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear() % 100;
|
||||
const century = yearPart > currentYear ? 1900 : 2000;
|
||||
const fullYear = century + yearPart;
|
||||
|
||||
const date = new Date(Date.UTC(fullYear, monthPart - 1, dayPart));
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof Intl !== 'undefined') {
|
||||
const localeOption = locale === 'default' ? undefined : locale;
|
||||
return new Intl.DateTimeFormat(localeOption, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
}).format(date);
|
||||
}
|
||||
} catch {
|
||||
// Fallback to ISO-like format below.
|
||||
}
|
||||
|
||||
const monthString = `${monthPart}`.padStart(2, '0');
|
||||
const dayString = `${dayPart}`.padStart(2, '0');
|
||||
return `${fullYear}-${monthString}-${dayString}`;
|
||||
}
|
||||
|
||||
export function normalizeMRZPayload(payload: MRZPayload): NormalizedMRZResult {
|
||||
let info: MRZInfo;
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
info = extractMRZInfo(payload);
|
||||
} else {
|
||||
const withRaw = payload as MRZInfo & { rawMRZ?: string; raw?: string; mrzString?: string };
|
||||
const rawCandidate = withRaw.rawMRZ ?? withRaw.raw ?? withRaw.mrzString;
|
||||
|
||||
if (typeof rawCandidate === 'string' && rawCandidate.trim().length > 0) {
|
||||
info = extractMRZInfo(rawCandidate);
|
||||
} else {
|
||||
info = {
|
||||
documentNumber: withRaw.documentNumber,
|
||||
dateOfBirth: withRaw.dateOfBirth,
|
||||
dateOfExpiry: withRaw.dateOfExpiry,
|
||||
issuingCountry: withRaw.issuingCountry,
|
||||
documentType: withRaw.documentType,
|
||||
validation: withRaw.validation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
info,
|
||||
readableBirthDate: formatMRZDate(info.dateOfBirth),
|
||||
readableExpiryDate: formatMRZDate(info.dateOfExpiry),
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentMetadata } from '@selfxyz/common/utils/types';
|
||||
|
||||
export function humanizeDocumentType(documentType: string): string {
|
||||
const toTitle = (s: string) => s.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import type { DocumentsAdapter } from '@selfxyz/mobile-sdk-alpha';
|
||||
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog, IDDocument, PassportData } from '@selfxyz/common/utils/types';
|
||||
import { getSKIPEM, initPassportDataParsing } from '@selfxyz/common';
|
||||
|
||||
const CATALOG_KEY = '@self_demo:document_catalog';
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
/**
|
||||
* Tests for crypto-polyfill.js demonstrating functional bugs:
|
||||
* 1. Method chaining breaks due to incorrect `this` binding
|
||||
* 2. RNG import fails - react-native-get-random-values doesn't export getRandomValues
|
||||
* 3. Buffer polyfill missing
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// Preserve original crypto
|
||||
const originalCrypto = global.crypto;
|
||||
|
||||
// Mock crypto.getRandomValues in jsdom environment
|
||||
if (typeof global.crypto === 'undefined' || !global.crypto.getRandomValues) {
|
||||
const mockGetRandomValues = vi.fn((array: Uint8Array) => {
|
||||
// Fill with predictable values for testing
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'crypto', {
|
||||
value: {
|
||||
getRandomValues: mockGetRandomValues,
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Mock Buffer globally to simulate React Native environment where Buffer is undefined
|
||||
const originalBuffer = global.Buffer;
|
||||
|
||||
describe('Crypto Polyfill Functional Bugs', () => {
|
||||
let crypto: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear module cache to get fresh instance
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore Buffer if we removed it
|
||||
global.Buffer = originalBuffer;
|
||||
// Restore crypto (use Object.defineProperty for read-only properties)
|
||||
if (originalCrypto) {
|
||||
Object.defineProperty(global, 'crypto', {
|
||||
value: originalCrypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Method Chaining Bug', () => {
|
||||
it('should allow method chaining with update() calls', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// This should work but currently fails due to `this` binding issue
|
||||
expect(() => {
|
||||
const hasher = crypto.createHash('sha256');
|
||||
const result = hasher.update('Hello ').update('World').digest('hex');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64); // SHA256 hex length
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should return the hasher instance from update() for chaining', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const hasher = crypto.createHash('sha256');
|
||||
const updateResult = hasher.update('test');
|
||||
|
||||
// This should be the same object for chaining
|
||||
expect(updateResult).toBe(hasher);
|
||||
expect(updateResult.update).toBeInstanceOf(Function);
|
||||
expect(updateResult.digest).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should produce the same result for chained vs separate calls', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Chained approach
|
||||
const chainedResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
|
||||
|
||||
// Separate calls approach
|
||||
const hasher = crypto.createHash('sha256');
|
||||
hasher.update('Hello ');
|
||||
hasher.update('World');
|
||||
const separateResult = hasher.digest('hex');
|
||||
|
||||
expect(chainedResult).toBe(separateResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RNG Import Bug', () => {
|
||||
it('should not try to destructure getRandomValues from react-native-get-random-values', async () => {
|
||||
// Mock the require to simulate the actual package behavior
|
||||
vi.doMock('react-native-get-random-values', () => {
|
||||
// This package doesn't export getRandomValues - it just polyfills globalThis.crypto
|
||||
global.crypto = global.crypto || ({} as typeof crypto);
|
||||
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
}) as any;
|
||||
return {}; // Empty export
|
||||
});
|
||||
|
||||
// Should now work because we use globalThis.crypto.getRandomValues, not destructuring
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
const result = crypto.randomBytes(16);
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.length).toBe(16);
|
||||
});
|
||||
|
||||
it('should throw helpful error when crypto.getRandomValues is not available', async () => {
|
||||
// Clear module cache and remove crypto polyfill
|
||||
vi.resetModules();
|
||||
vi.doMock('react-native-get-random-values', () => {
|
||||
// Mock a broken polyfill that doesn't install crypto
|
||||
return {};
|
||||
});
|
||||
|
||||
// Remove crypto to simulate missing polyfill
|
||||
const originalCrypto = global.crypto;
|
||||
delete (global as any).crypto;
|
||||
|
||||
await expect(async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
crypto.randomBytes(16);
|
||||
}).rejects.toThrow(/globalThis.crypto.getRandomValues is not available/);
|
||||
|
||||
global.crypto = originalCrypto;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Buffer Polyfill Bug', () => {
|
||||
it('should gracefully handle Buffer availability check', async () => {
|
||||
// This test verifies that the crypto polyfill checks for Buffer availability
|
||||
// Note: We can't actually delete Buffer because Vitest's worker threads need it
|
||||
// Instead, we verify that the polyfill works correctly with and without Buffer
|
||||
|
||||
// Import the polyfill normally
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Test that randomBytes returns a typed array
|
||||
const result = crypto.randomBytes(32);
|
||||
|
||||
// The result should be either a Buffer or Uint8Array (Buffer extends Uint8Array)
|
||||
// Buffer is available in Node.js environment, so we expect Buffer here
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.length).toBe(32);
|
||||
|
||||
// Verify it's a valid typed array with proper values
|
||||
expect(result.every((byte: number) => byte >= 0 && byte <= 255)).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with Buffer polyfill imported', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Simulate proper Buffer polyfill
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const result = crypto.createHash('sha256').update('test').digest('hex');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
|
||||
it('should handle different data types correctly with Buffer polyfill', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const hasher = crypto.createHash('sha256');
|
||||
|
||||
// Should handle strings
|
||||
hasher.update('string data');
|
||||
|
||||
// Should handle Uint8Array
|
||||
hasher.update(new Uint8Array([1, 2, 3, 4]));
|
||||
|
||||
// Should handle Buffer
|
||||
hasher.update(Buffer.from('buffer data'));
|
||||
|
||||
const result = hasher.digest('hex');
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should work end-to-end with all fixes applied', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Set up proper environment
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
global.crypto = global.crypto || ({} as typeof crypto);
|
||||
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
}) as any;
|
||||
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Test hash chaining
|
||||
const hashResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
|
||||
|
||||
expect(typeof hashResult).toBe('string');
|
||||
expect(hashResult.length).toBe(64);
|
||||
|
||||
// Test randomBytes
|
||||
const randomResult = crypto.randomBytes(32);
|
||||
expect(randomResult).toBeInstanceOf(Buffer);
|
||||
expect(randomResult.length).toBe(32);
|
||||
});
|
||||
});
|
||||
});
|
||||
421
packages/mobile-sdk-demo/tests/cryptoPolyfills.test.ts
Normal file
421
packages/mobile-sdk-demo/tests/cryptoPolyfills.test.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Comprehensive tests for crypto polyfills including:
|
||||
* 1. Functional bug tests for crypto-polyfill.js
|
||||
* 2. Ethers integration tests for crypto utilities
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { computeHmac, pbkdf2, randomBytes, sha256, sha512 } from '../src/utils/ethers';
|
||||
|
||||
// Preserve original crypto
|
||||
const originalCrypto = global.crypto;
|
||||
|
||||
// Mock crypto.getRandomValues in jsdom environment
|
||||
if (typeof global.crypto === 'undefined' || !global.crypto.getRandomValues) {
|
||||
const mockGetRandomValues = vi.fn((array: Uint8Array) => {
|
||||
// Fill with predictable values for testing
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'crypto', {
|
||||
value: {
|
||||
getRandomValues: mockGetRandomValues,
|
||||
},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Mock Buffer globally to simulate React Native environment where Buffer is undefined
|
||||
const originalBuffer = global.Buffer;
|
||||
|
||||
describe('Crypto Polyfills', () => {
|
||||
let crypto: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear module cache to get fresh instance
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore Buffer if we removed it
|
||||
global.Buffer = originalBuffer;
|
||||
// Restore crypto (use Object.defineProperty for read-only properties)
|
||||
if (originalCrypto) {
|
||||
Object.defineProperty(global, 'crypto', {
|
||||
value: originalCrypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Ethers Crypto Utilities', () => {
|
||||
describe('randomBytes', () => {
|
||||
it('should generate random bytes of specified length', () => {
|
||||
const bytes = randomBytes(32);
|
||||
expect(bytes).toBeInstanceOf(Uint8Array);
|
||||
expect(bytes.length).toBe(32);
|
||||
});
|
||||
|
||||
it('should generate different random bytes on each call', () => {
|
||||
const bytes1 = randomBytes(16);
|
||||
const bytes2 = randomBytes(16);
|
||||
expect(bytes1).not.toEqual(bytes2);
|
||||
});
|
||||
|
||||
it('should handle different lengths', () => {
|
||||
const bytes8 = randomBytes(8);
|
||||
const bytes64 = randomBytes(64);
|
||||
expect(bytes8.length).toBe(8);
|
||||
expect(bytes64.length).toBe(64);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sha256', () => {
|
||||
it('should hash data correctly', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash = sha256(data);
|
||||
|
||||
expect(hash).toBeInstanceOf(Uint8Array);
|
||||
expect(hash.length).toBe(32); // SHA-256 produces 32 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent hashes for same input', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash1 = sha256(data);
|
||||
const hash2 = sha256(data);
|
||||
|
||||
expect(hash1).toEqual(hash2);
|
||||
});
|
||||
|
||||
it('should produce different hashes for different inputs', () => {
|
||||
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
|
||||
const hash1 = sha256(data1);
|
||||
const hash2 = sha256(data2);
|
||||
|
||||
expect(hash1).not.toEqual(hash2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sha512', () => {
|
||||
it('should hash data correctly', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash = sha512(data);
|
||||
|
||||
expect(hash).toBeInstanceOf(Uint8Array);
|
||||
expect(hash.length).toBe(64); // SHA-512 produces 64 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent hashes for same input', () => {
|
||||
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const hash1 = sha512(data);
|
||||
const hash2 = sha512(data);
|
||||
|
||||
expect(hash1).toEqual(hash2);
|
||||
});
|
||||
|
||||
it('should produce different hashes for different inputs', () => {
|
||||
const data1 = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const data2 = new Uint8Array([1, 2, 3, 4, 6]);
|
||||
const hash1 = sha512(data1);
|
||||
const hash2 = sha512(data2);
|
||||
|
||||
expect(hash1).not.toEqual(hash2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeHmac', () => {
|
||||
it('should compute HMAC-SHA256 correctly', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac = computeHmac('sha256', key, data);
|
||||
|
||||
expect(hmac).toBeInstanceOf(Uint8Array);
|
||||
expect(hmac.length).toBe(32); // HMAC-SHA256 produces 32 bytes
|
||||
});
|
||||
|
||||
it('should compute HMAC-SHA512 correctly', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac = computeHmac('sha512', key, data);
|
||||
|
||||
expect(hmac).toBeInstanceOf(Uint8Array);
|
||||
expect(hmac.length).toBe(64); // HMAC-SHA512 produces 64 bytes
|
||||
});
|
||||
|
||||
it('should produce consistent HMAC for same inputs', () => {
|
||||
const key = new Uint8Array([1, 2, 3, 4]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac1 = computeHmac('sha256', key, data);
|
||||
const hmac2 = computeHmac('sha256', key, data);
|
||||
|
||||
expect(hmac1).toEqual(hmac2);
|
||||
});
|
||||
|
||||
it('should produce different HMAC for different keys', () => {
|
||||
const key1 = new Uint8Array([1, 2, 3, 4]);
|
||||
const key2 = new Uint8Array([1, 2, 3, 5]);
|
||||
const data = new Uint8Array([5, 6, 7, 8]);
|
||||
const hmac1 = computeHmac('sha256', key1, data);
|
||||
const hmac2 = computeHmac('sha256', key2, data);
|
||||
|
||||
expect(hmac1).not.toEqual(hmac2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pbkdf2', () => {
|
||||
it('should derive key using PBKDF2-SHA256', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
|
||||
expect(key).toBeInstanceOf(Uint8Array);
|
||||
expect(key.length).toBe(32);
|
||||
});
|
||||
|
||||
it('should derive key using PBKDF2-SHA512', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key = pbkdf2(password, salt, 1000, 64, 'sha512');
|
||||
|
||||
expect(key).toBeInstanceOf(Uint8Array);
|
||||
expect(key.length).toBe(64);
|
||||
});
|
||||
|
||||
it('should produce consistent keys for same inputs', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
|
||||
expect(key1).toEqual(key2);
|
||||
});
|
||||
|
||||
it('should produce different keys for different salts', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt1 = new Uint8Array([5, 6, 7, 8]);
|
||||
const salt2 = new Uint8Array([5, 6, 7, 9]);
|
||||
const key1 = pbkdf2(password, salt1, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt2, 1000, 32, 'sha256');
|
||||
|
||||
expect(key1).not.toEqual(key2);
|
||||
});
|
||||
|
||||
it('should handle different iteration counts', () => {
|
||||
const password = new Uint8Array([1, 2, 3, 4]);
|
||||
const salt = new Uint8Array([5, 6, 7, 8]);
|
||||
const key1 = pbkdf2(password, salt, 1000, 32, 'sha256');
|
||||
const key2 = pbkdf2(password, salt, 2000, 32, 'sha256');
|
||||
|
||||
expect(key1).not.toEqual(key2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ethers integration', () => {
|
||||
it('should have ethers.randomBytes registered', () => {
|
||||
// This test verifies that ethers.js is using our polyfill
|
||||
const { ethers } = require('ethers');
|
||||
expect(typeof ethers.randomBytes).toBe('function');
|
||||
|
||||
const bytes = ethers.randomBytes(16);
|
||||
expect(bytes).toBeInstanceOf(Uint8Array);
|
||||
expect(bytes.length).toBe(16);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Crypto Polyfill Functional Bugs', () => {
|
||||
describe('Method Chaining Bug', () => {
|
||||
it('should allow method chaining with update() calls', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// This should work but currently fails due to `this` binding issue
|
||||
expect(() => {
|
||||
const hasher = crypto.createHash('sha256');
|
||||
const result = hasher.update('Hello ').update('World').digest('hex');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64); // SHA256 hex length
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should return the hasher instance from update() for chaining', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const hasher = crypto.createHash('sha256');
|
||||
const updateResult = hasher.update('test');
|
||||
|
||||
// This should be the same object for chaining
|
||||
expect(updateResult).toBe(hasher);
|
||||
expect(updateResult.update).toBeInstanceOf(Function);
|
||||
expect(updateResult.digest).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should produce the same result for chained vs separate calls', async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Chained approach
|
||||
const chainedResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
|
||||
|
||||
// Separate calls approach
|
||||
const hasher = crypto.createHash('sha256');
|
||||
hasher.update('Hello ');
|
||||
hasher.update('World');
|
||||
const separateResult = hasher.digest('hex');
|
||||
|
||||
expect(chainedResult).toBe(separateResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RNG Import Bug', () => {
|
||||
it('should not try to destructure getRandomValues from react-native-get-random-values', async () => {
|
||||
// Mock the require to simulate the actual package behavior
|
||||
vi.doMock('react-native-get-random-values', () => {
|
||||
// This package doesn't export getRandomValues - it just polyfills globalThis.crypto
|
||||
global.crypto = global.crypto || ({} as typeof crypto);
|
||||
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
}) as any;
|
||||
return {}; // Empty export
|
||||
});
|
||||
|
||||
// Should now work because we use globalThis.crypto.getRandomValues, not destructuring
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
const result = crypto.randomBytes(16);
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.length).toBe(16);
|
||||
});
|
||||
|
||||
it('should throw helpful error when crypto.getRandomValues is not available', async () => {
|
||||
// Clear module cache and remove crypto polyfill
|
||||
vi.resetModules();
|
||||
vi.doMock('react-native-get-random-values', () => {
|
||||
// Mock a broken polyfill that doesn't install crypto
|
||||
return {};
|
||||
});
|
||||
|
||||
// Remove crypto to simulate missing polyfill
|
||||
const originalCrypto = global.crypto;
|
||||
delete (global as any).crypto;
|
||||
|
||||
await expect(async () => {
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
crypto.randomBytes(16);
|
||||
}).rejects.toThrow(/globalThis.crypto.getRandomValues is not available/);
|
||||
|
||||
global.crypto = originalCrypto;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Buffer Polyfill Bug', () => {
|
||||
it('should gracefully handle Buffer availability check', async () => {
|
||||
// This test verifies that the crypto polyfill checks for Buffer availability
|
||||
// Note: We can't actually delete Buffer because Vitest's worker threads need it
|
||||
// Instead, we verify that the polyfill works correctly with and without Buffer
|
||||
|
||||
// Import the polyfill normally
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Test that randomBytes returns a typed array
|
||||
const result = crypto.randomBytes(32);
|
||||
|
||||
// The result should be either a Buffer or Uint8Array (Buffer extends Uint8Array)
|
||||
// Buffer is available in Node.js environment, so we expect Buffer here
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.length).toBe(32);
|
||||
|
||||
// Verify it's a valid typed array with proper values
|
||||
expect(result.every((byte: number) => byte >= 0 && byte <= 255)).toBe(true);
|
||||
});
|
||||
|
||||
it('should work with Buffer polyfill imported', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Simulate proper Buffer polyfill
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const result = crypto.createHash('sha256').update('test').digest('hex');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
|
||||
it('should handle different data types correctly with Buffer polyfill', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
const hasher = crypto.createHash('sha256');
|
||||
|
||||
// Should handle strings
|
||||
hasher.update('string data');
|
||||
|
||||
// Should handle Uint8Array
|
||||
hasher.update(new Uint8Array([1, 2, 3, 4]));
|
||||
|
||||
// Should handle Buffer
|
||||
hasher.update(Buffer.from('buffer data'));
|
||||
|
||||
const result = hasher.digest('hex');
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBe(64);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should work end-to-end with all fixes applied', async () => {
|
||||
// Reset mocks for this test
|
||||
vi.unmock('buffer');
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Set up proper environment
|
||||
global.Buffer = require('buffer').Buffer;
|
||||
global.crypto = global.crypto || ({} as typeof crypto);
|
||||
global.crypto.getRandomValues = vi.fn((array: Uint8Array) => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = i % 256;
|
||||
}
|
||||
return array;
|
||||
}) as any;
|
||||
|
||||
crypto = await import('../src/polyfills/cryptoPolyfill.js');
|
||||
|
||||
// Test hash chaining
|
||||
const hashResult = crypto.createHash('sha256').update('Hello ').update('World').digest('hex');
|
||||
|
||||
expect(typeof hashResult).toBe('string');
|
||||
expect(hashResult.length).toBe(64);
|
||||
|
||||
// Test randomBytes
|
||||
const randomResult = crypto.randomBytes(32);
|
||||
expect(randomResult).toBeInstanceOf(Buffer);
|
||||
expect(randomResult.length).toBe(32);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { PassportData } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { PassportData } from '@selfxyz/common/utils/types';
|
||||
|
||||
describe('documentStore - BigInt serialization (simplified)', () => {
|
||||
it('should demonstrate the BigInt serialization problem with JSON.stringify/parse', () => {
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { DocumentCatalog } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentCatalog } from '@selfxyz/common/utils/types';
|
||||
|
||||
import { selectDocument, updateAfterDelete } from '../../src/lib/catalog';
|
||||
|
||||
|
||||
2
packages/mobile-sdk-demo/tests/mocks/mobile-sdk-alpha.ts
Normal file
2
packages/mobile-sdk-demo/tests/mocks/mobile-sdk-alpha.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { extractMRZInfo, formatDateToYYMMDD } from '../../../mobile-sdk-alpha/src/processing/mrz';
|
||||
export type { MRZInfo, MRZValidation } from '../../../mobile-sdk-alpha/src/types/public';
|
||||
45
packages/mobile-sdk-demo/tests/mocks/react-native.ts
Normal file
45
packages/mobile-sdk-demo/tests/mocks/react-native.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const AccessibilityInfo = {
|
||||
announceForAccessibility: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
export const ActivityIndicator = () => null;
|
||||
export const Alert = { alert: () => undefined };
|
||||
|
||||
export const PermissionsAndroid = {
|
||||
PERMISSIONS: { CAMERA: 'android.permission.CAMERA' },
|
||||
RESULTS: { GRANTED: 'granted', DENIED: 'denied' },
|
||||
request: async () => 'granted' as const,
|
||||
};
|
||||
|
||||
export const Platform = {
|
||||
OS: 'ios',
|
||||
select<T>(mapping: Record<string, T> & { default?: T }): T {
|
||||
if (Object.prototype.hasOwnProperty.call(mapping, 'ios')) {
|
||||
return mapping.ios as T;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(mapping, 'default')) {
|
||||
return mapping.default as T;
|
||||
}
|
||||
return Object.values(mapping)[0] as T;
|
||||
},
|
||||
};
|
||||
|
||||
export const StyleSheet = {
|
||||
create: <T extends Record<string, object>>(styles: T) => styles,
|
||||
};
|
||||
|
||||
export const Text = () => null;
|
||||
export const TouchableOpacity = () => null;
|
||||
export const View = () => null;
|
||||
|
||||
export default {
|
||||
AccessibilityInfo,
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
PermissionsAndroid,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { MRZInfo } from '@selfxyz/mobile-sdk-alpha';
|
||||
|
||||
import { buildValidationRows, formatMRZDate, humanizeDocumentType, normalizeMRZPayload } from '../../src/utils/camera';
|
||||
|
||||
describe('formatMRZDate', () => {
|
||||
it('formats valid YYMMDD strings into readable dates', () => {
|
||||
expect(formatMRZDate('740812', 'en-US')).toBe('August 12, 1974');
|
||||
expect(formatMRZDate('010101', 'en-US')).toBe('January 1, 2001');
|
||||
});
|
||||
|
||||
it('returns Unknown for invalid values', () => {
|
||||
expect(formatMRZDate('991332', 'en-US')).toBe('Unknown');
|
||||
expect(formatMRZDate('abc123', 'en-US')).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeMRZPayload', () => {
|
||||
it('parses raw MRZ strings and surfaces validation data', () => {
|
||||
const rawMRZ = 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10';
|
||||
|
||||
const normalized = normalizeMRZPayload(rawMRZ);
|
||||
|
||||
expect(normalized.info.documentNumber).toBe('L898902C3');
|
||||
expect(normalized.info.dateOfBirth).toBe('740812');
|
||||
expect(normalized.info.dateOfExpiry).toBe('120415');
|
||||
expect(normalized.info.validation?.overall).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves provided MRZ info when validation already exists', () => {
|
||||
const info: MRZInfo = {
|
||||
documentNumber: 'X1234567',
|
||||
dateOfBirth: '010101',
|
||||
dateOfExpiry: '251231',
|
||||
issuingCountry: 'UTO',
|
||||
documentType: 'P',
|
||||
validation: {
|
||||
format: true,
|
||||
passportNumberChecksum: true,
|
||||
dateOfBirthChecksum: true,
|
||||
dateOfExpiryChecksum: true,
|
||||
compositeChecksum: true,
|
||||
overall: true,
|
||||
},
|
||||
};
|
||||
|
||||
const normalized = normalizeMRZPayload(info);
|
||||
|
||||
expect(normalized.info).toEqual(info);
|
||||
expect(normalized.readableBirthDate).toBe(formatMRZDate('010101', 'en-US'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('humanizeDocumentType', () => {
|
||||
it('maps known document codes to friendly labels', () => {
|
||||
expect(humanizeDocumentType('P')).toBe('Passport');
|
||||
expect(humanizeDocumentType('I')).toBe('ID Card');
|
||||
});
|
||||
|
||||
it('falls back to normalized text for unknown values', () => {
|
||||
expect(humanizeDocumentType(' visa ')).toBe('VISA');
|
||||
expect(humanizeDocumentType('')).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildValidationRows', () => {
|
||||
it('returns null when validation is unavailable', () => {
|
||||
expect(buildValidationRows(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('maps MRZ validation flags into labelled rows', () => {
|
||||
const rows = buildValidationRows({
|
||||
format: true,
|
||||
passportNumberChecksum: true,
|
||||
dateOfBirthChecksum: false,
|
||||
dateOfExpiryChecksum: true,
|
||||
compositeChecksum: true,
|
||||
overall: false,
|
||||
});
|
||||
|
||||
expect(rows).toMatchObject([
|
||||
{ label: 'Format', value: true },
|
||||
{ label: 'Document number checksum', value: true },
|
||||
{ label: 'Date of birth checksum', value: false },
|
||||
{ label: 'Expiry date checksum', value: true },
|
||||
{ label: 'Composite checksum', value: true },
|
||||
{ label: 'Overall validation', value: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { DocumentMetadata } from '@selfxyz/common/dist/esm/src/utils/types.js';
|
||||
import type { DocumentMetadata } from '@selfxyz/common/utils/types';
|
||||
|
||||
import { formatDataPreview, humanizeDocumentType, maskId } from '../../src/utils/document';
|
||||
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/react-native/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ES2020",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@selfxyz/mobile-sdk-alpha": ["../../packages/mobile-sdk-alpha/dist/esm/index"],
|
||||
"@selfxyz/mobile-sdk-alpha/onboarding/*": ["../../packages/mobile-sdk-alpha/dist/esm/flows/onboarding/*"],
|
||||
"@selfxyz/mobile-sdk-alpha/disclosing/*": ["../../packages/mobile-sdk-alpha/dist/esm/flows/disclosing/*"]
|
||||
},
|
||||
"types": ["react-native", "vitest/globals"]
|
||||
},
|
||||
"include": ["src", "App.tsx", "index.js", "types/**/*", "__tests__/**/*", "tests/**/*"]
|
||||
"include": ["src", "App.tsx", "index.js", "types/**/*", "__tests__/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "../../packages/mobile-sdk-alpha/src"]
|
||||
}
|
||||
|
||||
@@ -7,14 +7,13 @@ import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, '../..');
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
include: ['__tests__/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
|
||||
include: ['tests/**/*.test.{ts,tsx}'],
|
||||
exclude: ['node_modules/**'],
|
||||
// Skip checking node_modules for faster testing
|
||||
server: {
|
||||
@@ -23,10 +22,22 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
esbuild: {
|
||||
target: 'node18',
|
||||
},
|
||||
build: {
|
||||
target: 'node18',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@selfxyz/common': resolve(repoRoot, 'common/dist/cjs/index.cjs'),
|
||||
'@selfxyz/mobile-sdk-alpha': resolve(repoRoot, 'packages/mobile-sdk-alpha/src/index.ts'),
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
find: '@selfxyz/mobile-sdk-alpha',
|
||||
replacement: resolve(__dirname, './tests/mocks/mobile-sdk-alpha.ts'),
|
||||
},
|
||||
{
|
||||
find: 'react-native',
|
||||
replacement: resolve(__dirname, './tests/mocks/react-native.ts'),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
350
yarn.lock
350
yarn.lock
@@ -7462,7 +7462,7 @@ __metadata:
|
||||
eslint-plugin-react: "npm:^7.37.5"
|
||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||
eslint-plugin-sort-exports: "npm:^0.9.1"
|
||||
jsdom: "npm:^24.0.0"
|
||||
jsdom: "npm:^25.0.1"
|
||||
node-forge: "npm:^1.3.1"
|
||||
prettier: "npm:^3.5.3"
|
||||
react: "npm:^18.3.1"
|
||||
@@ -7474,7 +7474,7 @@ __metadata:
|
||||
tsup: "npm:^8.0.1"
|
||||
typescript: "npm:^5.9.2"
|
||||
uuid: "npm:^11.1.0"
|
||||
vitest: "npm:^1.6.0"
|
||||
vitest: "npm:^2.1.8"
|
||||
xstate: "npm:^5.20.2"
|
||||
zustand: "npm:^4.5.2"
|
||||
peerDependencies:
|
||||
@@ -12674,17 +12674,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@vitest/expect@npm:1.6.1"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:1.6.1"
|
||||
"@vitest/utils": "npm:1.6.1"
|
||||
chai: "npm:^4.3.10"
|
||||
checksum: 10c0/278164b2a32a7019b443444f21111c5e32e4cadee026cae047ae2a3b347d99dca1d1fb7b79509c88b67dc3db19fa9a16265b7d7a8377485f7e37f7851e44495a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@vitest/expect@npm:2.1.9"
|
||||
@@ -12725,17 +12714,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@vitest/runner@npm:1.6.1"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:1.6.1"
|
||||
p-limit: "npm:^5.0.0"
|
||||
pathe: "npm:^1.1.1"
|
||||
checksum: 10c0/36333f1a596c4ad85d42c6126cc32959c984d584ef28d366d366fa3672678c1a0f5e5c2e8717a36675b6620b57e8830f765d6712d1687f163ed0a8ebf23c87db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@vitest/runner@npm:2.1.9"
|
||||
@@ -12746,17 +12724,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@vitest/snapshot@npm:1.6.1"
|
||||
dependencies:
|
||||
magic-string: "npm:^0.30.5"
|
||||
pathe: "npm:^1.1.1"
|
||||
pretty-format: "npm:^29.7.0"
|
||||
checksum: 10c0/68bbc3132c195ec37376469e4b183fc408e0aeedd827dffcc899aac378e9ea324825f0873062786e18f00e3da9dd8a93c9bb871c07471ee483e8df963cb272eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@vitest/snapshot@npm:2.1.9"
|
||||
@@ -12768,15 +12735,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@vitest/spy@npm:1.6.1"
|
||||
dependencies:
|
||||
tinyspy: "npm:^2.2.0"
|
||||
checksum: 10c0/5207ec0e7882819f0e0811293ae6d14163e26927e781bb4de7d40b3bd99c1fae656934c437bb7a30443a3e7e736c5bccb037bbf4436dbbc83d29e65247888885
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@vitest/spy@npm:2.1.9"
|
||||
@@ -12803,18 +12761,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "@vitest/utils@npm:1.6.1"
|
||||
dependencies:
|
||||
diff-sequences: "npm:^29.6.3"
|
||||
estree-walker: "npm:^3.0.3"
|
||||
loupe: "npm:^2.3.7"
|
||||
pretty-format: "npm:^29.7.0"
|
||||
checksum: 10c0/0d4c619e5688cbc22a60c412719c6baa40376b7671bdbdc3072552f5c5a5ee5d24a96ea328b054018debd49e0626a5e3db672921b2c6b5b17b9a52edd296806a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "@vitest/utils@npm:2.1.9"
|
||||
@@ -13322,7 +13268,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.3.2":
|
||||
"acorn-walk@npm:^8.1.1":
|
||||
version: 8.3.4
|
||||
resolution: "acorn-walk@npm:8.3.4"
|
||||
dependencies:
|
||||
@@ -14987,7 +14933,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chai@npm:^4.3.10, chai@npm:^4.3.6, chai@npm:^4.4.1":
|
||||
"chai@npm:^4.3.6, chai@npm:^4.4.1":
|
||||
version: 4.5.0
|
||||
resolution: "chai@npm:4.5.0"
|
||||
dependencies:
|
||||
@@ -16246,7 +16192,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cssstyle@npm:^4.0.1, cssstyle@npm:^4.1.0":
|
||||
"cssstyle@npm:^4.1.0":
|
||||
version: 4.6.0
|
||||
resolution: "cssstyle@npm:4.6.0"
|
||||
dependencies:
|
||||
@@ -18519,23 +18465,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"execa@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "execa@npm:8.0.1"
|
||||
dependencies:
|
||||
cross-spawn: "npm:^7.0.3"
|
||||
get-stream: "npm:^8.0.1"
|
||||
human-signals: "npm:^5.0.0"
|
||||
is-stream: "npm:^3.0.0"
|
||||
merge-stream: "npm:^2.0.0"
|
||||
npm-run-path: "npm:^5.1.0"
|
||||
onetime: "npm:^6.0.0"
|
||||
signal-exit: "npm:^4.1.0"
|
||||
strip-final-newline: "npm:^3.0.0"
|
||||
checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exit@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "exit@npm:0.1.2"
|
||||
@@ -19602,13 +19531,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-stream@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "get-stream@npm:8.0.1"
|
||||
checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-symbol-description@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "get-symbol-description@npm:1.1.0"
|
||||
@@ -20443,13 +20365,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"human-signals@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "human-signals@npm:5.0.0"
|
||||
checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"husky@npm:9.1.7":
|
||||
version: 9.1.7
|
||||
resolution: "husky@npm:9.1.7"
|
||||
@@ -21167,13 +21082,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-stream@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "is-stream@npm:3.0.0"
|
||||
checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-string@npm:^1.0.7, is-string@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "is-string@npm:1.1.1"
|
||||
@@ -22040,13 +21948,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-tokens@npm:^9.0.1":
|
||||
version: 9.0.1
|
||||
resolution: "js-tokens@npm:9.0.1"
|
||||
checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:3.x, js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.1":
|
||||
version: 3.14.1
|
||||
resolution: "js-yaml@npm:3.14.1"
|
||||
@@ -22122,40 +22023,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsdom@npm:^24.0.0":
|
||||
version: 24.1.3
|
||||
resolution: "jsdom@npm:24.1.3"
|
||||
dependencies:
|
||||
cssstyle: "npm:^4.0.1"
|
||||
data-urls: "npm:^5.0.0"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
form-data: "npm:^4.0.0"
|
||||
html-encoding-sniffer: "npm:^4.0.0"
|
||||
http-proxy-agent: "npm:^7.0.2"
|
||||
https-proxy-agent: "npm:^7.0.5"
|
||||
is-potential-custom-element-name: "npm:^1.0.1"
|
||||
nwsapi: "npm:^2.2.12"
|
||||
parse5: "npm:^7.1.2"
|
||||
rrweb-cssom: "npm:^0.7.1"
|
||||
saxes: "npm:^6.0.0"
|
||||
symbol-tree: "npm:^3.2.4"
|
||||
tough-cookie: "npm:^4.1.4"
|
||||
w3c-xmlserializer: "npm:^5.0.0"
|
||||
webidl-conversions: "npm:^7.0.0"
|
||||
whatwg-encoding: "npm:^3.1.1"
|
||||
whatwg-mimetype: "npm:^4.0.0"
|
||||
whatwg-url: "npm:^14.0.0"
|
||||
ws: "npm:^8.18.0"
|
||||
xml-name-validator: "npm:^5.0.0"
|
||||
peerDependencies:
|
||||
canvas: ^2.11.2
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
checksum: 10c0/e48b342afacd7418a23dac204a62deea729c50f4d072a7c04c09fd32355fdb4335f8779fa79fd0277a2dbeb2d356250a950955719d00047324b251233b11277f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsdom@npm:^25.0.1":
|
||||
version: 25.0.1
|
||||
resolution: "jsdom@npm:25.0.1"
|
||||
@@ -22764,16 +22631,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"local-pkg@npm:^0.5.0":
|
||||
version: 0.5.1
|
||||
resolution: "local-pkg@npm:0.5.1"
|
||||
dependencies:
|
||||
mlly: "npm:^1.7.3"
|
||||
pkg-types: "npm:^1.2.1"
|
||||
checksum: 10c0/ade8346f1dc04875921461adee3c40774b00d4b74095261222ebd4d5fd0a444676e36e325f76760f21af6a60bc82480e154909b54d2d9f7173671e36dacf1808
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"localforage@npm:^1.10.0":
|
||||
version: 1.10.0
|
||||
resolution: "localforage@npm:1.10.0"
|
||||
@@ -23009,7 +22866,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"loupe@npm:^2.3.6, loupe@npm:^2.3.7":
|
||||
"loupe@npm:^2.3.6":
|
||||
version: 2.3.7
|
||||
resolution: "loupe@npm:2.3.7"
|
||||
dependencies:
|
||||
@@ -23096,7 +22953,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.30.12, magic-string@npm:^0.30.17, magic-string@npm:^0.30.5":
|
||||
"magic-string@npm:^0.30.12, magic-string@npm:^0.30.17":
|
||||
version: 0.30.19
|
||||
resolution: "magic-string@npm:0.30.19"
|
||||
dependencies:
|
||||
@@ -23681,13 +23538,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-fn@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "mimic-fn@npm:4.0.0"
|
||||
checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-function@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "mimic-function@npm:5.0.1"
|
||||
@@ -23940,7 +23790,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mlly@npm:^1.7.3, mlly@npm:^1.7.4":
|
||||
"mlly@npm:^1.7.4":
|
||||
version: 1.8.0
|
||||
resolution: "mlly@npm:1.8.0"
|
||||
dependencies:
|
||||
@@ -24701,15 +24551,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"npm-run-path@npm:^5.1.0":
|
||||
version: 5.3.0
|
||||
resolution: "npm-run-path@npm:5.3.0"
|
||||
dependencies:
|
||||
path-key: "npm:^4.0.0"
|
||||
checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nth-check@npm:^2.0.1, nth-check@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "nth-check@npm:2.1.1"
|
||||
@@ -24916,15 +24757,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"onetime@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "onetime@npm:6.0.0"
|
||||
dependencies:
|
||||
mimic-fn: "npm:^4.0.0"
|
||||
checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"onetime@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "onetime@npm:7.0.0"
|
||||
@@ -25219,15 +25051,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-limit@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "p-limit@npm:5.0.0"
|
||||
dependencies:
|
||||
yocto-queue: "npm:^1.0.0"
|
||||
checksum: 10c0/574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-locate@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "p-locate@npm:3.0.0"
|
||||
@@ -25512,13 +25335,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-key@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "path-key@npm:4.0.0"
|
||||
checksum: 10c0/794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "path-parse@npm:1.0.7"
|
||||
@@ -25567,7 +25383,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pathe@npm:^1.1.1, pathe@npm:^1.1.2":
|
||||
"pathe@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "pathe@npm:1.1.2"
|
||||
checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897
|
||||
@@ -25697,7 +25513,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pkg-types@npm:^1.2.1, pkg-types@npm:^1.3.1":
|
||||
"pkg-types@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "pkg-types@npm:1.3.1"
|
||||
dependencies:
|
||||
@@ -26143,7 +25959,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"psl@npm:^1.1.33, psl@npm:^1.9.0":
|
||||
"psl@npm:^1.9.0":
|
||||
version: 1.15.0
|
||||
resolution: "psl@npm:1.15.0"
|
||||
dependencies:
|
||||
@@ -26259,13 +26075,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"querystringify@npm:^2.1.1":
|
||||
version: 2.2.0
|
||||
resolution: "querystringify@npm:2.2.0"
|
||||
checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"queue-microtask@npm:^1.2.2":
|
||||
version: 1.2.3
|
||||
resolution: "queue-microtask@npm:1.2.3"
|
||||
@@ -28886,7 +28695,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"std-env@npm:^3.5.0, std-env@npm:^3.8.0":
|
||||
"std-env@npm:^3.8.0":
|
||||
version: 3.9.0
|
||||
resolution: "std-env@npm:3.9.0"
|
||||
checksum: 10c0/4a6f9218aef3f41046c3c7ecf1f98df00b30a07f4f35c6d47b28329bc2531eef820828951c7d7b39a1c5eb19ad8a46e3ddfc7deb28f0a2f3ceebee11bab7ba50
|
||||
@@ -29206,13 +29015,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-final-newline@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "strip-final-newline@npm:3.0.0"
|
||||
checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-hex-prefix@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "strip-hex-prefix@npm:1.0.0"
|
||||
@@ -29245,15 +29047,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-literal@npm:^2.0.0":
|
||||
version: 2.1.1
|
||||
resolution: "strip-literal@npm:2.1.1"
|
||||
dependencies:
|
||||
js-tokens: "npm:^9.0.1"
|
||||
checksum: 10c0/66a7353f5ba1ae6a4fb2805b4aba228171847200640083117c41512692e6b2c020e18580402984f55c0ae69c30f857f9a55abd672863e4ca8fdb463fdf93ba19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strnum@npm:^1.1.1":
|
||||
version: 1.1.2
|
||||
resolution: "strnum@npm:1.1.2"
|
||||
@@ -29771,7 +29564,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinybench@npm:^2.5.1, tinybench@npm:^2.9.0":
|
||||
"tinybench@npm:^2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "tinybench@npm:2.9.0"
|
||||
checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c
|
||||
@@ -29812,13 +29605,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^0.8.3":
|
||||
version: 0.8.4
|
||||
resolution: "tinypool@npm:0.8.4"
|
||||
checksum: 10c0/779c790adcb0316a45359652f4b025958c1dff5a82460fe49f553c864309b12ad732c8288be52f852973bc76317f5e7b3598878aee0beb8a33322c0e72c4a66c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^1.0.1":
|
||||
version: 1.1.1
|
||||
resolution: "tinypool@npm:1.1.1"
|
||||
@@ -29833,13 +29619,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyspy@npm:^2.2.0":
|
||||
version: 2.2.1
|
||||
resolution: "tinyspy@npm:2.2.1"
|
||||
checksum: 10c0/0b4cfd07c09871e12c592dfa7b91528124dc49a4766a0b23350638c62e6a483d5a2a667de7e6282246c0d4f09996482ddaacbd01f0c05b7ed7e0f79d32409bdc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyspy@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "tinyspy@npm:3.0.2"
|
||||
@@ -29931,18 +29710,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tough-cookie@npm:^4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "tough-cookie@npm:4.1.4"
|
||||
dependencies:
|
||||
psl: "npm:^1.1.33"
|
||||
punycode: "npm:^2.1.1"
|
||||
universalify: "npm:^0.2.0"
|
||||
url-parse: "npm:^1.5.3"
|
||||
checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tough-cookie@npm:^5.0.0":
|
||||
version: 5.1.2
|
||||
resolution: "tough-cookie@npm:5.1.2"
|
||||
@@ -30676,13 +30443,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "universalify@npm:0.2.0"
|
||||
checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"universalify@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "universalify@npm:2.0.1"
|
||||
@@ -30787,16 +30547,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"url-parse@npm:^1.5.3":
|
||||
version: 1.5.10
|
||||
resolution: "url-parse@npm:1.5.10"
|
||||
dependencies:
|
||||
querystringify: "npm:^2.1.1"
|
||||
requires-port: "npm:^1.0.0"
|
||||
checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-callback-ref@npm:^1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "use-callback-ref@npm:1.3.3"
|
||||
@@ -30984,21 +30734,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:1.6.1":
|
||||
version: 1.6.1
|
||||
resolution: "vite-node@npm:1.6.1"
|
||||
dependencies:
|
||||
cac: "npm:^6.7.14"
|
||||
debug: "npm:^4.3.4"
|
||||
pathe: "npm:^1.1.1"
|
||||
picocolors: "npm:^1.0.0"
|
||||
vite: "npm:^5.0.0"
|
||||
bin:
|
||||
vite-node: vite-node.mjs
|
||||
checksum: 10c0/4d96da9f11bd0df8b60c46e65a740edaad7dd2d1aff3cdb3da5714ea8c10b5f2683111b60bfe45545c7e8c1f33e7e8a5095573d5e9ba55f50a845233292c2e02
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:2.1.9":
|
||||
version: 2.1.9
|
||||
resolution: "vite-node@npm:2.1.9"
|
||||
@@ -31180,56 +30915,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:^1.6.0":
|
||||
version: 1.6.1
|
||||
resolution: "vitest@npm:1.6.1"
|
||||
dependencies:
|
||||
"@vitest/expect": "npm:1.6.1"
|
||||
"@vitest/runner": "npm:1.6.1"
|
||||
"@vitest/snapshot": "npm:1.6.1"
|
||||
"@vitest/spy": "npm:1.6.1"
|
||||
"@vitest/utils": "npm:1.6.1"
|
||||
acorn-walk: "npm:^8.3.2"
|
||||
chai: "npm:^4.3.10"
|
||||
debug: "npm:^4.3.4"
|
||||
execa: "npm:^8.0.1"
|
||||
local-pkg: "npm:^0.5.0"
|
||||
magic-string: "npm:^0.30.5"
|
||||
pathe: "npm:^1.1.1"
|
||||
picocolors: "npm:^1.0.0"
|
||||
std-env: "npm:^3.5.0"
|
||||
strip-literal: "npm:^2.0.0"
|
||||
tinybench: "npm:^2.5.1"
|
||||
tinypool: "npm:^0.8.3"
|
||||
vite: "npm:^5.0.0"
|
||||
vite-node: "npm:1.6.1"
|
||||
why-is-node-running: "npm:^2.2.2"
|
||||
peerDependencies:
|
||||
"@edge-runtime/vm": "*"
|
||||
"@types/node": ^18.0.0 || >=20.0.0
|
||||
"@vitest/browser": 1.6.1
|
||||
"@vitest/ui": 1.6.1
|
||||
happy-dom: "*"
|
||||
jsdom: "*"
|
||||
peerDependenciesMeta:
|
||||
"@edge-runtime/vm":
|
||||
optional: true
|
||||
"@types/node":
|
||||
optional: true
|
||||
"@vitest/browser":
|
||||
optional: true
|
||||
"@vitest/ui":
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
bin:
|
||||
vitest: vitest.mjs
|
||||
checksum: 10c0/511d27d7f697683964826db2fad7ac303f9bc7eeb59d9422111dc488371ccf1f9eed47ac3a80eb47ca86b7242228ba5ca9cc3613290830d0e916973768cac215
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:^2.1.8":
|
||||
version: 2.1.9
|
||||
resolution: "vitest@npm:2.1.9"
|
||||
@@ -31835,7 +31520,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"why-is-node-running@npm:^2.2.2, why-is-node-running@npm:^2.3.0":
|
||||
"why-is-node-running@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "why-is-node-running@npm:2.3.0"
|
||||
dependencies:
|
||||
@@ -32253,13 +31938,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yocto-queue@npm:^1.0.0":
|
||||
version: 1.2.1
|
||||
resolution: "yocto-queue@npm:1.2.1"
|
||||
checksum: 10c0/5762caa3d0b421f4bdb7a1926b2ae2189fc6e4a14469258f183600028eb16db3e9e0306f46e8ebf5a52ff4b81a881f22637afefbef5399d6ad440824e9b27f9f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yoctocolors-cjs@npm:^2.1.2":
|
||||
version: 2.1.3
|
||||
resolution: "yoctocolors-cjs@npm:2.1.3"
|
||||
|
||||
Reference in New Issue
Block a user