From 1c4c42637938dbdbcc80e4001d60240f001b0738 Mon Sep 17 00:00:00 2001 From: seshanthS Date: Fri, 22 Aug 2025 14:55:48 +0530 Subject: [PATCH 1/5] fix: extractMRZ --- common/package.json | 2 +- .../screens/PassportCameraScreen.tsx | 4 - .../mobile-sdk-alpha/src/processing/mrz.ts | 98 ++++++++++++++----- packages/mobile-sdk-alpha/src/types/public.ts | 4 - .../tests/processing/mrz.test.ts | 26 ++++- 5 files changed, 100 insertions(+), 34 deletions(-) diff --git a/common/package.json b/common/package.json index 9a4cf02a7..9a48c42c3 100644 --- a/common/package.json +++ b/common/package.json @@ -281,7 +281,7 @@ "import": "./dist/esm/src/utils/passports/passport_parsing/parseDscCertificateData.js", "require": "./dist/cjs/src/utils/passports/passport_parsing/parseDscCertificateData.cjs" }, - "./utils/proving": { + "./utils/proving": { "types": "./dist/esm/src/utils/proving.d.ts", "import": "./dist/esm/src/utils/proving.js", "require": "./dist/cjs/src/utils/proving.cjs" diff --git a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx index 073323299..c24f6cfdf 100644 --- a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx +++ b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx @@ -14,10 +14,6 @@ export const PassportCameraScreen = ({ onMRZDetected }: PassportCameraProps) => passportNumber: 'L898902C3', dateOfBirth: '740812', dateOfExpiry: '120415', - surname: 'ERIKSSON', - givenNames: 'ANNA MARIA', - sex: 'F', - nationality: 'UTO', issuingCountry: 'UTO', documentType: 'P', validation: { diff --git a/packages/mobile-sdk-alpha/src/processing/mrz.ts b/packages/mobile-sdk-alpha/src/processing/mrz.ts index fbc45d7b9..000d0b5a6 100644 --- a/packages/mobile-sdk-alpha/src/processing/mrz.ts +++ b/packages/mobile-sdk-alpha/src/processing/mrz.ts @@ -111,9 +111,19 @@ function validateTD3Format(lines: string[]): boolean { if (lines.length !== 2) { return false; } + const TD3_line_2_REGEX = /^([A-Z0-9<]{9})([0-9ILDSOG])([A-Z<]{3})/; + const isTD3 = TD3_line_2_REGEX.test(lines[1]); + return isTD3; +} + +function validateTD1Format(lines: string[]): boolean { + console.log('validateTD1Format', lines); - // TD3 format: 2 lines, 44 characters each - return lines[0].length === 44 && lines[1].length === 44; + const concatenatedLines = lines[0] + lines[1]; + const TD1_REGEX = + /^(?[A-Z0-9<]{2})(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})(?[A-Z0-9<]{15})(?[0-9]{6})(?[0-9]{1})(?[MF<]{1})(?[0-9]{6})(?[0-9]{1})(?[A-Z<]{3})(?[A-Z0-9<]{7})/; + const isTD1 = TD1_REGEX.test(concatenatedLines) || lines[0].startsWith('I'); + return isTD1; } /** @@ -131,8 +141,6 @@ function extractTD3Info(lines: string[]): Omit { .slice(2, 5) .replace(/ { nationality = rawNat.slice(0, 3).replace(/[^A-Z]/g, ''); } const dateOfBirth = line2.slice(13, 19); - const sex = line2.slice(20, 21).replace(/ { + const line1 = lines[0]; + const line2 = lines[1]; + + const concatenatedLines = line1 + line2; + + return { + documentType: concatenatedLines.slice(0, 2), + issuingCountry: concatenatedLines.slice(2, 5), + passportNumber: concatenatedLines.slice(5, 14).replace(/ { + const line1 = lines[0]; + const line2 = lines[1]; + const concatenatedLines = line1 + line2; + + const documentNumber = concatenatedLines.slice(5, 14); + const documentNumberCheckDigit = concatenatedLines.slice(14, 15); + const dateOfBirth = concatenatedLines.slice(30, 36); + const dobCheckDigit = concatenatedLines.slice(36, 37); + const dateOfExpiry = concatenatedLines.slice(38, 44); + const expiryCheckDigit = concatenatedLines.slice(44, 45); + + return { + passportNumberChecksum: verifyCheckDigit(documentNumber, documentNumberCheckDigit), + dateOfBirthChecksum: verifyCheckDigit(dateOfBirth, dobCheckDigit), + dateOfExpiryChecksum: verifyCheckDigit(dateOfExpiry, expiryCheckDigit), + compositeChecksum: true, // TD1 doesn't have a composite check digit like TD3 + }; +} + /** * Validate all check digits for TD3 MRZ * TD3 Line 2 format: PASSPORT(9)CHECK(1)NATIONALITY(3)DOB(6)DOBCHECK(1)SEX(1)EXPIRY(6)EXPIRYCHECK(1)PERSONAL(14)PERSONALCHECK(1)FINALCHECK(1) @@ -215,25 +256,36 @@ export function extractMRZInfo(mrzString: string): MRZInfo { // Validate format const isValidTD3 = validateTD3Format(lines); + const isValidTD1 = validateTD1Format(lines); - if (!isValidTD3) { + if (!isValidTD3 && !isValidTD1) { throw new MrzParseError( - `Invalid MRZ format: Expected TD3 format (2 lines × 44 characters), got ${lines.length} lines with lengths [${lines.map(l => l.length).join(', ')}]`, + `Invalid MRZ format: Expected TD3 or TD1 format, got ${lines.length} lines with lengths [${lines.map(l => l.length).join(', ')}]`, ); } - // Extract basic information - const info = extractTD3Info(lines); - - // Validate check digits - const checksums = validateTD3CheckDigits(lines); - - // Create validation result - const validation: MRZValidation = { - format: isValidTD3, - ...checksums, - overall: isValidTD3 && Object.values(checksums).every(Boolean), - }; + let info: Omit; + let checksums: Omit; + let validation: MRZValidation; + + if (isValidTD3) { + // Extract basic information + info = extractTD3Info(lines); + checksums = validateTD3CheckDigits(lines); + validation = { + format: isValidTD3, + ...checksums, + overall: isValidTD3 && Object.values(checksums).every(Boolean), + }; + } else { + info = extractTD1Info(lines); + checksums = validateTD1CheckDigits(lines); + validation = { + format: isValidTD1, + ...checksums, + overall: isValidTD1 && Object.values(checksums).every(Boolean), + }; + } return { ...info, diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index dc13ffbe8..6f1a0776c 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -23,10 +23,6 @@ export interface MRZInfo { passportNumber: string; dateOfBirth: string; dateOfExpiry: string; - surname: string; - givenNames: string; - sex: string; - nationality: string; issuingCountry: string; documentType: string; validation: MRZValidation; diff --git a/packages/mobile-sdk-alpha/tests/processing/mrz.test.ts b/packages/mobile-sdk-alpha/tests/processing/mrz.test.ts index 4fb3619eb..b029eddc9 100644 --- a/packages/mobile-sdk-alpha/tests/processing/mrz.test.ts +++ b/packages/mobile-sdk-alpha/tests/processing/mrz.test.ts @@ -1,11 +1,13 @@ import { describe, expect, it } from 'vitest'; -import { formatDateToYYMMDD, MrzParseError } from '../../src'; -import { extractMRZInfo } from '../../src/mrz'; +import { MrzParseError } from '../../src/errors'; +import { extractMRZInfo, formatDateToYYMMDD } from '../../src/processing/mrz'; const sample = `P { it('parses valid TD3 MRZ', () => { const info = extractMRZInfo(sample); @@ -13,6 +15,26 @@ describe('extractMRZInfo', () => { expect(info.validation.overall).toBe(true); }); + it('parses valid TD1 MRZ', () => { + const info = extractMRZInfo(sampleTD1); + expect(info.passportNumber).toBe('X4RTBPFW4'); + expect(info.issuingCountry).toBe('FRA'); + expect(info.dateOfBirth).toBe('900713'); + expect(info.dateOfExpiry).toBe('300211'); + expect(info.validation.overall).toBe(true); + }); + + it('rejects invalid TD1 MRZ', () => { + const invalid = `FRAX4RTBPFW46`; + expect(() => extractMRZInfo(invalid)).toThrow(); + }); + + it('Fails overall validation for invalid TD1 MRZ', () => { + const invalid = `IDFRAX4RTBPFW46`; + const info = extractMRZInfo(invalid); + expect(info.validation.overall).toBe(false); + }); + it('rejects malformed MRZ', () => { const invalid = 'P extractMRZInfo(invalid)).toThrowError(MrzParseError); From 301da83ad128d2954149dbfb8696f5b0672498d2 Mon Sep 17 00:00:00 2001 From: seshanthS Date: Sun, 24 Aug 2025 01:23:53 +0530 Subject: [PATCH 2/5] yarn nice && yarn types --- app/src/components/native/PassportCamera.tsx | 4 -- .../mobile-sdk-alpha/src/processing/mrz.ts | 61 ------------------- 2 files changed, 65 deletions(-) diff --git a/app/src/components/native/PassportCamera.tsx b/app/src/components/native/PassportCamera.tsx index ea588e22c..d80596fca 100644 --- a/app/src/components/native/PassportCamera.tsx +++ b/app/src/components/native/PassportCamera.tsx @@ -102,10 +102,6 @@ export const PassportCamera: React.FC = ({ dateOfExpiry: event.nativeEvent.data.expiryDate, documentType: event.nativeEvent.data.documentType, issuingCountry: event.nativeEvent.data.countryCode, - nationality: event.nativeEvent.data.countryCode, // TODO: Verify if native module provides separate nationality code instead of defaulting to issuingCountry - surname: '', // Fill with defaults as they're required - givenNames: '', - sex: '', validation: { format: false, // Changed from true - avoid assuming validation success before actual checks passportNumberChecksum: false, // Changed from true - avoid assuming validation success before actual checks diff --git a/packages/mobile-sdk-alpha/src/processing/mrz.ts b/packages/mobile-sdk-alpha/src/processing/mrz.ts index 000d0b5a6..32ef28947 100644 --- a/packages/mobile-sdk-alpha/src/processing/mrz.ts +++ b/packages/mobile-sdk-alpha/src/processing/mrz.ts @@ -45,65 +45,6 @@ function verifyCheckDigit(field: string, expectedCheckDigit: string): boolean { } } -/** - * Parse names from MRZ format (surname<= 0; i--) { - const part = parts[i]; - // If a part contains '<' within it (not just trailing '<'), it's likely given names - if (part.includes('<') && !part.endsWith('<'.repeat(part.length))) { - givenNamesStartIndex = i; - break; - } - } - - // If we didn't find a clear given names section, use the first << as boundary - if (givenNamesStartIndex >= parts.length) { - const firstDoubleSeparator = nameField.indexOf('<<'); - if (firstDoubleSeparator === -1) { - return { - surname: nameField.replace(/[A-Z0-9<]{2})(?[A-Z<]{3})(?[A-Z0-9<]{9})(?[0-9]{1})(?[A-Z0-9<]{15})(?[0-9]{6})(?[0-9]{1})(?[MF<]{1})(?[0-9]{6})(?[0-9]{1})(?[A-Z<]{3})(?[A-Z0-9<]{7})/; From bb9f3caac02e5e3315b9136b4287fa246946b95a Mon Sep 17 00:00:00 2001 From: seshanthS Date: Sun, 24 Aug 2025 23:05:44 +0530 Subject: [PATCH 3/5] fix test: remove unused --- app/tests/src/components/PassportCamera.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tests/src/components/PassportCamera.test.tsx b/app/tests/src/components/PassportCamera.test.tsx index 7b6b5f452..32fb6778e 100644 --- a/app/tests/src/components/PassportCamera.test.tsx +++ b/app/tests/src/components/PassportCamera.test.tsx @@ -67,7 +67,6 @@ describe('PassportCamera components', () => { dateOfBirth: '900101', documentType: 'P', issuingCountry: 'UTO', - nationality: 'UTO', }), ); }); From 2836a251d9c3cb88dbb8a735cb461af0f7f54651 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 24 Aug 2025 12:36:42 -0700 Subject: [PATCH 4/5] fix mobile ci --- .github/workflows/mobile-ci.yml | 153 ++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 57 deletions(-) diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index fdae7dbd7..9617a23db 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -20,8 +20,13 @@ on: paths: - "common/**" - "app/**" - - ".github/workflows/app.yml" + - ".github/workflows/mobile-ci.yml" - ".github/actions/**" + workflow_dispatch: {} + +concurrency: + group: mobile-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: lint: @@ -45,16 +50,14 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - - name: Cache Node Modules - uses: actions/cache@v4 + - name: Cache Yarn + uses: ./.github/actions/cache-yarn with: path: | .yarn/cache node_modules app/node_modules - key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn- + cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - name: Install Dependencies uses: ./.github/actions/yarn-install - name: Build Dependencies @@ -88,16 +91,14 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - - name: Cache Node Modules - uses: actions/cache@v4 + - name: Cache Yarn + uses: ./.github/actions/cache-yarn with: path: | .yarn/cache node_modules app/node_modules - key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn- + cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - name: Install Dependencies uses: ./.github/actions/yarn-install @@ -107,7 +108,7 @@ jobs: - name: Test run: yarn test working-directory: ./app - build: + build-ios: runs-on: macos-latest env: # iOS project configuration - hardcoded for CI stability @@ -156,14 +157,14 @@ jobs: ruby-version: ${{ env.RUBY_VERSION }} bundler-cache: false working-directory: ./app - - - name: Cache Node modules - uses: actions/cache@v4 + - name: Cache Yarn + uses: ./.github/actions/cache-yarn with: - path: app/node_modules - key: ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('app/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.NODE_VERSION_SANITIZED }}- + path: | + .yarn/cache + node_modules + app/node_modules + cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} - name: Cache Ruby gems uses: ./.github/actions/cache-bundler with: @@ -177,6 +178,7 @@ jobs: app/ios/Pods ~/Library/Caches/CocoaPods lock-file: app/ios/Podfile.lock + cache-version: ${{ env.GH_CACHE_VERSION }} - name: Cache Xcode build uses: actions/cache@v4 with: @@ -195,31 +197,6 @@ jobs: key: ${{ runner.os }}-xcode-index-${{ hashFiles('app/ios/Podfile.lock') }} restore-keys: | ${{ runner.os }}-xcode-index- - - name: Cache Gradle - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('app/android/**/gradle-wrapper.properties', 'app/android/**/gradle-wrapper.jar') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Setup Java environment - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: ${{ env.JAVA_VERSION }} - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - with: - accept-android-sdk-licenses: true - - name: Cache NDK - uses: actions/cache@v4 - with: - path: ${{ env.ANDROID_HOME }}/ndk/${{ env.ANDROID_NDK_VERSION }} - key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }} - - name: Install NDK - run: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}" - name: Install Mobile Dependencies uses: ./.github/actions/yarn-install - name: Build Dependencies @@ -237,22 +214,23 @@ jobs: - name: Install iOS Dependencies run: | echo "Installing iOS dependencies..." - (cd app/ios && pod install --silent) || { echo "❌ Pod install failed"; exit 1; } - echo "✅ Pods installed successfully" - working-directory: ./app/ios + cd ios + # Reuse the same guarded flow as local to ensure reproducibility + bundle exec bash scripts/pod-install-with-cache-fix.sh + working-directory: ./app - name: Verify iOS Workspace run: | echo "Verifying iOS workspace setup..." - WORKSPACE_PATH="app/ios/${{ env.IOS_PROJECT_NAME }}.xcworkspace" + WORKSPACE_PATH="ios/${{ env.IOS_PROJECT_NAME }}.xcworkspace" if [ ! -d "$WORKSPACE_PATH" ]; then echo "❌ Workspace not found at: $WORKSPACE_PATH" echo "Available workspaces:" - find app/ios -name "*.xcworkspace" -type d + find ios -name "*.xcworkspace" -type d exit 1 fi - if [ ! -d "Pods" ]; then + if [ ! -d "ios/Pods" ]; then echo "❌ Pods directory is missing" exit 1 fi @@ -272,25 +250,86 @@ jobs: echo "✅ iOS workspace is properly configured" echo "✅ Using workspace: $WORKSPACE_PATH" echo "✅ Using scheme: ${{ env.IOS_PROJECT_SCHEME }}" - working-directory: ./app/ios + working-directory: ./app - name: Build iOS run: | - echo "Building iOS app..." + echo "Building iOS app for simulator (no signing required)..." echo "Project: ${{ env.IOS_PROJECT_NAME }}, Scheme: ${{ env.IOS_PROJECT_SCHEME }}" - WORKSPACE_PATH="app/ios/${{ env.IOS_PROJECT_NAME }}.xcworkspace" + WORKSPACE_PATH="ios/${{ env.IOS_PROJECT_NAME }}.xcworkspace" - # Use cached derived data and enable parallel builds for faster compilation + # Build for iOS Simulator to avoid code signing issues in CI xcodebuild -workspace "$WORKSPACE_PATH" \ -scheme ${{ env.IOS_PROJECT_SCHEME }} \ -configuration Release \ - -destination "generic/platform=iOS" \ - -derivedDataPath app/ios/build \ + -sdk iphonesimulator \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath ios/build \ -jobs "$(sysctl -n hw.ncpu)" \ -parallelizeTargets \ -quiet || { echo "❌ iOS build failed"; exit 1; } echo "✅ iOS build succeeded" working-directory: ./app + + build-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Read and sanitize Node.js version + shell: bash + run: | + if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then + echo "❌ .nvmrc is missing or empty"; exit 1; + fi + VERSION="$(tr -d '\r\n' < .nvmrc)" + VERSION="${VERSION#v}" + if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then + echo "Invalid .nvmrc content: '$VERSION'"; exit 1; + fi + echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV" + echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV" + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Cache Yarn + uses: ./.github/actions/cache-yarn + with: + path: | + .yarn/cache + node_modules + app/node_modules + cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.NODE_VERSION_SANITIZED }} + - name: Cache Gradle + uses: ./.github/actions/cache-gradle + with: + cache-version: ${{ env.GH_CACHE_VERSION }} + - name: Setup Java environment + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ env.JAVA_VERSION }} + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + with: + accept-android-sdk-licenses: true + - name: Cache NDK + uses: actions/cache@v4 + with: + path: ${{ env.ANDROID_HOME }}/ndk/${{ env.ANDROID_NDK_VERSION }} + key: ${{ runner.os }}-ndk-${{ env.ANDROID_NDK_VERSION }} + - name: Install NDK + run: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}" + - name: Install Mobile Dependencies + uses: ./.github/actions/yarn-install + - name: Build Dependencies + run: | + echo "Building dependencies..." + yarn workspace @selfxyz/mobile-app run build:deps --silent || { echo "❌ Dependency build failed"; exit 1; } + echo "✅ Dependencies built successfully" + working-directory: ./app - name: Build Android - run: yarn android + run: | + cd android + ./gradlew assembleDebug working-directory: ./app From 3803b3f9403b2a68ac37afe84994ade15dc92c41 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Sun, 24 Aug 2025 12:47:58 -0700 Subject: [PATCH 5/5] add script --- app/ios/scripts/pod-install-with-cache-fix.sh | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/ios/scripts/pod-install-with-cache-fix.sh diff --git a/app/ios/scripts/pod-install-with-cache-fix.sh b/app/ios/scripts/pod-install-with-cache-fix.sh new file mode 100644 index 000000000..f03b09bd7 --- /dev/null +++ b/app/ios/scripts/pod-install-with-cache-fix.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Pod install with hermes-engine cache fix for React Native upgrades +# This script handles CocoaPods cache mismatches that occur after React Native version upgrades + +set -e # Exit on any error + +echo "🧹 Clearing CocoaPods cache to prevent hermes-engine version conflicts..." +bundle exec pod cache clean --all > /dev/null 2>&1 || true +rm -rf ~/Library/Caches/CocoaPods > /dev/null 2>&1 || true + +echo "📦 Attempting pod install..." +if bundle exec pod install; then + echo "✅ Pods installed successfully" +else + echo "⚠️ Pod install failed, likely due to hermes-engine cache mismatch after React Native upgrade" + echo "🔧 Running targeted fix: bundle exec pod update hermes-engine..." + bundle exec pod update hermes-engine --no-repo-update + echo "🔄 Retrying pod install..." + bundle exec pod install + echo "✅ Pods installed successfully after cache fix" +fi