Merge pull request #1669 from selfxyz/release/staging-2026-01-29

chore: fix circuits tests in staging
This commit is contained in:
Justin Hernandez
2026-01-28 22:10:55 -08:00
committed by GitHub
69 changed files with 2623 additions and 869 deletions

View File

@@ -0,0 +1,47 @@
name: Cache Core SDK Build
description: Cache core SDK build artifacts (common, sdk/core)
inputs:
mode:
description: "save or restore"
required: true
cache-version:
description: Cache version string
required: false
default: v1
fail-on-cache-miss:
description: Fail if cache not found (restore mode only)
required: false
default: "false"
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.restore.outputs.cache-hit }}
runs:
using: composite
steps:
- id: restore
if: inputs.mode == 'restore'
uses: actions/cache/restore@v4
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
- id: save
if: inputs.mode == 'save'
uses: actions/cache/save@v4
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}

View File

@@ -0,0 +1,47 @@
name: Cache Mobile SDK Build
description: Cache mobile SDK build artifacts (common, mobile-sdk-alpha)
inputs:
mode:
description: "save or restore"
required: true
cache-version:
description: Cache version string
required: false
default: v1
fail-on-cache-miss:
description: Fail if cache not found (restore mode only)
required: false
default: "false"
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.restore.outputs.cache-hit }}
runs:
using: composite
steps:
- id: restore
if: inputs.mode == 'restore'
uses: actions/cache/restore@v4
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ inputs.cache-version }}-${{ github.sha }}
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
- id: save
if: inputs.mode == 'save'
uses: actions/cache/save@v4
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ inputs.cache-version }}-${{ github.sha }}

View File

@@ -0,0 +1,43 @@
name: Cache SDK Build
description: Cache SDK build artifacts (common, sdk-common, qrcode)
inputs:
mode:
description: "save or restore"
required: true
cache-version:
description: Cache version string
required: false
default: v1
fail-on-cache-miss:
description: Fail if cache not found (restore mode only)
required: false
default: "false"
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.restore.outputs.cache-hit }}
runs:
using: composite
steps:
- id: restore
if: inputs.mode == 'restore'
uses: actions/cache/restore@v4
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}
fail-on-cache-miss: ${{ inputs.fail-on-cache-miss }}
- id: save
if: inputs.mode == 'save'
uses: actions/cache/save@v4
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ inputs.cache-version }}-${{ github.sha }}

View File

@@ -33,6 +33,7 @@ concurrency:
jobs:
build:
runs-on: ["128ram"]
timeout-minutes: 720 # 12 hours
permissions:
contents: read
actions: read

View File

@@ -56,15 +56,10 @@ jobs:
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/core build
- name: Cache build artifacts
uses: actions/cache/save@v4
uses: ./.github/actions/cache-core-sdk-build
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ github.sha }}
mode: save
cache-version: v1
lint:
runs-on: ubuntu-latest
@@ -80,19 +75,19 @@ jobs:
corepack prepare yarn@4.12.0 --activate
- name: Restore build artifacts
id: build-cache
uses: actions/cache/restore@v4
uses: ./.github/actions/cache-core-sdk-build
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Install Dependencies
if: steps.build-cache.outputs.cache-hit != 'true'
uses: ./.github/actions/yarn-install
- name: Build dependencies (fallback on cache miss)
if: steps.build-cache.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/core build
- name: Run linter
run: yarn workspace @selfxyz/core lint
@@ -110,19 +105,19 @@ jobs:
corepack prepare yarn@4.12.0 --activate
- name: Restore build artifacts
id: build-cache
uses: actions/cache/restore@v4
uses: ./.github/actions/cache-core-sdk-build
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Install Dependencies
if: steps.build-cache.outputs.cache-hit != 'true'
uses: ./.github/actions/yarn-install
- name: Build dependencies (fallback on cache miss)
if: steps.build-cache.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/core build
- name: Type checking
run: yarn workspace @selfxyz/core types
@@ -140,18 +135,18 @@ jobs:
corepack prepare yarn@4.12.0 --activate
- name: Restore build artifacts
id: build-cache
uses: actions/cache/restore@v4
uses: ./.github/actions/cache-core-sdk-build
with:
path: |
common/dist
sdk/core/dist
node_modules
sdk/core/node_modules
common/node_modules
key: core-sdk-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Install Dependencies
if: steps.build-cache.outputs.cache-hit != 'true'
uses: ./.github/actions/yarn-install
- name: Build dependencies (fallback on cache miss)
if: steps.build-cache.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/core build
- name: Run tests
run: yarn workspace @selfxyz/core test

View File

@@ -21,15 +21,10 @@ jobs:
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Cache build artifacts
uses: actions/cache/save@v4
uses: ./.github/actions/cache-mobile-sdk-build
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ github.sha }}
mode: save
cache-version: v1
lint:
runs-on: ubuntu-latest
@@ -39,16 +34,17 @@ jobs:
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-mobile-sdk-build
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Run linter
run: yarn workspace @selfxyz/mobile-sdk-alpha lint
@@ -60,16 +56,17 @@ jobs:
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-mobile-sdk-build
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Check Prettier formatting
run: yarn workspace @selfxyz/mobile-sdk-alpha prettier --check .
@@ -81,16 +78,17 @@ jobs:
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-mobile-sdk-build
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Type checking
run: yarn workspace @selfxyz/mobile-sdk-alpha types
@@ -102,15 +100,16 @@ jobs:
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-mobile-sdk-build
with:
path: |
common/dist
packages/mobile-sdk-alpha/dist
node_modules
packages/mobile-sdk-alpha/node_modules
common/node_modules
key: mobile-sdk-alpha-build-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: v1
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/mobile-sdk-alpha build
- name: Run tests
run: yarn workspace @selfxyz/mobile-sdk-alpha test

View File

@@ -80,16 +80,14 @@ jobs:
- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
- name: Install Dependencies
uses: ./.github/actions/yarn-install
@@ -102,13 +100,10 @@ jobs:
yarn workspace @selfxyz/qrcode build
- name: Cache build artifacts
uses: actions/cache/save@v4
uses: ./.github/actions/cache-sdk-build
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
mode: save
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
# Quality checks job
quality-checks:
@@ -141,29 +136,32 @@ jobs:
- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-sdk-build
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/sdk-common build
yarn workspace @selfxyz/qrcode build
- name: Run linter
run: yarn workspace @selfxyz/qrcode lint:imports:check
@@ -210,29 +208,32 @@ jobs:
- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-sdk-build
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/sdk-common build
yarn workspace @selfxyz/qrcode build
- name: Verify build artifacts
run: |
@@ -273,29 +274,41 @@ jobs:
- name: Cache Yarn dependencies
id: yarn-cache
uses: actions/cache@v4
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
node_modules
sdk/qrcode/node_modules
common/node_modules
key: ${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-yarn-
cache-version: ${{ env.GH_YARN_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}
- name: Install Dependencies
uses: ./.github/actions/yarn-install
- name: Restore build artifacts
uses: actions/cache/restore@v4
id: restore-build
uses: ./.github/actions/cache-sdk-build
with:
path: |
common/dist
sdk/sdk-common/dist
sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true
mode: restore
cache-version: ${{ env.GH_SDK_CACHE_VERSION }}
fail-on-cache-miss: false
- name: Build dependencies (fallback on cache miss)
if: steps.restore-build.outputs.cache-hit != 'true'
run: |
yarn workspace @selfxyz/common build
yarn workspace @selfxyz/sdk-common build
yarn workspace @selfxyz/qrcode build
- name: Check for nested require() in tests
run: |
# Check SDK tests for nested require patterns that cause OOM
if grep -rE "require\(['\"]react(-native)?['\"])" sdk/qrcode/src/ sdk/qrcode/tests/ 2>/dev/null; then
echo "❌ Found nested require() patterns that cause OOM in CI"
exit 1
fi
echo "✅ No nested require() patterns found"
- name: Run tests
run: yarn workspace @selfxyz/qrcode test

View File

@@ -11,9 +11,9 @@ import Foundation
import React
#if !E2E_TESTING
import NFCPassportReader
import Mixpanel
#endif
import Security
import Mixpanel
import Sentry
#if !E2E_TESTING

View File

@@ -53,8 +53,6 @@ end
target "Self" do
# Native module exclusion for E2E testing is handled in react-native.config.cjs
config = use_native_modules!
use_frameworks!
# Skip NFCPassportReader for e2e testing to avoid build issues
unless ENV["E2E_TESTING"] == "1"
# Check if we're running in a selfxyz repo or an external fork
@@ -79,10 +77,13 @@ target "Self" do
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
end
# Explicitly declare Mixpanel to ensure it's available even in E2E builds
# (NFCPassportReader also includes Mixpanel, but is skipped during E2E testing)
pod "Mixpanel-swift", :modular_headers => true
pod "QKMRZScanner"
pod "lottie-ios"
pod "SwiftQRScanner", :git => "https://github.com/vinodiOS/SwiftQRScanner"
pod "Mixpanel-swift", "~> 5.0.0"
# RNReactNativeHapticFeedback is handled by autolinking
use_react_native!(

View File

@@ -1558,7 +1558,7 @@ PODS:
- React-Core
- react-native-netinfo (11.4.1):
- React-Core
- react-native-nfc-manager (3.16.3):
- react-native-nfc-manager (3.17.2):
- React-Core
- react-native-passkey (3.3.1):
- DoubleConversion
@@ -1984,7 +1984,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNDeviceInfo (14.1.1):
- RNDeviceInfo (15.0.1):
- React-Core
- RNFBApp (19.3.0):
- Firebase/CoreOnly (= 10.24.0)
@@ -2042,7 +2042,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNLocalize (3.6.0):
- RNLocalize (3.6.1):
- DoubleConversion
- glog
- hermes-engine
@@ -2129,7 +2129,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNSentry (7.0.1):
- RNSentry (7.0.0):
- DoubleConversion
- glog
- hermes-engine
@@ -2152,7 +2152,7 @@ PODS:
- ReactCommon/turbomodule/core
- Sentry/HybridSDK (= 8.53.2)
- Yoga
- RNSVG (15.15.0):
- RNSVG (15.12.1):
- DoubleConversion
- glog
- hermes-engine
@@ -2172,9 +2172,9 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNSVG/common (= 15.15.0)
- RNSVG/common (= 15.12.1)
- Yoga
- RNSVG/common (15.15.0):
- RNSVG/common (15.12.1):
- DoubleConversion
- glog
- hermes-engine
@@ -2223,7 +2223,7 @@ DEPENDENCIES:
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- lottie-ios
- lottie-react-native (from `../node_modules/lottie-react-native`)
- Mixpanel-swift (~> 5.0.0)
- Mixpanel-swift
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
- QKMRZScanner
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
@@ -2626,7 +2626,7 @@ SPEC CHECKSUMS:
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-mobilesdk-module: 4770cb45fdd19dc4eed04615f0fcdab013b3dfe2
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7
react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83
react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf
react-native-safe-area-context: a7aad44fe544b55e2369a3086e16a01be60ce398
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
@@ -2660,18 +2660,18 @@ SPEC CHECKSUMS:
ReactCommon: b2eb96a61b826ff327a773a74357b302cf6da678
RNCAsyncStorage: 0003b916f1a69fe2d20b7910e0d08da3d32c7bd6
RNCClipboard: a4827e134e4774e97fa86f7f986694dd89320f13
RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
RNFBApp: 4097f75673f8b42a7cd1ba17e6ea85a94b45e4d1
RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88
RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b
RNGestureHandler: a63b531307e5b2e6ea21d053a1a7ad4cf9695c57
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52
RNLocalize: 4f5e4a46d2bccd04ccb96721e438dcb9de17c2e0
RNLocalize: 2760999d1e2fc95fb7b7e5247631feb3c08156dc
RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73
RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8
RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0
RNSVG: 39476f26bbbe72ffe6194c6fc8f6acd588087957
RNSentry: f79dd124cc49088445c16d23955860dd0d1db6f3
RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb
segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
@@ -2681,6 +2681,6 @@ SPEC CHECKSUMS:
TwilioVideo: 9f51085d4e4fb3aff8e168b8215b31cb0f486a2f
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
PODFILE CHECKSUM: f03c12b5d96fb6e22afe20fba517840fef44e76f
PODFILE CHECKSUM: 8cfd84595c3e826f512f5c545d232a27f1850ff3
COCOAPODS: 1.16.2

View File

@@ -1,9 +1,9 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
import Foundation
import Mixpanel
#if !E2E_TESTING
import Mixpanel
import NFCPassportReader
public class SelfAnalytics: Analytics {

View File

@@ -72,6 +72,9 @@ const config = {
new RegExp(
'packages/mobile-sdk-alpha/node_modules/react-native-svg(/|$)',
),
new RegExp(
'packages/mobile-sdk-alpha/node_modules/react-native-webview(/|$)',
),
new RegExp('packages/mobile-sdk-demo/node_modules/react(/|$)'),
new RegExp('packages/mobile-sdk-demo/node_modules/react-dom(/|$)'),
new RegExp('packages/mobile-sdk-demo/node_modules/react-native(/|$)'),

View File

@@ -85,13 +85,13 @@
"react-native-webview": "13.16.0"
},
"dependencies": {
"@babel/runtime": "^7.28.3",
"@ethersproject/shims": "^5.7.0",
"@babel/runtime": "^7.28.6",
"@ethersproject/shims": "^5.8.0",
"@noble/hashes": "^1.5.0",
"@openpassport/zk-kit-imt": "^0.0.5",
"@openpassport/zk-kit-lean-imt": "^0.0.6",
"@openpassport/zk-kit-smt": "^0.0.1",
"@peculiar/x509": "^1.13.0",
"@peculiar/x509": "^1.14.3",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-clipboard/clipboard": "1.16.3",
"@react-native-community/blur": "^4.4.1",
@@ -108,7 +108,7 @@
"@selfxyz/euclid": "^0.6.1",
"@selfxyz/mobile-sdk-alpha": "workspace:^",
"@sentry/react": "^9.32.0",
"@sentry/react-native": "7.0.1",
"@sentry/react-native": "7.0.0",
"@stablelib/cbor": "^2.0.1",
"@sumsub/react-native-mobilesdk-module": "1.40.2",
"@tamagui/animations-css": "1.126.14",
@@ -122,7 +122,7 @@
"@turnkey/react-native-wallet-kit": "1.1.5",
"@walletconnect/react-native-compat": "^2.23.0",
"@xstate/react": "^5.0.3",
"asn1js": "^3.0.6",
"asn1js": "^3.0.7",
"axios": "^1.13.2",
"buffer": "^6.0.3",
"country-emoji": "^1.5.6",
@@ -136,8 +136,8 @@
"js-sha512": "^0.9.0",
"lottie-react": "^2.4.1",
"lottie-react-native": "7.2.2",
"node-forge": "^1.3.1",
"pkijs": "^3.2.5",
"node-forge": "^1.3.3",
"pkijs": "^3.3.3",
"poseidon-lite": "^0.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -147,7 +147,7 @@
"react-native-blur-effect": "^1.1.3",
"react-native-check-version": "^1.3.0",
"react-native-cloud-storage": "^2.2.2",
"react-native-device-info": "^14.0.4",
"react-native-device-info": "^15.0.1",
"react-native-dotenv": "^3.4.11",
"react-native-edge-to-edge": "^1.7.0",
"react-native-gesture-handler": "2.19.0",
@@ -156,18 +156,18 @@
"react-native-inappbrowser-reborn": "^3.7.0",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-localize": "^3.5.2",
"react-native-logs": "^5.3.0",
"react-native-nfc-manager": "3.16.3",
"react-native-passkey": "^3.3.1",
"react-native-localize": "^3.6.1",
"react-native-logs": "^5.5.0",
"react-native-nfc-manager": "3.17.2",
"react-native-passkey": "^3.3.2",
"react-native-passport-reader": "1.0.3",
"react-native-safe-area-context": "^5.6.1",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "4.15.3",
"react-native-sqlite-storage": "^6.0.1",
"react-native-svg": "15.15.0",
"react-native-svg": "15.12.1",
"react-native-svg-web": "1.0.9",
"react-native-url-polyfill": "^3.0.0",
"react-native-web": "^0.19.0",
"react-native-web": "^0.21.2",
"react-native-webview": "^13.16.0",
"react-qr-barcode-scanner": "^2.1.8",
"socket.io-client": "^4.8.3",
@@ -177,14 +177,14 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-syntax-flow": "^7.27.1",
"@babel/plugin-transform-classes": "^7.27.1",
"@babel/core": "^7.28.6",
"@babel/plugin-syntax-flow": "^7.28.6",
"@babel/plugin-transform-classes": "^7.28.6",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-env": "^7.28.3",
"@babel/preset-react": "^7.27.1",
"@babel/plugin-transform-private-methods": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@babel/preset-react": "^7.28.5",
"@react-native-community/cli": "^16.0.3",
"@react-native/babel-preset": "0.76.9",
"@react-native/eslint-config": "0.76.9",
@@ -210,11 +210,11 @@
"@types/react-test-renderer": "^18",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-react-swc": "^3.10.2",
"@vitejs/plugin-react-swc": "^4.2.2",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"constants-browserify": "^1.0.0",
"dompurify": "^3.2.6",
"dompurify": "^3.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "^3.7.0",
@@ -229,14 +229,15 @@
"jest": "^30.2.0",
"path-browserify": "^1.0.1",
"prettier": "^3.5.3",
"react-native-svg-transformer": "^1.5.1",
"prop-types": "^15.8.1",
"react-native-svg-transformer": "^1.5.2",
"react-test-renderer": "^18.3.1",
"rollup-plugin-visualizer": "^6.0.3",
"rollup-plugin-visualizer": "^6.0.5",
"stream-browserify": "^3.0.0",
"ts-morph": "^22.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
"vite": "^7.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},
"packageManager": "yarn@4.12.0",

View File

@@ -0,0 +1,27 @@
// 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 } from 'react';
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
import { useErrorInjectionStore } from '@/stores/errorInjectionStore';
import { IS_DEV_MODE } from '@/utils/devUtils';
/**
* Hook for checking if a specific error should be injected
* Only active in dev mode
*/
export function useErrorInjection() {
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
const shouldInjectError = useCallback(
(errorType: InjectedErrorType): boolean => {
if (!IS_DEV_MODE) return false;
return injectedErrors.includes(errorType);
},
[injectedErrors],
);
return { shouldInjectError };
}

View File

@@ -0,0 +1,127 @@
// 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, useState } from 'react';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha';
import { fetchAccessToken, launchSumsub } from '@/integrations/sumsub';
import type { SumsubResult } from '@/integrations/sumsub/types';
import type { RootStackParamList } from '@/navigation';
export type FallbackErrorSource =
| 'mrz_scan_failed'
| 'nfc_scan_failed'
| 'sumsub_initialization'
| 'sumsub_verification';
export interface UseSumsubLauncherOptions {
/**
* Country code for the user's document
*/
countryCode: string;
/**
* Error source to track where the Sumsub launch was initiated from
*/
errorSource: FallbackErrorSource;
/**
* Optional callback to handle successful verification
*/
onSuccess?: (result: SumsubResult) => void | Promise<void>;
/**
* Optional callback to handle user cancellation
*/
onCancel?: () => void | Promise<void>;
/**
* Optional callback to handle verification failure
*/
onError?: (error: unknown, result?: SumsubResult) => void | Promise<void>;
}
/**
* Custom hook for launching Sumsub verification with consistent error handling.
*
* Abstracts the common pattern of:
* 1. Fetching access token
* 2. Launching Sumsub SDK
* 3. Handling errors by navigating to fallback screen
* 4. Managing loading state
*
* @example
* ```tsx
* const { launchSumsubVerification, isLoading } = useSumsubLauncher({
* countryCode: 'US',
* errorSource: 'nfc_scan_failed',
* });
*
* <Button onPress={launchSumsubVerification} disabled={isLoading}>
* {isLoading ? 'Loading...' : 'Try Alternative Verification'}
* </Button>
* ```
*/
export const useSumsubLauncher = (options: UseSumsubLauncherOptions) => {
const { countryCode, errorSource, onSuccess, onCancel, onError } = options;
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const [isLoading, setIsLoading] = useState(false);
const launchSumsubVerification = useCallback(async () => {
setIsLoading(true);
try {
const accessToken = await fetchAccessToken();
const result = await launchSumsub({ accessToken: accessToken.token });
// Handle user cancellation
if (!result.success && result.status === 'Interrupted') {
await onCancel?.();
return;
}
// Handle verification failure
if (!result.success) {
const error = result.errorMsg || result.errorType || 'Unknown error';
const safeError = sanitizeErrorMessage(error);
console.error('Sumsub verification failed:', safeError);
// Call custom error handler if provided, otherwise navigate to fallback screen
if (onError) {
await onError(safeError, result);
} else {
navigation.navigate('RegistrationFallback', {
errorSource,
countryCode,
});
}
return;
}
// Handle success
await onSuccess?.(result);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const safeError = sanitizeErrorMessage(errorMessage);
console.error('Error launching alternative verification:', safeError);
// Call custom error handler if provided, otherwise navigate to fallback screen
if (onError) {
await onError(safeError);
} else {
navigation.navigate('RegistrationFallback', {
errorSource,
countryCode,
});
}
} finally {
setIsLoading(false);
}
}, [navigation, countryCode, errorSource, onSuccess, onCancel, onError]);
return {
launchSumsubVerification,
isLoading,
};
};

View File

@@ -19,6 +19,7 @@ import DocumentCameraTroubleScreen from '@/screens/documents/scanning/DocumentCa
import DocumentNFCMethodSelectionScreen from '@/screens/documents/scanning/DocumentNFCMethodSelectionScreen';
import DocumentNFCScanScreen from '@/screens/documents/scanning/DocumentNFCScanScreen';
import DocumentNFCTroubleScreen from '@/screens/documents/scanning/DocumentNFCTroubleScreen';
import RegistrationFallbackScreen from '@/screens/documents/scanning/RegistrationFallbackScreen';
import ConfirmBelongingScreen from '@/screens/documents/selection/ConfirmBelongingScreen';
import CountryPickerScreen from '@/screens/documents/selection/CountryPickerScreen';
import DocumentOnboardingScreen from '@/screens/documents/selection/DocumentOnboardingScreen';
@@ -155,6 +156,17 @@ const documentsScreens = {
errorType: 'general',
},
},
RegistrationFallback: {
screen: RegistrationFallbackScreen,
options: {
title: 'REGISTRATION',
headerShown: false,
} as NativeStackNavigationOptions,
initialParams: {
errorSource: 'sumsub_initialization',
countryCode: '',
},
},
};
export default documentsScreens;

View File

@@ -79,6 +79,7 @@ export type RootStackParamList = Omit<
| 'Home'
| 'IDPicker'
| 'IdDetails'
| 'RegistrationFallback'
| 'Loading'
| 'Modal'
| 'MockDataDeepLink'
@@ -127,6 +128,16 @@ export type RootStackParamList = Omit<
errorType: string;
};
// Registration Fallback screens
RegistrationFallback: {
errorSource:
| 'mrz_scan_failed'
| 'nfc_scan_failed'
| 'sumsub_initialization'
| 'sumsub_verification';
countryCode: string;
};
// Account/Recovery screens
AccountRecovery:
| {
@@ -190,6 +201,7 @@ export type RootStackParamList = Omit<
// Onboarding screens
Disclaimer: undefined;
KycSuccess: undefined;
// Dev screens
CreateMock: undefined;

View File

@@ -4,6 +4,7 @@
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
import AccountVerifiedSuccessScreen from '@/screens/onboarding/AccountVerifiedSuccessScreen';
import DisclaimerScreen from '@/screens/onboarding/DisclaimerScreen';
import SaveRecoveryPhraseScreen from '@/screens/onboarding/SaveRecoveryPhraseScreen';
@@ -30,6 +31,13 @@ const onboardingScreens = {
animation: 'slide_from_bottom',
} as NativeStackNavigationOptions,
},
KycSuccess: {
screen: KycSuccessScreen,
options: {
headerShown: false,
animation: 'slide_from_bottom',
} as NativeStackNavigationOptions,
},
};
export default onboardingScreens;

View File

@@ -13,9 +13,11 @@ import {
type LogLevel,
type NFCScanContext,
reactNativeScannerAdapter,
sanitizeErrorMessage,
SdkEvents,
SelfClientProvider as SDKSelfClientProvider,
type TrackEventParams,
useMRZStore,
webNFCScannerShim,
type WsConn,
} from '@selfxyz/mobile-sdk-alpha';
@@ -33,7 +35,12 @@ import {
setPassportKeychainErrorCallback,
} from '@/providers/passportDataProvider';
import { trackEvent, trackNfcEvent } from '@/services/analytics';
import {
type InjectedErrorType,
useErrorInjectionStore,
} from '@/stores/errorInjectionStore';
import { useSettingStore } from '@/stores/settingStore';
import { IS_DEV_MODE } from '@/utils/devUtils';
import {
registerModalCallbacks,
unregisterModalCallbacks,
@@ -68,7 +75,20 @@ function navigateIfReady<RouteName extends keyof RootStackParamList>(
}
export const SelfClientProvider = ({ children }: PropsWithChildren) => {
const config = useMemo(() => ({}), []);
const config = useMemo(
() => ({
devConfig: IS_DEV_MODE
? {
shouldTrigger: (errorType: string) => {
return useErrorInjectionStore
.getState()
.shouldTrigger(errorType as InjectedErrorType);
},
}
: undefined,
}),
[],
);
const adapters: Adapters = useMemo(
() => ({
scanner:
@@ -167,6 +187,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
const appListeners = useMemo(() => {
const { map, addListener } = createListenersMap();
// Track current countryCode for error navigation
let currentCountryCode = '';
addListener(SdkEvents.PROVING_PASSPORT_DATA_NOT_FOUND, () => {
if (navigationRef.isReady()) {
navigationRef.navigate('DocumentDataNotFound');
@@ -261,7 +284,10 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
});
addListener(SdkEvents.DOCUMENT_MRZ_READ_FAILURE, () => {
navigateIfReady('DocumentCameraTrouble');
navigateIfReady('RegistrationFallback', {
errorSource: 'mrz_scan_failed',
countryCode: currentCountryCode,
});
});
addListener(SdkEvents.PROVING_AADHAAR_UPLOAD_SUCCESS, () => {
@@ -280,6 +306,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
countryCode: string;
documentTypes: string[];
}) => {
currentCountryCode = countryCode;
// Store country code early so it's available for Sumsub fallback flows
useMRZStore.getState().update({ countryCode });
navigateIfReady('IDPicker', { countryCode, documentTypes });
},
);
@@ -300,14 +329,73 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
}
break;
case 'kyc':
fetchAccessToken()
.then(accessToken => {
launchSumsub({ accessToken: accessToken.token });
})
// TODO: show sumsub error screen
.catch(error => {
console.error('Error launching Sumsub:', error);
});
(async () => {
try {
// Dev-only: Check for injected initialization error
if (
useErrorInjectionStore
.getState()
.shouldTrigger('sumsub_initialization')
) {
console.log('[DEV] Injecting Sumsub initialization error');
throw new Error(
'Injected Sumsub initialization error for testing',
);
}
const accessToken = await fetchAccessToken();
const result = await launchSumsub({
accessToken: accessToken.token,
});
// User cancelled - return silently
if (!result.success && result.status === 'Interrupted') {
return;
}
// Dev-only: Check for injected verification error
const shouldInjectVerificationError = useErrorInjectionStore
.getState()
.shouldTrigger('sumsub_verification');
// Actual error from provider
if (!result.success || shouldInjectVerificationError) {
if (shouldInjectVerificationError) {
console.log('[DEV] Injecting Sumsub verification error');
} else {
const safeError = sanitizeErrorMessage(
result.errorMsg || result.errorType || 'unknown_error',
);
console.error('KYC provider failed:', safeError);
}
// Guard navigation call after async operations
if (navigationRef.isReady()) {
navigationRef.navigate('RegistrationFallback', {
errorSource: 'sumsub_verification',
countryCode,
});
}
return;
}
// Success case: navigate to KYC success screen
if (navigationRef.isReady()) {
navigationRef.navigate('KycSuccess');
}
} catch (error) {
const safeInitError = sanitizeErrorMessage(
error instanceof Error ? error.message : String(error),
);
console.error('Error in KYC flow:', safeInitError);
// Guard navigation call after async operations
if (navigationRef.isReady()) {
navigationRef.navigate('RegistrationFallback', {
errorSource: 'sumsub_initialization',
countryCode,
});
}
}
})();
break;
default:
if (countryCode) {

View File

@@ -107,8 +107,8 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ route }) => {
} else {
await init(selfClient, 'dsc', true);
}
} catch {
console.error('Error loading selected document:');
} catch (error) {
console.error('Error loading selected document:', error);
await init(selfClient, 'dsc', true);
} finally {
setIsInitializing(false);

View File

@@ -44,6 +44,12 @@ import {
subscribeToTopics,
unsubscribeFromTopics,
} from '@/services/notifications/notificationService';
import type { InjectedErrorType } from '@/stores/errorInjectionStore';
import {
ERROR_GROUPS,
ERROR_LABELS,
useErrorInjectionStore,
} from '@/stores/errorInjectionStore';
import { usePointEventStore } from '@/stores/pointEventStore';
import { useSettingStore } from '@/stores/settingStore';
import { IS_DEV_MODE } from '@/utils/devUtils';
@@ -390,6 +396,152 @@ const LogLevelSelector = ({
);
};
const ErrorInjectionSelector = () => {
const injectedErrors = useErrorInjectionStore(state => state.injectedErrors);
const setInjectedErrors = useErrorInjectionStore(
state => state.setInjectedErrors,
);
const clearAllErrors = useErrorInjectionStore(state => state.clearAllErrors);
const [open, setOpen] = useState(false);
// Single error selection - replace instead of toggle
const selectError = (errorType: InjectedErrorType) => {
// If clicking the same error, clear it; otherwise set the new one
if (injectedErrors.length === 1 && injectedErrors[0] === errorType) {
clearAllErrors();
} else {
setInjectedErrors([errorType]);
}
// Close the sheet after selection
setOpen(false);
};
const currentError = injectedErrors.length > 0 ? injectedErrors[0] : null;
const currentErrorLabel = currentError ? ERROR_LABELS[currentError] : null;
return (
<YStack gap="$2">
<Button
style={{ backgroundColor: 'white' }}
borderColor={slate200}
borderRadius="$2"
height="$5"
padding={0}
onPress={() => setOpen(true)}
>
<XStack
width="100%"
justifyContent="space-between"
paddingVertical="$3"
paddingLeft="$4"
paddingRight="$1.5"
>
<Text fontSize="$5" color={slate500} fontFamily={dinot}>
{currentErrorLabel || 'Select onboarding error to test'}
</Text>
<ChevronDown color={slate500} strokeWidth={2.5} />
</XStack>
</Button>
{currentError && (
<Button
backgroundColor={red500}
borderRadius="$2"
height="$5"
onPress={clearAllErrors}
pressStyle={{
opacity: 0.8,
scale: 0.98,
}}
>
<Text color={white} fontSize="$5" fontFamily={dinot}>
Clear
</Text>
</Button>
)}
<Sheet
modal
open={open}
onOpenChange={setOpen}
snapPoints={[85]}
animation="medium"
dismissOnSnapToBottom
>
<Sheet.Overlay />
<Sheet.Frame
backgroundColor={white}
borderTopLeftRadius="$9"
borderTopRightRadius="$9"
>
<YStack padding="$4">
<XStack
alignItems="center"
justifyContent="space-between"
marginBottom="$4"
>
<Text fontSize="$8" fontFamily={dinot}>
Onboarding Error Testing
</Text>
<Button
onPress={() => setOpen(false)}
padding="$2"
backgroundColor="transparent"
>
<ChevronDown
color={slate500}
strokeWidth={2.5}
style={{ transform: [{ rotate: '180deg' }] }}
/>
</Button>
</XStack>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
>
{Object.entries(ERROR_GROUPS).map(([groupName, errors]) => (
<YStack key={groupName} marginBottom="$4">
<Text
fontSize="$6"
fontFamily={dinot}
fontWeight="600"
color={slate800}
marginBottom="$2"
>
{groupName}
</Text>
{errors.map((errorType: InjectedErrorType) => (
<TouchableOpacity
key={errorType}
onPress={() => selectError(errorType)}
>
<XStack
paddingVertical="$3"
paddingHorizontal="$2"
borderBottomWidth={1}
borderBottomColor={slate200}
alignItems="center"
justifyContent="space-between"
>
<Text fontSize="$5" color={slate600} fontFamily={dinot}>
{ERROR_LABELS[errorType]}
</Text>
{currentError === errorType && (
<Check color={slate600} size={20} />
)}
</XStack>
</TouchableOpacity>
))}
</YStack>
))}
</ScrollView>
</YStack>
</Sheet.Frame>
</Sheet>
</YStack>
);
};
const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
const { clearDocumentCatalogForMigrationTesting } = usePassport();
const clearPointEvents = usePointEventStore(state => state.clearEvents);
@@ -779,6 +931,16 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
/>
</ParameterSection>
{IS_DEV_MODE && (
<ParameterSection
icon={<BugIcon />}
title="Onboarding Error Testing"
description="Test onboarding error flows"
>
<ErrorInjectionSelector />
</ParameterSection>
)}
{Platform.OS === 'android' && (
<ParameterSection
icon={<BugIcon />}

View File

@@ -3,9 +3,11 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React, { useEffect } from 'react';
import { YStack } from 'tamagui';
import { Caption } from '@selfxyz/mobile-sdk-alpha/components';
import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components';
import { slate500, slate700 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import Activity from '@/assets/icons/activity.svg';
import PassportCameraBulb from '@/assets/icons/passport_camera_bulb.svg';
@@ -15,6 +17,7 @@ import Star from '@/assets/icons/star.svg';
import type { TipProps } from '@/components/Tips';
import Tips from '@/components/Tips';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import { flush as flushAnalytics } from '@/services/analytics';
@@ -48,6 +51,13 @@ const tips: TipProps[] = [
const DocumentCameraTroubleScreen: React.FC = () => {
const go = useHapticNavigation('DocumentCamera', { action: 'cancel' });
const selfClient = useSelfClient();
const { useMRZStore } = selfClient;
const { countryCode } = useMRZStore();
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
countryCode,
errorSource: 'sumsub_initialization',
});
// error screen, flush analytics
useEffect(() => {
@@ -64,10 +74,28 @@ const DocumentCameraTroubleScreen: React.FC = () => {
</Caption>
}
footer={
<Caption size="large" style={{ color: slate500 }}>
Following these steps should help your phone's camera capture the ID
page quickly and clearly!
</Caption>
<YStack gap="$3">
<Caption size="large" style={{ color: slate500 }}>
Following these steps should help your phone's camera capture the ID
page quickly and clearly!
</Caption>
<Caption
size="large"
style={{ color: slate500, marginTop: 12, marginBottom: 8 }}
>
Or try an alternative verification method:
</Caption>
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
</YStack>
}
>
<Tips items={tips} />

View File

@@ -54,6 +54,7 @@ import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import passportVerifyAnimation from '@/assets/animations/passport_verify.json';
import NFC_IMAGE from '@/assets/images/nfc.png';
import { logNFCEvent } from '@/config/sentry';
import { useErrorInjection } from '@/hooks/useErrorInjection';
import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import {
@@ -106,8 +107,9 @@ const DocumentNFCScanScreen: React.FC = () => {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<DocumentNFCScanRoute>();
const { showModal } = useFeedback();
useFeedback();
useFeedbackAutoHide();
const { shouldInjectError } = useErrorInjection();
const {
passportNumber,
dateOfBirth,
@@ -189,18 +191,12 @@ const DocumentNFCScanScreen: React.FC = () => {
},
{ message: sanitizeErrorMessage(message) },
);
showModal({
titleText: 'NFC Scan Error',
bodyText: message,
buttonText: SUPPORT_FORM_BUTTON_TEXT,
secondaryButtonText: 'Help',
preventDismiss: false,
onButtonPress: openSupportForm,
onSecondaryButtonPress: goToNFCTrouble,
onModalDismiss: () => {},
navigation.navigate('RegistrationFallback', {
errorSource: 'nfc_scan_failed',
countryCode,
});
},
[baseContext, showModal, goToNFCTrouble],
[baseContext, navigation, countryCode],
);
const checkNfcSupport = useCallback(async () => {
@@ -324,6 +320,18 @@ const DocumentNFCScanScreen: React.FC = () => {
}, 30000);
try {
// Dev-only: Check for injected timeout error
if (shouldInjectError('nfc_timeout')) {
console.log('[DEV] Injecting NFC timeout error');
throw new Error('Injected timeout error for testing');
}
// Dev-only: Check for injected module unavailable error
if (shouldInjectError('nfc_module_unavailable')) {
console.log('[DEV] Injecting NFC module unavailable error');
throw new Error('NFC scanning is currently unavailable');
}
const {
canNumber,
useCan,
@@ -376,6 +384,12 @@ const DocumentNFCScanScreen: React.FC = () => {
);
let passportData: PassportData | null = null;
try {
// Dev-only: Check for injected parse failure error
if (shouldInjectError('nfc_parse_failure')) {
console.log('[DEV] Injecting NFC parse failure error');
throw new Error('Failed to parse NFC response');
}
passportData = parseScanResponse(scanResponse);
} catch (e: unknown) {
console.error('Parsing NFC Response Unsuccessful');
@@ -452,6 +466,7 @@ const DocumentNFCScanScreen: React.FC = () => {
navigation,
openErrorModal,
trackEvent,
shouldInjectError,
]);
const navigateToHome = useHapticNavigation('Home', {

View File

@@ -7,13 +7,15 @@ import { View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { YStack } from 'tamagui';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { Caption, SecondaryButton } from '@selfxyz/mobile-sdk-alpha/components';
import { slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { slate500, slate700 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import type { TipProps } from '@/components/Tips';
import Tips from '@/components/Tips';
import { useFeedbackAutoHide } from '@/hooks/useFeedbackAutoHide';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
import SimpleScrolledTitleLayout from '@/layouts/SimpleScrolledTitleLayout';
import { flushAllAnalytics } from '@/services/analytics';
import { openSupportForm, SUPPORT_FORM_BUTTON_TEXT } from '@/services/support';
@@ -50,6 +52,13 @@ const DocumentNFCTroubleScreen: React.FC = () => {
const goToNFCMethodSelection = useHapticNavigation(
'DocumentNFCMethodSelection',
);
const selfClient = useSelfClient();
const { useMRZStore } = selfClient;
const { countryCode } = useMRZStore();
const { launchSumsubVerification, isLoading } = useSumsubLauncher({
countryCode,
errorSource: 'sumsub_initialization',
});
useFeedbackAutoHide();
// error screen, flush analytics
@@ -71,9 +80,24 @@ const DocumentNFCTroubleScreen: React.FC = () => {
secondaryButtonText="Open NFC Options"
onSecondaryButtonPress={goToNFCMethodSelection}
footer={
<SecondaryButton onPress={openSupportForm} style={{ marginBottom: 0 }}>
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
<YStack gap="$3">
<SecondaryButton
onPress={openSupportForm}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{SUPPORT_FORM_BUTTON_TEXT}
</SecondaryButton>
<SecondaryButton
onPress={launchSumsubVerification}
disabled={isLoading}
textColor={slate700}
style={{ marginBottom: 0 }}
>
{isLoading ? 'Loading...' : 'Try Alternative Verification'}
</SecondaryButton>
</YStack>
}
>
<YStack

View File

@@ -0,0 +1,326 @@
// 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, { useCallback } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Button, XStack, YStack } from 'tamagui';
import type { RouteProp } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { HelpCircle, X } from '@tamagui/lucide-icons';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import {
BodyText,
PrimaryButton,
SecondaryButton,
} from '@selfxyz/mobile-sdk-alpha/components';
import {
black,
cyan300,
slate100,
slate200,
slate300,
slate500,
white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
import { useSafeBottomPadding } from '@selfxyz/mobile-sdk-alpha/hooks';
import WarningIcon from '@/assets/images/warning.svg';
import { useSumsubLauncher } from '@/hooks/useSumsubLauncher';
import { buttonTap } from '@/integrations/haptics';
import type { RootStackParamList } from '@/navigation';
import { extraYPadding } from '@/utils/styleUtils';
type FallbackErrorSource =
| 'mrz_scan_failed'
| 'nfc_scan_failed'
| 'sumsub_initialization'
| 'sumsub_verification';
type RegistrationFallbackRouteParams = {
errorSource: FallbackErrorSource;
countryCode: string;
};
type RegistrationFallbackRoute = RouteProp<
Record<string, RegistrationFallbackRouteParams>,
string
>;
const getHeaderTitle = (errorSource: FallbackErrorSource): string => {
switch (errorSource) {
case 'mrz_scan_failed':
return 'MRZ SCAN';
case 'nfc_scan_failed':
return 'NFC SCAN';
default:
return 'REGISTRATION';
}
};
const getCurrentStep = (errorSource: FallbackErrorSource): number => {
switch (errorSource) {
case 'mrz_scan_failed':
return 1; // Step 1: MRZ scanning
case 'nfc_scan_failed':
return 2; // Step 2: NFC reading
case 'sumsub_initialization':
case 'sumsub_verification':
return 3; // Step 3: Proving/verification
default:
return 1;
}
};
const getRetryButtonText = (errorSource: FallbackErrorSource): string => {
switch (errorSource) {
case 'mrz_scan_failed':
return 'Try scanning again';
case 'nfc_scan_failed':
return 'Try reading again';
default:
return 'Try again';
}
};
const getErrorMessages = (
errorSource: FallbackErrorSource,
): { title: string; description: string; canRetryOriginal: boolean } => {
switch (errorSource) {
case 'mrz_scan_failed':
return {
title: 'There was a problem scanning your document',
description: 'Make sure the document is clearly visible and try again',
canRetryOriginal: true,
};
case 'nfc_scan_failed':
return {
title: 'There was a problem reading the chip',
description: 'Make sure NFC is enabled and try again',
canRetryOriginal: true,
};
case 'sumsub_initialization':
return {
title: 'Connection Error',
description:
'Unable to connect to verification service. Please check your internet connection and try again.',
canRetryOriginal: false,
};
case 'sumsub_verification':
return {
title: 'Verification Error',
description:
'Something went wrong during the verification process. Please try again.',
canRetryOriginal: false,
};
}
};
const RegistrationFallbackScreen: React.FC = () => {
const insets = useSafeAreaInsets();
const paddingBottom = useSafeBottomPadding(extraYPadding + 35);
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<RegistrationFallbackRoute>();
const selfClient = useSelfClient();
const { trackEvent, useMRZStore } = selfClient;
const storeCountryCode = useMRZStore(state => state.countryCode);
const errorSource = route.params?.errorSource || 'sumsub_initialization';
// Use country code from route params, or fall back to MRZ store
const countryCode = route.params?.countryCode || storeCountryCode || '';
const headerTitle = getHeaderTitle(errorSource);
const retryButtonText = getRetryButtonText(errorSource);
const currentStep = getCurrentStep(errorSource);
const { title, description, canRetryOriginal } =
getErrorMessages(errorSource);
const { launchSumsubVerification, isLoading: isRetrying } = useSumsubLauncher(
{
countryCode,
errorSource,
onCancel: () => {
navigation.goBack();
},
onError: (_error, _result) => {
// Stay on this screen - user can try again
// Error is already logged in the hook
},
onSuccess: () => {
// Success - provider handles its own success UI
// The screen will be navigated away by the provider's flow
},
},
);
const handleClose = useCallback(() => {
buttonTap();
navigation.goBack();
}, [navigation]);
const handleTryAlternative = useCallback(async () => {
trackEvent('REGISTRATION_FALLBACK_TRY_ALTERNATIVE', { errorSource });
await launchSumsubVerification();
}, [errorSource, launchSumsubVerification, trackEvent]);
const handleRetryOriginal = useCallback(() => {
trackEvent('REGISTRATION_FALLBACK_RETRY_ORIGINAL', { errorSource });
// Navigate back to the appropriate screen based on error source
if (errorSource === 'mrz_scan_failed') {
navigation.navigate('DocumentCamera');
} else if (errorSource === 'nfc_scan_failed') {
navigation.navigate('DocumentNFCScan', {});
} else if (errorSource === 'sumsub_initialization') {
// Go back to ID Picker
navigation.goBack();
}
// TODO: Handle 'sumsub_verification' case - currently falls through without action
// which could leave users stuck when tapping "Try again" after Sumsub verification failure.
// Consider: calling launchSumsubVerification() or navigating to appropriate retry screen.
// Need to determine the correct retry behavior for failed Sumsub verifications.
}, [errorSource, navigation, trackEvent]);
return (
<YStack flex={1} backgroundColor={slate100}>
{/* Header */}
<YStack backgroundColor={slate100}>
<XStack
backgroundColor={slate100}
padding={20}
justifyContent="space-between"
alignItems="center"
paddingTop={Math.max(insets.top, 15) + extraYPadding}
paddingBottom={10}
>
<Button
unstyled
onPress={handleClose}
padding={8}
borderRadius={20}
hitSlop={10}
>
<X size={24} color={black} />
</Button>
<BodyText
style={{
fontSize: 16,
color: black,
fontWeight: '600',
fontFamily: dinot,
}}
>
{headerTitle}
</BodyText>
<Button
unstyled
padding={8}
borderRadius={20}
hitSlop={10}
width={32}
height={32}
justifyContent="center"
alignItems="center"
disabled
>
<HelpCircle size={20} color={black} opacity={0} />
</Button>
</XStack>
{/* Progress Bar */}
<YStack paddingHorizontal={40} paddingBottom={10}>
<XStack gap={3} height={6}>
{[1, 2, 3, 4].map(step => (
<YStack
key={step}
flex={1}
backgroundColor={step === currentStep ? cyan300 : slate300}
borderRadius={10}
/>
))}
</XStack>
</YStack>
</YStack>
{/* Warning Icon */}
<YStack flex={1} paddingHorizontal={20} paddingTop={20}>
<YStack
flex={1}
justifyContent="center"
alignItems="center"
paddingVertical={20}
>
<WarningIcon width={120} height={120} />
</YStack>
</YStack>
{/* Error Message */}
<YStack
paddingHorizontal={20}
paddingTop={20}
alignItems="center"
paddingVertical={25}
>
<BodyText style={{ fontSize: 19, textAlign: 'center', color: black }}>
{title}
</BodyText>
<BodyText
style={{
marginTop: 6,
fontSize: 17,
textAlign: 'center',
color: slate500,
}}
>
{description}
</BodyText>
</YStack>
{/* Top Button - Retry */}
{canRetryOriginal && (
<YStack paddingHorizontal={25} paddingBottom={20}>
<PrimaryButton onPress={handleRetryOriginal} disabled={isRetrying}>
{retryButtonText}
</PrimaryButton>
</YStack>
)}
{/* Bottom Section with Grey Line Separator */}
<YStack
paddingHorizontal={25}
backgroundColor={white}
paddingBottom={paddingBottom}
paddingTop={25}
gap="$3"
borderTopWidth={1}
borderTopColor={slate200}
>
<SecondaryButton onPress={handleTryAlternative} disabled={isRetrying}>
{isRetrying ? 'Loading...' : 'Try a different method'}
</SecondaryButton>
{/* Footer Text */}
<BodyText
style={{
fontSize: 15,
textAlign: 'center',
color: slate500,
fontStyle: 'italic',
marginTop: 8,
}}
>
Registering with alternative methods may take longer to verify your
document.
</BodyText>
</YStack>
</YStack>
);
};
export default RegistrationFallbackScreen;

View File

@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
import {
AbstractButton,
Description,
Title,
} from '@selfxyz/mobile-sdk-alpha/components';
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { buttonTap } from '@/integrations/haptics';
import type { RootStackParamList } from '@/navigation';
import { requestNotificationPermission } from '@/services/notifications/notificationService';
const KycSuccessScreen: React.FC = () => {
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const insets = useSafeAreaInsets();
const handleReceiveUpdates = async () => {
buttonTap();
await requestNotificationPermission();
// Navigate to Home regardless of permission result
navigation.navigate('Home', {});
};
const handleCheckLater = () => {
buttonTap();
navigation.navigate('Home', {});
};
return (
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
<View style={styles.centerSection}>
<View style={styles.animationContainer}>
<DelayedLottieView
autoPlay
loop={true}
source={loadingAnimation}
style={styles.animation}
cacheComposition={true}
renderMode="HARDWARE"
/>
</View>
<YStack
paddingHorizontal={24}
justifyContent="center"
alignItems="center"
gap={12}
>
<Title style={styles.title}>Your ID is being verified</Title>
<Description style={styles.description}>
Turn on push notifications to receive an update on your
verification. It's also safe the close the app and come back later.
</Description>
</YStack>
</View>
<YStack gap={12} paddingHorizontal={20} paddingBottom={24}>
<AbstractButton
bgColor={white}
color={black}
onPress={handleReceiveUpdates}
>
Receive live updates
</AbstractButton>
<AbstractButton
bgColor="transparent"
color={white}
borderColor="rgba(255, 255, 255, 0.3)"
borderWidth={1}
onPress={handleCheckLater}
>
I will check back later
</AbstractButton>
</YStack>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: black,
},
centerSection: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
animationContainer: {
width: 80,
height: 80,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 32,
},
animation: {
width: 160,
height: 160,
},
title: {
color: white,
textAlign: 'center',
fontSize: 28,
letterSpacing: 1,
},
description: {
color: white,
textAlign: 'center',
fontSize: 18,
},
});
export default KycSuccessScreen;

View File

@@ -0,0 +1,103 @@
// 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 { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { IS_DEV_MODE } from '@/utils/devUtils';
export type InjectedErrorType =
| 'mrz_invalid_format'
| 'mrz_unknown_error'
| 'nfc_timeout'
| 'nfc_module_unavailable'
| 'nfc_parse_failure'
| 'api_network_error'
| 'api_timeout'
| 'sumsub_initialization'
| 'sumsub_verification';
export const ERROR_GROUPS = {
MRZ: ['mrz_invalid_format', 'mrz_unknown_error'] as InjectedErrorType[],
NFC: [
'nfc_timeout',
'nfc_module_unavailable',
'nfc_parse_failure',
] as InjectedErrorType[],
API: ['api_network_error', 'api_timeout'] as InjectedErrorType[],
Sumsub: [
'sumsub_initialization',
'sumsub_verification',
] as InjectedErrorType[],
};
export const ERROR_LABELS: Record<InjectedErrorType, string> = {
mrz_invalid_format: 'MRZ: Invalid format',
mrz_unknown_error: 'MRZ: Unknown error',
nfc_timeout: 'NFC: Timeout',
nfc_module_unavailable: 'NFC: Module unavailable',
nfc_parse_failure: 'NFC: Parse failure',
api_network_error: 'API: Network error',
api_timeout: 'API: Timeout',
sumsub_initialization: 'Sumsub: Initialization',
sumsub_verification: 'Sumsub: Verification',
};
interface ErrorInjectionState {
injectedErrors: InjectedErrorType[];
// Actions
setInjectedErrors: (errors: InjectedErrorType[]) => void;
toggleError: (error: InjectedErrorType) => void;
clearError: (error: InjectedErrorType) => void;
clearAllErrors: () => void;
shouldTrigger: (error: InjectedErrorType) => boolean;
}
export const useErrorInjectionStore = create<ErrorInjectionState>()(
persist(
(set, get) => ({
injectedErrors: [],
setInjectedErrors: (errors: InjectedErrorType[]) => {
if (!IS_DEV_MODE) return;
set({ injectedErrors: errors });
},
toggleError: (error: InjectedErrorType) => {
if (!IS_DEV_MODE) return;
set(state => {
const hasError = state.injectedErrors.includes(error);
return {
injectedErrors: hasError
? state.injectedErrors.filter(e => e !== error)
: [...state.injectedErrors, error],
};
});
},
clearError: (error: InjectedErrorType) => {
if (!IS_DEV_MODE) return;
set(state => ({
injectedErrors: state.injectedErrors.filter(e => e !== error),
}));
},
clearAllErrors: () => {
if (!IS_DEV_MODE) return;
set({ injectedErrors: [] });
},
shouldTrigger: (error: InjectedErrorType) => {
if (!IS_DEV_MODE) return false;
const state = get();
return state.injectedErrors.includes(error);
},
}),
{
name: 'error-injection-storage',
storage: createJSONStorage(() => AsyncStorage),
},
),
);

View File

@@ -85,6 +85,7 @@ describe('navigation', () => {
'Home',
'IDPicker',
'IdDetails',
'KycSuccess',
'Loading',
'ManageDocuments',
'MockDataDeepLink',
@@ -101,6 +102,7 @@ describe('navigation', () => {
'QRCodeViewFinder',
'RecoverWithPhrase',
'Referral',
'RegistrationFallback',
'SaveRecoveryPhrase',
'Settings',
'ShowRecoveryPhrase',

View File

@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import React from 'react';
import { useNavigation } from '@react-navigation/native';
import { render } from '@testing-library/react-native';
import ErrorBoundary from '@/components/ErrorBoundary';
import KycSuccessScreen from '@/screens/kyc/KycSuccessScreen';
import * as notificationService from '@/services/notifications/notificationService';
// Note: While jest.setup.js provides comprehensive React Native mocking,
// react-test-renderer requires component-based mocks (functions) rather than
// string-based mocks for proper rendering. This minimal mock provides the
// specific components needed for this test without using requireActual to
// avoid memory issues (see .cursor/rules/test-memory-optimization.mdc).
jest.mock('react-native', () => ({
__esModule: true,
Platform: { OS: 'ios', select: jest.fn() },
StyleSheet: {
create: (styles: any) => styles,
flatten: (style: any) => style,
},
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
jest.mock('react-native-edge-to-edge', () => ({
SystemBars: () => null,
}));
jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: jest.fn(() => ({ top: 0, bottom: 0 })),
}));
jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
// Mock Tamagui components
jest.mock('tamagui', () => ({
__esModule: true,
YStack: ({ children, ...props }: any) => <div {...props}>{children}</div>,
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
}));
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
DelayedLottieView: () => null,
}));
jest.mock('@selfxyz/mobile-sdk-alpha/constants/colors', () => ({
black: '#000000',
white: '#FFFFFF',
}));
jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({
AbstractButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="abstract-button" type="button">
{children}
</button>
),
PrimaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="primary-button" type="button">
{children}
</button>
),
SecondaryButton: ({ children, onPress }: any) => (
<button onClick={onPress} data-testid="secondary-button" type="button">
{children}
</button>
),
Title: ({ children }: any) => <div data-testid="title">{children}</div>,
Description: ({ children }: any) => (
<div data-testid="description">{children}</div>
),
}));
jest.mock('@/integrations/haptics', () => ({
buttonTap: jest.fn(),
}));
jest.mock('@/services/notifications/notificationService', () => ({
requestNotificationPermission: jest.fn(),
}));
jest.mock('@/config/sentry', () => ({
captureException: jest.fn(),
}));
jest.mock('@/services/analytics', () => ({
flushAllAnalytics: jest.fn(),
trackNfcEvent: jest.fn(),
}));
const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;
describe('KycSuccessScreen', () => {
const mockNavigate = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
mockUseNavigation.mockReturnValue({
navigate: mockNavigate,
} as any);
});
it('should render the screen without errors', () => {
const { root } = render(<KycSuccessScreen />);
expect(root).toBeTruthy();
});
it('should have navigation available', () => {
render(<KycSuccessScreen />);
expect(mockUseNavigation).toHaveBeenCalled();
});
it('should have notification service available', () => {
render(<KycSuccessScreen />);
expect(notificationService.requestNotificationPermission).toBeDefined();
});
it('renders fallback on render error', () => {
// Mock console.error to suppress error boundary error logs during test
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
// Create a component that throws an error during render
const ThrowError = () => {
throw new Error('Test render error');
};
// Render the error-throwing component wrapped in ErrorBoundary
const { root } = render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
// Verify the error boundary fallback UI is displayed
// Use a more flexible matcher since the text is nested in mocked components
expect(root.findByType('span').props.children).toBe(
'Something went wrong. Please restart the app.',
);
// Restore console.error
consoleErrorSpy.mockRestore();
});
});

View File

@@ -23,8 +23,8 @@
"test-custom-hasher": "yarn test-base 'tests/other_circuits/custom_hasher.test.ts' --exit",
"test-disclose": "yarn test-base 'tests/disclose/vc_and_disclose.test.ts' --exit",
"test-disclose-aadhaar": "yarn test-base 'tests/disclose/vc_and_disclose_aadhaar.test.ts' --exit",
"test-disclose-kyc": "yarn test-base 'tests/disclose/vc_and_disclose_kyc.test.ts' --exit",
"test-disclose-id": "yarn test-base 'tests/disclose/vc_and_disclose_id.test.ts' --exit",
"test-disclose-kyc": "yarn test-base 'tests/disclose/vc_and_disclose_kyc.test.ts' --exit",
"test-dsc": "yarn test-base --max-old-space-size=51200 'tests/dsc/dsc.test.ts' --exit",
"test-ecdsa": "yarn test-base 'tests/utils/ecdsa.test.ts' --exit",
"test-gcp-jwt-verifier": "yarn test-base 'tests/gcp_jwt_verifier/gcp_jwt_verifier.test.ts' --exit",
@@ -35,8 +35,8 @@
"test-qr-extractor": "yarn test-base 'tests/other_circuits/qrdata_extractor.test.ts' --exit",
"test-register": "yarn test-base --max-old-space-size=40960 'tests/register/register.test.ts' --exit",
"test-register-aadhaar": "yarn test-base 'tests/register/register_aadhaar.test.ts' --exit",
"test-register-kyc": "yarn test-base 'tests/register/register_kyc.test.ts' --exit",
"test-register-id": "yarn test-base --max-old-space-size=40960 'tests/register_id/register_id.test.ts' --exit",
"test-register-kyc": "yarn test-base 'tests/register/register_kyc.test.ts' --exit",
"test-rsa": "yarn test-base 'tests/utils/rsaPkcs1v1_5.test.ts' --exit",
"test-rsa-pss": "yarn test-base 'tests/utils/rsapss.test.ts' --exit"
},
@@ -77,6 +77,7 @@
"snarkjs": "^0.7.1"
},
"devDependencies": {
"@babel/core": "^7.28.6",
"@types/chai": "4.3.11",
"@types/chai-as-promised": "^7.1.6",
"@types/circomlibjs": "^0.1.6",
@@ -85,6 +86,8 @@
"@types/node-forge": "^1.3.5",
"@yarnpkg/sdks": "^3.2.0",
"chai": "^4.4.1",
"eslint": "^8.57.0",
"eslint-plugin-import": "^2.31.0",
"mocha": "^10.7.3",
"prettier": "^3.5.3",
"ts-mocha": "^10.0.0",

View File

@@ -15,9 +15,9 @@ OUTPUT_DIR="build/${CIRCUIT_TYPE}"
# Define circuits and their configurations
# format: name:poweroftau:build_flag
CIRCUITS=(
# "vc_and_disclose:20:true"
# "vc_and_disclose_id:20:true"
# "vc_and_disclose_aadhaar:20:true"
"vc_and_disclose:20:true"
"vc_and_disclose_id:20:true"
"vc_and_disclose_aadhaar:20:true"
"vc_and_disclose_kyc:17:true"
)

View File

@@ -38,7 +38,7 @@ describe('OFAC - Name and DOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -51,7 +51,7 @@ describe('OFAC - Name and DOB match', async function () {
const dummy_kyc_input = serializeKycData(NON_OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(NON_OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -64,7 +64,7 @@ describe('OFAC - Name and DOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
smt_leaf_key: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
};
@@ -79,7 +79,7 @@ describe('OFAC - Name and DOB match', async function () {
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
ofacInputs.smt_siblings[0] = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -92,7 +92,7 @@ describe('OFAC - Name and DOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, namedob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
smt_root: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
};
@@ -129,7 +129,7 @@ describe('OFAC - Name and YOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -142,7 +142,7 @@ describe('OFAC - Name and YOB match', async function () {
const dummy_kyc_input = serializeKycData(NON_OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(NON_OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -155,7 +155,7 @@ describe('OFAC - Name and YOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
smt_leaf_key: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
};
@@ -170,7 +170,7 @@ describe('OFAC - Name and YOB match', async function () {
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
ofacInputs.smt_siblings[0] = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
};
@@ -183,7 +183,7 @@ describe('OFAC - Name and YOB match', async function () {
const dummy_kyc_input = serializeKycData(OFAC_DUMMY_INPUT);
const ofacInputs = generateCircuitInputsOfac(OFAC_DUMMY_INPUT, nameyob_smt, proofLevel);
const inputs = {
kyc_data: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
data_padded: dummy_kyc_input.split('').map((x) => x.charCodeAt(0)),
...ofacInputs,
smt_root: BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString(),
};

View File

@@ -685,12 +685,12 @@
"@openpassport/zk-kit-imt": "^0.0.5",
"@openpassport/zk-kit-lean-imt": "^0.0.6",
"@openpassport/zk-kit-smt": "^0.0.1",
"@peculiar/x509": "^1.12.3",
"@peculiar/x509": "^1.14.3",
"@stablelib/cbor": "^2.0.1",
"@zk-kit/baby-jubjub": "^1.0.3",
"@zk-kit/eddsa-poseidon": "^1.1.0",
"asn1.js": "^5.4.1",
"asn1js": "^3.0.5",
"asn1js": "^3.0.7",
"axios": "^1.7.2",
"buffer": "^6.0.3",
"country-emoji": "^1.5.6",
@@ -706,7 +706,7 @@
"jsrsasign": "^11.1.0",
"node-forge": "github:remicolin/forge#17a11a632dd0e50343b3b8393245a2696f78afbb",
"path": "^0.12.7",
"pkijs": "^3.2.4",
"pkijs": "^3.3.3",
"poseidon-lite": "^0.2.0",
"snarkjs": "^0.7.5",
"typescript-parser": "^2.6.1",
@@ -727,7 +727,7 @@
"eslint-plugin-sort-exports": "^0.9.1",
"prettier": "^3.5.3",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
},
"packageManager": "yarn@4.12.0",

View File

@@ -313,8 +313,8 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
);
} else if (attestationId == AttestationId.KYC) {
IIdentityRegistryKycV1($._registries[attestationId]).registerCommitment(
registerCircuitProof.pubSignals[CircuitConstantsV2.SELFRICA_NULLIFIER_INDEX],
registerCircuitProof.pubSignals[CircuitConstantsV2.SELFRICA_COMMITMENT_INDEX]
registerCircuitProof.pubSignals[CircuitConstantsV2.KYC_NULLIFIER_INDEX],
registerCircuitProof.pubSignals[CircuitConstantsV2.KYC_COMMITMENT_INDEX]
);
} else {
revert InvalidAttestationId();
@@ -876,7 +876,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
* @notice Performs current date validation with format-aware parsing
* @dev Handles three date formats:
* - E_PASSPORT/EU_ID_CARD: 6 ASCII chars (YYMMDD)
* - SELFRICA_ID_CARD: 8 ASCII digits (YYYYMMDD)
* - KYC: 8 ASCII digits (YYYYMMDD)
* - AADHAAR: 3 numeric signals (year, month, day)
* @param attestationId The attestation type to determine date format
* @param vcAndDiscloseProof The proof containing date information
@@ -900,7 +900,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
}
currentTimestamp = Formatter.proofDateToUnixTimestamp(dateNum);
} else if (attestationId == AttestationId.KYC) {
// SELFRICA: 8 ASCII digits (YYYYMMDD)
// KYC: 8 ASCII digits (YYYYMMDD)
uint256[3] memory dateNum; // [year, month, day]
unchecked {
for (uint256 i; i < 4; ++i)
@@ -1016,7 +1016,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
/**
* @notice Creates verification output based on attestation type.
* @dev Formats proof data into the appropriate output structure for the attestation type.
* @param attestationId The attestation identifier (passport, EU ID card, Aadhaar, or Selfrica).
* @param attestationId The attestation identifier (passport, EU ID card, Aadhaar, or KYC).
* @param vcAndDiscloseProof The VC and Disclose proof data.
* @param indices The circuit-specific indices for extracting proof values.
* @param userIdentifier The user identifier to include in the output.

View File

@@ -8,7 +8,7 @@ pragma solidity 0.8.28;
* - E_PASSPORT (1): Electronic passports with NFC chip
* - EU_ID_CARD (2): EU biometric ID cards with NFC chip
* - AADHAAR (3): Indian Aadhaar identity documents
* - SELFRICA_ID_CARD (4): African identity documents via Selfrica/SmileID
* - KYC (4): African identity documents via SumSub
*/
library AttestationId {
/// @notice Identifier for an E-PASSPORT attestation (electronic passports with NFC chip).
@@ -20,6 +20,6 @@ library AttestationId {
/// @notice Identifier for an AADHAAR attestation (Indian Aadhaar identity documents).
bytes32 constant AADHAAR = bytes32(uint256(3));
/// @notice Identifier for a SELFRICA_ID_CARD attestation (African identity documents via Selfrica/SmileID).
/// @notice Identifier for a KYC attestation (African identity documents via SumSub).
bytes32 constant KYC = bytes32(uint256(4));
}

View File

@@ -55,15 +55,15 @@ library CircuitConstantsV2 {
uint256 constant AADHAAR_TIMESTAMP_INDEX = 3;
// ---------------------------
// Selfrica Circuit Constants
// KYC Circuit Constants
// ---------------------------
/**
* @notice Index to access the pubkey commitment in the Selfrica circuit public signals.
* @notice Index to access the pubkey commitment in the KYC circuit public signals.
*/
uint256 constant SELFRICA_NULLIFIER_INDEX = 0;
uint256 constant SELFRICA_COMMITMENT_INDEX = 1;
uint256 constant SELFRICA_PUBKEY_COMMITMENT_INDEX = 2;
uint256 constant SELFRICA_ATTESTATION_ID_INDEX = 3;
uint256 constant KYC_NULLIFIER_INDEX = 0;
uint256 constant KYC_COMMITMENT_INDEX = 1;
uint256 constant KYC_PUBKEY_COMMITMENT_INDEX = 2;
uint256 constant KYC_ATTESTATION_ID_INDEX = 3;
// -------------------------------------
// VC and Disclose Circuit Constants

View File

@@ -2,8 +2,8 @@
pragma solidity 0.8.28;
/**
* @title IIdentityRegistrySelfricaV1
* @notice Interface for the Identity Registry Selfrica v1.
* @title IIdentityRegistryKycV1
* @notice Interface for the Identity Registry KYC v1.
* @dev This interface exposes only the external functions accessible by regular callers,
* i.e. functions that are not owner-restricted.
*/

View File

@@ -64,7 +64,7 @@ interface IAadhaarRegisterCircuitVerifier {
) external view returns (bool isValid);
}
interface ISelfricaRegisterCircuitVerifier {
interface IKycRegisterCircuitVerifier {
/**
* @notice Verifies a given register circuit proof.
* @dev This function checks the validity of the provided proof parameters.

View File

@@ -50,7 +50,7 @@ interface IVcAndDiscloseAadhaarCircuitVerifier {
) external view returns (bool);
}
interface IVcAndDiscloseSelfricaCircuitVerifier {
interface IVcAndDiscloseKycCircuitVerifier {
/**
* @notice Verifies a given VC and Disclose zero-knowledge proof.
* @dev This function checks the validity of the provided proof parameters.

View File

@@ -38,8 +38,8 @@ library CustomVerifier {
SelfStructs.AadhaarOutput memory aadhaarOutput = abi.decode(proofOutput, (SelfStructs.AadhaarOutput));
return CustomVerifier.verifyAadhaar(verificationConfig, aadhaarOutput);
} else if (attestationId == AttestationId.KYC) {
SelfStructs.KycOutput memory selfricaOutput = abi.decode(proofOutput, (SelfStructs.KycOutput));
return CustomVerifier.verifySelfrica(verificationConfig, selfricaOutput);
SelfStructs.KycOutput memory kycOutput = abi.decode(proofOutput, (SelfStructs.KycOutput));
return CustomVerifier.verifyKyc(verificationConfig, kycOutput);
} else {
revert InvalidAttestationId();
}
@@ -298,20 +298,20 @@ library CustomVerifier {
}
/**
* @notice Verifies a Selfrica output.
* @notice Verifies a KYC output.
* @param verificationConfig The verification configuration.
* @param selfricaOutput The Selfrica output from the circuit.
* @param kycOutput The KYC output from the circuit.
* @return genericDiscloseOutput The generic disclose output.
*/
function verifySelfrica(
function verifyKyc(
SelfStructs.VerificationConfigV2 memory verificationConfig,
SelfStructs.KycOutput memory selfricaOutput
SelfStructs.KycOutput memory kycOutput
) internal pure returns (SelfStructs.GenericDiscloseOutputV2 memory) {
if (verificationConfig.ofacEnabled[1] || verificationConfig.ofacEnabled[2]) {
if (
!CircuitAttributeHandlerV2.compareOfac(
AttestationId.KYC,
selfricaOutput.revealedDataPacked,
kycOutput.revealedDataPacked,
false,
verificationConfig.ofacEnabled[1],
verificationConfig.ofacEnabled[2]
@@ -323,9 +323,7 @@ library CustomVerifier {
if (verificationConfig.forbiddenCountriesEnabled) {
for (uint256 i = 0; i < 4; i++) {
if (
selfricaOutput.forbiddenCountriesListPacked[i] != verificationConfig.forbiddenCountriesListPacked[i]
) {
if (kycOutput.forbiddenCountriesListPacked[i] != verificationConfig.forbiddenCountriesListPacked[i]) {
revert InvalidForbiddenCountries();
}
}
@@ -335,7 +333,7 @@ library CustomVerifier {
if (
!CircuitAttributeHandlerV2.compareOlderThanNumeric(
AttestationId.KYC,
selfricaOutput.revealedDataPacked,
kycOutput.revealedDataPacked,
verificationConfig.olderThan
)
) {
@@ -345,27 +343,27 @@ library CustomVerifier {
SelfStructs.GenericDiscloseOutputV2 memory genericDiscloseOutput = SelfStructs.GenericDiscloseOutputV2({
attestationId: AttestationId.KYC,
userIdentifier: selfricaOutput.userIdentifier,
nullifier: selfricaOutput.nullifier,
forbiddenCountriesListPacked: selfricaOutput.forbiddenCountriesListPacked,
userIdentifier: kycOutput.userIdentifier,
nullifier: kycOutput.nullifier,
forbiddenCountriesListPacked: kycOutput.forbiddenCountriesListPacked,
issuingState: "UNAVAILABLE",
name: CircuitAttributeHandlerV2.getName(AttestationId.KYC, selfricaOutput.revealedDataPacked),
idNumber: CircuitAttributeHandlerV2.getDocumentNumber(AttestationId.KYC, selfricaOutput.revealedDataPacked),
nationality: CircuitAttributeHandlerV2.getNationality(AttestationId.KYC, selfricaOutput.revealedDataPacked),
name: CircuitAttributeHandlerV2.getName(AttestationId.KYC, kycOutput.revealedDataPacked),
idNumber: CircuitAttributeHandlerV2.getDocumentNumber(AttestationId.KYC, kycOutput.revealedDataPacked),
nationality: CircuitAttributeHandlerV2.getNationality(AttestationId.KYC, kycOutput.revealedDataPacked),
dateOfBirth: CircuitAttributeHandlerV2.getDateOfBirthFullYear(
AttestationId.KYC,
selfricaOutput.revealedDataPacked
kycOutput.revealedDataPacked
),
gender: CircuitAttributeHandlerV2.getGender(AttestationId.KYC, selfricaOutput.revealedDataPacked),
gender: CircuitAttributeHandlerV2.getGender(AttestationId.KYC, kycOutput.revealedDataPacked),
expiryDate: CircuitAttributeHandlerV2.getExpiryDateFullYear(
AttestationId.KYC,
selfricaOutput.revealedDataPacked
kycOutput.revealedDataPacked
),
olderThan: verificationConfig.olderThan,
ofac: [
false,
CircuitAttributeHandlerV2.getNameAndDobOfac(AttestationId.KYC, selfricaOutput.revealedDataPacked),
CircuitAttributeHandlerV2.getNameAndYobOfac(AttestationId.KYC, selfricaOutput.revealedDataPacked)
CircuitAttributeHandlerV2.getNameAndDobOfac(AttestationId.KYC, kycOutput.revealedDataPacked),
CircuitAttributeHandlerV2.getNameAndYobOfac(AttestationId.KYC, kycOutput.revealedDataPacked)
]
});

View File

@@ -5,7 +5,7 @@ import {AttestationId} from "../constants/AttestationId.sol";
import {GenericProofStruct} from "../interfaces/IRegisterCircuitVerifier.sol";
import {IVcAndDiscloseCircuitVerifier} from "../interfaces/IVcAndDiscloseCircuitVerifier.sol";
import {IVcAndDiscloseAadhaarCircuitVerifier} from "../interfaces/IVcAndDiscloseCircuitVerifier.sol";
import {IVcAndDiscloseSelfricaCircuitVerifier} from "../interfaces/IVcAndDiscloseCircuitVerifier.sol";
import {IVcAndDiscloseKycCircuitVerifier} from "../interfaces/IVcAndDiscloseCircuitVerifier.sol";
/**
* @title ProofVerifierLib
@@ -25,7 +25,7 @@ library ProofVerifierLib {
* @dev Handles different attestation types with different public signal counts:
* - E_PASSPORT and EU_ID_CARD: 21 public signals
* - AADHAAR: 19 public signals
* - SELFRICA_ID_CARD: 28 public signals
* - KYC: 29 public signals
* @param attestationId The type of attestation being verified
* @param verifierAddress The address of the verifier contract
* @param vcAndDiscloseProof The proof data including public signals
@@ -73,7 +73,7 @@ library ProofVerifierLib {
}
if (
!IVcAndDiscloseSelfricaCircuitVerifier(verifierAddress).verifyProof(
!IVcAndDiscloseKycCircuitVerifier(verifierAddress).verifyProof(
vcAndDiscloseProof.a,
vcAndDiscloseProof.b,
vcAndDiscloseProof.c,

View File

@@ -6,7 +6,7 @@ import {CircuitConstantsV2} from "../constants/CircuitConstantsV2.sol";
import {GenericProofStruct} from "../interfaces/IRegisterCircuitVerifier.sol";
import {IRegisterCircuitVerifier} from "../interfaces/IRegisterCircuitVerifier.sol";
import {IAadhaarRegisterCircuitVerifier} from "../interfaces/IRegisterCircuitVerifier.sol";
import {ISelfricaRegisterCircuitVerifier} from "../interfaces/IRegisterCircuitVerifier.sol";
import {IKycRegisterCircuitVerifier} from "../interfaces/IRegisterCircuitVerifier.sol";
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.sol";
import {IIdentityRegistryIdCardV1} from "../interfaces/IIdentityRegistryIdCardV1.sol";
import {IIdentityRegistryAadhaarV1} from "../interfaces/IIdentityRegistryAadhaarV1.sol";
@@ -100,7 +100,7 @@ library RegisterProofVerifierLib {
} else if (attestationId == AttestationId.KYC) {
if (
!IIdentityRegistryKycV1(registryAddress).checkPubkeyCommitment(
registerCircuitProof.pubSignals[CircuitConstantsV2.SELFRICA_PUBKEY_COMMITMENT_INDEX]
registerCircuitProof.pubSignals[CircuitConstantsV2.KYC_PUBKEY_COMMITMENT_INDEX]
)
) {
revert InvalidPubkeyCommitment();
@@ -158,7 +158,7 @@ library RegisterProofVerifierLib {
registerCircuitProof.pubSignals[3]
];
if (
!ISelfricaRegisterCircuitVerifier(verifier).verifyProof(
!IKycRegisterCircuitVerifier(verifier).verifyProof(
registerCircuitProof.a,
registerCircuitProof.b,
registerCircuitProof.c,

View File

@@ -32,11 +32,11 @@ import {Formatter} from "../libraries/Formatter.sol";
*/
/**
* @title IdentityRegistrySelfricaStorageV1
* @dev Abstract contract for storage layout of IdentityRegistrySelfricaImplV1.
* @title IdentityRegistryKycStorageV1
* @dev Abstract contract for storage layout of IdentityRegistryKycImplV1.
* Inherits from ImplRoot to provide upgradeable functionality.
*/
abstract contract IdentityRegistrySelfricaStorageV1 is ImplRoot {
abstract contract IdentityRegistryKycStorageV1 is ImplRoot {
// =============================================
// Storage Variables
// =============================================
@@ -56,7 +56,7 @@ abstract contract IdentityRegistrySelfricaStorageV1 is ImplRoot {
/// @notice Mapping from nullifier to a boolean indicating registration.
mapping(uint256 => bool) internal _nullifiers;
/// @notice Pubkey commitments registered for Selfrica.
/// @notice Pubkey commitments registered for KYC.
mapping(uint256 => bool) internal _isRegisteredPubkeyCommitment;
/// @notice Current name and date of birth OFAC root.
@@ -110,11 +110,11 @@ interface IPCR0Manager {
}
/**
* @title IdentityRegistrySelfricaImplV1
* @title IdentityRegistryKycImplV1
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
* @dev Inherits from IdentityRegistrySelfricaStorageV1 and implements IIdentityRegistrySelfricaV1.
* @dev Inherits from IdentityRegistryKycStorageV1 and implements IIdentityRegistryKycV1.
*/
contract IdentityRegistrySelfricaImplV1 is IdentityRegistrySelfricaStorageV1, IIdentityRegistryKycV1 {
contract IdentityRegistryKycImplV1 is IdentityRegistryKycStorageV1, IIdentityRegistryKycV1 {
using InternalLeanIMT for LeanIMTData;
// ====================================================

View File

@@ -38,7 +38,7 @@ function getHubImplV2InitializeData() {
* - Verification configs via setVerificationConfigV2()
*
* Post-deployment configuration steps:
* 1. Set registry addresses for each attestation type (E_PASSPORT, EU_ID_CARD, AADHAAR, SELFRICA_ID_CARD)
* 1. Set registry addresses for each attestation type (E_PASSPORT, EU_ID_CARD, AADHAAR, KYC)
* 2. Configure circuit verifiers for different signature types
* 3. Set up verification configurations using setVerificationConfigV2()
* 4. Transfer ownership to the appropriate address if needed

View File

@@ -16,9 +16,8 @@ import RegisterVerifierArtifactLocal from "../../artifacts/contracts/verifiers/l
import RegisterIdVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register_id/Verifier_register_id_sha256_sha256_sha256_rsa_65537_4096_staging.sol/Verifier_register_id_sha256_sha256_sha256_rsa_65537_4096_staging.json";
import RegisterAadhaarVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_aadhaar_staging.sol/Verifier_register_aadhaar_staging.json";
import DscVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/dsc/Verifier_dsc_sha256_rsa_65537_4096_staging.sol/Verifier_dsc_sha256_rsa_65537_4096_staging.json";
import RegisterSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_kyc_staging.sol/Verifier_register_kyc_staging.json";
// import GCPJWTVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/gcp_jwt_verifier/Verifier_gcp_jwt_verifier_staging.sol/Verifier_gcp_jwt_verifier_staging.json";
import VcAndDiscloseSelfricaVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/disclose/Verifier_vc_and_disclose_kyc_staging.sol/Verifier_vc_and_disclose_kyc_staging.json";
import RegisterKycVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/register/Verifier_register_kyc_staging.sol/Verifier_register_kyc_staging.json";
import VcAndDiscloseKycVerifierArtifactLocal from "../../artifacts/contracts/verifiers/local/staging/disclose/Verifier_vc_and_disclose_kyc_staging.sol/Verifier_vc_and_disclose_kyc_staging.json";
export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
let identityVerificationHubV2: any;
@@ -29,16 +28,16 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
let identityRegistryIdImpl: any;
let identityRegistryAadhaarImpl: any;
let identityRegistryAadhaarProxy: any;
let identityRegistrySelfricaImpl: any;
let identityRegistrySelfricaProxy: any;
let identityRegistryKycImpl: any;
let identityRegistryKycProxy: any;
let vcAndDiscloseVerifier: any;
let vcAndDiscloseIdVerifier: any;
let vcAndDiscloseAadhaarVerifier: any;
let vcAndDiscloseSelfricaVerifier: any;
let vcAndDiscloseKycVerifier: any;
let registerVerifier: any;
let registerIdVerifier: any;
let registerAadhaarVerifier: any;
let registerSelfricaVerifier: any;
let registerKycVerifier: any;
let dscVerifier: any;
let testSelfVerificationRoot: any;
let owner: HardhatEthersSigner;
@@ -92,16 +91,16 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await vcAndDiscloseAadhaarVerifier.waitForDeployment();
}
let vcAndDiscloseSelfricaVerifierArtifact;
// Deploy VC and Disclose Selfrica verifier
let vcAndDiscloseKycVerifierArtifact;
// Deploy VC and Disclose KYC verifier
{
vcAndDiscloseSelfricaVerifierArtifact = VcAndDiscloseSelfricaVerifierArtifactLocal;
const vcAndDiscloseSelfricaVerifierFactory = await ethers.getContractFactory(
vcAndDiscloseSelfricaVerifierArtifact.abi,
vcAndDiscloseSelfricaVerifierArtifact.bytecode,
vcAndDiscloseKycVerifierArtifact = VcAndDiscloseKycVerifierArtifactLocal;
const vcAndDiscloseKycVerifierFactory = await ethers.getContractFactory(
vcAndDiscloseKycVerifierArtifact.abi,
vcAndDiscloseKycVerifierArtifact.bytecode,
);
vcAndDiscloseSelfricaVerifier = await vcAndDiscloseSelfricaVerifierFactory.connect(owner).deploy();
await vcAndDiscloseSelfricaVerifier.waitForDeployment();
vcAndDiscloseKycVerifier = await vcAndDiscloseKycVerifierFactory.connect(owner).deploy();
await vcAndDiscloseKycVerifier.waitForDeployment();
}
// Deploy register verifier
@@ -140,16 +139,16 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await registerAadhaarVerifier.waitForDeployment();
}
// Deploy register selfrica verifier
let registerSelfricaVerifierArtifact, registerSelfricaVerifierFactory;
// Deploy register kyc verifier
let registerKycVerifierArtifact, registerKycVerifierFactory;
{
registerSelfricaVerifierArtifact = RegisterSelfricaVerifierArtifactLocal;
registerSelfricaVerifierFactory = await ethers.getContractFactory(
registerSelfricaVerifierArtifact.abi,
registerSelfricaVerifierArtifact.bytecode,
registerKycVerifierArtifact = RegisterKycVerifierArtifactLocal;
registerKycVerifierFactory = await ethers.getContractFactory(
registerKycVerifierArtifact.abi,
registerKycVerifierArtifact.bytecode,
);
registerSelfricaVerifier = await registerSelfricaVerifierFactory.connect(owner).deploy();
await registerSelfricaVerifier.waitForDeployment();
registerKycVerifier = await registerKycVerifierFactory.connect(owner).deploy();
await registerKycVerifier.waitForDeployment();
}
// Deploy dsc verifier
@@ -257,16 +256,16 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await identityRegistryAadhaarImpl.waitForDeployment();
}
// Deploy IdentityRegistrySelfricaImplV1 for Selfrica
let IdentityRegistrySelfricaImplFactory;
// Deploy IdentityRegistryKycImplV1 for KYC
let IdentityRegistryKycImplFactory;
{
IdentityRegistrySelfricaImplFactory = await ethers.getContractFactory("IdentityRegistrySelfricaImplV1", {
IdentityRegistryKycImplFactory = await ethers.getContractFactory("IdentityRegistryKycImplV1", {
libraries: {
PoseidonT3: poseidonT3.target,
},
});
identityRegistrySelfricaImpl = await IdentityRegistrySelfricaImplFactory.connect(owner).deploy();
await identityRegistrySelfricaImpl.waitForDeployment();
identityRegistryKycImpl = await IdentityRegistryKycImplFactory.connect(owner).deploy();
await identityRegistryKycImpl.waitForDeployment();
}
// Deploy IdentityVerificationHubImplV2
@@ -324,18 +323,18 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await identityRegistryAadhaarProxy.waitForDeployment();
}
// Deploy Selfrica registry with temporary hub address and local PCR0Manager
let registrySelfricaInitData, registrySelfricaProxyFactory;
// Deploy Kyc registry with temporary hub address and local PCR0Manager
let registryKycInitData, registryKycProxyFactory;
{
registrySelfricaInitData = identityRegistrySelfricaImpl.interface.encodeFunctionData("initialize", [
registryKycInitData = identityRegistryKycImpl.interface.encodeFunctionData("initialize", [
temporaryHubAddress,
pcr0Manager.target,
]);
registrySelfricaProxyFactory = await ethers.getContractFactory("IdentityRegistry");
identityRegistrySelfricaProxy = await registrySelfricaProxyFactory
registryKycProxyFactory = await ethers.getContractFactory("IdentityRegistry");
identityRegistryKycProxy = await registryKycProxyFactory
.connect(owner)
.deploy(identityRegistrySelfricaImpl.target, registrySelfricaInitData);
await identityRegistrySelfricaProxy.waitForDeployment();
.deploy(identityRegistryKycImpl.target, registryKycInitData);
await identityRegistryKycProxy.waitForDeployment();
}
// Deploy hub V2 with simple initialization (V2 has different initialization)
@@ -374,17 +373,14 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await updateAadhaarHubTx.wait();
}
let registrySelfricaContract, updateSelfricaHubTx;
let registryKycContract, updateKycHubTx;
{
registrySelfricaContract = await ethers.getContractAt(
"IdentityRegistrySelfricaImplV1",
identityRegistrySelfricaProxy.target,
);
updateSelfricaHubTx = await registrySelfricaContract.updateHub(identityVerificationHubV2.target);
await updateSelfricaHubTx.wait();
registryKycContract = await ethers.getContractAt("IdentityRegistryKycImplV1", identityRegistryKycProxy.target);
updateKycHubTx = await registryKycContract.updateHub(identityVerificationHubV2.target);
await updateKycHubTx.wait();
// Configure GCP JWT verifier for Selfrica
await registrySelfricaContract.updateGCPJWTVerifier(gcpJwtVerifier.target);
// Configure GCP JWT verifier for Kyc
await registryKycContract.updateGCPJWTVerifier(gcpJwtVerifier.target);
}
let hubContract;
@@ -412,8 +408,8 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
nameAndYob_smt,
nameDobAadhar_smt,
nameYobAadhar_smt,
nameAndDob_selfrica_smt,
nameAndYob_selfrica_smt,
nameAndDob_kyc_smt,
nameAndYob_kyc_smt,
} = getSMTs();
// Update passport roots
@@ -429,27 +425,27 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
await registryAadhaarContract.updateNameAndDobOfacRoot(nameDobAadhar_smt.root, { from: owner });
await registryAadhaarContract.updateNameAndYobOfacRoot(nameYobAadhar_smt.root, { from: owner });
// Update Selfrica roots
await registrySelfricaContract.updateNameAndDobOfacRoot(nameAndDob_selfrica_smt.root, { from: owner });
await registrySelfricaContract.updateNameAndYobOfacRoot(nameAndYob_selfrica_smt.root, { from: owner });
// Update Kyc roots
await registryKycContract.updateNameAndDobOfacRoot(nameAndDob_kyc_smt.root, { from: owner });
await registryKycContract.updateNameAndYobOfacRoot(nameAndYob_kyc_smt.root, { from: owner });
// Register verifiers with the hub
const E_PASSPORT = ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(1), 32));
const EU_ID_CARD = ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(2), 32));
const AADHAAR = ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(3), 32));
const SELFRICA = ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(4), 32));
const Kyc = ethers.hexlify(ethers.zeroPadValue(ethers.toBeHex(4), 32));
// Update registries in the hub
await hubContract.updateRegistry(E_PASSPORT, identityRegistryProxy.target);
await hubContract.updateRegistry(EU_ID_CARD, identityRegistryIdProxy.target);
await hubContract.updateRegistry(AADHAAR, identityRegistryAadhaarProxy.target);
await hubContract.updateRegistry(SELFRICA, identityRegistrySelfricaProxy.target);
await hubContract.updateRegistry(Kyc, identityRegistryKycProxy.target);
// Update VC and Disclose verifiers
await hubContract.updateVcAndDiscloseCircuit(E_PASSPORT, vcAndDiscloseVerifier.target);
await hubContract.updateVcAndDiscloseCircuit(EU_ID_CARD, vcAndDiscloseIdVerifier.target);
await hubContract.updateVcAndDiscloseCircuit(AADHAAR, vcAndDiscloseAadhaarVerifier.target);
await hubContract.updateVcAndDiscloseCircuit(SELFRICA, vcAndDiscloseSelfricaVerifier.target);
await hubContract.updateVcAndDiscloseCircuit(Kyc, vcAndDiscloseKycVerifier.target);
// Update register verifiers
await hubContract.updateRegisterCircuitVerifier(
@@ -463,7 +459,7 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
registerIdVerifier.target,
);
await hubContract.updateRegisterCircuitVerifier(AADHAAR, 0, registerAadhaarVerifier.target);
await hubContract.updateRegisterCircuitVerifier(SELFRICA, 0, registerSelfricaVerifier.target);
await hubContract.updateRegisterCircuitVerifier(Kyc, 0, registerKycVerifier.target);
// Update DSC verifiers
await hubContract.updateDscVerifier(E_PASSPORT, DscVerifierId.dsc_sha256_rsa_65537_4096, dscVerifier.target);
@@ -487,12 +483,12 @@ export async function deploySystemFixturesV2(): Promise<DeployedActorsV2> {
registryId: registryIdContract,
registryAadhaarImpl: identityRegistryAadhaarImpl,
registryAadhaar: registryAadhaarContract,
registrySelfrica: registrySelfricaContract,
registrySelfricaImpl: identityRegistrySelfricaImpl,
registryKyc: registryKycContract,
registryKycImpl: identityRegistryKycImpl,
vcAndDisclose: vcAndDiscloseVerifier,
vcAndDiscloseId: vcAndDiscloseIdVerifier,
vcAndDiscloseAadhaar: vcAndDiscloseAadhaarVerifier,
vcAndDiscloseSelfrica: vcAndDiscloseSelfricaVerifier,
vcAndDiscloseKyc: vcAndDiscloseKycVerifier,
aadhaarPubkey: aadhaarPubkeyCommitment,
register: registerVerifier,
registerId: RegisterVerifierId.register_sha256_sha256_sha256_rsa_65537_4096,

View File

@@ -86,8 +86,8 @@ const vcAndDiscloseCircuitsAadhaar: CircuitArtifacts = {
},
};
const vcAndDiscloseCircuitsSelfrica: CircuitArtifacts = {
vc_and_disclose_selfrica: {
const vcAndDiscloseCircuitsKyc: CircuitArtifacts = {
vc_and_disclose_kyc: {
wasm: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_js/vc_and_disclose_kyc.wasm",
zkey: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_final.zkey",
vkey: "../circuits/build/disclose/vc_and_disclose_kyc/vc_and_disclose_kyc_vkey.json",
@@ -206,7 +206,7 @@ export async function generateRegisterAadhaarProof(
return fixedProof;
}
export async function generateRegisterSelfricaProof(
export async function generateRegisterKycProof(
secret: string,
//return type of prepareAadhaarTestData
inputs: Awaited<ReturnType<typeof generateMockKycRegisterInput>>,
@@ -529,11 +529,11 @@ export async function generateVcAndDiscloseAadhaarProof(
return fixedProof;
}
export async function generateVcAndDiscloseSelfricaProof(
export async function generateVcAndDiscloseKycProof(
inputs: ReturnType<typeof generateKycDiscloseInput>,
): Promise<GenericProofStructStruct> {
const circuitName = "vc_and_disclose_selfrica";
const circuitArtifacts = vcAndDiscloseCircuitsSelfrica;
const circuitName = "vc_and_disclose_kyc";
const circuitArtifacts = vcAndDiscloseCircuitsKyc;
const artifactKey = circuitName;
const vcAndDiscloseProof = await groth16.fullProve(
@@ -545,7 +545,7 @@ export async function generateVcAndDiscloseSelfricaProof(
const vKey = JSON.parse(fs.readFileSync(circuitArtifacts[artifactKey].vkey, "utf8"));
const isValid = await groth16.verify(vKey, vcAndDiscloseProof.publicSignals, vcAndDiscloseProof.proof);
if (!isValid) {
throw new Error("Generated VC and Disclose Selfrica proof verification failed");
throw new Error("Generated VC and Disclose KYC proof verification failed");
}
const rawCallData = await groth16.exportSolidityCallData(vcAndDiscloseProof.proof, vcAndDiscloseProof.publicSignals);
@@ -589,12 +589,8 @@ export function getSMTs() {
) as typeof SMT;
const nameAndDob_id_smt = importSMTFromJsonFile("../circuits/tests/consts/ofac/nameAndDobSMT_ID.json") as typeof SMT;
const nameAndYob_id_smt = importSMTFromJsonFile("../circuits/tests/consts/ofac/nameAndYobSMT_ID.json") as typeof SMT;
const nameAndDob_selfrica_smt = importSMTFromJsonFile(
"../circuits/tests/consts/ofac/nameAndDobSelfricaSMT.json",
) as typeof SMT;
const nameAndYob_selfrica_smt = importSMTFromJsonFile(
"../circuits/tests/consts/ofac/nameAndYobSelfricaSMT.json",
) as typeof SMT;
const nameAndDob_kyc_smt = importSMTFromJsonFile("../circuits/tests/consts/ofac/nameAndDobKycSMT.json") as typeof SMT;
const nameAndYob_kyc_smt = importSMTFromJsonFile("../circuits/tests/consts/ofac/nameAndYobKycSMT.json") as typeof SMT;
return {
passportNo_smt,
@@ -604,8 +600,8 @@ export function getSMTs() {
nameAndYob_id_smt,
nameDobAadhar_smt,
nameYobAadhar_smt,
nameAndDob_selfrica_smt,
nameAndYob_selfrica_smt,
nameAndDob_kyc_smt,
nameAndYob_kyc_smt,
};
}

View File

@@ -10,12 +10,11 @@ import {
IdentityRegistry,
IdentityRegistryImplV1,
IdentityRegistryIdCardImplV1,
IdentityRegistrySelfricaImplV1,
TestSelfVerificationRoot,
Verifier_vc_and_disclose_staging as LocalVerifier,
Verifier_vc_and_disclose_id_staging as LocalIdCardVerifier,
Verifier_vc_and_disclose_aadhaar_staging as LocalAadhaarVerifier,
Verifier_vc_and_disclose_selfrica_staging as LocalSelfricaVerifier,
Verifier_vc_and_disclose_kyc_staging as LocalKycVerifier,
Verifier_vc_and_disclose as ProdVerifier,
Verifier_vc_and_disclose_id as ProdIdCardVerifier,
Verifier_register_sha256_sha256_sha256_rsa_65537_4096 as ProdRegisterVerifier,
@@ -25,7 +24,7 @@ import {
Verifier_dsc_sha256_rsa_65537_4096 as ProdDscVerifier,
Verifier_dsc_sha256_rsa_65537_4096_staging as LocalDscVerifier,
IIdentityVerificationHubV1,
IVcAndDiscloseSelfricaCircuitVerifier,
IVcAndDiscloseKycCircuitVerifier,
IVcAndDiscloseAadhaarCircuitVerifier,
IIdentityVerificationHubV2,
IIdentityRegistryIdCardV1,
@@ -35,6 +34,7 @@ import {
IVcAndDiscloseCircuitVerifier,
IdentityRegistryAadhaarImplV1,
PCR0Manager,
IdentityRegistryKycImplV1,
} from "../../typechain-types";
import { DscVerifierId, RegisterVerifierId } from "@selfxyz/common";
@@ -82,11 +82,11 @@ export interface DeployedActorsV2 {
registryId: IdentityRegistryIdCardImplV1;
registryAadhaarImpl: IdentityRegistryAadhaarImplV1;
registryAadhaar: IdentityRegistryAadhaarImplV1;
registrySelfrica: IdentityRegistrySelfricaImplV1;
registrySelfricaImpl: IdentityRegistrySelfricaImplV1;
registryKyc: IdentityRegistryKycImplV1;
registryKycImpl: IdentityRegistryKycImplV1;
vcAndDisclose: VcAndDiscloseVerifier;
vcAndDiscloseAadhaar: LocalAadhaarVerifier;
vcAndDiscloseSelfrica: LocalSelfricaVerifier;
vcAndDiscloseKyc: LocalKycVerifier;
aadhaarPubkey: bigint;
vcAndDiscloseId: VcAndDiscloseIdVerifier;
register: RegisterVerifier;

View File

@@ -14,15 +14,15 @@ import { generateKycDiscloseInput } from "@selfxyz/common";
import { getSMTs } from "../utils/generateProof";
import { getPackedForbiddenCountries } from "@selfxyz/common/utils/contracts/forbiddenCountries";
import { BigNumberish } from "ethers";
import { generateVcAndDiscloseSelfricaProof } from "../utils/generateProof";
import { generateVcAndDiscloseKycProof } from "../utils/generateProof";
import { KYC_ATTESTATION_ID } from "@selfxyz/common/constants/constants";
import { poseidon2 } from "poseidon-lite";
// Selfrica circuit indices - matches CircuitConstantsV2.getDiscloseIndices(SELFRICA_ID_CARD)
// KYC circuit indices - matches CircuitConstantsV2.getDiscloseIndices(KYC_ID_CARD)
// See CircuitConstantsV2.sol for full layout documentation
const SELFRICA_CURRENT_DATE_INDEX = 21;
const KYC_CURRENT_DATE_INDEX = 21;
describe("Self Verification Flow V2 - Selfrica", () => {
describe("Self Verification Flow V2 - KYC", () => {
let deployedActors: DeployedActorsV2;
let snapshotId: string;
let nullifier: any;
@@ -51,8 +51,8 @@ describe("Self Verification Flow V2 - Selfrica", () => {
const userData = "test-user-data-for-verification";
userIdentifierHash = BigInt(calculateUserIdentifierHash(destChainId, user1Address.slice(2), userData).toString());
nameAndDob_smt = getSMTs().nameAndDob_selfrica_smt;
nameAndYob_smt = getSMTs().nameAndYob_selfrica_smt;
nameAndDob_smt = getSMTs().nameAndDob_kyc_smt;
nameAndYob_smt = getSMTs().nameAndYob_kyc_smt;
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
const LeanIMT = await import("@openpassport/zk-kit-lean-imt").then((mod) => mod.LeanIMT);
@@ -78,7 +78,7 @@ describe("Self Verification Flow V2 - Selfrica", () => {
nullifier = packBytesAndPoseidon(nullifier);
const commitment = poseidon2([BigInt(testInputs.secret), packBytesAndPoseidon(dataPadded)]);
await deployedActors.registrySelfrica.devAddIdentityCommitment(nullifier, commitment);
await deployedActors.registryKyc.devAddIdentityCommitment(nullifier, commitment);
forbiddenCountriesList = [] as Country3LetterCode[];
forbiddenCountriesListPacked = getPackedForbiddenCountries(forbiddenCountriesList);
@@ -97,7 +97,7 @@ describe("Self Verification Flow V2 - Selfrica", () => {
};
await deployedActors.testSelfVerificationRoot.setVerificationConfig(verificationConfigV2);
baseVcAndDiscloseProof = await generateVcAndDiscloseSelfricaProof(testInputs);
baseVcAndDiscloseProof = await generateVcAndDiscloseKycProof(testInputs);
snapshotId = await ethers.provider.send("evm_snapshot", []);
});
@@ -302,7 +302,7 @@ describe("Self Verification Flow V2 - Selfrica", () => {
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32);
const clonedPubSignal = structuredClone(baseVcAndDiscloseProof.pubSignals);
// scopeIndex for Selfrica is 16
// scopeIndex for KYC is 16
clonedPubSignal[16] = 1n;
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
@@ -348,7 +348,7 @@ describe("Self Verification Flow V2 - Selfrica", () => {
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32);
const clonedPubSignal = structuredClone(baseVcAndDiscloseProof.pubSignals);
// userIdentifierIndex for Selfrica is 20
// userIdentifierIndex for KYC is 20
clonedPubSignal[20] = 1n;
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
@@ -395,8 +395,8 @@ describe("Self Verification Flow V2 - Selfrica", () => {
const clonedPubSignal = structuredClone(baseVcAndDiscloseProof.pubSignals);
// Modify current date at the correct index using BigInt for safe arithmetic
const currentDateValue = BigInt(clonedPubSignal[SELFRICA_CURRENT_DATE_INDEX]);
clonedPubSignal[SELFRICA_CURRENT_DATE_INDEX] = (currentDateValue + 2n).toString();
const currentDateValue = BigInt(clonedPubSignal[KYC_CURRENT_DATE_INDEX]);
clonedPubSignal[KYC_CURRENT_DATE_INDEX] = (currentDateValue + 2n).toString();
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],
@@ -443,8 +443,8 @@ describe("Self Verification Flow V2 - Selfrica", () => {
const clonedPubSignal = structuredClone(baseVcAndDiscloseProof.pubSignals);
// Modify current date at the correct index using BigInt for safe arithmetic
const currentDateValue = BigInt(clonedPubSignal[SELFRICA_CURRENT_DATE_INDEX]);
clonedPubSignal[SELFRICA_CURRENT_DATE_INDEX] = (currentDateValue - 1n).toString();
const currentDateValue = BigInt(clonedPubSignal[KYC_CURRENT_DATE_INDEX]);
clonedPubSignal[KYC_CURRENT_DATE_INDEX] = (currentDateValue - 1n).toString();
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],
@@ -733,7 +733,7 @@ describe("Self Verification Flow V2 - Selfrica", () => {
KYC_ATTESTATION_ID,
);
const newProof = await generateVcAndDiscloseSelfricaProof(inputs);
const newProof = await generateVcAndDiscloseKycProof(inputs);
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32);
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],

View File

@@ -3,7 +3,7 @@ import { deploySystemFixturesV2 } from "../utils/deploymentV2";
import { DeployedActorsV2 } from "../utils/types";
import { KYC_ATTESTATION_ID } from "@selfxyz/common/constants/constants";
import { generateMockKycRegisterInput } from "@selfxyz/common/utils/kyc/generateInputs";
import { generateRegisterSelfricaProof } from "../utils/generateProof";
import { generateRegisterKycProof } from "../utils/generateProof";
import { expect } from "chai";
function getCurrentDateDigitsYYMMDDHHMMSS(hoursOffset: number = 0): bigint[] {
@@ -46,7 +46,7 @@ function packUint256ToHexFields(value: bigint): [bigint, bigint, bigint] {
return [p0, p1, p2];
}
describe("Selfrica Registration test", function () {
describe("KYC Registration test", function () {
this.timeout(0);
let deployedActors: DeployedActorsV2;
@@ -60,10 +60,10 @@ describe("Selfrica Registration test", function () {
attestationIdBytes32 = ethers.zeroPadValue(ethers.toBeHex(BigInt(KYC_ATTESTATION_ID)), 32);
// Set the owner as the TEE for all tests
await deployedActors.registrySelfrica.updateTEE(await deployedActors.owner.getAddress());
await deployedActors.registryKyc.updateTEE(await deployedActors.owner.getAddress());
// Set the GCP root CA pubkey hash
await deployedActors.registrySelfrica.updateGCPRootCAPubkeyHash(GCP_ROOT_CA_PUBKEY_HASH);
await deployedActors.registryKyc.updateGCPRootCAPubkeyHash(GCP_ROOT_CA_PUBKEY_HASH);
console.log("🎉 System deployment and initial setup completed!");
});
@@ -77,7 +77,7 @@ describe("Selfrica Registration test", function () {
});
describe("Identity Commitment", () => {
let selfricaData: any;
let kycData: any;
let registerProof: any;
let registerSecret: string;
let mockVerifier: any;
@@ -87,14 +87,14 @@ describe("Selfrica Registration test", function () {
before(async () => {
registerSecret = "12345";
selfricaData = await generateMockKycRegisterInput(undefined, true, registerSecret);
registerProof = await generateRegisterSelfricaProof(registerSecret, selfricaData);
kycData = await generateMockKycRegisterInput(undefined, true, registerSecret);
registerProof = await generateRegisterKycProof(registerSecret, kycData);
// Deploy and set mock GCP JWT verifier
const MockVerifierFactory = await ethers.getContractFactory("MockGCPJWTVerifier");
mockVerifier = await MockVerifierFactory.deploy();
await mockVerifier.waitForDeployment();
await deployedActors.registrySelfrica.updateGCPJWTVerifier(mockVerifier.target);
await deployedActors.registryKyc.updateGCPJWTVerifier(mockVerifier.target);
// Get the pubkey commitment from the register proof and pack as hex
const pubkeyCommitment = registerProof.pubSignals[registerProof.pubSignals.length - 2];
@@ -145,19 +145,14 @@ describe("Selfrica Registration test", function () {
});
it("should successfully register an identity commitment", async () => {
await deployedActors.registrySelfrica.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignals,
);
await deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals);
await expect(deployedActors.hub.registerCommitment(attestationIdBytes32, 0n, registerProof)).to.emit(
deployedActors.registrySelfrica,
deployedActors.registryKyc,
"CommitmentRegistered",
);
const isRegistered = await deployedActors.registrySelfrica.nullifiers(registerProof.pubSignals[0]);
const isRegistered = await deployedActors.registryKyc.nullifiers(registerProof.pubSignals[0]);
expect(isRegistered).to.be.true;
});
@@ -168,12 +163,7 @@ describe("Selfrica Registration test", function () {
});
it("should not register an identity commitment if the proof is invalid", async () => {
await deployedActors.registrySelfrica.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignals,
);
await deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals);
const invalidRegisterProof = structuredClone(registerProof);
invalidRegisterProof.pubSignals[1] = 0n;
@@ -229,21 +219,21 @@ describe("Selfrica Registration test", function () {
};
it("should have correct GCP root CA pubkey hash", async () => {
const contractHash = await deployedActors.registrySelfrica.gcpRootCAPubkeyHash();
const contractHash = await deployedActors.registryKyc.gcpRootCAPubkeyHash();
expect(contractHash).to.equal(GCP_ROOT_CA_PUBKEY_HASH);
});
it("should allow owner to update GCP root CA pubkey hash", async () => {
const newHash = 12345n;
await deployedActors.registrySelfrica.updateGCPRootCAPubkeyHash(newHash);
const contractHash = await deployedActors.registrySelfrica.gcpRootCAPubkeyHash();
await deployedActors.registryKyc.updateGCPRootCAPubkeyHash(newHash);
const contractHash = await deployedActors.registryKyc.gcpRootCAPubkeyHash();
expect(contractHash).to.equal(newHash);
});
it("should not allow non-owner to update GCP root CA pubkey hash", async () => {
await expect(
deployedActors.registrySelfrica.connect(deployedActors.user1).updateGCPRootCAPubkeyHash(12345n),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
deployedActors.registryKyc.connect(deployedActors.user1).updateGCPRootCAPubkeyHash(12345n),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "AccessControlUnauthorizedAccount");
});
it("should fail with INVALID_IMAGE when image hash not in PCR0Manager", async () => {
@@ -259,21 +249,21 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_IMAGE");
deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_IMAGE");
});
it("should not allow non-owner to update GCP JWT verifier", async () => {
await expect(
deployedActors.registrySelfrica
deployedActors.registryKyc
.connect(deployedActors.user1)
.updateGCPJWTVerifier(ethers.Wallet.createRandom().address),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
).to.be.revertedWithCustomError(deployedActors.registryKyc, "AccessControlUnauthorizedAccount");
});
it("should allow owner to update GCP JWT verifier", async () => {
const newVerifier = ethers.Wallet.createRandom().address;
await deployedActors.registrySelfrica.updateGCPJWTVerifier(newVerifier);
await deployedActors.registryKyc.updateGCPJWTVerifier(newVerifier);
});
describe("TEE Access Control", () => {
@@ -290,28 +280,28 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica
deployedActors.registryKyc
.connect(deployedActors.user1)
.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "ONLY_TEE_CAN_ACCESS");
).to.be.revertedWithCustomError(deployedActors.registryKyc, "ONLY_TEE_CAN_ACCESS");
});
it("should not allow non-owner to update TEE", async () => {
await expect(
deployedActors.registrySelfrica.connect(deployedActors.user1).updateTEE(ethers.Wallet.createRandom().address),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "AccessControlUnauthorizedAccount");
deployedActors.registryKyc.connect(deployedActors.user1).updateTEE(ethers.Wallet.createRandom().address),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "AccessControlUnauthorizedAccount");
});
it("should allow owner to update TEE", async () => {
const newTee = ethers.Wallet.createRandom().address;
await deployedActors.registrySelfrica.updateTEE(newTee);
expect(await deployedActors.registrySelfrica.tee()).to.equal(newTee);
await deployedActors.registryKyc.updateTEE(newTee);
expect(await deployedActors.registryKyc.tee()).to.equal(newTee);
});
it("should fail with TEE_NOT_SET when TEE address is zero", async () => {
// Deploy minimal fresh registry to test uninitialized TEE state
const freshImpl = await (
await ethers.getContractFactory("IdentityRegistrySelfricaImplV1", {
await ethers.getContractFactory("IdentityRegistryKycImplV1", {
libraries: { PoseidonT3: deployedActors.poseidonT3.target },
})
).deploy();
@@ -324,7 +314,7 @@ describe("Selfrica Registration test", function () {
await ethers.getContractFactory("IdentityRegistry")
).deploy(freshImpl.target, initData);
const freshRegistry = await ethers.getContractAt("IdentityRegistrySelfricaImplV1", freshProxy.target);
const freshRegistry = await ethers.getContractAt("IdentityRegistryKycImplV1", freshProxy.target);
await freshRegistry.updateGCPJWTVerifier(deployedActors.gcpJwtVerifier.target);
const mockPubSignals: bigint[] = [
@@ -373,13 +363,13 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(
deployedActors.registryKyc.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignalsPast,
),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_TIMESTAMP");
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_TIMESTAMP");
// Create a timestamp 2 hours in the future (more than 1 hour threshold)
const nextHourDate = getCurrentDateDigitsYYMMDDHHMMSS(2);
@@ -396,13 +386,13 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(
deployedActors.registryKyc.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignalsFuture,
),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_TIMESTAMP");
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_TIMESTAMP");
});
});
@@ -413,7 +403,7 @@ describe("Selfrica Registration test", function () {
const MockVerifierFactory = await ethers.getContractFactory("MockGCPJWTVerifier");
mockVerifier = await MockVerifierFactory.deploy();
await mockVerifier.waitForDeployment();
await deployedActors.registrySelfrica.updateGCPJWTVerifier(mockVerifier.target);
await deployedActors.registryKyc.updateGCPJWTVerifier(mockVerifier.target);
});
afterEach(async () => {
@@ -434,13 +424,8 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignals,
),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_PROOF");
deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_PROOF");
});
it("should fail with INVALID_ROOT_CA when root CA hash does not match", async () => {
@@ -456,13 +441,8 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignals,
),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_ROOT_CA");
deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_ROOT_CA");
});
it("should fail with INVALID_IMAGE when image hash not in PCR0Manager", async () => {
@@ -478,13 +458,8 @@ describe("Selfrica Registration test", function () {
];
await expect(
deployedActors.registrySelfrica.registerPubkeyCommitment(
mockProof.a,
mockProof.b,
mockProof.c,
mockPubSignals,
),
).to.be.revertedWithCustomError(deployedActors.registrySelfrica, "INVALID_IMAGE");
deployedActors.registryKyc.registerPubkeyCommitment(mockProof.a, mockProof.b, mockProof.c, mockPubSignals),
).to.be.revertedWithCustomError(deployedActors.registryKyc, "INVALID_IMAGE");
});
});
});

View File

@@ -37,16 +37,17 @@
"types": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts --exclude @selfxyz/common --exclude @selfxyz/mobile-app -i --all run types"
},
"resolutions": {
"@babel/core": "^7.28.4",
"@babel/runtime": "^7.28.4",
"@babel/core": "^7.28.6",
"@babel/runtime": "^7.28.6",
"@noble/curves": "1.9.7",
"@noble/hashes": "1.8.0",
"ethereum-cryptography": "3.2.0",
"@swc/core": "1.7.36",
"@tamagui/animations-react-native": "1.126.14",
"@tamagui/toast": "1.126.14",
"@types/node": "^22.18.3",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"ethereum-cryptography": "3.2.0",
"punycode": "npm:punycode.js@^2.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -56,7 +57,7 @@
"react-native-webview": "13.16.0"
},
"dependencies": {
"@babel/runtime": "^7.28.3",
"@babel/runtime": "^7.28.6",
"js-sha1": "^0.7.0",
"react": "^18.3.1",
"react-native": "0.76.9",
@@ -71,7 +72,7 @@
"patch-package": "^8.0.0",
"prettier": "^3.6.2",
"tsx": "^4.21.0",
"typescript": "^5.9.2"
"typescript": "^5.9.3"
},
"packageManager": "yarn@4.12.0",
"engines": {

View File

@@ -149,14 +149,14 @@
"watch": "pkill -f 'tsup.*--watch' 2>/dev/null || true && tsup && yarn postbuild && tsup --watch"
},
"dependencies": {
"@babel/runtime": "^7.28.3",
"@babel/runtime": "^7.28.6",
"@selfxyz/common": "workspace:^",
"@selfxyz/euclid": "^0.6.1",
"@xstate/react": "^5.0.5",
"node-forge": "^1.3.1",
"react-native-nfc-manager": "^3.17.1",
"node-forge": "^1.3.3",
"react-native-nfc-manager": "^3.17.2",
"react-native-svg-circle-country-flags": "^0.2.2",
"socket.io-client": "^4.8.1",
"socket.io-client": "^4.8.3",
"uuid": "^11.1.0",
"xstate": "^5.20.2",
"zustand": "^4.5.2"
@@ -183,11 +183,14 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-native": "0.76.9",
"react-native-blur-effect": "^1.1.3",
"react-native-haptic-feedback": "^2.3.3",
"react-native-localize": "^3.5.2",
"react-native-web": "^0.21.1",
"react-native-localize": "^3.6.1",
"react-native-svg": "15.15.1",
"react-native-web": "^0.21.2",
"react-native-webview": "13.16.0",
"tsup": "^8.0.1",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
},
"peerDependencies": {

View File

@@ -224,5 +224,7 @@ export function createSelfClient({
useSelfAppStore,
useProtocolStore,
useMRZStore,
// Expose config for internal SDK use
config: cfg,
};
}

View File

@@ -4,7 +4,7 @@
import type { Config } from '../types/public';
export const defaultConfig: Required<Config> = {
export const defaultConfig: Omit<Required<Config>, 'devConfig'> & Pick<Config, 'devConfig'> = {
timeouts: { scanMs: 60000 },
// in future this can be used to enable/disable experimental features
features: {},

View File

@@ -4,11 +4,14 @@
import type { Config } from '../types/public';
export function mergeConfig(base: Required<Config>, override: Config): Required<Config> {
type BaseConfig = Omit<Required<Config>, 'devConfig'> & Pick<Config, 'devConfig'>;
export function mergeConfig(base: BaseConfig, override: Config): BaseConfig {
return {
...base,
...override,
timeouts: { ...base.timeouts, ...(override.timeouts ?? {}) },
features: { ...base.features, ...(override.features ?? {}) },
devConfig: override.devConfig ?? base.devConfig,
};
}

View File

@@ -12,6 +12,9 @@ import { checkScannedInfo, formatDateToYYMMDD } from '../../processing/mrz';
import { SdkEvents } from '../../types/events';
import type { MRZInfo } from '../../types/public';
// Dev-only error injection - uses injected devConfig from SDK context
// No cross-package requires needed
export type { MRZScannerViewProps } from '../../components/MRZScannerView';
export { MRZScannerView } from '../../components/MRZScannerView';
@@ -28,12 +31,25 @@ const calculateScanDurationSeconds = (scanStartTimeRef: RefObject<number>) => {
export function useReadMRZ(scanStartTimeRef: RefObject<number>) {
const selfClient = useSelfClient();
const shouldTrigger = selfClient.config?.devConfig?.shouldTrigger;
return {
onPassportRead: useCallback(
(error: Error | null, result?: MRZInfo) => {
const scanDurationSeconds = calculateScanDurationSeconds(scanStartTimeRef);
// Dev-only: Check for injected unknown error
if (shouldTrigger?.('mrz_unknown_error')) {
console.log('[DEV] Injecting MRZ unknown error');
selfClient.trackEvent(PassportEvents.CAMERA_SCAN_FAILED, {
reason: 'unknown_error',
error: 'Injected error for testing',
duration_seconds: parseFloat(scanDurationSeconds),
});
selfClient.emit(SdkEvents.DOCUMENT_MRZ_READ_FAILURE);
return;
}
if (error) {
console.error(error);
@@ -63,7 +79,16 @@ export function useReadMRZ(scanStartTimeRef: RefObject<number>) {
const formattedDateOfBirth = Platform.OS === 'ios' ? formatDateToYYMMDD(dateOfBirth) : dateOfBirth;
const formattedDateOfExpiry = Platform.OS === 'ios' ? formatDateToYYMMDD(dateOfExpiry) : dateOfExpiry;
if (!checkScannedInfo(documentNumber, formattedDateOfBirth, formattedDateOfExpiry)) {
// Dev-only: Check for injected invalid format error
const shouldInjectInvalidFormat = shouldTrigger?.('mrz_invalid_format') || false;
if (
shouldInjectInvalidFormat ||
!checkScannedInfo(documentNumber, formattedDateOfBirth, formattedDateOfExpiry)
) {
if (shouldInjectInvalidFormat) {
console.log('[DEV] Injecting MRZ invalid format error');
}
selfClient.trackEvent(PassportEvents.CAMERA_SCAN_FAILED, {
reason: 'invalid_format',
passportNumberLength: documentNumber.length,
@@ -90,7 +115,7 @@ export function useReadMRZ(scanStartTimeRef: RefObject<number>) {
selfClient.emit(SdkEvents.DOCUMENT_MRZ_READ_SUCCESS);
},
[selfClient],
[selfClient, shouldTrigger],
),
};
}

View File

@@ -138,5 +138,6 @@ export { parseNFCResponse, scanNFC } from './nfc';
export { reactNativeScannerAdapter } from './adapters/react-native/nfc-scanner';
export { sanitizeErrorMessage } from './utils/utils';
export { useCountries } from './documents/useCountries';
export { useMRZStore } from './stores/mrzStore';
export { webNFCScannerShim } from './adapters/web/shims';

View File

@@ -35,6 +35,18 @@ export interface Config {
* treated as `false` and the SDK will continue using legacy flows.
*/
features?: Record<string, boolean>;
/**
* Optional dev-mode configuration for error injection and testing. Should
* only be provided in development builds. Production builds should omit this.
*/
devConfig?: {
/**
* Callback to check if a specific error type should be injected for testing.
* @param errorType - The type of error to check (e.g., 'mrz_unknown_error')
* @returns true if the error should be injected, false otherwise
*/
shouldTrigger?: (errorType: string) => boolean;
};
}
/**
@@ -375,6 +387,9 @@ export interface SelfClient {
useProtocolStore: ReturnType<typeof create<ProtocolState, []>>;
/** Zustand store hook mirroring {@link MRZState}. */
useMRZStore: ReturnType<typeof create<MRZState, []>>;
/** The merged configuration object passed to createSelfClient. */
config: Config;
}
/** Function returned by {@link SelfClient.on} to detach a listener. */

View File

@@ -8,7 +8,7 @@ import { mergeConfig } from '../src/config/merge';
import type { Config } from '../src/types/public';
describe('mergeConfig', () => {
const baseConfig: Required<Config> = {
const baseConfig: Omit<Required<Config>, 'devConfig'> & Pick<Config, 'devConfig'> = {
timeouts: {
scanMs: 30000,
},

View File

@@ -28,7 +28,7 @@
"types": "yarn prebuild && tsc --noEmit"
},
"dependencies": {
"@babel/runtime": "^7.28.3",
"@babel/runtime": "^7.28.6",
"@faker-js/faker": "^10.0.0",
"@noble/hashes": "^1.5.0",
"@react-native-async-storage/async-storage": "^2.2.0",
@@ -46,16 +46,16 @@
"react-native-get-random-values": "^1.11.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-keychain": "^10.0.0",
"react-native-localize": "^3.5.4",
"react-native-safe-area-context": "^5.6.1",
"react-native-svg": "15.12.1",
"react-native-localize": "^3.6.1",
"react-native-safe-area-context": "^5.6.2",
"react-native-svg": "15.15.1",
"react-native-vector-icons": "^10.3.0",
"react-native-webview": "13.16.0",
"stream-browserify": "^3.0.0",
"util": "^0.12.5"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/core": "^7.28.6",
"@react-native-community/cli": "^16.0.3",
"@react-native/gradle-plugin": "0.76.9",
"@react-native/metro-config": "0.76.9",
@@ -83,8 +83,8 @@
"metro-react-native-babel-preset": "0.76.9",
"prettier": "^3.6.2",
"react-dom": "^18.3.1",
"react-native-svg-transformer": "^1.5.1",
"typescript": "^5.9.2",
"react-native-svg-transformer": "^1.5.2",
"typescript": "^5.9.3",
"vitest": "^2.1.8"
}
}

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle b/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
index 1234567..abcdefg 100644
index 0000000..0000001 100644
--- a/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
+++ b/node_modules/@sumsub/react-native-mobilesdk-module/android/build.gradle
@@ -69,9 +69,9 @@ dependencies {
@@ -77,11 +77,11 @@ dependencies {
implementation "com.sumsub.sns:idensic-mobile-sdk:1.40.2"
// remove comment to enable Device Intelligence
@@ -12,3 +12,7 @@ index 1234567..abcdefg 100644
- // implementation "com.sumsub.sns:idensic-mobile-sdk-videoident:1.40.2"
+ implementation "com.sumsub.sns:idensic-mobile-sdk-videoident:1.40.2"
// remove comment if you need EID support
// implementation "com.sumsub.sns:idensic-mobile-sdk-eid:1.40.2"
// remove comment if you need NFC support
// implementation "com.sumsub.sns:idensic-mobile-sdk-nfc:1.40.2"
}

View File

@@ -1,44 +0,0 @@
diff --git a/node_modules/ethereum-cryptography/utils.js b/node_modules/ethereum-cryptography/utils.js
index cedfa36..b494c49 100644
--- a/node_modules/ethereum-cryptography/utils.js
+++ b/node_modules/ethereum-cryptography/utils.js
@@ -8,11 +8,18 @@ exports.bytesToUtf8 = bytesToUtf8;
exports.hexToBytes = hexToBytes;
exports.equalsBytes = equalsBytes;
exports.wrapHash = wrapHash;
-const _assert_1 = __importDefault(require("@noble/hashes/_assert"));
+const assertModule = __importDefault(require("@noble/hashes/_assert"));
const utils_1 = require("@noble/hashes/utils");
-const assertBool = _assert_1.default.bool;
+const assertBool = (assertModule.default && assertModule.default.bool) ||
+ assertModule.bool ||
+ ((value) => {
+ if (typeof value !== "boolean")
+ throw new TypeError(`Expected boolean, not ${value}`);
+ });
exports.assertBool = assertBool;
-const assertBytes = _assert_1.default.bytes;
+const assertBytes = (assertModule.default && assertModule.default.bytes) ||
+ assertModule.bytes ||
+ assertModule.abytes;
exports.assertBytes = assertBytes;
var utils_2 = require("@noble/hashes/utils");
Object.defineProperty(exports, "bytesToHex", { enumerable: true, get: function () { return utils_2.bytesToHex; } });
diff --git a/node_modules/ethereum-cryptography/esm/utils.js b/node_modules/ethereum-cryptography/esm/utils.js
index 8e771ea..b3eed9d 100644
--- a/node_modules/ethereum-cryptography/esm/utils.js
+++ b/node_modules/ethereum-cryptography/esm/utils.js
@@ -1,7 +1,11 @@
import assert from "@noble/hashes/_assert";
import { hexToBytes as _hexToBytes } from "@noble/hashes/utils";
-const assertBool = assert.bool;
-const assertBytes = assert.bytes;
+const assertBool = (assert?.bool) ||
+ ((value) => {
+ if (typeof value !== "boolean")
+ throw new TypeError(`Expected boolean, not ${value}`);
+ });
+const assertBytes = assert.bytes || assert.abytes;
export { assertBool, assertBytes };
export { bytesToHex, bytesToHex as toHex, concatBytes, createView, utf8ToBytes } from "@noble/hashes/utils";
// buf.toString('utf8') -> bytesToUtf8(buf)

View File

@@ -0,0 +1,15 @@
diff --git a/node_modules/react-native-gesture-handler/android/build.gradle b/node_modules/react-native-gesture-handler/android/build.gradle
--- a/node_modules/react-native-gesture-handler/android/build.gradle
+++ b/node_modules/react-native-gesture-handler/android/build.gradle
@@ -229,9 +229,10 @@
}
def kotlin_version = safeExtGet('kotlinVersion', project.properties['RNGH_kotlinVersion'])
+def reactNativeDependency = safeExtGet("reactNativeDependency", "com.facebook.react:react-android")
dependencies {
- implementation 'com.facebook.react:react-native:+' // from node_modules
+ implementation reactNativeDependency
if (shouldUseCommonInterfaceFromReanimated()) {

View File

@@ -1,98 +0,0 @@
diff --git a/node_modules/react-native-svg/android/.project b/node_modules/react-native-svg/android/.project
new file mode 100644
index 0000000..dd0da62
--- /dev/null
+++ b/node_modules/react-native-svg/android/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>react-native-svg</name>
+ <comment>Project OpenPassport-android-react-native-svg created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+ <filteredResources>
+ <filter>
+ <id>1759738232589</id>
+ <name></name>
+ <type>30</type>
+ <matcher>
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+ </matcher>
+ </filter>
+ </filteredResources>
+</projectDescription>
diff --git a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp
index 11718dd..fe993b3 100644
--- a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp
+++ b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp
@@ -28,8 +28,8 @@ void RNSVGLayoutableShadowNode::setZeroDimensions() {
// views in the layout inspector when Yoga attempts to interpret SVG
// properties like width when viewBox scale is set.
auto style = yogaNode_.style();
- style.setDimension(yoga::Dimension::Width, yoga::StyleSizeLength::points(0));
- style.setDimension(yoga::Dimension::Height, yoga::StyleSizeLength::points(0));
+ style.setDimension(yoga::Dimension::Width, yoga::StyleLength::points(0));
+ style.setDimension(yoga::Dimension::Height, yoga::StyleLength::points(0));
yogaNode_.setStyle(style);
}
diff --git a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak
new file mode 100644
index 0000000..11718dd
--- /dev/null
+++ b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak
@@ -0,0 +1,43 @@
+#include "RNSVGLayoutableShadowNode.h"
+#include <react/renderer/core/LayoutContext.h>
+
+namespace facebook::react {
+
+RNSVGLayoutableShadowNode::RNSVGLayoutableShadowNode(
+ const ShadowNodeFragment &fragment,
+ const ShadowNodeFamily::Shared &family,
+ ShadowNodeTraits traits)
+ : YogaLayoutableShadowNode(fragment, family, traits) {
+ if (std::strcmp(this->getComponentName(), "RNSVGGroup") != 0) {
+ setZeroDimensions();
+ }
+}
+
+RNSVGLayoutableShadowNode::RNSVGLayoutableShadowNode(
+ const ShadowNode &sourceShadowNode,
+ const ShadowNodeFragment &fragment)
+ : YogaLayoutableShadowNode(sourceShadowNode, fragment) {
+ if (std::strcmp(this->getComponentName(), "RNSVGGroup") != 0) {
+ setZeroDimensions();
+ }
+}
+
+void RNSVGLayoutableShadowNode::setZeroDimensions() {
+ // SVG handles its layout manually on the native side and does not depend on
+ // the Yoga layout. Setting the dimensions to 0 eliminates randomly positioned
+ // views in the layout inspector when Yoga attempts to interpret SVG
+ // properties like width when viewBox scale is set.
+ auto style = yogaNode_.style();
+ style.setDimension(yoga::Dimension::Width, yoga::StyleSizeLength::points(0));
+ style.setDimension(yoga::Dimension::Height, yoga::StyleSizeLength::points(0));
+ yogaNode_.setStyle(style);
+}
+
+void RNSVGLayoutableShadowNode::layout(LayoutContext layoutContext) {
+ auto affectedNodes = layoutContext.affectedNodes;
+ layoutContext.affectedNodes = nullptr;
+ YogaLayoutableShadowNode::layout(layoutContext);
+ layoutContext.affectedNodes = affectedNodes;
+}
+
+} // namespace facebook::react

View File

@@ -88,30 +88,45 @@ if (!isExecutableAvailableOnPath('patch-package')) {
process.exit(0);
}
// Workspaces with isolated node_modules due to nmHoistingLimits: workspaces
// Most packages are in workspace node_modules, not root
const workspaceRoots = [
{ name: 'app', path: path.join(repositoryRootPath, 'app') },
{ name: 'contracts', path: path.join(repositoryRootPath, 'contracts') }
];
// Run patch-package with better error handling
try {
const rootPatchRun = spawnSync('patch-package', ['--patch-dir', 'patches'], {
cwd: repositoryRootPath,
shell: true,
stdio: isCI ? 'pipe' : 'inherit',
timeout: 30000
});
if (rootPatchRun.status === 0) {
if (!isCI) console.log('✓ Patches applied to root workspace');
} else {
const errorOutput = rootPatchRun.stderr?.toString() || rootPatchRun.stdout?.toString() || '';
console.error(`patch-package failed for root workspace (exit code ${rootPatchRun.status})`);
if (errorOutput) console.error(errorOutput);
if (!isCI) process.exit(1);
let anyPatchApplied = false;
let anyPatchFailed = false;
// Try root node_modules first (some packages may be hoisted here)
const rootNodeModules = path.join(repositoryRootPath, 'node_modules');
if (fs.existsSync(rootNodeModules)) {
const rootPatchRun = spawnSync('patch-package', ['--patch-dir', 'patches'], {
cwd: repositoryRootPath,
shell: true,
stdio: 'pipe', // Always capture output to check for real errors vs missing packages
timeout: 30000
});
const output = rootPatchRun.stdout?.toString() || '';
const stderrOutput = rootPatchRun.stderr?.toString() || '';
const hasRealError = (output.includes('**ERROR**') && !output.includes('which is not present at')) ||
(stderrOutput.length > 0 && rootPatchRun.status !== 0);
if (rootPatchRun.status === 0) {
if (!isCI) console.log('✓ Patches applied to root workspace');
anyPatchApplied = true;
} else if (hasRealError) {
console.error(`patch-package failed for root workspace`);
console.error(output);
if (stderrOutput) console.error(stderrOutput);
anyPatchFailed = true;
}
// If packages are just missing (not hoisted to root), that's expected - continue to workspace patches
}
// Also patch app/node_modules if it exists
// Workspaces with isolated node_modules due to limited hoisting
const workspaceRoots = [
{ name: 'app', path: path.join(repositoryRootPath, 'app') },
{ name: 'contracts', path: path.join(repositoryRootPath, 'contracts') }
];
// Apply patches to workspace node_modules (where most packages are with nmHoistingLimits)
for (const workspace of workspaceRoots) {
const workspaceNodeModules = path.join(workspace.path, 'node_modules');
if (!fs.existsSync(workspaceNodeModules)) continue;
@@ -119,19 +134,41 @@ try {
const workspacePatchRun = spawnSync('patch-package', ['--patch-dir', '../patches'], {
cwd: workspace.path,
shell: true,
stdio: isCI ? 'pipe' : 'inherit',
stdio: 'pipe',
timeout: 30000
});
const output = workspacePatchRun.stdout?.toString() || '';
const stderrOutput = workspacePatchRun.stderr?.toString() || '';
const hasRealError = (output.includes('**ERROR**') && !output.includes('which is not present at')) ||
(stderrOutput.length > 0 && workspacePatchRun.status !== 0);
if (workspacePatchRun.status === 0) {
if (!isCI) console.log(`✓ Patches applied to ${workspace.name} workspace`);
} else {
const errorOutput = workspacePatchRun.stderr?.toString() || workspacePatchRun.stdout?.toString() || '';
console.error(`patch-package failed for ${workspace.name} workspace (exit code ${workspacePatchRun.status})`);
if (errorOutput) console.error(errorOutput);
if (!isCI) process.exit(1);
anyPatchApplied = true;
} else if (hasRealError) {
console.error(`patch-package failed for ${workspace.name} workspace`);
console.error(output);
if (stderrOutput) console.error(stderrOutput);
anyPatchFailed = true;
}
}
if (anyPatchFailed && !isCI) {
console.error('Some patches failed to apply. Check if patch versions match installed package versions.');
process.exit(1);
}
if (anyPatchFailed && isCI) {
console.warn('⚠️ CI Warning: Some patches failed to apply. Review patch compatibility.');
}
if (anyPatchApplied) {
if (!isCI) console.log('✓ patch-package completed');
else console.log('patch-package completed');
} else {
if (!isCI) console.log('patch-package: no patches applied (packages may be in different locations)');
else console.log('patch-package: no patches applied');
}
} catch (error) {
if (isCI) {
console.log('patch-package: error during execution (CI mode):', error.message);

View File

@@ -51,7 +51,7 @@
"js-sha1": "^0.7.0",
"js-sha256": "^0.11.0",
"js-sha512": "^0.9.0",
"node-forge": "^1.3.1",
"node-forge": "^1.3.3",
"poseidon-lite": "^0.3.0",
"snarkjs": "^0.7.4",
"uuid": "^11.1.0"
@@ -72,7 +72,7 @@
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"typechain": "^8.3.2",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"webpack": "^5.95.0"
},
"engines": {

View File

@@ -38,7 +38,7 @@
"dependencies": {
"angularx-qrcode": "^20.0.0",
"lottie-web": "^5.12.2",
"socket.io-client": "^4.8.1",
"socket.io-client": "^4.8.3",
"uuid": "^11.1.0"
},
"devDependencies": {
@@ -68,7 +68,7 @@
"prettier": "^3.5.3",
"rxjs": "^7.8.0",
"tslib": "^2.6.0",
"typescript": "~5.9.0",
"typescript": "~5.9.3",
"zone.js": "^0.15.0"
},
"peerDependencies": {

View File

@@ -72,11 +72,11 @@
"js-sha256": "^0.11.0",
"js-sha512": "^0.9.0",
"lottie-react": "^2.4.0",
"node-forge": "^1.3.1",
"node-forge": "^1.3.3",
"poseidon-lite": "^0.3.0",
"qrcode.react": "^4.1.0",
"react-spinners": "^0.14.1",
"socket.io-client": "^4.8.1",
"socket.io-client": "^4.8.3",
"uuid": "^11.1.0"
},
"devDependencies": {
@@ -104,7 +104,7 @@
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"webpack": "^5.95.0"
},
"peerDependencies": {

View File

@@ -32,6 +32,6 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"typescript": "^5.9.2"
"typescript": "^5.9.3"
}
}

932
yarn.lock

File diff suppressed because it is too large Load Diff