mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
Merge pull request #1853 from selfxyz/release/staging-2026-03-13
Release to Staging v2.9.16 - 2026-03-13
This commit is contained in:
16
.github/workflows/rn-sdk-test-app-ci.yml
vendored
16
.github/workflows/rn-sdk-test-app-ci.yml
vendored
@@ -37,8 +37,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ No nested require() patterns found"
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Build webview-bridge
|
||||
run: yarn workspace @selfxyz/webview-bridge build
|
||||
- name: Build webview-app
|
||||
run: yarn workspace @selfxyz/webview-app build
|
||||
- name: Typecheck rn-sdk
|
||||
run: yarn workspace @selfxyz/rn-sdk typecheck
|
||||
- name: Test rn-sdk
|
||||
@@ -94,8 +100,18 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha iOS artifacts
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build:ios
|
||||
- name: Build mobile-sdk-alpha TypeScript bundle
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build:ts-only
|
||||
- name: Build webview-bridge
|
||||
run: yarn workspace @selfxyz/webview-bridge build
|
||||
- name: Build webview-app
|
||||
run: yarn workspace @selfxyz/webview-app build
|
||||
- name: Build rn-sdk (includes asset copy)
|
||||
run: yarn workspace @selfxyz/rn-sdk build
|
||||
- name: Install CocoaPods dependencies
|
||||
working-directory: packages/rn-sdk-test-app/ios
|
||||
run: pod install
|
||||
|
||||
12
.github/workflows/webview-app-ci.yml
vendored
12
.github/workflows/webview-app-ci.yml
vendored
@@ -6,6 +6,8 @@ permissions:
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- "packages/webview-app/**"
|
||||
- "packages/webview-bridge/**"
|
||||
- ".github/workflows/webview-app-ci.yml"
|
||||
@@ -13,6 +15,8 @@ on:
|
||||
push:
|
||||
branches: [dev, staging, main]
|
||||
paths:
|
||||
- "common/**"
|
||||
- "packages/mobile-sdk-alpha/**"
|
||||
- "packages/webview-app/**"
|
||||
- "packages/webview-bridge/**"
|
||||
- ".github/workflows/webview-app-ci.yml"
|
||||
@@ -26,6 +30,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Build webview-bridge
|
||||
run: yarn workspace @selfxyz/webview-bridge build
|
||||
- name: Build webview-app
|
||||
@@ -38,6 +46,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Build webview-bridge
|
||||
run: yarn workspace @selfxyz/webview-bridge build
|
||||
- name: Typecheck
|
||||
|
||||
8
.github/workflows/webview-bridge-ci.yml
vendored
8
.github/workflows/webview-bridge-ci.yml
vendored
@@ -24,6 +24,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Build
|
||||
run: yarn workspace @selfxyz/webview-bridge build
|
||||
|
||||
@@ -34,6 +38,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build common
|
||||
run: yarn workspace @selfxyz/common build
|
||||
- name: Build mobile-sdk-alpha
|
||||
run: yarn workspace @selfxyz/mobile-sdk-alpha build
|
||||
- name: Typecheck
|
||||
run: yarn workspace @selfxyz/webview-bridge typecheck
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ Key files:
|
||||
- `specs/projects/sdk/OVERVIEW.md` — Architecture, bridge protocol, module table, execution status
|
||||
- `specs/projects/sdk/workstreams/*/SPEC.md` — Durable workstream context, invariants, backlog, active plan links
|
||||
- `specs/projects/sdk/workstreams/*/plans/*.md` — PR-sized execution plans
|
||||
- `specs/projects/sdk/paused/*/SPEC.md` — Paused workstreams retained for future reuse (native-shells, integrations, rn-sdk, native-consolidation)
|
||||
|
||||
**Before implementing SDK work:** Read `CLAUDE.md` Key Rules and the relevant workstream `SPEC.md` under `specs/projects/sdk/workstreams/`. These specs contain explicit constraints ("You will NOT..."), validation commands, and file ownership boundaries that prevent common mistakes.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ nvm use && corepack enable && yarn install
|
||||
- **Spec naming and structure must be context-first.** Use doc-type file names (for example `OVERVIEW.md`, `SPEC.md`) and do not repeat project prefixes in file names. Use descriptive labels in markdown links — `[SDK Overview](./OVERVIEW.md)` not `[OVERVIEW.md](./OVERVIEW.md)` — so the link text is meaningful without folder context.
|
||||
- **No singleton spec folders.** Do not create a folder that exists only to hold one markdown file; keep single docs at the nearest meaningful project/shared root.
|
||||
- **Workstream spec names are fixed.** Under `workstreams/<scope>/`, use `SPEC.md` (context + implementation in one file); use `SPEC-<TOPIC>.md` only when multiple implementation specs are needed in that same folder.
|
||||
- **Use the two-layer spec model.** `INDEX.md` and `OVERVIEW.md` are stable project context. Each workstream `SPEC.md` is durable context plus backlog. PR execution lives in `workstreams/<scope>/plans/<BACKLOG-ID>-<slug>.md`.
|
||||
- **Use the two-layer spec model.** `INDEX.md` and `OVERVIEW.md` are stable project context. Each workstream `SPEC.md` is durable context plus backlog. PR execution lives in `workstreams/<scope>/plans/<BACKLOG-ID>-<slug>.md`. Paused workstreams live under `specs/projects/sdk/paused/<scope>/` with the same structure.
|
||||
- **Test value over mock wiring.** Prefer tests that validate behavior. Avoid tests that only assert mocks were called unless that is the behavior being validated.
|
||||
- **PR size target:** 1k–3k LOC changed. Smaller is fine for focused fixes. If >3k, add a brief justification for why it can’t be split.
|
||||
- **No generated artifacts in source PRs.** Do not commit build outputs or generated assets unless the build system requires them for runtime or distribution.
|
||||
|
||||
@@ -117,6 +117,38 @@ yarn types # Verify type checking
|
||||
- For Android: Ensure emulator is running or device is connected before `yarn android`
|
||||
- Metro bundler starts automatically; use `yarn start` to run it separately
|
||||
|
||||
#### iOS Simulator Selection
|
||||
|
||||
`yarn ios` now selects a simulator by UDID, shuts down stale booted simulators, explicitly boots the chosen device, waits for boot completion, then starts the React Native iOS build against that simulator.
|
||||
|
||||
| Env var | Purpose |
|
||||
|---|---|
|
||||
| `IOS_SIMULATOR_DEVICE` | Case-insensitive iPhone name substring filter, for example `iPhone 16 Pro` |
|
||||
| `IOS_SIMULATOR_RUNTIME` | iOS runtime version filter, for example `18.4` or `18-4` |
|
||||
|
||||
Default device priority when no env vars are set:
|
||||
|
||||
- `iPhone 16 Pro`
|
||||
- `iPhone 16`
|
||||
- `iPhone 15 Pro`
|
||||
- `iPhone 15`
|
||||
- First available iPhone on the newest installed iOS runtime
|
||||
|
||||
`IOS_SIMULATOR_DEVICE` uses a case-insensitive substring match. If multiple devices match, the launcher uses the first match from the newest matching runtime after applying the default priority order.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
yarn ios
|
||||
IOS_SIMULATOR_DEVICE="iPhone 16 Pro" yarn ios
|
||||
IOS_SIMULATOR_RUNTIME="18.4" yarn ios
|
||||
IOS_SIMULATOR_DEVICE="iPhone 15" IOS_SIMULATOR_RUNTIME="18-4" yarn ios
|
||||
```
|
||||
|
||||
If a pinned simulator cannot be found, the launcher exits with a readable error that includes the available iPhone simulators for the matching runtimes.
|
||||
|
||||
The launcher currently shuts down all booted simulators before booting the selected one. If you keep other simulators open for unrelated work, relaunch them after `yarn ios`.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
The app uses Maestro for end-to-end testing. **E2E tests run automatically in CI/CD pipelines - they are not required to run locally.**
|
||||
|
||||
@@ -23,7 +23,7 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1223.0)
|
||||
aws-partitions (1.1224.0)
|
||||
aws-sdk-core (3.243.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -101,8 +101,9 @@ GEM
|
||||
drb (2.2.3)
|
||||
emoji_regex (3.2.3)
|
||||
escape (0.0.4)
|
||||
ethon (0.15.0)
|
||||
ethon (0.18.0)
|
||||
ffi (>= 1.15.0)
|
||||
logger
|
||||
excon (0.112.0)
|
||||
faraday (1.10.5)
|
||||
faraday-em_http (~> 1.0)
|
||||
@@ -129,10 +130,10 @@ GEM
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.232.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
@@ -237,7 +238,7 @@ GEM
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.19.0)
|
||||
json (2.19.1)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
@@ -271,7 +272,7 @@ GEM
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.3.0)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby-macho (2.5.1)
|
||||
@@ -297,8 +298,8 @@ GEM
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
typhoeus (1.5.0)
|
||||
ethon (>= 0.9.0, < 0.16.0)
|
||||
typhoeus (1.6.0)
|
||||
ethon (>= 0.18.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uber (0.1.0)
|
||||
|
||||
@@ -225,7 +225,7 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation("net.java.dev.jna:jna:5.16.0")
|
||||
implementation("net.java.dev.jna:jna:5.16.0@aar")
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
|
||||
@@ -24,7 +24,6 @@ buildscript {
|
||||
classpath('com.android.tools.build:gradle:8.11.2')
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
// Removed firebase-crashlytics (no usages)—add back 3.x if still applied in modules.
|
||||
// Removed rust-android-gradle plugin; keep only if you re-enable Rust integration.
|
||||
@@ -61,9 +60,6 @@ allprojects {
|
||||
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.hasProperty('android') && project.android.buildFeatures.compose) {
|
||||
project.apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
}
|
||||
if (project.hasProperty('android')) {
|
||||
android {
|
||||
def manifestFile = project.file('src/main/AndroidManifest.xml')
|
||||
|
||||
@@ -13,54 +13,29 @@ import React
|
||||
import NFCPassportReader
|
||||
import Mixpanel
|
||||
#endif
|
||||
import Security
|
||||
import Sentry
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
extension CertificateType {
|
||||
func stringValue() -> String {
|
||||
switch self {
|
||||
case .documentSigningCertificate:
|
||||
return "documentSigningCertificate"
|
||||
case .issuerSigningCertificate:
|
||||
return "issuerSigningCertificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper function to map the keys of a dictionary
|
||||
extension Dictionary {
|
||||
func mapKeys<T: Hashable>(_ transform: (Key) -> T) -> Dictionary<T, Value> {
|
||||
Dictionary<T, Value>(uniqueKeysWithValues: map { (transform($0.key), $0.value) })
|
||||
}
|
||||
}
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 15, *)
|
||||
@objc(PassportReader)
|
||||
class PassportReader: NSObject {
|
||||
private var passportReader: NFCPassportReader.PassportReader
|
||||
private var analytics: SelfAnalytics?
|
||||
|
||||
override init() {
|
||||
self.passportReader = NFCPassportReader.PassportReader()
|
||||
super.init()
|
||||
}
|
||||
|
||||
private var analytics: SelfAnalytics?
|
||||
private var currentSessionId: String?
|
||||
|
||||
private func logNfc(level: SentryLevel, message: String, stage: String, useCANBool: Bool, sessionId: String, extras: [String: Any] = [:]) {
|
||||
let data: [String: Any] = [
|
||||
"session_id": sessionId,
|
||||
"platform": "ios",
|
||||
"scan_type": useCANBool ? "can" : "mrz",
|
||||
"stage": stage
|
||||
"stage": stage,
|
||||
].merging(extras) { (_, new) in new }
|
||||
|
||||
if level == .error {
|
||||
// For errors, capture a message (this will include all previous breadcrumbs)
|
||||
SentrySDK.configureScope { scope in
|
||||
scope.setTag(value: sessionId, key: "session_id")
|
||||
scope.setTag(value: "ios", key: "platform")
|
||||
@@ -72,7 +47,6 @@ class PassportReader: NSObject {
|
||||
}
|
||||
SentrySDK.capture(message: message)
|
||||
} else {
|
||||
// For info/warn, add as breadcrumb only
|
||||
let breadcrumb = Breadcrumb(level: level, category: "nfc")
|
||||
breadcrumb.message = message
|
||||
breadcrumb.data = data.mapValues { "\($0)" }
|
||||
@@ -101,390 +75,72 @@ class PassportReader: NSObject {
|
||||
analytics?.flush()
|
||||
}
|
||||
|
||||
func getMRZKey(passportNumber: String, dateOfBirth: String, dateOfExpiry: String ) -> String {
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:sessionId:resolve:reject:)
|
||||
func scanPassport(
|
||||
_ passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: NSNumber,
|
||||
skipPACE: NSNumber,
|
||||
skipCA: NSNumber,
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
sessionId: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
let useCANBool = useCan.boolValue
|
||||
let skipPACEBool = skipPACE.boolValue
|
||||
let skipCABool = skipCA.boolValue
|
||||
let extendedModeBool = extendedMode.boolValue
|
||||
let usePacePollingBool = usePacePolling.boolValue
|
||||
|
||||
// Pad fields if necessary
|
||||
let pptNr = pad( passportNumber, fieldLength:9)
|
||||
let dob = pad( dateOfBirth, fieldLength:6)
|
||||
let exp = pad( dateOfExpiry, fieldLength:6)
|
||||
|
||||
// Calculate checksums
|
||||
let passportNrChksum = calcCheckSum(pptNr)
|
||||
let dateOfBirthChksum = calcCheckSum(dob)
|
||||
let expiryDateChksum = calcCheckSum(exp)
|
||||
|
||||
let mrzKey = "\(pptNr)\(passportNrChksum)\(dob)\(dateOfBirthChksum)\(exp)\(expiryDateChksum)"
|
||||
|
||||
return mrzKey
|
||||
}
|
||||
|
||||
func pad( _ value : String, fieldLength:Int ) -> String {
|
||||
// Pad out field lengths with < if they are too short
|
||||
let paddedValue = (value + String(repeating: "<", count: fieldLength)).prefix(fieldLength)
|
||||
return String(paddedValue)
|
||||
}
|
||||
|
||||
func calcCheckSum( _ checkString : String ) -> Int {
|
||||
let characterDict = ["0" : "0", "1" : "1", "2" : "2", "3" : "3", "4" : "4", "5" : "5", "6" : "6", "7" : "7", "8" : "8", "9" : "9", "<" : "0", " " : "0", "A" : "10", "B" : "11", "C" : "12", "D" : "13", "E" : "14", "F" : "15", "G" : "16", "H" : "17", "I" : "18", "J" : "19", "K" : "20", "L" : "21", "M" : "22", "N" : "23", "O" : "24", "P" : "25", "Q" : "26", "R" : "27", "S" : "28","T" : "29", "U" : "30", "V" : "31", "W" : "32", "X" : "33", "Y" : "34", "Z" : "35"]
|
||||
|
||||
var sum = 0
|
||||
var m = 0
|
||||
let multipliers : [Int] = [7, 3, 1]
|
||||
for c in checkString {
|
||||
guard let lookup = characterDict["\(c)"],
|
||||
let number = Int(lookup) else { return 0 }
|
||||
let product = number * multipliers[m]
|
||||
sum += product
|
||||
m = (m+1) % 3
|
||||
}
|
||||
|
||||
return (sum % 10)
|
||||
}
|
||||
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:sessionId:resolve:reject:)
|
||||
func scanPassport(
|
||||
_ passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: NSNumber,
|
||||
skipPACE: NSNumber,
|
||||
skipCA: NSNumber,
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
sessionId: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
let useCANBool = useCan.boolValue
|
||||
let skipPACEBool = skipPACE.boolValue
|
||||
let skipCABool = skipCA.boolValue
|
||||
let extendedModeBool = extendedMode.boolValue
|
||||
let usePacePollingBool = usePacePolling.boolValue
|
||||
self.currentSessionId = sessionId
|
||||
logNfc(level: .info, message: "scan_start", stage: "start", useCANBool: useCANBool, sessionId: sessionId)
|
||||
|
||||
let customMessageHandler : (NFCViewDisplayMessage)->String? = { (displayMessage) in
|
||||
switch displayMessage {
|
||||
case .requestPresentPassport:
|
||||
return "Hold your iPhone against an NFC enabled passport."
|
||||
default:
|
||||
// Return nil for all other messages so we use the provided default
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
NativeLoggerBridge.logInfo(category: "NFC", message: "NFC passport scan started", data: [
|
||||
"passportNumber": passportNumber,
|
||||
"useCAN": useCANBool,
|
||||
"skipPACE": skipPACEBool,
|
||||
"skipCA": skipCABool,
|
||||
"extendedMode": extendedModeBool,
|
||||
"usePacePolling": usePacePollingBool
|
||||
])
|
||||
|
||||
do {
|
||||
let password: String
|
||||
var passwordType:PACEPasswordType
|
||||
if useCANBool {
|
||||
if canNumber.count != 6 {
|
||||
reject("E_PASSPORT_READ", "CAN number must be 6 digits", nil)
|
||||
return
|
||||
}
|
||||
password = canNumber
|
||||
passwordType = PACEPasswordType.can
|
||||
} else {
|
||||
password = getMRZKey( passportNumber: passportNumber, dateOfBirth: dateOfBirth, dateOfExpiry: dateOfExpiry)
|
||||
passwordType = PACEPasswordType.mrz
|
||||
}
|
||||
// let masterListURL = Bundle.main.url(forResource: "masterList", withExtension: ".pem")
|
||||
// passportReader.setMasterListURL( masterListURL! )
|
||||
|
||||
let passport = try await self.passportReader.readPassport(
|
||||
password: password,
|
||||
type: passwordType,
|
||||
tags: [.COM, .DG1, .SOD],
|
||||
skipCA: skipCABool,
|
||||
skipPACE: skipPACEBool,
|
||||
useExtendedMode: extendedModeBool,
|
||||
usePacePolling: usePacePollingBool,
|
||||
customDisplayMessage: customMessageHandler
|
||||
PassportReaderCore.scanPassport(
|
||||
reader: passportReader,
|
||||
passportNumber: passportNumber,
|
||||
dateOfBirth: dateOfBirth,
|
||||
dateOfExpiry: dateOfExpiry,
|
||||
canNumber: canNumber,
|
||||
useCan: useCANBool,
|
||||
skipPACE: skipPACEBool,
|
||||
skipCA: skipCABool,
|
||||
extendedMode: extendedModeBool,
|
||||
usePacePolling: usePacePollingBool,
|
||||
onStart: { [weak self] in
|
||||
self?.logNfc(level: .info, message: "scan_start", stage: "start", useCANBool: useCANBool, sessionId: sessionId)
|
||||
NativeLoggerBridge.logInfo(category: "NFC", message: "NFC passport scan started", data: [
|
||||
"useCAN": useCANBool,
|
||||
"skipPACE": skipPACEBool,
|
||||
"skipCA": skipCABool,
|
||||
"extendedMode": extendedModeBool,
|
||||
"usePacePolling": usePacePollingBool,
|
||||
])
|
||||
},
|
||||
onSuccess: { [weak self] in
|
||||
self?.logNfc(level: .info, message: "scan_success", stage: "complete", useCANBool: useCANBool, sessionId: sessionId)
|
||||
},
|
||||
onFailure: { [weak self] error in
|
||||
self?.logNfc(
|
||||
level: .warning,
|
||||
message: "scan_failed",
|
||||
stage: "error",
|
||||
useCANBool: useCANBool,
|
||||
sessionId: sessionId,
|
||||
extras: ["error": error.localizedDescription]
|
||||
)
|
||||
},
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
)
|
||||
|
||||
var ret = [String:String]()
|
||||
//print("documentType", passport.documentType)
|
||||
|
||||
ret["documentType"] = passport.documentType
|
||||
ret["documentSubType"] = passport.documentSubType
|
||||
ret["documentNumber"] = passport.documentNumber
|
||||
ret["issuingAuthority"] = passport.issuingAuthority
|
||||
ret["documentExpiryDate"] = passport.documentExpiryDate
|
||||
ret["dateOfBirth"] = passport.dateOfBirth
|
||||
ret["gender"] = passport.gender
|
||||
ret["nationality"] = passport.nationality
|
||||
ret["lastName"] = passport.lastName
|
||||
ret["firstName"] = passport.firstName
|
||||
ret["passportMRZ"] = passport.passportMRZ
|
||||
ret["placeOfBirth"] = passport.placeOfBirth
|
||||
ret["residenceAddress"] = passport.residenceAddress
|
||||
ret["phoneNumber"] = passport.phoneNumber
|
||||
ret["personalNumber"] = passport.personalNumber
|
||||
|
||||
// let passportPhotoData = passport.passportPhoto // [UInt8]
|
||||
// if let passportPhotoData = passport.passportPhoto {
|
||||
// let data = Data(passportPhotoData)
|
||||
// let base64String = data.base64EncodedString()
|
||||
|
||||
// ret["passportPhoto"] = base64String
|
||||
// }
|
||||
|
||||
// documentSigningCertificate
|
||||
// countrySigningCertificate
|
||||
|
||||
if let serializedDocumentSigningCertificate = serializeX509Wrapper(passport.documentSigningCertificate) {
|
||||
ret["documentSigningCertificate"] = serializedDocumentSigningCertificate
|
||||
}
|
||||
|
||||
if let serializedCountrySigningCertificate = serializeX509Wrapper(passport.countrySigningCertificate) {
|
||||
ret["countrySigningCertificate"] = serializedCountrySigningCertificate
|
||||
}
|
||||
//print("passport.documentSigningCertificate", passport.documentSigningCertificate)
|
||||
//print("passport.countrySigningCertificate", passport.countrySigningCertificate)
|
||||
|
||||
ret["LDSVersion"] = passport.LDSVersion
|
||||
ret["dataGroupsPresent"] = passport.dataGroupsPresent.joined(separator: ", ")
|
||||
|
||||
//print("passport.LDSVersion", passport.LDSVersion)
|
||||
|
||||
// ret["dataGroupsAvailable"] = passport.dataGroupsAvailable.map(dataGroupIdToString)
|
||||
|
||||
//print("passport.dataGroupsAvailable", passport.dataGroupsAvailable)
|
||||
//print("passport.dataGroupsRead", passport.dataGroupsRead)
|
||||
//print("passport.dataGroupHashes", passport.dataGroupHashes)
|
||||
|
||||
// do {
|
||||
// let dataGroupsReadData = try JSONSerialization.data(withJSONObject: passport.dataGroupsRead.mapValues { self.convertDataGroupToSerializableFormat($0) }, options: [])
|
||||
// let dataGroupsReadJsonString = String(data: dataGroupsReadData, encoding: .utf8) ?? ""
|
||||
// ret["dataGroupsRead"] = dataGroupsReadJsonString
|
||||
// } catch {
|
||||
// //print("Error serializing dataGroupsRead: \(error)")
|
||||
// }
|
||||
|
||||
// ret["dataGroupsRead"] = passport.dataGroupsRead.mapValues { convertDataGroupToSerializableFormat($0) }
|
||||
do {
|
||||
let dataGroupHashesDict = passport.dataGroupHashes.mapKeys { "\($0)" }
|
||||
let serializableDataGroupHashes = dataGroupHashesDict.mapValues { convertDataGroupHashToSerializableFormat($0) }
|
||||
let dataGroupHashesData = try JSONSerialization.data(withJSONObject: serializableDataGroupHashes, options: [])
|
||||
let dataGroupHashesJsonString = String(data: dataGroupHashesData, encoding: .utf8) ?? ""
|
||||
ret["dataGroupHashes"] = dataGroupHashesJsonString
|
||||
} catch {
|
||||
//print("Error serializing dataGroupHashes: \(error)")
|
||||
}
|
||||
|
||||
|
||||
// cardAccess
|
||||
// BACStatus
|
||||
// PACEStatus
|
||||
// chipAuthenticationStatus
|
||||
ret["passportCorrectlySigned"] = String(passport.passportCorrectlySigned)
|
||||
ret["documentSigningCertificateVerified"] = String(passport.documentSigningCertificateVerified)
|
||||
ret["passportDataNotTampered"] = String(passport.passportDataNotTampered)
|
||||
ret["activeAuthenticationPassed"] = String(passport.activeAuthenticationPassed)
|
||||
ret["activeAuthenticationChallenge"] = encodeByteArrayToHexString(passport.activeAuthenticationChallenge)
|
||||
ret["activeAuthenticationSignature"] = encodeByteArrayToHexString(passport.activeAuthenticationSignature)
|
||||
ret["verificationErrors"] = encodeErrors(passport.verificationErrors).joined(separator: ", ")
|
||||
|
||||
ret["isPACESupported"] = String(passport.isPACESupported)
|
||||
ret["isChipAuthenticationSupported"] = String(passport.isChipAuthenticationSupported)
|
||||
|
||||
// passportImage
|
||||
// signatureImage
|
||||
|
||||
// activeAuthenticationSupported
|
||||
|
||||
//print("passport.certificateSigningGroups", passport.certificateSigningGroups)
|
||||
|
||||
// ret["certificateSigningGroups"] = passport.certificateSigningGroups.mapKeys(certificateTypeToString).mapValues(encodeX509WrapperToJsonString)
|
||||
// if let passportDataElements = passport.passportDataElements {
|
||||
// ret["passportDataElements"] = passportDataElements
|
||||
// } else {
|
||||
// ret["passportDataElements"] = [:]
|
||||
// }
|
||||
|
||||
do {
|
||||
// although this line won't be reached if there is an error, Its better to handle it here instead of crashing the app
|
||||
if let sod = try passport.getDataGroup(DataGroupId.SOD) as? SOD {
|
||||
// ret["concatenatedDataHashes"] = try sod.getEncapsulatedContent().base64EncodedString() // this is what we call concatenatedDataHashes, not the true eContent
|
||||
ret["eContentBase64"] = try sod.getEncapsulatedContent().base64EncodedString() // this is what we call concatenatedDataHashes, not the true eContent
|
||||
|
||||
ret["signatureAlgorithm"] = try sod.getSignatureAlgorithm()
|
||||
ret["encapsulatedContentDigestAlgorithm"] = try sod.getEncapsulatedContentDigestAlgorithm()
|
||||
|
||||
let messageDigestFromSignedAttributes = try sod.getMessageDigestFromSignedAttributes()
|
||||
let signedAttributes = try sod.getSignedAttributes()
|
||||
//print("messageDigestFromSignedAttributes", messageDigestFromSignedAttributes)
|
||||
|
||||
ret["signedAttributes"] = signedAttributes.base64EncodedString()
|
||||
// if let pubKey = convertOpaquePointerToSecKey(opaquePointer: sod.pubKey),
|
||||
// let serializedPublicKey = serializePublicKey(pubKey) {
|
||||
// ret["publicKeyBase64"] = serializedPublicKey
|
||||
// } else {
|
||||
// // Handle the case where pubKey is nil
|
||||
// }
|
||||
|
||||
if let serializedSignature = serializeSignature(from: sod) {
|
||||
ret["signatureBase64"] = serializedSignature
|
||||
}
|
||||
} else {
|
||||
print("SOD not found or could not be cast to SOD")
|
||||
reject("E_PASSPORT_READ", "SODNotFound : SOD not found or could not be cast to SOD", nil)
|
||||
return
|
||||
}
|
||||
|
||||
} catch {
|
||||
//print("Error serializing SOD data: \(error)")
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
|
||||
let stringified = String(data: try JSONEncoder().encode(ret), encoding: .utf8)
|
||||
logNfc(level: .info, message: "scan_success", stage: "complete", useCANBool: useCANBool, sessionId: sessionId)
|
||||
resolve(stringified)
|
||||
} catch {
|
||||
logNfc(level: .warning, message: "scan_failed", stage: "error", useCANBool: useCANBool, sessionId: sessionId, extras: ["error": error.localizedDescription])
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mrz ✅
|
||||
// dataHashes ✅
|
||||
// eContentBytes ✅
|
||||
// pubkey
|
||||
// signature ✅
|
||||
|
||||
// func convertOpaquePointerToSecKey(opaquePointer: OpaquePointer?) -> SecKey? {
|
||||
// guard let opaquePointer = opaquePointer else { return nil }
|
||||
|
||||
// // Assuming the key is in DER format
|
||||
// // Replace with actual code to convert OpaquePointer to Data
|
||||
// let keyData = Data(bytes: opaquePointer, count: keyLength) // Replace `keyLength` with actual length of key data
|
||||
|
||||
// let attributes: [String: Any] = [
|
||||
// kSecAttrKeyType as String: kSecAttrKeyTypeRSA, // or kSecAttrKeyTypeECSECPrimeRandom for ECDSA
|
||||
// kSecAttrKeyClass as String: kSecAttrKeyClassPublic
|
||||
// ]
|
||||
|
||||
// var error: Unmanaged<CFError>?
|
||||
// let secKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)
|
||||
|
||||
// if let error = error {
|
||||
// //print("Error creating SecKey: \(error.takeRetainedValue())")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// return secKey
|
||||
// }
|
||||
|
||||
func serializePublicKey(_ publicKey: SecKey) -> String? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
||||
//print("Error serializing public key: \(error!.takeRetainedValue() as Error)")
|
||||
return nil
|
||||
}
|
||||
return publicKeyData.base64EncodedString()
|
||||
}
|
||||
|
||||
func serializeSignature(from sod: SOD) -> String? {
|
||||
do {
|
||||
let signature = try sod.getSignature()
|
||||
return signature.base64EncodedString()
|
||||
} catch {
|
||||
//print("Error extracting signature: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func serializeX509Wrapper(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate = certificate else { return nil }
|
||||
|
||||
let itemsDict = certificate.getItemsAsDict()
|
||||
var certInfoStringKeys = [String: String]()
|
||||
|
||||
// Convert CertificateItem keys to String keys
|
||||
for (key, value) in itemsDict {
|
||||
certInfoStringKeys[key.rawValue] = value
|
||||
}
|
||||
|
||||
// Add PEM representation
|
||||
let certPEM = certificate.certToPEM()
|
||||
certInfoStringKeys["PEM"] = certPEM
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certInfoStringKeys, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
//print("Error serializing X509Wrapper: \(error)")
|
||||
return nil
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
func encodeX509WrapperToJsonString(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate = certificate else { return nil }
|
||||
let certificateItems = certificate.getItemsAsDict()
|
||||
|
||||
// Convert certificate items to JSON
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certificateItems, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
//print("Error serializing certificate items to JSON: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func encodeByteArrayToHexString(_ byteArray: [UInt8]) -> String {
|
||||
return byteArray.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
func encodeErrors(_ errors: [Error]) -> [String] {
|
||||
return errors.map { $0.localizedDescription }
|
||||
}
|
||||
|
||||
func convertDataGroupHashToSerializableFormat(_ dataGroupHash: DataGroupHash) -> [String: Any] {
|
||||
return [
|
||||
"id": dataGroupHash.id,
|
||||
"sodHash": dataGroupHash.sodHash,
|
||||
"computedHash": dataGroupHash.computedHash,
|
||||
"match": dataGroupHash.match
|
||||
]
|
||||
}
|
||||
|
||||
func dataGroupIdToString(_ id: DataGroupId) -> String {
|
||||
return String(id.rawValue) // or any other method to get a string representation
|
||||
}
|
||||
|
||||
func certificateTypeToString(_ type: CertificateType) -> String {
|
||||
return type.stringValue()
|
||||
}
|
||||
|
||||
func convertDataGroupToSerializableFormat(_ dataGroup: DataGroup) -> [String: Any] {
|
||||
return [
|
||||
"datagroupType": dataGroupIdToString(dataGroup.datagroupType),
|
||||
"body": encodeByteArrayToHexString(dataGroup.body),
|
||||
"data": encodeByteArrayToHexString(dataGroup.data)
|
||||
]
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
#else
|
||||
// E2E Testing stub implementation
|
||||
@available(iOS 15, *)
|
||||
@objc(PassportReader)
|
||||
class PassportReader: NSObject {
|
||||
@@ -494,7 +150,14 @@ class PassportReader: NSObject {
|
||||
|
||||
@objc(configure:enableDebugLogs:)
|
||||
func configure(token: String, enableDebugLogs: Bool) {
|
||||
// No-op for E2E testing
|
||||
}
|
||||
|
||||
@objc(trackEvent:properties:)
|
||||
func trackEvent(_ name: String, properties: [String: Any]?) {
|
||||
}
|
||||
|
||||
@objc(flush)
|
||||
func flush() {
|
||||
}
|
||||
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:sessionId:resolve:reject:)
|
||||
@@ -509,13 +172,15 @@ class PassportReader: NSObject {
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
sessionId: String,
|
||||
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
reject("E2E_TESTING", "NFC scanning not available in E2E testing mode", nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
304
app/ios/PassportReaderCore.swift
Normal file
304
app/ios/PassportReaderCore.swift
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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 Foundation
|
||||
import React
|
||||
#if !E2E_TESTING
|
||||
import NFCPassportReader
|
||||
import Security
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
extension CertificateType {
|
||||
func stringValue() -> String {
|
||||
switch self {
|
||||
case .documentSigningCertificate:
|
||||
return "documentSigningCertificate"
|
||||
case .issuerSigningCertificate:
|
||||
return "issuerSigningCertificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Dictionary {
|
||||
func mapKeys<T: Hashable>(_ transform: (Key) -> T) -> Dictionary<T, Value> {
|
||||
Dictionary<T, Value>(uniqueKeysWithValues: map { (transform($0.key), $0.value) })
|
||||
}
|
||||
}
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 15, *)
|
||||
enum PassportReaderCore {
|
||||
static func getMRZKey(passportNumber: String, dateOfBirth: String, dateOfExpiry: String) -> String {
|
||||
let pptNr = pad(passportNumber, fieldLength: 9)
|
||||
let dob = pad(dateOfBirth, fieldLength: 6)
|
||||
let exp = pad(dateOfExpiry, fieldLength: 6)
|
||||
|
||||
let passportNrChksum = calcCheckSum(pptNr)
|
||||
let dateOfBirthChksum = calcCheckSum(dob)
|
||||
let expiryDateChksum = calcCheckSum(exp)
|
||||
|
||||
return "\(pptNr)\(passportNrChksum)\(dob)\(dateOfBirthChksum)\(exp)\(expiryDateChksum)"
|
||||
}
|
||||
|
||||
static func pad(_ value: String, fieldLength: Int) -> String {
|
||||
let paddedValue = (value + String(repeating: "<", count: fieldLength)).prefix(fieldLength)
|
||||
return String(paddedValue)
|
||||
}
|
||||
|
||||
static func calcCheckSum(_ checkString: String) -> Int {
|
||||
let characterDict = [
|
||||
"0": "0", "1": "1", "2": "2", "3": "3", "4": "4",
|
||||
"5": "5", "6": "6", "7": "7", "8": "8", "9": "9",
|
||||
"<": "0", " ": "0",
|
||||
"A": "10", "B": "11", "C": "12", "D": "13", "E": "14",
|
||||
"F": "15", "G": "16", "H": "17", "I": "18", "J": "19",
|
||||
"K": "20", "L": "21", "M": "22", "N": "23", "O": "24",
|
||||
"P": "25", "Q": "26", "R": "27", "S": "28", "T": "29",
|
||||
"U": "30", "V": "31", "W": "32", "X": "33", "Y": "34",
|
||||
"Z": "35",
|
||||
]
|
||||
|
||||
var sum = 0
|
||||
var m = 0
|
||||
let multipliers: [Int] = [7, 3, 1]
|
||||
|
||||
for c in checkString {
|
||||
guard let lookup = characterDict["\(c)"], let number = Int(lookup) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
sum += number * multipliers[m]
|
||||
m = (m + 1) % 3
|
||||
}
|
||||
|
||||
return sum % 10
|
||||
}
|
||||
|
||||
static func scanPassport(
|
||||
reader: NFCPassportReader.PassportReader,
|
||||
passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: Bool,
|
||||
skipPACE: Bool,
|
||||
skipCA: Bool,
|
||||
extendedMode: Bool,
|
||||
usePacePolling: Bool,
|
||||
onStart: (() -> Void)? = nil,
|
||||
onSuccess: (() -> Void)? = nil,
|
||||
onFailure: ((Error) -> Void)? = nil,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
let customMessageHandler: (NFCViewDisplayMessage) -> String? = { displayMessage in
|
||||
switch displayMessage {
|
||||
case .requestPresentPassport:
|
||||
return "Hold your iPhone against an NFC enabled passport."
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
onStart?()
|
||||
|
||||
Task {
|
||||
do {
|
||||
let password: String
|
||||
let passwordType: PACEPasswordType
|
||||
|
||||
if useCan {
|
||||
if canNumber.count != 6 {
|
||||
reject("E_PASSPORT_READ", "CAN number must be 6 digits", nil)
|
||||
return
|
||||
}
|
||||
|
||||
password = canNumber
|
||||
passwordType = .can
|
||||
} else {
|
||||
password = getMRZKey(
|
||||
passportNumber: passportNumber,
|
||||
dateOfBirth: dateOfBirth,
|
||||
dateOfExpiry: dateOfExpiry
|
||||
)
|
||||
passwordType = .mrz
|
||||
}
|
||||
|
||||
let passport = try await reader.readPassport(
|
||||
password: password,
|
||||
type: passwordType,
|
||||
tags: [.COM, .DG1, .SOD],
|
||||
skipCA: skipCA,
|
||||
skipPACE: skipPACE,
|
||||
useExtendedMode: extendedMode,
|
||||
usePacePolling: usePacePolling,
|
||||
customDisplayMessage: customMessageHandler
|
||||
)
|
||||
|
||||
var ret = [String: String]()
|
||||
ret["documentType"] = passport.documentType
|
||||
ret["documentSubType"] = passport.documentSubType
|
||||
ret["documentNumber"] = passport.documentNumber
|
||||
ret["issuingAuthority"] = passport.issuingAuthority
|
||||
ret["documentExpiryDate"] = passport.documentExpiryDate
|
||||
ret["dateOfBirth"] = passport.dateOfBirth
|
||||
ret["gender"] = passport.gender
|
||||
ret["nationality"] = passport.nationality
|
||||
ret["lastName"] = passport.lastName
|
||||
ret["firstName"] = passport.firstName
|
||||
ret["passportMRZ"] = passport.passportMRZ
|
||||
ret["placeOfBirth"] = passport.placeOfBirth
|
||||
ret["residenceAddress"] = passport.residenceAddress
|
||||
ret["phoneNumber"] = passport.phoneNumber
|
||||
ret["personalNumber"] = passport.personalNumber
|
||||
|
||||
if let serializedDocumentSigningCertificate = serializeX509Wrapper(passport.documentSigningCertificate) {
|
||||
ret["documentSigningCertificate"] = serializedDocumentSigningCertificate
|
||||
}
|
||||
|
||||
if let serializedCountrySigningCertificate = serializeX509Wrapper(passport.countrySigningCertificate) {
|
||||
ret["countrySigningCertificate"] = serializedCountrySigningCertificate
|
||||
}
|
||||
|
||||
ret["LDSVersion"] = passport.LDSVersion
|
||||
ret["dataGroupsPresent"] = passport.dataGroupsPresent.joined(separator: ", ")
|
||||
|
||||
do {
|
||||
let dataGroupHashesDict = passport.dataGroupHashes.mapKeys { "\($0)" }
|
||||
let serializableDataGroupHashes = dataGroupHashesDict.mapValues { convertDataGroupHashToSerializableFormat($0) }
|
||||
let dataGroupHashesData = try JSONSerialization.data(withJSONObject: serializableDataGroupHashes, options: [])
|
||||
let dataGroupHashesJsonString = String(data: dataGroupHashesData, encoding: .utf8) ?? ""
|
||||
ret["dataGroupHashes"] = dataGroupHashesJsonString
|
||||
} catch {
|
||||
}
|
||||
|
||||
ret["passportCorrectlySigned"] = String(passport.passportCorrectlySigned)
|
||||
ret["documentSigningCertificateVerified"] = String(passport.documentSigningCertificateVerified)
|
||||
ret["passportDataNotTampered"] = String(passport.passportDataNotTampered)
|
||||
ret["activeAuthenticationPassed"] = String(passport.activeAuthenticationPassed)
|
||||
ret["activeAuthenticationChallenge"] = encodeByteArrayToHexString(passport.activeAuthenticationChallenge)
|
||||
ret["activeAuthenticationSignature"] = encodeByteArrayToHexString(passport.activeAuthenticationSignature)
|
||||
ret["verificationErrors"] = encodeErrors(passport.verificationErrors).joined(separator: ", ")
|
||||
ret["isPACESupported"] = String(passport.isPACESupported)
|
||||
ret["isChipAuthenticationSupported"] = String(passport.isChipAuthenticationSupported)
|
||||
|
||||
do {
|
||||
if let sod = try passport.getDataGroup(DataGroupId.SOD) as? SOD {
|
||||
ret["eContentBase64"] = try sod.getEncapsulatedContent().base64EncodedString()
|
||||
ret["signatureAlgorithm"] = try sod.getSignatureAlgorithm()
|
||||
ret["encapsulatedContentDigestAlgorithm"] = try sod.getEncapsulatedContentDigestAlgorithm()
|
||||
_ = try sod.getMessageDigestFromSignedAttributes()
|
||||
let signedAttributes = try sod.getSignedAttributes()
|
||||
ret["signedAttributes"] = signedAttributes.base64EncodedString()
|
||||
|
||||
if let serializedSignature = serializeSignature(from: sod) {
|
||||
ret["signatureBase64"] = serializedSignature
|
||||
}
|
||||
} else {
|
||||
print("SOD not found or could not be cast to SOD")
|
||||
reject("E_PASSPORT_READ", "SODNotFound : SOD not found or could not be cast to SOD", nil)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
return
|
||||
}
|
||||
|
||||
let stringified = String(data: try JSONEncoder().encode(ret), encoding: .utf8)
|
||||
onSuccess?()
|
||||
resolve(stringified)
|
||||
} catch {
|
||||
onFailure?(error)
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func serializePublicKey(_ publicKey: SecKey) -> String? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
||||
return nil
|
||||
}
|
||||
return publicKeyData.base64EncodedString()
|
||||
}
|
||||
|
||||
static func serializeSignature(from sod: SOD) -> String? {
|
||||
do {
|
||||
let signature = try sod.getSignature()
|
||||
return signature.base64EncodedString()
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func serializeX509Wrapper(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let itemsDict = certificate.getItemsAsDict()
|
||||
var certInfoStringKeys = [String: String]()
|
||||
|
||||
for (key, value) in itemsDict {
|
||||
certInfoStringKeys[key.rawValue] = value
|
||||
}
|
||||
|
||||
certInfoStringKeys["PEM"] = certificate.certToPEM()
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certInfoStringKeys, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeX509WrapperToJsonString(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certificate.getItemsAsDict(), options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeByteArrayToHexString(_ byteArray: [UInt8]) -> String {
|
||||
byteArray.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
static func encodeErrors(_ errors: [Error]) -> [String] {
|
||||
errors.map { $0.localizedDescription }
|
||||
}
|
||||
|
||||
static func convertDataGroupHashToSerializableFormat(_ dataGroupHash: DataGroupHash) -> [String: Any] {
|
||||
[
|
||||
"id": dataGroupHash.id,
|
||||
"sodHash": dataGroupHash.sodHash,
|
||||
"computedHash": dataGroupHash.computedHash,
|
||||
"match": dataGroupHash.match,
|
||||
]
|
||||
}
|
||||
|
||||
static func dataGroupIdToString(_ id: DataGroupId) -> String {
|
||||
String(id.rawValue)
|
||||
}
|
||||
|
||||
static func certificateTypeToString(_ type: CertificateType) -> String {
|
||||
type.stringValue()
|
||||
}
|
||||
|
||||
static func convertDataGroupToSerializableFormat(_ dataGroup: DataGroup) -> [String: Any] {
|
||||
[
|
||||
"datagroupType": dataGroupIdToString(dataGroup.datagroupType),
|
||||
"body": encodeByteArrayToHexString(dataGroup.body),
|
||||
"data": encodeByteArrayToHexString(dataGroup.data),
|
||||
]
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -38,6 +38,52 @@ prepare_react_native_project!
|
||||
flipper_enabled = ENV["NO_FLIPPER"] != "1"
|
||||
flipper_config = { "Flipper" => flipper_enabled ? "~> 0.125.0" : nil }
|
||||
|
||||
def ios_simulator_arm64_supported?(xcframework_path)
|
||||
return false unless Dir.exist?(xcframework_path)
|
||||
|
||||
Dir.children(xcframework_path).any? do |slice_name|
|
||||
slice_name.include?("simulator") && slice_name.include?("arm64")
|
||||
end
|
||||
end
|
||||
|
||||
def simulator_arm64_audit_entries
|
||||
[
|
||||
{
|
||||
name: "FingerprintPro",
|
||||
path: "Pods/FingerprintPro/FingerprintPro.xcframework",
|
||||
},
|
||||
{
|
||||
name: "IdensicMobileSDK",
|
||||
path: "Pods/IdensicMobileSDK/IdensicMobileSDK.xcframework",
|
||||
},
|
||||
{
|
||||
name: "IdensicMobileSDK_Fisherman",
|
||||
path: "Pods/IdensicMobileSDK/IdensicMobileSDK_Fisherman.xcframework",
|
||||
},
|
||||
{
|
||||
name: "OpenSSL",
|
||||
path: "Pods/OpenSSL-Universal/Frameworks/OpenSSL.xcframework",
|
||||
},
|
||||
{
|
||||
name: "libtesseract",
|
||||
path: "Pods/SwiftyTesseract/SwiftyTesseract/libtesseract.xcframework",
|
||||
},
|
||||
{
|
||||
name: "Hermes",
|
||||
path: "Pods/hermes-engine/destroot/Library/Frameworks/universal/hermes.xcframework",
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
def simulator_arm64_blockers
|
||||
simulator_arm64_audit_entries.filter_map do |entry|
|
||||
full_path = File.join(Dir.pwd, entry[:path])
|
||||
next if ios_simulator_arm64_supported?(full_path)
|
||||
|
||||
"#{entry[:name]} (#{entry[:path]})"
|
||||
end
|
||||
end
|
||||
|
||||
linkage = ENV["USE_FRAMEWORKS"]
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
@@ -109,6 +155,15 @@ target "Self" do
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
simulator_arm64_blocking_pods = simulator_arm64_blockers
|
||||
use_rosetta_simulator_builds = simulator_arm64_blocking_pods.any?
|
||||
|
||||
if use_rosetta_simulator_builds
|
||||
Pod::UI.puts "Using Rosetta iOS simulator builds; arm64 simulator slices missing for: #{simulator_arm64_blocking_pods.join(', ')}".yellow
|
||||
else
|
||||
Pod::UI.puts "All audited binary pods include arm64 simulator slices; enabling native arm64 iOS simulator builds.".green
|
||||
end
|
||||
|
||||
installer.generated_projects.each do |project|
|
||||
project.targets.each do |target|
|
||||
if target.name == "RNZipArchive"
|
||||
@@ -194,8 +249,8 @@ target "Self" do
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["CODE_SIGNING_ALLOWED"] = "NO"
|
||||
|
||||
# Fix for Rosetta emulator builds - exclude arm64 for simulator
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
# Keep the current simulator flow stable until every audited binary pod ships arm64 simulator slices.
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = use_rosetta_simulator_builds ? "arm64" : ""
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,28 +12,6 @@ PODS:
|
||||
- boost (1.84.0)
|
||||
- BVLinearGradient (2.8.3):
|
||||
- React-Core
|
||||
- dotlottie-react-native (0.5.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- LottieFiles-dotLottie-iOS (~> 0.9)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXApplication (6.0.2):
|
||||
- ExpoModulesCore
|
||||
@@ -171,8 +149,29 @@ PODS:
|
||||
- IdensicMobileSDK/Fisherman (1.40.2):
|
||||
- FingerprintPro (~> 2.11)
|
||||
- IdensicMobileSDK/Core
|
||||
- lottie-ios (4.6.0)
|
||||
- LottieFiles-dotLottie-iOS (0.11.1)
|
||||
- lottie-ios (4.5.0)
|
||||
- lottie-react-native (7.2.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- lottie-ios (= 4.5.0)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- Mixpanel-swift (5.0.0):
|
||||
- Mixpanel-swift/Complete (= 5.0.0)
|
||||
- Mixpanel-swift/Complete (5.0.0)
|
||||
@@ -2160,7 +2159,6 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||
- "dotlottie-react-native (from `../node_modules/@lottiefiles/dotlottie-react-native`)"
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXApplication (from `../node_modules/expo-application/ios`)
|
||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||
@@ -2177,6 +2175,7 @@ DEPENDENCIES:
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- lottie-ios
|
||||
- lottie-react-native (from `../node_modules/lottie-react-native`)
|
||||
- Mixpanel-swift (~> 5.0.0)
|
||||
- "NFCPassportReader (from `git@github.com:selfxyz/NFCPassportReader.git`, commit `9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b`)"
|
||||
- QKMRZScanner
|
||||
@@ -2297,7 +2296,6 @@ SPEC REPOS:
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- lottie-ios
|
||||
- LottieFiles-dotLottie-iOS
|
||||
- Mixpanel-swift
|
||||
- nanopb
|
||||
- OpenSSL-Universal
|
||||
@@ -2313,8 +2311,6 @@ EXTERNAL SOURCES:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
BVLinearGradient:
|
||||
:path: "../node_modules/react-native-linear-gradient"
|
||||
dotlottie-react-native:
|
||||
:path: "../node_modules/@lottiefiles/dotlottie-react-native"
|
||||
DoubleConversion:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXApplication:
|
||||
@@ -2346,6 +2342,8 @@ EXTERNAL SOURCES:
|
||||
hermes-engine:
|
||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-2024-11-25-RNv0.77.0-d4f25d534ab744866448b36ca3bf3d97c08e638c
|
||||
lottie-react-native:
|
||||
:path: "../node_modules/lottie-react-native"
|
||||
NFCPassportReader:
|
||||
:commit: 9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b
|
||||
:git: "git@github.com:selfxyz/NFCPassportReader.git"
|
||||
@@ -2544,18 +2542,17 @@ SPEC CHECKSUMS:
|
||||
AppAuth: 1c1a8afa7e12f2ec3a294d9882dfa5ab7d3cb063
|
||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
|
||||
dotlottie-react-native: 056445614fe969f8d8d90a744944089261e6a620
|
||||
BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3
|
||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||
EXApplication: 4c72f6017a14a65e338c5e74fca418f35141e819
|
||||
EXConstants: fcfc75800824ac2d5c592b5bc74130bad17b146b
|
||||
Expo: 4bb70893882e6382b41d1e910d7226c6a1b85f0a
|
||||
ExpoAdapterGoogleSignIn: ab4d9fc38cb91077a4138d178395525ec65d0c2e
|
||||
ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516
|
||||
ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655
|
||||
ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188
|
||||
ExpoKeepAwake: b0171a73665bfcefcfcc311742a72a956e6aa680
|
||||
ExpoModulesCore: bcee92d3a2c68c408b2d8da43e3094109340dc17
|
||||
EXApplication: 27a524f5c3e671c6218220fba04752629466a1a9
|
||||
EXConstants: a1f35b9aabbb3c6791f8e67722579b1ffcdd3f18
|
||||
Expo: 03dca15247583ca0d09d3cee37fb2d5a0b878f04
|
||||
ExpoAdapterGoogleSignIn: 3332ac2d96d803350f53f84047244b35e2efc994
|
||||
ExpoAsset: 0687fe05f5d051c4a34dd1f9440bd00858413cfe
|
||||
ExpoFileSystem: c8c19bf80d914c83dda3beb8569d7fb603be0970
|
||||
ExpoFont: 773955186469acc5108ff569712a2d243857475f
|
||||
ExpoKeepAwake: 2a5f15dd4964cba8002c9a36676319a3394c85c7
|
||||
ExpoModulesCore: 0b8556860296c2ac2a0f393764c9ef78170a1558
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 2bc03a5cf64e29c611bbc5d7eb9d9f7431f37ee6
|
||||
FingerprintPro: 2f419138022451a72f783db9c94967f5a68e9977
|
||||
@@ -2578,8 +2575,8 @@ SPEC CHECKSUMS:
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef
|
||||
IdensicMobileSDK: 00b13320e1b1e0574e68475bd0fbc7cd30fce26e
|
||||
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
|
||||
LottieFiles-dotLottie-iOS: e9b34e7cff6d04f5affd97336c2dab934b86e6fb
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: 57ffc63285c1e5dcad6743dca0d3e1ed34cac598
|
||||
Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
NFCPassportReader: 48873f856f91215dbfa1eaaec20eae639672862e
|
||||
@@ -2593,95 +2590,95 @@ SPEC CHECKSUMS:
|
||||
RCTTypeSafety: 50d6ec72a3d13cf77e041ff43a0617050fb98e3f
|
||||
React: e46fdbd82d2de942970c106677056f3bdd438d82
|
||||
React-callinvoker: b027ad895934b5f27ce166d095ed0d272d7df619
|
||||
React-Core: 92733c8280b1642afed7ebfb3c523feaec946ece
|
||||
React-CoreModules: e2dfd87b6fdb9d969b16871655885a4d89a2a9f4
|
||||
React-cxxreact: d1a70e78543bb5b159fdaf6c52cadd33c1ae3244
|
||||
React-Core: 36b7f20f655d47a35046e2b02c9aa5a8f1bcb61e
|
||||
React-CoreModules: 7fac6030d37165c251a7bd4bde3333212544da3c
|
||||
React-cxxreact: 0ead442ecaa248e7f71719e286510676495ae26d
|
||||
React-debug: c17d400ddcb2c45aa4f5efedeb443c72b58b40aa
|
||||
React-defaultsnativemodule: af13e4f2629106aede1d6286921f852715017d64
|
||||
React-domnativemodule: b6785fc507cfcbdf24509a0be26fdac7454f7ea3
|
||||
React-Fabric: 5f8c48a36ff906a0e8761ff914ef368f67a25b59
|
||||
React-FabricComponents: 2ba16205b15ce80460a1dcc3725b3926493b47f8
|
||||
React-FabricImage: d1b0c203284c0ab077277a54830e4de4c0134908
|
||||
React-defaultsnativemodule: d8ddce2020fede6b0a6d3cccc3fbb1fedf1aab37
|
||||
React-domnativemodule: 17da9148ba917807b9bab6c4e1fddbc11303be64
|
||||
React-Fabric: fda27452bab6f8b5213f33c1d59a24f6c6b66579
|
||||
React-FabricComponents: 10623f84dcb5ae9b2bbe98f577546b10fa459fdb
|
||||
React-FabricImage: 2237e1c2089eb4e55541485e173f96af43afca7d
|
||||
React-featureflags: 94805545eda554c548e3615f248f4f4c65ef279e
|
||||
React-featureflagsnativemodule: 0ab7272372052fe9dc561dc2e4bbd4fd8ab11ea4
|
||||
React-graphics: 6800e73b337075ad0cb9226c1592ed1a91703244
|
||||
React-hermes: bf50c8272cb562300a54a621aa69dc12a0b4fcf2
|
||||
React-idlecallbacksnativemodule: 57d5b25440ed0478966710675354eac676508ff5
|
||||
React-ImageManager: fff4c0c50041d7b8f67d6f435e7a4b1e9125ad27
|
||||
React-jserrorhandler: 4abc5dfa7d5fb7bfba328faddfa97dc90441c276
|
||||
React-jsi: 19e77567e235d06b7e8f425d2a6c1e948ab286e9
|
||||
React-jsiexecutor: fe6ad8b9a2bf97e435fc1c969c80ed7f447ed68e
|
||||
React-jsinspector: 01aa56b6037c65a6ec4432a120aa74cc6fdf514f
|
||||
React-jsitracing: cb05a2c5c36eb212be028e26c38028f0d352c16b
|
||||
React-logger: 02e5802824aa9b15cb7df42e10a91abead83cd8d
|
||||
React-Mapbuffer: bbd3be71ef32e8198ac0f78b841662103e032ffe
|
||||
React-microtasksnativemodule: 8e65fc37744388153b9bca94552d04955d852058
|
||||
react-native-app-auth: e21c8ee920876b960e38c9381971bd189ebea06b
|
||||
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
|
||||
react-native-blur: 745703f35133ed6a1210d4bbff358a631911f002
|
||||
react-native-cloud-storage: 796c793dc354bb49f9df27ca25eed0f79a15549e
|
||||
react-native-compat: 10b5f906b469268eaceca83ea2393c177f1ce18a
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-mobilesdk-module: 08c16fea2be97669f8e4c38153106e5fe698126a
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83
|
||||
react-native-passkey: 8818f842d1b80e45c06e906a5c85964719782bf5
|
||||
react-native-safe-area-context: 5b5d3eb6ec9ef848f16c064a4eab4a92c7d7895e
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-webview: 05734d99f1e422c5ddfeefbd083d53abd78fccb1
|
||||
React-featureflagsnativemodule: b71dc56c26b09c5becaabc59d90eb6715a76d01e
|
||||
React-graphics: f81c5369a01264f5e5f2ab7b2e7fbe769c94ed42
|
||||
React-hermes: 13e1c1c9222503bcd7ad450370c5a26dc9b46ebe
|
||||
React-idlecallbacksnativemodule: 16c2ade55cf3537f7d6d1afb7acb230d65b1d63c
|
||||
React-ImageManager: 130248847aada2e9485db30cef63284ffc2f0846
|
||||
React-jserrorhandler: ef0948d6835b991094660d93cb7dcf3446d065f5
|
||||
React-jsi: 931610846e52e5d157f4bc3f71a14f9a53573abd
|
||||
React-jsiexecutor: 3f5fb21d47c5c72c13a1710b288d78c8209a38f9
|
||||
React-jsinspector: 231977808d975ea2ad045b910623651ef7219657
|
||||
React-jsitracing: 9b717dd9c91915ccf51af10df94e8c38de722786
|
||||
React-logger: 9a0c4e1e41cd640ac49d69aacadab783f7e0096b
|
||||
React-Mapbuffer: 257e617e7554c0ec448d13d38b13ee3cbdd3c5eb
|
||||
React-microtasksnativemodule: fa9db75d61e2053274057767ced1a2e2c485b0fa
|
||||
react-native-app-auth: 9b0a0e3ca279c3426a451e2607c8483808b8ed4a
|
||||
react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc
|
||||
react-native-blur: 6782cb12b39a0200ad2a782fb9a5529c2c83c33b
|
||||
react-native-cloud-storage: 8dc640aac2cf6e8a6231cc49696e8f8405b716bf
|
||||
react-native-compat: c6ac08d44535eb2b3735a2491318136dbdecf271
|
||||
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
|
||||
react-native-mobilesdk-module: 9c0b53eeea509a7ca1a6429632f2144a597ad824
|
||||
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
|
||||
react-native-nfc-manager: ef3b44c4f1975ab16d6109bb1671ab68068aba58
|
||||
react-native-passkey: 12d9c47c848f939b608e1c2d31197312be53f71e
|
||||
react-native-safe-area-context: 4a867695ce0b837b7fedc90c5629d4322be4222f
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-webview: de5205a97121427588aff27de2ddea4cc9fc0a19
|
||||
React-nativeconfig: 334c9961d74ddd3bc203afb92ee574ed01c7c755
|
||||
React-NativeModulesApple: bf996c9e3b86e579e6e8635633b721c165a60b2c
|
||||
React-perflogger: 721172bda31a65ce7b7a0c3bf3de96f12ef6f45d
|
||||
React-performancetimeline: a23bfc89694e13ead855f25049bb9d60ce3704a2
|
||||
React-NativeModulesApple: e55f72e014482edd711542815a98b865ee6de9a1
|
||||
React-perflogger: 15a7bcb6c46eae8a981f7add8c9f4172e2372324
|
||||
React-performancetimeline: 9fb03db27775ddef6a98e3d22811acf210f07ba4
|
||||
React-RCTActionSheet: 25eb72eabade4095bfaf6cd9c5c965c76865daa8
|
||||
React-RCTAnimation: 8efbd0a4a71fd3dbe84e6d08b92bec5728b7524b
|
||||
React-RCTAppDelegate: 8ff6da817adefd15d4e25ade53a477c344f9b213
|
||||
React-RCTBlob: 6056bd62a56a6d2dad55cdf195949db1de623e14
|
||||
React-RCTFabric: 113fe8b6532ac21a6a46700b2650b8d458020ee4
|
||||
React-RCTFBReactNativeSpec: 4214925b1c4829fb1e73bfbacb301244b522dc11
|
||||
React-RCTImage: 7b3f38c77e183bdcb43dbcd7b5842b96c814889a
|
||||
React-RCTLinking: 6cca74db71b23f670b72e45603e615c2b72b2235
|
||||
React-RCTNetwork: 5791b0718eff20c12f6f3d62e2ad50cff4b5c8a0
|
||||
React-RCTSettings: 84154e31a232b5b03b6b7a89924a267c431ccf16
|
||||
React-RCTText: cd49cb4442ee7f64b0415b27745d2495cb40cfaa
|
||||
React-RCTVibration: 2a7432e61d42f802716bd67edc793b5e5f58971a
|
||||
React-RCTAnimation: 04c987fa858fa16169f543d29edb4140bd35afa9
|
||||
React-RCTAppDelegate: b2707904e4f8ad92fd052e62684bf0c3b88381cc
|
||||
React-RCTBlob: 1f214a7211632515805dd1f1b81fac70d12f812d
|
||||
React-RCTFabric: 0838a13e11c221d1d5648257b2ca31fede22874b
|
||||
React-RCTFBReactNativeSpec: 60d72b45a150ca35748b9a77028674b1e56a2e43
|
||||
React-RCTImage: e516d72739797fb7c1dac5c691f02a0f5445c290
|
||||
React-RCTLinking: 1e5554afe4f959696ad3285738c1510f2592f220
|
||||
React-RCTNetwork: 65e1e52c8614dcab342fa1eaec750ca818160e74
|
||||
React-RCTSettings: e86c204b481ef9264929fe00d1fdd04ce561748a
|
||||
React-RCTText: 15f14d6f9b75e64ffe749c75e30ff047cf0fa1be
|
||||
React-RCTVibration: 8d9078d5432972fe12d9f1526b38f504ad3d45cb
|
||||
React-rendererconsistency: 9da9009da0eafdf005a77a260b1dbea274a90aa8
|
||||
React-rendererdebug: 4b9e70532888e08f41c5fcbcbc050e99a590839c
|
||||
React-rendererdebug: bb56856ce3901396c959ddcf0991f7a3a162f4c5
|
||||
React-rncore: d380e5c97ec669c0bd097612cd98247597a32679
|
||||
React-RuntimeApple: 0088247d510e7eb4a3a2ecc0964411266730d10d
|
||||
React-RuntimeCore: 0e45d29ad4057b029db38e92ab24d4294253c6e3
|
||||
React-RuntimeApple: 559b3d8f068335e896224b8365fd8cee814e6652
|
||||
React-RuntimeCore: 87c25d97233f61b68bb254360e2724c01eb93198
|
||||
React-runtimeexecutor: f9ae11481be048438640085c1e8266d6afebae44
|
||||
React-RuntimeHermes: 4d6bbb8c4832794c34fc2a0301a885a9e8c936d5
|
||||
React-runtimescheduler: bb1282886aa8ba594ff5704c14ba19af1551149f
|
||||
React-RuntimeHermes: 0cba4a2b329dcb8392754dd20a839709c7e3389f
|
||||
React-runtimescheduler: 62d73526c3471884a896328e11a930ea4b42dfe1
|
||||
React-timing: 9b94f0fb713587a697ce56b0fc7cb31cb5be70a5
|
||||
React-utils: 07c3365e9dcbb8940e912ce099b20fb0e56dbacf
|
||||
ReactAppDependencyProvider: 6e8d68583f39dc31ee65235110287277eb8556ef
|
||||
ReactCodegen: 58a974a1a86362975fd49596480c5f0f17ee06a2
|
||||
ReactCommon: e686c5766f0ebe5293be5a3957b833645cdac8ad
|
||||
RNAppleAuthentication: a89c9804592b38ed4ab11f0aee68d05ba12ad432
|
||||
RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce
|
||||
RNCClipboard: 9f7b908de4bf4353871fb454c15fc03db4917b88
|
||||
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
|
||||
RNFBApp: 4105e54d9ca4a1c10893a032268470f670181110
|
||||
RNFBMessaging: 6857871d9dff8f26b0c325fc7d97ba69cb77d213
|
||||
RNFBRemoteConfig: 8d3675f18c052483ce294bb97b857428467fb41e
|
||||
RNGestureHandler: 36aca36e4ef19f55dbf97239199d00fd58494e34
|
||||
RNGoogleSignin: 60c3f470558dbff0ae54f2f164ef82a89d3eb561
|
||||
RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20
|
||||
RNKeychain: 35beaa17938f7d8e4990d8a38fad5f8a748fc47c
|
||||
RNLocalize: 67cd0eece3ba20fb5dae7625d77f02e88d3d9573
|
||||
RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5
|
||||
RNScreens: b0811b109e1a0b8b579f3348018e177bee374840
|
||||
RNSentry: 98ab9f6a16c9596e36565ccf1a5871323f334766
|
||||
RNSVG: d926926b169d8b81eb06aeb69734076e1dd566a3
|
||||
segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7
|
||||
React-utils: 9e73840482020d1914b68089e807b3f2f56b10a3
|
||||
ReactAppDependencyProvider: 3d947e9d62f351c06c71497e1be897e6006dc303
|
||||
ReactCodegen: e92a1659b32705bd8ee0d2ba016d6993a4ade05b
|
||||
ReactCommon: a02340b2a1a76f3703298a4680bb03277ca87440
|
||||
RNAppleAuthentication: d6fe579e5f43cf8db54bdc48518bccea61c592a4
|
||||
RNCAsyncStorage: 481acf401089f312189e100815088ea5dafc583c
|
||||
RNCClipboard: 4d8c76e488f1491e5235901b7028ff53a678bd94
|
||||
RNDeviceInfo: 53f9c84e28e854a308d3e548e25ef120b4615531
|
||||
RNFBApp: b67ded6e4b0a6c0fee5e4f8e75e3a31567949a08
|
||||
RNFBMessaging: 7202ad4c49bdcdc7fd3634d85827a6305049c148
|
||||
RNFBRemoteConfig: d3b7942bc4a2e16e162b841c0df2a869015d98c9
|
||||
RNGestureHandler: 75a1894590b15c560094c2b09c5dce6a64eefa29
|
||||
RNGoogleSignin: bd5e55072fc89c69e3eb139be2a9c8935d0a0f2c
|
||||
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
||||
RNKeychain: 774184659ed098fd715a4976d44e2003c829934f
|
||||
RNLocalize: 37ca6516cd717a04a8d85a17f9a3879a728ce179
|
||||
RNReactNativeHapticFeedback: a49e613d48d721c99cad9689a490554104c22154
|
||||
RNScreens: cc97e4382039563c725394067185356352df69ad
|
||||
RNSentry: f343c58d33eb8351a5b5cfbb157d3527e2f59645
|
||||
RNSVG: 2b1b9e597b2a0847e2963aefe17d976d5c882f3f
|
||||
segment-analytics-react-native: 05c3bf2adb8a3be2c273808a6fdaced06d927917
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594
|
||||
sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60
|
||||
SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
Yoga: c34725819ab0a5962e85455b9e56679b306910ee
|
||||
|
||||
PODFILE CHECKSUM: a95943ec849e3235c1bfecf266b2a6c6ffa3d0d6
|
||||
PODFILE CHECKSUM: b55b83b47bd348c6768b89bdab74c5d2e9e09320
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
8FBA8FA051DB77C9C52EDDF3 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A78F43717F170EC139960991 /* ExpoModulesProvider.swift */; };
|
||||
905B70052A72767900AFA232 /* PassportReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905B70042A72767900AFA232 /* PassportReader.swift */; };
|
||||
905B70072A72774000AFA232 /* PassportReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 905B70062A72774000AFA232 /* PassportReader.m */; };
|
||||
90D1C0012F00000000AFA232 /* PassportReaderCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90D1C0002F00000000AFA232 /* PassportReaderCore.swift */; };
|
||||
97E31F23A5A11A2C115FE2BB /* Pods_Self.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0823092D57FC544FD63682A /* Pods_Self.framework */; };
|
||||
AE6147EC2DC95A8D00445C0F /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */; };
|
||||
B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */; };
|
||||
@@ -71,6 +72,7 @@
|
||||
905B70042A72767900AFA232 /* PassportReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportReader.swift; sourceTree = "<group>"; };
|
||||
905B70062A72774000AFA232 /* PassportReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PassportReader.m; sourceTree = "<group>"; };
|
||||
905B70082A729CD400AFA232 /* OpenPassport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = OpenPassport.entitlements; path = OpenPassport/OpenPassport.entitlements; sourceTree = "<group>"; };
|
||||
90D1C0002F00000000AFA232 /* PassportReaderCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportReaderCore.swift; sourceTree = "<group>"; };
|
||||
9BF744D9A73A4BAC96EC569A /* DINOT-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Medium.otf"; path = "../src/assets/fonts/DINOT-Medium.otf"; sourceTree = "<group>"; };
|
||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Bold.otf"; path = "../src/assets/fonts/DINOT-Bold.otf"; sourceTree = "<group>"; };
|
||||
A78F43717F170EC139960991 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Self/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
@@ -130,6 +132,7 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
905B70042A72767900AFA232 /* PassportReader.swift */,
|
||||
90D1C0002F00000000AFA232 /* PassportReaderCore.swift */,
|
||||
165E76C22B8DC8370000FA90 /* ScannerHostingController.swift */,
|
||||
905B70062A72774000AFA232 /* PassportReader.m */,
|
||||
165E76BC2B8DC4A00000FA90 /* MRZScannerModule.swift */,
|
||||
@@ -446,6 +449,7 @@
|
||||
BB000003000000000000001A /* MrzResultMapper.swift in Sources */,
|
||||
165E76BF2B8DC53A0000FA90 /* MRZScannerModule.m in Sources */,
|
||||
905B70052A72767900AFA232 /* PassportReader.swift in Sources */,
|
||||
90D1C0012F00000000AFA232 /* PassportReaderCore.swift in Sources */,
|
||||
BF5649262F43B1EB00DE07A1 /* AppDelegate.swift in Sources */,
|
||||
165E76C32B8DC8370000FA90 /* ScannerHostingController.swift in Sources */,
|
||||
E9F9A99C2D57FE2900E1362E /* PassportOCRViewManager.m in Sources */,
|
||||
|
||||
@@ -31,8 +31,7 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'^@env$': '<rootDir>/tests/__setup__/@env.js',
|
||||
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',
|
||||
'\\.(png|jpg|jpeg|gif|webp|lottie)$':
|
||||
'<rootDir>/tests/__setup__/imageMock.js',
|
||||
'\\.(png|jpg|jpeg|gif|webp)$': '<rootDir>/tests/__setup__/imageMock.js',
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
'^@$': '<rootDir>/src',
|
||||
'^@tests/(.*)$': '<rootDir>/tests/src/$1',
|
||||
@@ -51,8 +50,6 @@ module.exports = {
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/flows/disclosing/$1.cjs',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)\\.json$':
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/$1.json',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)\\.lottie$':
|
||||
'<rootDir>/tests/__setup__/imageMock.js',
|
||||
'^@selfxyz/mobile-sdk-alpha/(.*)$':
|
||||
'<rootDir>/../packages/mobile-sdk-alpha/dist/cjs/$1.cjs',
|
||||
// Fix snarkjs resolution for @anon-aadhaar/core
|
||||
|
||||
@@ -68,7 +68,7 @@ const config = {
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/react-dom(/|$)'),
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/react-native(/|$)'),
|
||||
new RegExp(
|
||||
'packages/mobile-sdk-alpha/node_modules/@lottiefiles/dotlottie-react-native(/|$)',
|
||||
'packages/mobile-sdk-alpha/node_modules/lottie-react-native(/|$)',
|
||||
),
|
||||
new RegExp('packages/mobile-sdk-alpha/node_modules/scheduler(/|$)'),
|
||||
new RegExp(
|
||||
@@ -117,8 +117,8 @@ const config = {
|
||||
// Support package exports with conditions
|
||||
unstable_conditionNames: ['react-native', 'import', 'require'],
|
||||
|
||||
// SVG support + dotLottie binary assets
|
||||
assetExts: [...assetExts.filter(ext => ext !== 'svg'), 'lottie'],
|
||||
// SVG support
|
||||
assetExts: assetExts.filter(ext => ext !== 'svg'),
|
||||
sourceExts: [...sourceExts, 'svg'],
|
||||
|
||||
// Custom resolver to handle both .js imports in TypeScript and Node.js modules
|
||||
@@ -133,41 +133,6 @@ const config = {
|
||||
'packages/mobile-sdk-alpha',
|
||||
);
|
||||
|
||||
// Deduplicate SDK animation imports — resolve to app's single copy when possible
|
||||
if (
|
||||
/\.(json|lottie)$/.test(moduleName) &&
|
||||
context.originModulePath?.includes('mobile-sdk-alpha')
|
||||
) {
|
||||
// Extract the animation-relative path from either bare or relative specifiers
|
||||
let animRelPath;
|
||||
if (moduleName.startsWith('src/animations/')) {
|
||||
animRelPath = moduleName.replace('src/animations/', '');
|
||||
} else if (/\/animations\//.test(moduleName)) {
|
||||
animRelPath = moduleName.split('/animations/').pop();
|
||||
}
|
||||
|
||||
if (animRelPath) {
|
||||
// Try app's animations first (deduplication)
|
||||
const appAnimPath = path.resolve(
|
||||
projectRoot,
|
||||
'src/assets/animations',
|
||||
animRelPath,
|
||||
);
|
||||
if (fs.existsSync(appAnimPath)) {
|
||||
return { type: 'assetFiles', filePaths: [appAnimPath] };
|
||||
}
|
||||
// Fall back to SDK's own copy (for SDK-only animations like loading/*)
|
||||
const sdkAnimPath = path.resolve(
|
||||
sdkAlphaPath,
|
||||
'src/animations',
|
||||
animRelPath,
|
||||
);
|
||||
if (fs.existsSync(sdkAnimPath)) {
|
||||
return { type: 'assetFiles', filePaths: [sdkAnimPath] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom resolver to handle Node.js modules and dynamic flow imports
|
||||
if (moduleName.startsWith('@selfxyz/mobile-sdk-alpha/')) {
|
||||
const subPath = moduleName.replace('@selfxyz/mobile-sdk-alpha/', '');
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"analyze:tree-shaking:web": "yarn web:build && node ./scripts/analyze-tree-shaking.cjs web",
|
||||
"android": "yarn build:deps && yarn setup:android-deps && react-native run-android",
|
||||
"android:ci": "./scripts/mobile-ci-build-android.sh",
|
||||
"animations:convert": "node ./scripts/convert-to-dotlottie.mjs",
|
||||
"build:deps": "yarn workspaces foreach --from @selfxyz/mobile-app --topological --recursive run build",
|
||||
"bump-version:major": "npm version major && yarn sync-versions",
|
||||
"bump-version:minor": "npm version minor && yarn sync-versions",
|
||||
@@ -89,8 +88,6 @@
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@ethersproject/shims": "^5.8.0",
|
||||
"@invertase/react-native-apple-authentication": "^2.5.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.17.15",
|
||||
"@lottiefiles/dotlottie-react-native": "0.5.0",
|
||||
"@noble/hashes": "^1.5.0",
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
"@peculiar/x509": "^1.14.3",
|
||||
@@ -133,6 +130,12 @@
|
||||
"ethers": "^6.11.0",
|
||||
"expo": "~52.0.40",
|
||||
"expo-application": "~6.0.2",
|
||||
"hash.js": "^1.1.7",
|
||||
"js-sha1": "^0.7.0",
|
||||
"js-sha256": "^0.11.1",
|
||||
"js-sha512": "^0.9.0",
|
||||
"lottie-react": "^2.4.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"node-forge": "^1.3.3",
|
||||
"pkijs": "^3.3.3",
|
||||
"poseidon-lite": "^0.2.0",
|
||||
@@ -182,7 +185,6 @@
|
||||
"@babel/plugin-transform-private-methods": "^7.28.6",
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@dotlottie/dotlottie-js": "^1.6.2",
|
||||
"@react-native-community/cli": "^16.0.3",
|
||||
"@react-native/babel-preset": "0.77.0",
|
||||
"@react-native/eslint-config": "0.77.0",
|
||||
|
||||
@@ -16,9 +16,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
|
||||
|
||||
// Bundle size thresholds in MB - easy to update!
|
||||
const BUNDLE_THRESHOLDS_MB = {
|
||||
// TODO: fix temporary bundle bump
|
||||
ios: 46,
|
||||
android: 46,
|
||||
ios: 48,
|
||||
android: 48,
|
||||
};
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
214
app/scripts/capture-ios-crash-artifacts.cjs
Normal file
214
app/scripts/capture-ios-crash-artifacts.cjs
Normal file
@@ -0,0 +1,214 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const APP_NAME = process.env.IOS_CRASH_CAPTURE_APP_NAME || 'OpenPassport';
|
||||
const WINDOW_MINUTES = process.env.IOS_CRASH_CAPTURE_WINDOW_MINUTES || '5';
|
||||
const OUTPUT_ROOT =
|
||||
process.env.IOS_CRASH_CAPTURE_OUTPUT_DIR ||
|
||||
path.join(os.homedir(), 'Desktop', 'ios-crash-capture');
|
||||
const DERIVED_DATA_ROOT = path.join(
|
||||
os.homedir(),
|
||||
'Library',
|
||||
'Developer',
|
||||
'Xcode',
|
||||
'DerivedData',
|
||||
);
|
||||
const DIAGNOSTIC_REPORTS_DIR = path.join(
|
||||
os.homedir(),
|
||||
'Library',
|
||||
'Logs',
|
||||
'DiagnosticReports',
|
||||
);
|
||||
|
||||
function timestamp() {
|
||||
return new Date().toISOString().replace(/[:]/g, '-');
|
||||
}
|
||||
|
||||
function ensureDir(directory) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
encoding: 'utf8',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function writeFile(filePath, contents) {
|
||||
fs.writeFileSync(filePath, contents, 'utf8');
|
||||
}
|
||||
|
||||
function copyFileIfPresent(sourcePath, destinationPath) {
|
||||
if (fs.existsSync(sourcePath)) {
|
||||
fs.copyFileSync(sourcePath, destinationPath);
|
||||
}
|
||||
}
|
||||
|
||||
function getLatestLaunchResult() {
|
||||
if (!fs.existsSync(DERIVED_DATA_ROOT)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
|
||||
for (const derivedDataEntry of fs.readdirSync(DERIVED_DATA_ROOT, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (
|
||||
!derivedDataEntry.isDirectory() ||
|
||||
!derivedDataEntry.name.startsWith(`${APP_NAME}-`)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const launchDir = path.join(
|
||||
DERIVED_DATA_ROOT,
|
||||
derivedDataEntry.name,
|
||||
'Logs',
|
||||
'Launch',
|
||||
);
|
||||
|
||||
if (!fs.existsSync(launchDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const launchEntry of fs.readdirSync(launchDir, {
|
||||
withFileTypes: true,
|
||||
})) {
|
||||
if (
|
||||
!launchEntry.isDirectory() ||
|
||||
!launchEntry.name.endsWith('.xcresult')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = path.join(launchDir, launchEntry.name);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
candidates.push({
|
||||
mtimeMs: stat.mtimeMs,
|
||||
path: fullPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
||||
|
||||
return candidates[0]?.path ?? null;
|
||||
}
|
||||
|
||||
function getRecentDiagnosticReports() {
|
||||
if (!fs.existsSync(DIAGNOSTIC_REPORTS_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cutoffMs =
|
||||
Date.now() - (Number.parseInt(WINDOW_MINUTES, 10) + 10) * 60 * 1000;
|
||||
|
||||
return fs
|
||||
.readdirSync(DIAGNOSTIC_REPORTS_DIR, { withFileTypes: true })
|
||||
.filter(entry => entry.isFile())
|
||||
.map(entry => {
|
||||
const fullPath = path.join(DIAGNOSTIC_REPORTS_DIR, entry.name);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
return {
|
||||
mtimeMs: stat.mtimeMs,
|
||||
name: entry.name,
|
||||
path: fullPath,
|
||||
};
|
||||
})
|
||||
.filter(entry => entry.mtimeMs >= cutoffMs)
|
||||
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
function captureUnifiedLog(outputDir) {
|
||||
const predicate = `(process == "${APP_NAME}" OR eventMessage CONTAINS[c] "${APP_NAME}" OR eventMessage CONTAINS[c] "Self.app" OR process == "SpringBoard" OR process == "runningboardd" OR eventMessage CONTAINS[c] "jetsam" OR eventMessage CONTAINS[c] "terminated" OR eventMessage CONTAINS[c] "killed")`;
|
||||
const logOutput = runCommand(
|
||||
'log',
|
||||
[
|
||||
'show',
|
||||
'--style',
|
||||
'compact',
|
||||
'--last',
|
||||
`${WINDOW_MINUTES}m`,
|
||||
'--predicate',
|
||||
predicate,
|
||||
],
|
||||
{ maxBuffer: 20 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
writeFile(path.join(outputDir, 'unified.log'), logOutput);
|
||||
}
|
||||
|
||||
function captureDiagnosticReports(outputDir) {
|
||||
const listing = fs.existsSync(DIAGNOSTIC_REPORTS_DIR)
|
||||
? runCommand('ls', ['-lt', DIAGNOSTIC_REPORTS_DIR])
|
||||
: 'DiagnosticReports directory not found.\n';
|
||||
|
||||
writeFile(path.join(outputDir, 'diagnosticreports.txt'), listing);
|
||||
|
||||
const reports = getRecentDiagnosticReports();
|
||||
const reportsDir = path.join(outputDir, 'diagnosticreports');
|
||||
ensureDir(reportsDir);
|
||||
|
||||
for (const report of reports) {
|
||||
copyFileIfPresent(report.path, path.join(reportsDir, report.name));
|
||||
}
|
||||
}
|
||||
|
||||
function captureLatestLaunchResult(outputDir) {
|
||||
const xcresultPath = getLatestLaunchResult();
|
||||
|
||||
if (!xcresultPath) {
|
||||
writeFile(
|
||||
path.join(outputDir, 'launch-result.txt'),
|
||||
'No Launch xcresult bundle found in DerivedData.\n',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
writeFile(
|
||||
path.join(outputDir, 'launch-result-path.txt'),
|
||||
`${xcresultPath}\n`,
|
||||
);
|
||||
|
||||
const xcresultJson = runCommand('xcrun', [
|
||||
'xcresulttool',
|
||||
'get',
|
||||
'object',
|
||||
'--legacy',
|
||||
'--path',
|
||||
xcresultPath,
|
||||
'--format',
|
||||
'json',
|
||||
]);
|
||||
|
||||
writeFile(path.join(outputDir, 'launch-result.json'), xcresultJson);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const outputDir = path.join(OUTPUT_ROOT, timestamp());
|
||||
ensureDir(outputDir);
|
||||
|
||||
captureUnifiedLog(outputDir);
|
||||
captureDiagnosticReports(outputDir);
|
||||
captureLatestLaunchResult(outputDir);
|
||||
|
||||
console.log(`Saved iOS crash artifacts to ${outputDir}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`Failed to capture iOS crash artifacts: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
// Convert large Lottie JSON animations to compressed dotLottie format.
|
||||
// Usage: node scripts/convert-to-dotlottie.mjs
|
||||
//
|
||||
// Web compatibility: @lottiefiles/dotlottie-react-native supports .lottie
|
||||
// natively, but lottie-web does NOT. If these animations are ever used on
|
||||
// the web, use
|
||||
// @lottiefiles/dotlottie-web (or its React/Vue wrappers) instead of lottie-web.
|
||||
|
||||
import { readFileSync, writeFileSync, statSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { DotLottie } from '@dotlottie/dotlottie-js';
|
||||
|
||||
const files = process.argv.slice(2);
|
||||
|
||||
if (files.length === 0) {
|
||||
console.error(
|
||||
'Usage: node convert-to-dotlottie.mjs <file1.json> [file2.json ...]',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const jsonData = readFileSync(file, 'utf-8');
|
||||
const animName = basename(file, '.json');
|
||||
const dir = dirname(file);
|
||||
const outFile = join(dir, `${animName}.lottie`);
|
||||
|
||||
const dotlottie = new DotLottie();
|
||||
dotlottie.addAnimation({
|
||||
id: animName,
|
||||
data: JSON.parse(jsonData),
|
||||
});
|
||||
|
||||
const buffer = await dotlottie.build();
|
||||
writeFileSync(outFile, Buffer.from(await buffer.toArrayBuffer()));
|
||||
|
||||
const jsonSize = statSync(file).size;
|
||||
const lottieSize = statSync(outFile).size;
|
||||
const pct = ((1 - lottieSize / jsonSize) * 100).toFixed(1);
|
||||
console.log(
|
||||
`${basename(file)} → ${basename(outFile)}: ${(jsonSize / 1024).toFixed(0)}KB → ${(lottieSize / 1024).toFixed(0)}KB (${pct}% smaller)`,
|
||||
);
|
||||
}
|
||||
@@ -2,51 +2,178 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { execFileSync } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
try {
|
||||
// Get list of available simulators
|
||||
const output = execSync('xcrun simctl list devices available --json', {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const APP_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
const devices = JSON.parse(output).devices;
|
||||
const DEVICE_PRIORITY = [
|
||||
'iPhone 16 Pro',
|
||||
'iPhone 16',
|
||||
'iPhone 15 Pro',
|
||||
'iPhone 15',
|
||||
];
|
||||
|
||||
// Find first available iPhone simulator (prefer latest iOS version)
|
||||
let firstSimulator = null;
|
||||
function normalizeRuntimeVersion(runtime) {
|
||||
return runtime.replace(/\./g, '-');
|
||||
}
|
||||
|
||||
// Get iOS runtime keys sorted in reverse (latest first)
|
||||
const runtimeKeys = Object.keys(devices)
|
||||
.filter(key => key.includes('iOS'))
|
||||
.sort()
|
||||
.reverse();
|
||||
function extractRuntimeVersion(runtime) {
|
||||
const match = runtime.match(/iOS-(\d+(?:-\d+)*)/i);
|
||||
|
||||
for (const runtime of runtimeKeys) {
|
||||
const iPhones = devices[runtime].filter(
|
||||
device => device.name.startsWith('iPhone') && device.isAvailable,
|
||||
);
|
||||
if (!match) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (iPhones.length > 0) {
|
||||
firstSimulator = iPhones[0].name;
|
||||
break;
|
||||
return match[1].split('-').map(part => Number.parseInt(part, 10));
|
||||
}
|
||||
|
||||
function compareRuntimeVersions(left, right) {
|
||||
const leftParts = extractRuntimeVersion(left);
|
||||
const rightParts = extractRuntimeVersion(right);
|
||||
const length = Math.max(leftParts.length, rightParts.length);
|
||||
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const leftPart = leftParts[index] ?? 0;
|
||||
const rightPart = rightParts[index] ?? 0;
|
||||
|
||||
if (leftPart !== rightPart) {
|
||||
return rightPart - leftPart;
|
||||
}
|
||||
}
|
||||
|
||||
if (!firstSimulator) {
|
||||
console.error('No available iPhone simulators found');
|
||||
process.exit(1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function selectDevice(devicesJson) {
|
||||
const runtimeFilter = process.env.IOS_SIMULATOR_RUNTIME?.trim();
|
||||
const deviceFilter = process.env.IOS_SIMULATOR_DEVICE?.trim().toLowerCase();
|
||||
|
||||
let runtimeKeys = Object.keys(devicesJson).filter(runtime =>
|
||||
runtime.includes('SimRuntime.iOS-'),
|
||||
);
|
||||
|
||||
if (runtimeFilter) {
|
||||
const normalizedRuntime = normalizeRuntimeVersion(runtimeFilter);
|
||||
const expectedSuffix = `SimRuntime.iOS-${normalizedRuntime}`;
|
||||
|
||||
runtimeKeys = runtimeKeys.filter(runtime =>
|
||||
runtime.endsWith(expectedSuffix),
|
||||
);
|
||||
|
||||
if (runtimeKeys.length === 0) {
|
||||
throw new Error(`No iOS runtime matching "${runtimeFilter}" found`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Using simulator: ${firstSimulator}`);
|
||||
runtimeKeys.sort(compareRuntimeVersions);
|
||||
|
||||
// Run the iOS build with the selected simulator
|
||||
execSync(
|
||||
`react-native run-ios --scheme OpenPassport --simulator="${firstSimulator}"`,
|
||||
const availableIPhones = runtimeKeys.flatMap(runtime =>
|
||||
devicesJson[runtime]
|
||||
.filter(device => device.isAvailable && device.name.startsWith('iPhone'))
|
||||
.map(device => ({
|
||||
name: device.name,
|
||||
udid: device.udid,
|
||||
runtime,
|
||||
})),
|
||||
);
|
||||
|
||||
if (availableIPhones.length === 0) {
|
||||
throw new Error('No available iPhone simulators found');
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
const selectedDevice =
|
||||
availableIPhones.find(
|
||||
device => device.name.toLowerCase() === deviceFilter,
|
||||
) ??
|
||||
availableIPhones.find(device =>
|
||||
device.name.toLowerCase().includes(deviceFilter),
|
||||
);
|
||||
|
||||
if (!selectedDevice) {
|
||||
const availableNames = [
|
||||
...new Set(availableIPhones.map(device => device.name)),
|
||||
];
|
||||
throw new Error(
|
||||
`No available iPhone matching "${process.env.IOS_SIMULATOR_DEVICE}". Available: ${availableNames.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
for (const deviceName of DEVICE_PRIORITY) {
|
||||
const prioritizedDevice = availableIPhones.find(
|
||||
device => device.name === deviceName,
|
||||
);
|
||||
|
||||
if (prioritizedDevice) {
|
||||
return prioritizedDevice;
|
||||
}
|
||||
}
|
||||
|
||||
return availableIPhones[0];
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
cwd: APP_ROOT,
|
||||
stdio: 'inherit',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const output = execFileSync(
|
||||
'xcrun',
|
||||
['simctl', 'list', 'devices', 'available', '--json'],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to run iOS simulator:', error.message);
|
||||
process.exit(1);
|
||||
const { devices } = JSON.parse(output);
|
||||
const simulator = selectDevice(devices);
|
||||
|
||||
console.log(`Simulator: ${simulator.name} (${simulator.runtime})`);
|
||||
console.log(`UDID: ${simulator.udid}`);
|
||||
|
||||
try {
|
||||
runCommand('xcrun', ['simctl', 'shutdown', 'all']);
|
||||
} catch {
|
||||
// Benign on fresh machines with no booted simulators.
|
||||
}
|
||||
|
||||
runCommand('xcrun', ['simctl', 'boot', simulator.udid]);
|
||||
runCommand('xcrun', ['simctl', 'bootstatus', simulator.udid, '-b']);
|
||||
sleep(5000);
|
||||
runCommand('yarn', [
|
||||
'react-native',
|
||||
'run-ios',
|
||||
'--scheme',
|
||||
'OpenPassport',
|
||||
'--udid',
|
||||
simulator.udid,
|
||||
]);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(`iOS simulator launch failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compareRuntimeVersions,
|
||||
extractRuntimeVersion,
|
||||
main,
|
||||
normalizeRuntimeVersion,
|
||||
selectDevice,
|
||||
};
|
||||
|
||||
1
app/src/assets/animations/launch_onboarding.json
Normal file
1
app/src/assets/animations/launch_onboarding.json
Normal file
File diff suppressed because one or more lines are too long
@@ -3,8 +3,6 @@
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
export const loadMiscAnimation = () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
Promise.resolve(require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie'));
|
||||
import('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json');
|
||||
export const loadPassportAnimation = () =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
Promise.resolve(require('@/assets/animations/passport_verify.lottie'));
|
||||
import('@/assets/animations/passport_verify.json');
|
||||
|
||||
1
app/src/assets/animations/passport_onboarding.json
Normal file
1
app/src/assets/animations/passport_onboarding.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/passport_scan.json
Normal file
1
app/src/assets/animations/passport_scan.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/passport_verify.json
Normal file
1
app/src/assets/animations/passport_verify.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/proof_failed.json
Normal file
1
app/src/assets/animations/proof_failed.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/proof_success.json
Normal file
1
app/src/assets/animations/proof_success.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/qr_scan.json
Normal file
1
app/src/assets/animations/qr_scan.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/splash.json
Normal file
1
app/src/assets/animations/splash.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
app/src/assets/animations/warning.json
Normal file
1
app/src/assets/animations/warning.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -2,12 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import React from 'react';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Text, View, XStack, YStack } from 'tamagui';
|
||||
|
||||
import type { DotLottieSource } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
black,
|
||||
cyan300,
|
||||
@@ -24,7 +24,7 @@ import Plus from '@/assets/icons/plus_slate600.svg';
|
||||
import { extraYPadding } from '@/utils/styleUtils';
|
||||
|
||||
interface LoadingUIProps {
|
||||
animationSource: DotLottieSource;
|
||||
animationSource: LottieView['props']['source'];
|
||||
shouldLoopAnimation: boolean;
|
||||
actionText: string;
|
||||
actionSubText: string;
|
||||
@@ -117,7 +117,7 @@ const LoadingUI: React.FC<LoadingUIProps> = ({
|
||||
elevation={8}
|
||||
>
|
||||
<YStack alignItems="center" paddingHorizontal={10} flex={1}>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={shouldLoopAnimation}
|
||||
source={animationSource}
|
||||
|
||||
@@ -113,10 +113,16 @@ export const IDSelectorItem: React.FC<IDSelectorItemProps> = ({
|
||||
fontSize={18}
|
||||
fontWeight="500"
|
||||
color={textColor}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{documentName}
|
||||
</Text>
|
||||
<Text fontFamily={dinot} fontSize={14} color={subtitleColor}>
|
||||
<Text
|
||||
fontFamily={dinot}
|
||||
fontSize={14}
|
||||
color={subtitleColor}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{subtitleText}
|
||||
</Text>
|
||||
</YStack>
|
||||
|
||||
@@ -81,6 +81,7 @@ export const IDSelectorSheet: React.FC<IDSelectorSheetProps> = ({
|
||||
fontWeight="500"
|
||||
color={black}
|
||||
marginBottom={32}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Select an ID
|
||||
</Text>
|
||||
@@ -140,6 +141,7 @@ export const IDSelectorSheet: React.FC<IDSelectorSheetProps> = ({
|
||||
fontSize={18}
|
||||
fontWeight="500"
|
||||
color={black}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Dismiss
|
||||
</Text>
|
||||
@@ -162,6 +164,7 @@ export const IDSelectorSheet: React.FC<IDSelectorSheetProps> = ({
|
||||
fontSize={18}
|
||||
fontWeight="500"
|
||||
color={white}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Select
|
||||
</Text>
|
||||
|
||||
@@ -92,6 +92,7 @@ export const BottomActionBar: React.FC<BottomActionBarProps> = ({
|
||||
fontSize={18}
|
||||
color={proofRequestColors.slate900}
|
||||
numberOfLines={1}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{selectedDocumentName}
|
||||
</Text>
|
||||
@@ -135,6 +136,7 @@ export const BottomActionBar: React.FC<BottomActionBarProps> = ({
|
||||
fontSize={18}
|
||||
color={proofRequestColors.white}
|
||||
textAlign="center"
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Select
|
||||
</Text>
|
||||
|
||||
@@ -56,6 +56,7 @@ export const ConnectedWalletBadge: React.FC<ConnectedWalletBadgeProps> = ({
|
||||
fontSize={12}
|
||||
color={proofRequestColors.white}
|
||||
textTransform="uppercase"
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
@@ -68,6 +69,7 @@ export const ConnectedWalletBadge: React.FC<ConnectedWalletBadgeProps> = ({
|
||||
fontSize={12}
|
||||
color={proofRequestColors.white}
|
||||
textAlign="right"
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-address`}
|
||||
>
|
||||
{truncateAddress(address)}
|
||||
|
||||
@@ -62,6 +62,7 @@ export const DisclosureItem: React.FC<DisclosureItemProps> = ({
|
||||
color={proofRequestColors.slate900}
|
||||
textTransform="uppercase"
|
||||
letterSpacing={0.48}
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-text`}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -41,6 +41,7 @@ export const ProofMetadataBar: React.FC<ProofMetadataBarProps> = ({
|
||||
fontWeight="500"
|
||||
color={proofRequestColors.slate400}
|
||||
textTransform="uppercase"
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Proofs Requested
|
||||
</Text>
|
||||
@@ -52,6 +53,7 @@ export const ProofMetadataBar: React.FC<ProofMetadataBarProps> = ({
|
||||
fontSize={12}
|
||||
fontWeight="500"
|
||||
color={proofRequestColors.slate400}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
•
|
||||
</Text>
|
||||
@@ -62,6 +64,7 @@ export const ProofMetadataBar: React.FC<ProofMetadataBarProps> = ({
|
||||
fontSize={12}
|
||||
fontWeight="500"
|
||||
color={proofRequestColors.slate400}
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-timestamp`}
|
||||
>
|
||||
{timestamp}
|
||||
|
||||
@@ -68,18 +68,34 @@ export const ProofRequestCard: React.FC<ProofRequestCardProps> = ({
|
||||
// Build request message with highlighted app name and document type
|
||||
const requestMessage = (
|
||||
<>
|
||||
<Text color={proofRequestColors.white} fontFamily={dinot}>
|
||||
<Text
|
||||
color={proofRequestColors.white}
|
||||
fontFamily={dinot}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{appName}
|
||||
</Text>
|
||||
<Text color={proofRequestColors.slate400} fontFamily={dinot}>
|
||||
<Text
|
||||
color={proofRequestColors.slate400}
|
||||
fontFamily={dinot}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{
|
||||
' is requesting access to the following information from your verified '
|
||||
}
|
||||
</Text>
|
||||
<Text color={proofRequestColors.white} fontFamily={dinot}>
|
||||
<Text
|
||||
color={proofRequestColors.white}
|
||||
fontFamily={dinot}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
{documentType}
|
||||
</Text>
|
||||
<Text color={proofRequestColors.slate400} fontFamily={dinot}>
|
||||
<Text
|
||||
color={proofRequestColors.slate400}
|
||||
fontFamily={dinot}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
.
|
||||
</Text>
|
||||
</>
|
||||
|
||||
@@ -61,12 +61,13 @@ export const ProofRequestHeader: React.FC<ProofRequestHeaderProps> = ({
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<YStack>
|
||||
<YStack flexShrink={1}>
|
||||
<Text
|
||||
fontFamily={advercase}
|
||||
fontSize={28}
|
||||
color={proofRequestColors.white}
|
||||
letterSpacing={1}
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-app-name`}
|
||||
>
|
||||
{appName}
|
||||
@@ -77,6 +78,7 @@ export const ProofRequestHeader: React.FC<ProofRequestHeaderProps> = ({
|
||||
fontFamily={plexMono}
|
||||
fontSize={12}
|
||||
color={proofRequestColors.zinc500}
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-app-url`}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
@@ -95,6 +97,7 @@ export const ProofRequestHeader: React.FC<ProofRequestHeaderProps> = ({
|
||||
color={proofRequestColors.slate400}
|
||||
lineHeight={24}
|
||||
minHeight={75}
|
||||
allowFontScaling={false}
|
||||
testID={`${testID}-request-message`}
|
||||
>
|
||||
{requestMessage}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { SENTRY_DSN } from '@env';
|
||||
import {
|
||||
addBreadcrumb,
|
||||
@@ -141,20 +143,69 @@ export const captureMessage = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getSentryRuntimeFlags = () => {
|
||||
const disableSimulatorHeavyIntegrations = isIosSimulator();
|
||||
|
||||
return {
|
||||
disableSimulatorHeavyIntegrations,
|
||||
enableFeedbackScreenshots: !disableSimulatorHeavyIntegrations,
|
||||
replaysOnErrorSampleRate: disableSimulatorHeavyIntegrations ? 0 : 1.0,
|
||||
replaysSessionSampleRate: disableSimulatorHeavyIntegrations ? 0 : 0.1,
|
||||
};
|
||||
};
|
||||
|
||||
export const initSentry = () => {
|
||||
if (isSentryDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
disableSimulatorHeavyIntegrations,
|
||||
enableFeedbackScreenshots,
|
||||
replaysOnErrorSampleRate,
|
||||
replaysSessionSampleRate,
|
||||
} = getSentryRuntimeFlags();
|
||||
const integrations = [
|
||||
consoleLoggingIntegration({
|
||||
levels: ['log', 'error', 'warn', 'info', 'debug'],
|
||||
}),
|
||||
feedbackIntegration({
|
||||
buttonOptions: {
|
||||
styles: {
|
||||
triggerButton: {
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: undefined,
|
||||
marginTop: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
enableTakeScreenshot: enableFeedbackScreenshots,
|
||||
namePlaceholder: 'Fullname',
|
||||
emailPlaceholder: 'Email',
|
||||
}),
|
||||
];
|
||||
|
||||
if (!disableSimulatorHeavyIntegrations) {
|
||||
integrations.unshift(
|
||||
mobileReplayIntegration({
|
||||
maskAllText: true,
|
||||
maskAllImages: false,
|
||||
maskAllVectors: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
sentryInit({
|
||||
dsn: SENTRY_DSN,
|
||||
debug: false,
|
||||
enableAutoSessionTracking: true,
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 1.0,
|
||||
// Session Replay
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
// Replay and screenshots are disabled on iOS simulator to reduce cold-start pressure.
|
||||
replaysSessionSampleRate,
|
||||
replaysOnErrorSampleRate,
|
||||
// Disable collection of PII data
|
||||
beforeSend(event) {
|
||||
// Remove PII data
|
||||
@@ -164,38 +215,16 @@ export const initSentry = () => {
|
||||
}
|
||||
return event;
|
||||
},
|
||||
integrations: [
|
||||
mobileReplayIntegration({
|
||||
maskAllText: true,
|
||||
maskAllImages: false,
|
||||
maskAllVectors: false,
|
||||
}),
|
||||
consoleLoggingIntegration({
|
||||
levels: ['log', 'error', 'warn', 'info', 'debug'],
|
||||
}),
|
||||
feedbackIntegration({
|
||||
buttonOptions: {
|
||||
styles: {
|
||||
triggerButton: {
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 20,
|
||||
bottom: undefined,
|
||||
marginTop: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
enableTakeScreenshot: true,
|
||||
namePlaceholder: 'Fullname',
|
||||
emailPlaceholder: 'Email',
|
||||
}),
|
||||
],
|
||||
integrations,
|
||||
_experiments: {
|
||||
enableLogs: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const isIosSimulator = () =>
|
||||
Platform.OS === 'ios' && DeviceInfo.isEmulatorSync();
|
||||
|
||||
export const isSentryDisabled = !SENTRY_DSN;
|
||||
|
||||
type LogLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
@@ -160,6 +160,16 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
|
||||
`crypto.sign adapter not implemented for keyRef: ${_keyRef}`,
|
||||
);
|
||||
},
|
||||
async generateKey(_keyRef: string): Promise<{ keyRef: string }> {
|
||||
throw new Error(
|
||||
'Key generation is not implemented in the app crypto adapter.',
|
||||
);
|
||||
},
|
||||
async getPublicKey(_keyRef: string): Promise<Uint8Array> {
|
||||
throw new Error(
|
||||
'Public key retrieval is not implemented in the app crypto adapter.',
|
||||
);
|
||||
},
|
||||
},
|
||||
analytics: {
|
||||
trackEvent: (event: string, data?: TrackEventParams) => {
|
||||
|
||||
@@ -15,7 +15,8 @@ import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { X } from '@tamagui/lucide-icons';
|
||||
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import youWinAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/youWin.json';
|
||||
import { PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
@@ -29,9 +30,6 @@ import SelfLogo from '@/assets/logos/self.svg';
|
||||
import { SystemBars } from '@/components/SystemBars';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const youWinAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/youWin.lottie');
|
||||
|
||||
const GratificationScreen: React.FC = () => {
|
||||
const { top, bottom } = useSafeAreaInsets();
|
||||
const navigation =
|
||||
@@ -68,7 +66,7 @@ const GratificationScreen: React.FC = () => {
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={youWinAnimation}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import type { StaticScreenProps } from '@react-navigation/native';
|
||||
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
|
||||
|
||||
import type { DocumentCategory } from '@selfxyz/common/utils/types';
|
||||
import type {
|
||||
DotLottieSource,
|
||||
ProvingStateType,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import type { ProvingStateType } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
advercase,
|
||||
dinot,
|
||||
loadSelectedDocument,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
|
||||
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
|
||||
import {
|
||||
black,
|
||||
slate400,
|
||||
@@ -31,11 +31,6 @@ import { getLoadingScreenText } from '@/proving/loadingScreenStateText';
|
||||
import { setupNotifications } from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const failAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/fail.lottie');
|
||||
const proveLoadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/prove.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
type LoadingScreenParams = {
|
||||
documentCategory?: DocumentCategory;
|
||||
signatureAlgorithm?: string;
|
||||
@@ -43,6 +38,7 @@ type LoadingScreenParams = {
|
||||
};
|
||||
|
||||
type LoadingScreenProps = StaticScreenProps<LoadingScreenParams>;
|
||||
|
||||
// Define all terminal states that should stop animations and haptics
|
||||
const terminalStates: ProvingStateType[] = [
|
||||
'completed',
|
||||
@@ -59,9 +55,9 @@ const LoadingScreen: React.FC<LoadingScreenProps> = ({ route }) => {
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
|
||||
// Animation states
|
||||
const [animationSource, setAnimationSource] = useState<DotLottieSource>(
|
||||
proveLoadingAnimation,
|
||||
);
|
||||
const [animationSource, setAnimationSource] = useState<
|
||||
LottieView['props']['source']
|
||||
>(proveLoadingAnimation);
|
||||
|
||||
// Loading text state
|
||||
const [loadingText, setLoadingText] = useState<{
|
||||
|
||||
@@ -8,12 +8,13 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DelayedLottieView,
|
||||
hasAnyValidRegisteredDocument,
|
||||
LottieAnimation,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import splashAnimation from '@/assets/animations/splash.json';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import {
|
||||
@@ -31,9 +32,6 @@ import {
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const splashAnimation = require('@/assets/animations/splash.lottie');
|
||||
|
||||
const SplashScreen: React.FC = ({}) => {
|
||||
const selfClient = useSelfClient();
|
||||
const navigation =
|
||||
@@ -109,10 +107,13 @@ const SplashScreen: React.FC = ({}) => {
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsAnimationFinished(prev => {
|
||||
if (!prev) console.warn('SplashScreen: animation timeout, proceeding');
|
||||
if (!prev) {
|
||||
console.warn('SplashScreen: animation timeout, proceeding');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
@@ -136,7 +137,7 @@ const SplashScreen: React.FC = ({}) => {
|
||||
}, [isAnimationFinished, nextScreen, queuedDeepLink, navigation, selfClient]);
|
||||
|
||||
return (
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={splashAnimation}
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Adapt, Button, Select, Sheet, Text, XStack, YStack } from 'tamagui';
|
||||
import { Check, ChevronDown } from '@tamagui/lucide-icons';
|
||||
|
||||
import type {
|
||||
DotLottieSource,
|
||||
provingMachineCircuitType,
|
||||
ProvingStateType,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import failAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/fail.json';
|
||||
import proveLoadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/prove.json';
|
||||
import { slate200, slate500 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts';
|
||||
|
||||
import LoadingUI from '@/components/LoadingUI';
|
||||
import { getLoadingScreenText } from '@/proving/loadingScreenStateText';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const failAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/fail.lottie');
|
||||
const proveLoadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/prove.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
const allProvingStates = [
|
||||
'idle',
|
||||
'parsing_id_document',
|
||||
@@ -44,9 +41,9 @@ const DevLoadingScreen: React.FC = () => {
|
||||
const [currentState, setCurrentState] = useState<ProvingStateType>('idle');
|
||||
const [documentType, setDocumentType] =
|
||||
useState<provingMachineCircuitType>('dsc');
|
||||
const [animationSource, setAnimationSource] = useState<DotLottieSource>(
|
||||
proveLoadingAnimation,
|
||||
);
|
||||
const [animationSource, setAnimationSource] = useState<
|
||||
LottieView['props']['source']
|
||||
>(proveLoadingAnimation);
|
||||
const [loadingText, setLoadingText] = useState<{
|
||||
actionText: string;
|
||||
actionSubText: string;
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import {
|
||||
DelayedLottieView,
|
||||
dinot,
|
||||
LottieAnimation,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
useReadMRZ,
|
||||
} from '@selfxyz/mobile-sdk-alpha/onboarding/read-mrz';
|
||||
|
||||
import passportScanAnimation from '@/assets/animations/passport_scan.json';
|
||||
import Scan from '@/assets/icons/passport_camera_scan.svg';
|
||||
import { PassportCamera } from '@/components/native/PassportCamera';
|
||||
import { useErrorInjection } from '@/hooks/useErrorInjection';
|
||||
@@ -39,9 +40,6 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportScanAnimation = require('@/assets/animations/passport_scan.lottie');
|
||||
|
||||
const DocumentCameraScreen: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
const navigation =
|
||||
@@ -89,7 +87,7 @@ const DocumentCameraScreen: React.FC = () => {
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<PassportCamera onPassportRead={onPassportRead} isMounted={isFocused} />
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop
|
||||
source={passportScanAnimation}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -21,7 +22,6 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
||||
import NfcManager from 'react-native-nfc-manager';
|
||||
import { Button, Image, XStack } from 'tamagui';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Dotlottie } from '@lottiefiles/dotlottie-react-native';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
import {
|
||||
useFocusEffect,
|
||||
@@ -32,11 +32,7 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { CircleHelp } from '@tamagui/lucide-icons';
|
||||
|
||||
import type { PassportData } from '@selfxyz/common/types';
|
||||
import {
|
||||
LottieAnimation,
|
||||
sanitizeErrorMessage,
|
||||
useSelfClient,
|
||||
} from '@selfxyz/mobile-sdk-alpha';
|
||||
import { sanitizeErrorMessage, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
BodyText,
|
||||
ButtonsContainer,
|
||||
@@ -55,6 +51,7 @@ import {
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
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';
|
||||
@@ -103,9 +100,6 @@ type DocumentNFCScanRoute = RouteProp<
|
||||
string
|
||||
>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportVerifyAnimation = require('@/assets/animations/passport_verify.lottie');
|
||||
|
||||
const DocumentNFCScanScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent, useMRZStore } = selfClient;
|
||||
@@ -142,10 +136,14 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
[route.params?.useCan],
|
||||
);
|
||||
|
||||
const animationRef = useRef<Dotlottie | null>(null);
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
animationRef.current?.play();
|
||||
const timer = setTimeout(() => {
|
||||
animationRef.current?.play();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -547,7 +545,7 @@ const DocumentNFCScanScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={slate100}>
|
||||
<LottieAnimation
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import type { Dotlottie } from '@lottiefiles/dotlottie-react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Additional,
|
||||
ButtonsContainer,
|
||||
@@ -24,14 +24,12 @@ import {
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import passportOnboardingAnimation from '@/assets/animations/passport_onboarding.json';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import { impactLight } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import { getDocumentScanPrompt } from '@/utils/documentAttributes';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const passportOnboardingAnimation = require('@/assets/animations/passport_onboarding.lottie');
|
||||
|
||||
const DocumentOnboardingScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const selfClient = useSelfClient();
|
||||
@@ -39,7 +37,7 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
state => state.documentType,
|
||||
);
|
||||
const handleCameraPress = useHapticNavigation('DocumentCamera');
|
||||
const animationRef = useRef<Dotlottie | null>(null);
|
||||
const animationRef = useRef<LottieView>(null);
|
||||
|
||||
const scanPrompt = getDocumentScanPrompt(selectedDocumentType);
|
||||
|
||||
@@ -60,7 +58,7 @@ const DocumentOnboardingScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection roundTop backgroundColor={black}>
|
||||
<LottieAnimation
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
autoPlay={false}
|
||||
loop={false}
|
||||
|
||||
@@ -11,7 +11,8 @@ import type { StaticScreenProps } from '@react-navigation/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
|
||||
import {
|
||||
AbstractButton,
|
||||
Description,
|
||||
@@ -31,9 +32,6 @@ import {
|
||||
} from '@/services/notifications/notificationService';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const loadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie');
|
||||
|
||||
type KycSuccessRouteParams = StaticScreenProps<
|
||||
| {
|
||||
userId?: string;
|
||||
@@ -114,7 +112,7 @@ const KycSuccessScreen: React.FC<KycSuccessRouteParams> = ({
|
||||
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
|
||||
<View style={styles.centerSection}>
|
||||
<View style={styles.animationContainer}>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={true}
|
||||
source={loadingAnimation}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Description,
|
||||
PrimaryButton,
|
||||
@@ -16,14 +16,12 @@ import {
|
||||
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import proofSuccessAnimation from '@/assets/animations/proof_success.json';
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { styles } from '@/screens/verification/ProofRequestStatusScreen';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const proofSuccessAnimation = require('@/assets/animations/proof_success.lottie');
|
||||
|
||||
const AccountVerifiedSuccessScreen: React.FC = ({}) => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
@@ -31,7 +29,7 @@ const AccountVerifiedSuccessScreen: React.FC = ({}) => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={white}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black} roundTop>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={proofSuccessAnimation}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { YStack } from 'tamagui';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { LottieAnimation } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Caution,
|
||||
PrimaryButton,
|
||||
@@ -17,14 +17,12 @@ import {
|
||||
import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import warningAnimation from '@/assets/animations/warning.json';
|
||||
import { confirmTap, notificationWarning } from '@/integrations/haptics';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const warningAnimation = require('@/assets/animations/warning.lottie');
|
||||
|
||||
const DisclaimerScreen: React.FC = () => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
@@ -37,7 +35,7 @@ const DisclaimerScreen: React.FC = () => {
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor={black}>
|
||||
<ExpandableBottomLayout.TopSection backgroundColor={black}>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={false}
|
||||
source={warningAnimation}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type { LottieViewProps } from 'lottie-react-native';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import { ScrollView, Spinner } from 'tamagui';
|
||||
import { useIsFocused, useNavigation } from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import type { DotLottieSource } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import loadingAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/misc.json';
|
||||
import {
|
||||
BodyText,
|
||||
Description,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import failAnimation from '@/assets/animations/proof_failed.json';
|
||||
import succesAnimation from '@/assets/animations/proof_success.json';
|
||||
import useHapticNavigation from '@/hooks/useHapticNavigation';
|
||||
import {
|
||||
buttonTap,
|
||||
@@ -32,13 +35,6 @@ import { getWhiteListedDisclosureAddresses } from '@/services/points/utils';
|
||||
import { useProofHistoryStore } from '@/stores/proofHistoryStore';
|
||||
import { ProofStatus } from '@/stores/proofTypes';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports -- binary assets loaded by Metro */
|
||||
const loadingAnimation = require('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie');
|
||||
|
||||
const failAnimation = require('@/assets/animations/proof_failed.lottie');
|
||||
const succesAnimation = require('@/assets/animations/proof_success.lottie');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
const SuccessScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent } = selfClient;
|
||||
@@ -59,7 +55,7 @@ const SuccessScreen: React.FC = () => {
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const [animationSource, setAnimationSource] =
|
||||
useState<DotLottieSource>(loadingAnimation);
|
||||
useState<LottieViewProps['source']>(loadingAnimation);
|
||||
const [countdown, setCountdown] = useState<number | null>(null);
|
||||
const [countdownStarted, setCountdownStarted] = useState(false);
|
||||
const [whitelistedPoints, setWhitelistedPoints] = useState<number | null>(
|
||||
@@ -217,7 +213,7 @@ const SuccessScreen: React.FC = () => {
|
||||
marginTop={20}
|
||||
backgroundColor={black}
|
||||
>
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop={animationSource === loadingAnimation}
|
||||
source={animationSource}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@react-navigation/native';
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { LottieAnimation, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { DelayedLottieView, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import {
|
||||
Additional,
|
||||
Description,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import qrScanAnimation from '@/assets/animations/qr_scan.json';
|
||||
import QRScan from '@/assets/icons/qr_code.svg';
|
||||
import type { QRCodeScannerViewProps } from '@/components/native/QRCodeScanner';
|
||||
import { QRCodeScannerView } from '@/components/native/QRCodeScanner';
|
||||
@@ -37,9 +38,6 @@ import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { RootStackParamList } from '@/navigation';
|
||||
import { parseAndValidateUrlParams } from '@/navigation/deeplinks';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- binary asset loaded by Metro
|
||||
const qrScanAnimation = require('@/assets/animations/qr_scan.lottie');
|
||||
|
||||
const QRCodeViewFinderScreen: React.FC = () => {
|
||||
const selfClient = useSelfClient();
|
||||
const { trackEvent } = selfClient;
|
||||
@@ -165,7 +163,7 @@ const QRCodeViewFinderScreen: React.FC = () => {
|
||||
{shouldRenderCamera && (
|
||||
<>
|
||||
<QRCodeScannerView onQRData={onQRData} isMounted={isFocused} />
|
||||
<LottieAnimation
|
||||
<DelayedLottieView
|
||||
autoPlay
|
||||
loop
|
||||
source={qrScanAnimation}
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
export const mockCrypto: CryptoAdapter = {
|
||||
hash: async () => new Uint8Array(),
|
||||
sign: async () => new Uint8Array(),
|
||||
generateKey: async (keyRef: string) => ({ keyRef }),
|
||||
getPublicKey: async (_keyRef: string) => new Uint8Array(),
|
||||
};
|
||||
|
||||
export const mockDocuments: DocumentsAdapter = {
|
||||
|
||||
59
app/tests/src/config/sentry.test.ts
Normal file
59
app/tests/src/config/sentry.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { getSentryRuntimeFlags, isIosSimulator } from '@/config/sentry';
|
||||
|
||||
let mockIsEmulator = false;
|
||||
|
||||
jest.mock('react-native-device-info', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
isEmulatorSync: jest.fn(() => mockIsEmulator),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@sentry/react-native', () => ({
|
||||
__esModule: true,
|
||||
addBreadcrumb: jest.fn(),
|
||||
captureException: jest.fn(),
|
||||
captureFeedback: jest.fn(),
|
||||
captureMessage: jest.fn(),
|
||||
consoleLoggingIntegration: jest.fn(),
|
||||
feedbackIntegration: jest.fn(),
|
||||
init: jest.fn(),
|
||||
mobileReplayIntegration: jest.fn(),
|
||||
withScope: jest.fn(),
|
||||
wrap: jest.fn(component => component),
|
||||
}));
|
||||
|
||||
describe('sentry simulator isolation flags', () => {
|
||||
beforeEach(() => {
|
||||
mockIsEmulator = false;
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
it('detects iOS simulator runtime', () => {
|
||||
mockIsEmulator = true;
|
||||
|
||||
expect(isIosSimulator()).toBe(true);
|
||||
expect(getSentryRuntimeFlags()).toEqual({
|
||||
disableSimulatorHeavyIntegrations: true,
|
||||
enableFeedbackScreenshots: false,
|
||||
replaysOnErrorSampleRate: 0,
|
||||
replaysSessionSampleRate: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps replay and screenshots enabled off simulator', () => {
|
||||
expect(isIosSimulator()).toBe(false);
|
||||
expect(getSentryRuntimeFlags()).toEqual({
|
||||
disableSimulatorHeavyIntegrations: false,
|
||||
enableFeedbackScreenshots: true,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -82,7 +82,7 @@ jest.mock('tamagui', () => {
|
||||
});
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
|
||||
LottieAnimation: ({ onAnimationFinish }: any) => {
|
||||
DelayedLottieView: ({ onAnimationFinish }: any) => {
|
||||
// Simulate animation finishing immediately
|
||||
setTimeout(() => {
|
||||
onAnimationFinish?.();
|
||||
|
||||
@@ -102,7 +102,7 @@ jest.mock('@selfxyz/mobile-sdk-alpha/constants/analytics', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.lottie', () => 1);
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha/animations/loading/misc.json', () => ({}));
|
||||
|
||||
jest.mock('@/integrations/haptics', () => ({
|
||||
buttonTap: jest.fn(),
|
||||
@@ -125,7 +125,7 @@ jest.mock('@/services/analytics', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('@selfxyz/mobile-sdk-alpha', () => ({
|
||||
LottieAnimation: () => null,
|
||||
DelayedLottieView: () => null,
|
||||
useSelfClient: jest.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ export default defineConfig({
|
||||
root: 'web',
|
||||
publicDir: 'web',
|
||||
envDir: '..', // This is the directory where Vite will look for .env files relative to the root
|
||||
assetsInclude: ['**/*.lottie'],
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.web.tsx',
|
||||
@@ -35,7 +34,7 @@ export default defineConfig({
|
||||
'@': resolve(__dirname, 'src'),
|
||||
'@/package.json': resolve(__dirname, 'package.json'),
|
||||
'react-native-svg': 'react-native-svg-web',
|
||||
'@lottiefiles/dotlottie-react-native': '@lottiefiles/dotlottie-react',
|
||||
'lottie-react-native': 'lottie-react',
|
||||
'@react-native-community/blur': resolve(
|
||||
__dirname,
|
||||
'src/devtools/mocks/react-native-community-blur.ts',
|
||||
@@ -165,10 +164,7 @@ export default defineConfig({
|
||||
'vendor-analytics-sentry': ['@sentry/react', '@sentry/react-native'],
|
||||
|
||||
// Animations
|
||||
'vendor-animations-lottie': [
|
||||
'@lottiefiles/dotlottie-react-native',
|
||||
'@lottiefiles/dotlottie-react',
|
||||
],
|
||||
'vendor-animations-lottie': ['lottie-react-native', 'lottie-react'],
|
||||
|
||||
// WebSocket and Socket.IO
|
||||
'vendor-websocket': ['socket.io-client'],
|
||||
@@ -196,7 +192,7 @@ export default defineConfig({
|
||||
|
||||
// Large animations - split out heavy Lottie files
|
||||
'animations-passport-onboarding': [
|
||||
'./src/assets/animations/passport_onboarding.lottie',
|
||||
'./src/assets/animations/passport_onboarding.json',
|
||||
],
|
||||
|
||||
// Other screens
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"docstrings": "yarn docstrings:app && yarn docstrings:sdk",
|
||||
"docstrings:app": "yarn tsx scripts/docstring-report.ts \"app/src/**/*.{ts,tsx}\" --label \"Mobile App\" --write-report docs/coverage/app.json",
|
||||
"docstrings:sdk": "yarn tsx scripts/docstring-report.ts \"packages/mobile-sdk-alpha/src/**/*.{ts,tsx}\" --label \"Mobile SDK Alpha\" --write-report docs/coverage/sdk.json",
|
||||
"format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format",
|
||||
"format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && GRADLE_USER_HOME=/tmp/self-gradle-format yarn workspace @selfxyz/kmp-sdk format && GRADLE_USER_HOME=/tmp/self-gradle-format yarn workspace @selfxyz/kmp-sdk-test-app format && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root --exclude @selfxyz/kmp-sdk --exclude @selfxyz/kmp-sdk-test-app run format",
|
||||
"format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false",
|
||||
"format:root": "echo 'format markdown' && yarn prettier --parser markdown --write '*.md' 'docs/**/*.md' 'specs/**/*.md' && echo 'format yaml' && yarn prettier --parser yaml --write .*.{yml,yaml} --single-quote false && yarn prettier --write scripts/**/*.{js,mjs,ts} && yarn prettier --parser json --write scripts/**/*.json",
|
||||
"gitleaks": "gitleaks protect --staged --redact --config=gitleaks-override.toml",
|
||||
|
||||
@@ -10,6 +10,10 @@ import androidx.compose.runtime.setValue
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import xyz.self.sdk.api.SelfSdk
|
||||
import xyz.self.sdk.api.SelfSdkCallback
|
||||
import xyz.self.sdk.api.SelfSdkConfig
|
||||
@@ -34,6 +38,36 @@ sealed class Screen {
|
||||
class MainViewModel(
|
||||
private val sdk: SelfSdk = SelfSdk.configure(SelfSdkConfig(debug = false)),
|
||||
) {
|
||||
private fun stringifyClaims(claims: Map<String, Any?>?): Map<String, String>? =
|
||||
claims?.mapValues { (_, value) -> value.toJsonString() }
|
||||
|
||||
private fun Any?.toJsonString(): String =
|
||||
when (this) {
|
||||
null -> "null"
|
||||
is String -> this
|
||||
is Boolean, is Number -> toString()
|
||||
is Map<*, *> -> JsonObject(
|
||||
entries.filter { it.key is String }
|
||||
.associate { (k, v) -> k as String to v.toJsonElement() },
|
||||
).toString()
|
||||
is List<*> -> JsonArray(map { it.toJsonElement() }).toString()
|
||||
else -> toString()
|
||||
}
|
||||
|
||||
private fun Any?.toJsonElement(): kotlinx.serialization.json.JsonElement =
|
||||
when (this) {
|
||||
null -> JsonNull
|
||||
is String -> JsonPrimitive(this)
|
||||
is Boolean -> JsonPrimitive(this)
|
||||
is Number -> JsonPrimitive(this)
|
||||
is Map<*, *> -> JsonObject(
|
||||
entries.filter { it.key is String }
|
||||
.associate { (k, v) -> k as String to v.toJsonElement() },
|
||||
)
|
||||
is List<*> -> JsonArray(map { it.toJsonElement() })
|
||||
else -> JsonPrimitive(toString())
|
||||
}
|
||||
|
||||
var currentScreen by mutableStateOf<Screen>(Screen.Home)
|
||||
private set
|
||||
|
||||
@@ -72,7 +106,7 @@ class MainViewModel(
|
||||
val newState = HomeState(
|
||||
isVerified = true,
|
||||
lastProofDate = result.verificationId,
|
||||
verifiedClaims = result.claims,
|
||||
verifiedClaims = stringifyClaims(result.claims),
|
||||
)
|
||||
homeState = newState
|
||||
AppStorage.save(HOME_STATE_KEY, Json.encodeToString(newState))
|
||||
@@ -112,7 +146,7 @@ class MainViewModel(
|
||||
HomeState(
|
||||
isVerified = true,
|
||||
lastProofDate = result.verificationId,
|
||||
verifiedClaims = result.claims,
|
||||
verifiedClaims = stringifyClaims(result.claims),
|
||||
)
|
||||
AppStorage.save(HOME_STATE_KEY, Json.encodeToString(homeState))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.self.sdk.webview.SelfVerificationActivity
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
@@ -260,7 +259,7 @@ actual class SelfSdk private constructor(
|
||||
val resultType = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE)
|
||||
if (resultDataJson != null) {
|
||||
try {
|
||||
val result = deserializeResult(resultDataJson)
|
||||
val result = deserializeVerificationResult(resultDataJson)
|
||||
callback.onSuccess(result)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(
|
||||
@@ -271,9 +270,7 @@ actual class SelfSdk private constructor(
|
||||
)
|
||||
}
|
||||
} else if (resultType != null) {
|
||||
callback.onSuccess(
|
||||
VerificationResult(success = true, type = resultType),
|
||||
)
|
||||
callback.onSuccess(VerificationResult(success = true))
|
||||
} else {
|
||||
callback.onFailure(
|
||||
SelfSdkError(
|
||||
@@ -310,17 +307,13 @@ actual class SelfSdk private constructor(
|
||||
/**
|
||||
* Serializes VerificationRequest to JSON string for passing via Intent.
|
||||
*/
|
||||
private fun serializeRequest(request: VerificationRequest): String = Json.encodeToString(VerificationRequest.serializer(), request)
|
||||
private fun serializeRequest(request: VerificationRequest): String =
|
||||
verificationResultJson.encodeToString(VerificationRequest.serializer(), request)
|
||||
|
||||
/**
|
||||
* Serializes SelfSdkConfig to JSON string for passing via Intent.
|
||||
*/
|
||||
private fun serializeConfig(config: SelfSdkConfig): String = Json.encodeToString(SelfSdkConfig.serializer(), config)
|
||||
|
||||
/**
|
||||
* Deserializes VerificationResult from JSON string.
|
||||
*/
|
||||
private fun deserializeResult(json: String): VerificationResult = Json.decodeFromString(VerificationResult.serializer(), json)
|
||||
private fun serializeConfig(config: SelfSdkConfig): String = verificationResultJson.encodeToString(SelfSdkConfig.serializer(), config)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ package xyz.self.sdk.handlers
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.api.serializeVerificationResult
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
@@ -62,34 +62,30 @@ class LifecycleBridgeHandler(
|
||||
* Used to communicate verification results back to the host app.
|
||||
*/
|
||||
private fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
val type = params["type"]?.jsonPrimitive?.content
|
||||
val success = params["success"]?.jsonPrimitive?.content?.toBoolean() ?: false
|
||||
val data = params["data"]?.toString()
|
||||
val errorCode = params["errorCode"]?.jsonPrimitive?.content
|
||||
val errorMessage = params["errorMessage"]?.jsonPrimitive?.content
|
||||
|
||||
activity.runOnUiThread {
|
||||
val intent = Intent()
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE, type)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent)
|
||||
} else if (success && data != null) {
|
||||
// Success result
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_DATA, data)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent)
|
||||
} else if (!success && errorCode != null) {
|
||||
// Error result
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, errorCode)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, errorMessage ?: "Unknown error")
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_ERROR, intent)
|
||||
} else {
|
||||
// Cancelled or invalid result
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED, intent)
|
||||
try {
|
||||
when (val outcome = resolveLifecycleSetResult(params)) {
|
||||
is LifecycleSetResultOutcome.Success -> {
|
||||
intent.putExtra(
|
||||
SelfVerificationActivity.EXTRA_RESULT_DATA,
|
||||
serializeVerificationResult(outcome.result),
|
||||
)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent)
|
||||
}
|
||||
is LifecycleSetResultOutcome.Failure -> {
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, outcome.error.code)
|
||||
intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, outcome.error.message)
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_ERROR, intent)
|
||||
}
|
||||
LifecycleSetResultOutcome.Cancelled -> {
|
||||
activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED, intent)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -83,6 +83,7 @@ class NfcBridgeHandler(
|
||||
}
|
||||
|
||||
private suspend fun scan(params: Map<String, JsonElement>): JsonElement {
|
||||
NfcApduPolicy.requireSupportedParams(params)
|
||||
val scanParams = json.decodeFromJsonElement(NfcScanParams.serializer(), JsonObject(params))
|
||||
|
||||
pushProgress("waiting_for_tag", 0, "Hold your phone near the passport")
|
||||
|
||||
@@ -4,17 +4,32 @@
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VerificationResult(
|
||||
val success: Boolean,
|
||||
val type: String? = null,
|
||||
val userId: String? = null,
|
||||
val verificationId: String? = null,
|
||||
val proof: String? = null,
|
||||
val claims: Map<String, String>? = null,
|
||||
)
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonEncoder
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
|
||||
@Serializable
|
||||
data class SelfSdkError(
|
||||
@@ -22,6 +37,16 @@ data class SelfSdkError(
|
||||
val message: String,
|
||||
)
|
||||
|
||||
@Serializable(with = VerificationResultSerializer::class)
|
||||
data class VerificationResult(
|
||||
val success: Boolean,
|
||||
val userId: String? = null,
|
||||
val verificationId: String? = null,
|
||||
val proof: String? = null,
|
||||
val claims: Map<String, Any?>? = null,
|
||||
val error: SelfSdkError? = null,
|
||||
)
|
||||
|
||||
interface SelfSdkCallback {
|
||||
fun onSuccess(result: VerificationResult)
|
||||
|
||||
@@ -29,3 +54,101 @@ interface SelfSdkCallback {
|
||||
|
||||
fun onCancelled()
|
||||
}
|
||||
|
||||
internal object VerificationResultSerializer : KSerializer<VerificationResult> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
buildClassSerialDescriptor("xyz.self.sdk.api.VerificationResult") {
|
||||
element<Boolean>("success")
|
||||
element<String?>("userId", isOptional = true)
|
||||
element<String?>("verificationId", isOptional = true)
|
||||
element<String?>("proof", isOptional = true)
|
||||
element<Map<String, JsonElement>?>("claims", isOptional = true)
|
||||
element<SelfSdkError?>("error", isOptional = true)
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: VerificationResult,
|
||||
) {
|
||||
require(encoder is JsonEncoder) { "VerificationResultSerializer only supports JSON" }
|
||||
|
||||
val payload =
|
||||
buildJsonObject {
|
||||
put("success", value.success)
|
||||
value.userId?.let { put("userId", it) }
|
||||
value.verificationId?.let { put("verificationId", it) }
|
||||
value.proof?.let { put("proof", it) }
|
||||
value.claims?.let { claims ->
|
||||
putJsonObject("claims") {
|
||||
claims.forEach { (key, claimValue) ->
|
||||
put(key, claimValue.toJsonElement())
|
||||
}
|
||||
}
|
||||
}
|
||||
value.error?.let { error ->
|
||||
put("error", encoder.json.encodeToJsonElement(SelfSdkError.serializer(), error))
|
||||
}
|
||||
}
|
||||
|
||||
encoder.encodeJsonElement(payload)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): VerificationResult {
|
||||
require(decoder is JsonDecoder) { "VerificationResultSerializer only supports JSON" }
|
||||
|
||||
val payload = decoder.decodeJsonElement().jsonObject
|
||||
|
||||
return VerificationResult(
|
||||
success = payload["success"]?.jsonPrimitive?.booleanOrNull ?: false,
|
||||
userId = payload["userId"]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull,
|
||||
verificationId = payload["verificationId"]?.takeUnless { it is JsonNull }?.jsonPrimitive?.contentOrNull,
|
||||
proof = payload["proof"]?.takeUnless { it is JsonNull }?.let(::jsonElementToProofString),
|
||||
claims = payload["claims"]?.takeUnless { it is JsonNull }?.jsonObject?.mapValues { (_, value) -> value.toKotlinValue() },
|
||||
error =
|
||||
payload["error"]?.takeUnless { it is JsonNull }?.let { error ->
|
||||
decoder.json.decodeFromJsonElement(SelfSdkError.serializer(), error)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toJsonElement(): JsonElement =
|
||||
when (this) {
|
||||
null -> JsonNull
|
||||
is JsonElement -> this
|
||||
is String -> JsonPrimitive(this)
|
||||
is Boolean -> JsonPrimitive(this)
|
||||
is Int -> JsonPrimitive(this)
|
||||
is Long -> JsonPrimitive(this)
|
||||
is Float -> JsonPrimitive(this)
|
||||
is Double -> JsonPrimitive(this)
|
||||
is Number -> JsonPrimitive(this.toDouble())
|
||||
is Map<*, *> ->
|
||||
JsonObject(
|
||||
entries
|
||||
.filter { it.key is String }
|
||||
.associate { (key, value) -> key as String to value.toJsonElement() },
|
||||
)
|
||||
is List<*> -> JsonArray(map { it.toJsonElement() })
|
||||
else -> JsonPrimitive(toString())
|
||||
}
|
||||
|
||||
internal fun JsonElement.toKotlinValue(): Any? =
|
||||
when (this) {
|
||||
JsonNull -> null
|
||||
is JsonObject -> entries.associate { (key, value) -> key to value.toKotlinValue() }
|
||||
is JsonArray -> map { it.toKotlinValue() }
|
||||
is JsonPrimitive ->
|
||||
booleanOrNull
|
||||
?: intOrNull
|
||||
?: longOrNull
|
||||
?: doubleOrNull
|
||||
?: contentOrNull
|
||||
}
|
||||
|
||||
private fun jsonElementToProofString(element: JsonElement): String? =
|
||||
when (element) {
|
||||
JsonNull -> null
|
||||
is JsonPrimitive -> element.contentOrNull
|
||||
else -> element.toString()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal val verificationResultJson =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
internal fun serializeVerificationResult(result: VerificationResult): String =
|
||||
verificationResultJson.encodeToString(VerificationResult.serializer(), result)
|
||||
|
||||
internal fun deserializeVerificationResult(json: String): VerificationResult =
|
||||
verificationResultJson.decodeFromString(VerificationResult.serializer(), json)
|
||||
|
||||
internal fun verificationResultFromLifecycleParams(params: Map<String, JsonElement>): VerificationResult =
|
||||
VerificationResult(
|
||||
success = true,
|
||||
userId = runCatching { params["userId"]?.jsonPrimitive?.contentOrNull }.getOrNull(),
|
||||
verificationId = runCatching { params["verificationId"]?.jsonPrimitive?.contentOrNull }.getOrNull(),
|
||||
proof = params["proof"]?.takeUnless { it is JsonNull }?.let(::lifecycleProofString),
|
||||
claims = (params["claims"] as? JsonObject)?.mapValues { (_, value) -> value.toKotlinValue() },
|
||||
)
|
||||
|
||||
private fun lifecycleProofString(element: JsonElement): String? =
|
||||
when (element) {
|
||||
JsonNull -> null
|
||||
is JsonObject -> element.toString()
|
||||
else -> runCatching { element.jsonPrimitive.contentOrNull }.getOrNull() ?: element.toString()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.api.SelfSdkError
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
import xyz.self.sdk.api.deserializeVerificationResult
|
||||
import xyz.self.sdk.api.verificationResultFromLifecycleParams
|
||||
|
||||
internal sealed interface LifecycleSetResultOutcome {
|
||||
data class Success(
|
||||
val result: VerificationResult,
|
||||
) : LifecycleSetResultOutcome
|
||||
|
||||
data class Failure(
|
||||
val error: SelfSdkError,
|
||||
) : LifecycleSetResultOutcome
|
||||
|
||||
data object Cancelled : LifecycleSetResultOutcome
|
||||
}
|
||||
|
||||
internal fun resolveLifecycleSetResult(params: Map<String, JsonElement>): LifecycleSetResultOutcome {
|
||||
val type = params["type"]?.jsonPrimitive?.contentOrNull
|
||||
val successText = params["success"]?.jsonPrimitive?.contentOrNull
|
||||
val success = successText?.toBoolean()
|
||||
val data = params["data"]?.toString()
|
||||
val errorCode = params["errorCode"]?.jsonPrimitive?.contentOrNull
|
||||
val errorMessage = params["errorMessage"]?.jsonPrimitive?.contentOrNull
|
||||
|
||||
if (type != null) {
|
||||
return when {
|
||||
errorCode != null ->
|
||||
LifecycleSetResultOutcome.Failure(
|
||||
SelfSdkError(
|
||||
code = errorCode,
|
||||
message = errorMessage ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
success == false -> LifecycleSetResultOutcome.Cancelled
|
||||
else -> LifecycleSetResultOutcome.Success(verificationResultFromLifecycleParams(params))
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
errorCode != null ->
|
||||
LifecycleSetResultOutcome.Failure(
|
||||
SelfSdkError(
|
||||
code = errorCode,
|
||||
message = errorMessage ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
success == true && data != null ->
|
||||
try {
|
||||
LifecycleSetResultOutcome.Success(deserializeVerificationResult(data))
|
||||
} catch (e: Exception) {
|
||||
LifecycleSetResultOutcome.Failure(
|
||||
SelfSdkError(
|
||||
code = "PARSE_ERROR",
|
||||
message = "Failed to parse verification result: ${e.message}",
|
||||
),
|
||||
)
|
||||
}
|
||||
success == true && (params.containsKey("userId") || params.containsKey("verificationId")) ->
|
||||
LifecycleSetResultOutcome.Success(verificationResultFromLifecycleParams(params))
|
||||
else -> LifecycleSetResultOutcome.Cancelled
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
|
||||
internal object NfcApduPolicy {
|
||||
private const val APDU_COMMANDS_PARAM = "apduCommands"
|
||||
private const val REJECTION_CODE = "NFC_APDU_NOT_ALLOWED"
|
||||
private const val REJECTION_MESSAGE = "Raw APDU commands are not supported by the KMP NFC bridge"
|
||||
|
||||
fun requireSupportedParams(params: Map<String, JsonElement>) {
|
||||
val apduCommands = params[APDU_COMMANDS_PARAM] ?: return
|
||||
if (apduCommands == JsonNull) return
|
||||
if (apduCommands is JsonArray && apduCommands.isEmpty()) return
|
||||
|
||||
throw BridgeHandlerException(REJECTION_CODE, REJECTION_MESSAGE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.api
|
||||
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class VerificationResultJsonTest {
|
||||
@Test
|
||||
fun deserializeVerificationResult_supports_canonical_claim_types() {
|
||||
val result =
|
||||
deserializeVerificationResult(
|
||||
"""
|
||||
{
|
||||
"success": true,
|
||||
"userId": "user-1",
|
||||
"verificationId": "verification-1",
|
||||
"proof": "{\"proof\":\"ok\"}",
|
||||
"claims": {
|
||||
"nationality": "UTO",
|
||||
"ageOver18": true,
|
||||
"score": 42,
|
||||
"document": {
|
||||
"issuingCountry": "UTO"
|
||||
},
|
||||
"disclosures": ["nationality", "age_over_18"]
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
assertEquals("UTO", result.claims?.get("nationality"))
|
||||
assertEquals(true, result.claims?.get("ageOver18"))
|
||||
assertEquals(42, result.claims?.get("score"))
|
||||
assertEquals(
|
||||
mapOf("issuingCountry" to "UTO"),
|
||||
result.claims?.get("document"),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("nationality", "age_over_18"),
|
||||
result.claims?.get("disclosures"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun verificationResultFromLifecycleParams_ignores_legacy_type_field() {
|
||||
val result =
|
||||
verificationResultFromLifecycleParams(
|
||||
buildJsonObject {
|
||||
put("type", "proofRequested")
|
||||
put("userId", "user-1")
|
||||
putJsonObject("claims") {
|
||||
put("ageOver18", JsonPrimitive(true))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assertTrue(result.success)
|
||||
assertEquals("user-1", result.userId)
|
||||
assertEquals(mapOf("ageOver18" to true), result.claims)
|
||||
assertNull(result.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeVerificationResult_roundtrips_nested_claims() {
|
||||
val encoded =
|
||||
serializeVerificationResult(
|
||||
VerificationResult(
|
||||
success = true,
|
||||
claims =
|
||||
mapOf(
|
||||
"document" to mapOf("issuingCountry" to "UTO"),
|
||||
"scores" to listOf(1, 2, 3),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val decoded = deserializeVerificationResult(encoded)
|
||||
assertEquals(
|
||||
mapOf("issuingCountry" to "UTO"),
|
||||
decoded.claims?.get("document"),
|
||||
)
|
||||
assertEquals(listOf(1, 2, 3), decoded.claims?.get("scores"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class LifecycleBridgeHandlerTest {
|
||||
private val json = Json
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_flatPayloadWithTypeAndSuccessRoutesToSuccess() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"type": "verification_result",
|
||||
"success": true,
|
||||
"userId": "user-1",
|
||||
"verificationId": "verification-1",
|
||||
"proof": "proof-1",
|
||||
"claims": {
|
||||
"ageOver18": true
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val success = assertIs<LifecycleSetResultOutcome.Success>(outcome)
|
||||
assertEquals(true, success.result.success)
|
||||
assertEquals("user-1", success.result.userId)
|
||||
assertEquals("verification-1", success.result.verificationId)
|
||||
assertEquals("proof-1", success.result.proof)
|
||||
assertEquals(true, success.result.claims?.get("ageOver18"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_flatPayloadWithTypeAndErrorRoutesToFailure() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"type": "verification_result",
|
||||
"errorCode": "VERIFICATION_FAILED",
|
||||
"errorMessage": "Proof generation failed"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val failure = assertIs<LifecycleSetResultOutcome.Failure>(outcome)
|
||||
assertEquals("VERIFICATION_FAILED", failure.error.code)
|
||||
assertEquals("Proof generation failed", failure.error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_flatPayloadWithTypeAndExplicitFalseRoutesToCancelled() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"type": "verification_result",
|
||||
"success": false
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
assertIs<LifecycleSetResultOutcome.Cancelled>(outcome)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_legacyPayloadWithSuccessDataAndErrorCodeRoutesToFailure() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true,
|
||||
"userId": "user-1",
|
||||
"verificationId": "verification-1",
|
||||
"proof": "proof-1",
|
||||
"claims": {
|
||||
"ageOver18": true
|
||||
}
|
||||
},
|
||||
"errorCode": "VERIFICATION_FAILED",
|
||||
"errorMessage": "Proof generation failed"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val failure = assertIs<LifecycleSetResultOutcome.Failure>(outcome)
|
||||
assertEquals("VERIFICATION_FAILED", failure.error.code)
|
||||
assertEquals("Proof generation failed", failure.error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_flatPayloadWithoutTypeOrDataRoutesToSuccess() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"success": true,
|
||||
"userId": "user-1",
|
||||
"verificationId": "verif-1",
|
||||
"claims": {
|
||||
"resultType": "proofRequested"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
val success = assertIs<LifecycleSetResultOutcome.Success>(outcome)
|
||||
assertEquals(true, success.result.success)
|
||||
assertEquals("user-1", success.result.userId)
|
||||
assertEquals("verif-1", success.result.verificationId)
|
||||
assertEquals("proofRequested", success.result.claims?.get("resultType"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveLifecycleSetResult_bareSuccessTrueWithoutIdentifiersRoutesToCancelled() {
|
||||
val outcome =
|
||||
resolveLifecycleSetResult(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
|
||||
assertIs<LifecycleSetResultOutcome.Cancelled>(outcome)
|
||||
}
|
||||
|
||||
private fun params(rawJson: String) = json.parseToJsonElement(rawJson).jsonObject
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class NfcApduPolicyTest {
|
||||
private val json = Json
|
||||
|
||||
@Test
|
||||
fun requireSupportedParams_allowsStandardPassportScanParams() {
|
||||
NfcApduPolicy.requireSupportedParams(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"passportNumber": "L898902C3",
|
||||
"dateOfBirth": "690806",
|
||||
"dateOfExpiry": "060815",
|
||||
"sessionId": "session-1"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requireSupportedParams_allowsEmptyApduCommandList() {
|
||||
NfcApduPolicy.requireSupportedParams(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"passportNumber": "L898902C3",
|
||||
"dateOfBirth": "690806",
|
||||
"dateOfExpiry": "060815",
|
||||
"sessionId": "session-1",
|
||||
"apduCommands": []
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requireSupportedParams_rejectsNonEmptyApduCommandList() {
|
||||
val error =
|
||||
assertFailsWith<BridgeHandlerException> {
|
||||
NfcApduPolicy.requireSupportedParams(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"passportNumber": "L898902C3",
|
||||
"dateOfBirth": "690806",
|
||||
"dateOfExpiry": "060815",
|
||||
"sessionId": "session-1",
|
||||
"apduCommands": ["00A4040C07A0000002471001"]
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("NFC_APDU_NOT_ALLOWED", error.code)
|
||||
assertEquals("Raw APDU commands are not supported by the KMP NFC bridge", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requireSupportedParams_rejectsMalformedApduCommandParam() {
|
||||
val error =
|
||||
assertFailsWith<BridgeHandlerException> {
|
||||
NfcApduPolicy.requireSupportedParams(
|
||||
params(
|
||||
"""
|
||||
{
|
||||
"passportNumber": "L898902C3",
|
||||
"dateOfBirth": "690806",
|
||||
"dateOfExpiry": "060815",
|
||||
"sessionId": "session-1",
|
||||
"apduCommands": "00A4040C07A0000002471001"
|
||||
}
|
||||
""".trimIndent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("NFC_APDU_NOT_ALLOWED", error.code)
|
||||
}
|
||||
|
||||
private fun params(rawJson: String) = json.parseToJsonElement(rawJson).jsonObject
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package xyz.self.sdk.models
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.self.sdk.api.SelfSdkConfig
|
||||
import xyz.self.sdk.api.SelfSdkError
|
||||
import xyz.self.sdk.api.VerificationRequest
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
import kotlin.test.Test
|
||||
@@ -128,11 +129,16 @@ class ModelSerializationTest {
|
||||
val result =
|
||||
VerificationResult(
|
||||
success = true,
|
||||
type = "proofGenerated",
|
||||
userId = "user-1",
|
||||
verificationId = "verification-123",
|
||||
proof = "proof-bytes",
|
||||
claims = mapOf("nationality" to "UTO"),
|
||||
claims =
|
||||
mapOf(
|
||||
"nationality" to "UTO",
|
||||
"ageOver18" to true,
|
||||
"document" to mapOf("issuingCountry" to "UTO"),
|
||||
),
|
||||
error = SelfSdkError(code = "IGNORED", message = "Only for serialization coverage"),
|
||||
)
|
||||
val encoded = json.encodeToString(result)
|
||||
val decoded = json.decodeFromString<VerificationResult>(encoded)
|
||||
|
||||
@@ -6,12 +6,8 @@ package xyz.self.sdk.handlers
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import xyz.self.sdk.api.SelfSdkCallback
|
||||
import xyz.self.sdk.api.SelfSdkError
|
||||
import xyz.self.sdk.api.VerificationResult
|
||||
import xyz.self.sdk.bridge.BridgeDomain
|
||||
import xyz.self.sdk.bridge.BridgeHandler
|
||||
import xyz.self.sdk.bridge.BridgeHandlerException
|
||||
@@ -75,42 +71,15 @@ class LifecycleBridgeHandler : BridgeHandler {
|
||||
|
||||
private suspend fun setResult(params: Map<String, JsonElement>): JsonElement? {
|
||||
val state = consumeLifecycleState()
|
||||
val type = params["type"]?.jsonPrimitive?.content
|
||||
val success = params["success"]?.jsonPrimitive?.content?.toBoolean() ?: false
|
||||
val data = params["data"]?.toString()
|
||||
val errorCode = params["errorCode"]?.jsonPrimitive?.content
|
||||
val errorMessage = params["errorMessage"]?.jsonPrimitive?.content
|
||||
|
||||
if (type != null) {
|
||||
// Flat lifecycle payload is a protocol-level success signal.
|
||||
// `type` communicates what completed (e.g. proofRequested).
|
||||
state.callback?.onSuccess(
|
||||
VerificationResult(success = true, type = type),
|
||||
)
|
||||
} else if (success && data != null) {
|
||||
try {
|
||||
val result = Json.decodeFromString(VerificationResult.serializer(), data)
|
||||
state.callback?.onSuccess(result)
|
||||
} catch (e: Exception) {
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = "PARSE_ERROR",
|
||||
message = "Failed to parse verification result: ${e.message}",
|
||||
),
|
||||
)
|
||||
try {
|
||||
when (val outcome = resolveLifecycleSetResult(params)) {
|
||||
is LifecycleSetResultOutcome.Success -> state.callback?.onSuccess(outcome.result)
|
||||
is LifecycleSetResultOutcome.Failure -> state.callback?.onFailure(outcome.error)
|
||||
LifecycleSetResultOutcome.Cancelled -> state.callback?.onCancelled()
|
||||
}
|
||||
} else if (!success && errorCode != null) {
|
||||
state.callback?.onFailure(
|
||||
SelfSdkError(
|
||||
code = errorCode,
|
||||
message = errorMessage ?: "Unknown error",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
state.callback?.onCancelled()
|
||||
} finally {
|
||||
state.dismiss?.invoke()
|
||||
}
|
||||
|
||||
state.dismiss?.invoke()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ class NfcBridgeHandler(
|
||||
}
|
||||
|
||||
private suspend fun scan(params: Map<String, JsonElement>): JsonElement {
|
||||
NfcApduPolicy.requireSupportedParams(params)
|
||||
val provider =
|
||||
SdkProviderRegistry.nfc
|
||||
?: throw BridgeHandlerException("NOT_CONFIGURED", "NFC provider not configured")
|
||||
|
||||
@@ -15,28 +15,6 @@ import React
|
||||
#if !E2E_TESTING
|
||||
import NFCPassportReader
|
||||
#endif
|
||||
import Security
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
extension CertificateType {
|
||||
func stringValue() -> String {
|
||||
switch self {
|
||||
case .documentSigningCertificate:
|
||||
return "documentSigningCertificate"
|
||||
case .issuerSigningCertificate:
|
||||
return "issuerSigningCertificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper function to map the keys of a dictionary
|
||||
extension Dictionary {
|
||||
func mapKeys<T: Hashable>(_ transform: (Key) -> T) -> Dictionary<T, Value> {
|
||||
Dictionary<T, Value>(uniqueKeysWithValues: map { (transform($0.key), $0.value) })
|
||||
}
|
||||
}
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 15, *)
|
||||
@@ -54,376 +32,42 @@ class PassportReader: NSObject {
|
||||
self.passportReader = NFCPassportReader.PassportReader()
|
||||
}
|
||||
|
||||
func getMRZKey(passportNumber: String, dateOfBirth: String, dateOfExpiry: String ) -> String {
|
||||
|
||||
// Pad fields if necessary
|
||||
let pptNr = pad( passportNumber, fieldLength:9)
|
||||
let dob = pad( dateOfBirth, fieldLength:6)
|
||||
let exp = pad( dateOfExpiry, fieldLength:6)
|
||||
|
||||
// Calculate checksums
|
||||
let passportNrChksum = calcCheckSum(pptNr)
|
||||
let dateOfBirthChksum = calcCheckSum(dob)
|
||||
let expiryDateChksum = calcCheckSum(exp)
|
||||
|
||||
let mrzKey = "\(pptNr)\(passportNrChksum)\(dob)\(dateOfBirthChksum)\(exp)\(expiryDateChksum)"
|
||||
|
||||
return mrzKey
|
||||
}
|
||||
|
||||
func pad( _ value : String, fieldLength:Int ) -> String {
|
||||
// Pad out field lengths with < if they are too short
|
||||
let paddedValue = (value + String(repeating: "<", count: fieldLength)).prefix(fieldLength)
|
||||
return String(paddedValue)
|
||||
}
|
||||
|
||||
func calcCheckSum( _ checkString : String ) -> Int {
|
||||
let characterDict = ["0" : "0", "1" : "1", "2" : "2", "3" : "3", "4" : "4", "5" : "5", "6" : "6", "7" : "7", "8" : "8", "9" : "9", "<" : "0", " " : "0", "A" : "10", "B" : "11", "C" : "12", "D" : "13", "E" : "14", "F" : "15", "G" : "16", "H" : "17", "I" : "18", "J" : "19", "K" : "20", "L" : "21", "M" : "22", "N" : "23", "O" : "24", "P" : "25", "Q" : "26", "R" : "27", "S" : "28","T" : "29", "U" : "30", "V" : "31", "W" : "32", "X" : "33", "Y" : "34", "Z" : "35"]
|
||||
|
||||
var sum = 0
|
||||
var m = 0
|
||||
let multipliers : [Int] = [7, 3, 1]
|
||||
for c in checkString {
|
||||
guard let lookup = characterDict["\(c)"],
|
||||
let number = Int(lookup) else { return 0 }
|
||||
let product = number * multipliers[m]
|
||||
sum += product
|
||||
m = (m+1) % 3
|
||||
}
|
||||
|
||||
return (sum % 10)
|
||||
}
|
||||
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:)
|
||||
func scanPassport(
|
||||
_ passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: NSNumber,
|
||||
skipPACE: NSNumber,
|
||||
skipCA: NSNumber,
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
let useCANBool = useCan.boolValue
|
||||
let skipPACEBool = skipPACE.boolValue
|
||||
let skipCABool = skipCA.boolValue
|
||||
let extendedModeBool = extendedMode.boolValue
|
||||
let usePacePollingBool = usePacePolling.boolValue
|
||||
|
||||
let customMessageHandler : (NFCViewDisplayMessage)->String? = { (displayMessage) in
|
||||
switch displayMessage {
|
||||
case .requestPresentPassport:
|
||||
return "Hold your iPhone against an NFC enabled passport."
|
||||
default:
|
||||
// Return nil for all other messages so we use the provided default
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Task { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let password: String
|
||||
var passwordType:PACEPasswordType
|
||||
if useCANBool {
|
||||
if canNumber.count != 6 {
|
||||
reject("E_PASSPORT_READ", "CAN number must be 6 digits", nil)
|
||||
return
|
||||
}
|
||||
password = canNumber
|
||||
passwordType = PACEPasswordType.can
|
||||
} else {
|
||||
password = getMRZKey( passportNumber: passportNumber, dateOfBirth: dateOfBirth, dateOfExpiry: dateOfExpiry)
|
||||
passwordType = PACEPasswordType.mrz
|
||||
}
|
||||
// let masterListURL = Bundle.main.url(forResource: "masterList", withExtension: ".pem")
|
||||
// passportReader.setMasterListURL( masterListURL! )
|
||||
|
||||
let passport = try await self.passportReader.readPassport(
|
||||
password: password,
|
||||
type: passwordType,
|
||||
tags: [.COM, .DG1, .SOD],
|
||||
skipCA: skipCABool,
|
||||
skipPACE: skipPACEBool,
|
||||
useExtendedMode: extendedModeBool,
|
||||
customDisplayMessage: customMessageHandler
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:)
|
||||
func scanPassport(
|
||||
_ passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: NSNumber,
|
||||
skipPACE: NSNumber,
|
||||
skipCA: NSNumber,
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
PassportReaderCore.scanPassport(
|
||||
reader: passportReader,
|
||||
passportNumber: passportNumber,
|
||||
dateOfBirth: dateOfBirth,
|
||||
dateOfExpiry: dateOfExpiry,
|
||||
canNumber: canNumber,
|
||||
useCan: useCan.boolValue,
|
||||
skipPACE: skipPACE.boolValue,
|
||||
skipCA: skipCA.boolValue,
|
||||
extendedMode: extendedMode.boolValue,
|
||||
usePacePolling: usePacePolling.boolValue,
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
)
|
||||
|
||||
var ret = [String:String]()
|
||||
//print("documentType", passport.documentType)
|
||||
|
||||
ret["documentType"] = passport.documentType
|
||||
ret["documentSubType"] = passport.documentSubType
|
||||
ret["documentNumber"] = passport.documentNumber
|
||||
ret["issuingAuthority"] = passport.issuingAuthority
|
||||
ret["documentExpiryDate"] = passport.documentExpiryDate
|
||||
ret["dateOfBirth"] = passport.dateOfBirth
|
||||
ret["gender"] = passport.gender
|
||||
ret["nationality"] = passport.nationality
|
||||
ret["lastName"] = passport.lastName
|
||||
ret["firstName"] = passport.firstName
|
||||
ret["passportMRZ"] = passport.passportMRZ
|
||||
ret["placeOfBirth"] = passport.placeOfBirth
|
||||
ret["residenceAddress"] = passport.residenceAddress
|
||||
ret["phoneNumber"] = passport.phoneNumber
|
||||
ret["personalNumber"] = passport.personalNumber
|
||||
|
||||
// let passportPhotoData = passport.passportPhoto // [UInt8]
|
||||
// if let passportPhotoData = passport.passportPhoto {
|
||||
// let data = Data(passportPhotoData)
|
||||
// let base64String = data.base64EncodedString()
|
||||
|
||||
// ret["passportPhoto"] = base64String
|
||||
// }
|
||||
|
||||
// documentSigningCertificate
|
||||
// countrySigningCertificate
|
||||
|
||||
if let serializedDocumentSigningCertificate = serializeX509Wrapper(passport.documentSigningCertificate) {
|
||||
ret["documentSigningCertificate"] = serializedDocumentSigningCertificate
|
||||
}
|
||||
|
||||
if let serializedCountrySigningCertificate = serializeX509Wrapper(passport.countrySigningCertificate) {
|
||||
ret["countrySigningCertificate"] = serializedCountrySigningCertificate
|
||||
}
|
||||
//print("passport.documentSigningCertificate", passport.documentSigningCertificate)
|
||||
//print("passport.countrySigningCertificate", passport.countrySigningCertificate)
|
||||
|
||||
ret["LDSVersion"] = passport.LDSVersion
|
||||
ret["dataGroupsPresent"] = passport.dataGroupsPresent.joined(separator: ", ")
|
||||
|
||||
//print("passport.LDSVersion", passport.LDSVersion)
|
||||
|
||||
// ret["dataGroupsAvailable"] = passport.dataGroupsAvailable.map(dataGroupIdToString)
|
||||
|
||||
//print("passport.dataGroupsAvailable", passport.dataGroupsAvailable)
|
||||
//print("passport.dataGroupsRead", passport.dataGroupsRead)
|
||||
//print("passport.dataGroupHashes", passport.dataGroupHashes)
|
||||
|
||||
// do {
|
||||
// let dataGroupsReadData = try JSONSerialization.data(withJSONObject: passport.dataGroupsRead.mapValues { self.convertDataGroupToSerializableFormat($0) }, options: [])
|
||||
// let dataGroupsReadJsonString = String(data: dataGroupsReadData, encoding: .utf8) ?? ""
|
||||
// ret["dataGroupsRead"] = dataGroupsReadJsonString
|
||||
// } catch {
|
||||
// //print("Error serializing dataGroupsRead: \(error)")
|
||||
// }
|
||||
|
||||
// ret["dataGroupsRead"] = passport.dataGroupsRead.mapValues { convertDataGroupToSerializableFormat($0) }
|
||||
do {
|
||||
let dataGroupHashesDict = passport.dataGroupHashes.mapKeys { "\($0)" }
|
||||
let serializableDataGroupHashes = dataGroupHashesDict.mapValues { convertDataGroupHashToSerializableFormat($0) }
|
||||
let dataGroupHashesData = try JSONSerialization.data(withJSONObject: serializableDataGroupHashes, options: [])
|
||||
let dataGroupHashesJsonString = String(data: dataGroupHashesData, encoding: .utf8) ?? ""
|
||||
ret["dataGroupHashes"] = dataGroupHashesJsonString
|
||||
} catch {
|
||||
//print("Error serializing dataGroupHashes: \(error)")
|
||||
}
|
||||
|
||||
|
||||
// cardAccess
|
||||
// BACStatus
|
||||
// PACEStatus
|
||||
// chipAuthenticationStatus
|
||||
ret["passportCorrectlySigned"] = String(passport.passportCorrectlySigned)
|
||||
ret["documentSigningCertificateVerified"] = String(passport.documentSigningCertificateVerified)
|
||||
ret["passportDataNotTampered"] = String(passport.passportDataNotTampered)
|
||||
ret["activeAuthenticationPassed"] = String(passport.activeAuthenticationPassed)
|
||||
ret["activeAuthenticationChallenge"] = encodeByteArrayToHexString(passport.activeAuthenticationChallenge)
|
||||
ret["activeAuthenticationSignature"] = encodeByteArrayToHexString(passport.activeAuthenticationSignature)
|
||||
ret["verificationErrors"] = encodeErrors(passport.verificationErrors).joined(separator: ", ")
|
||||
|
||||
ret["isPACESupported"] = String(passport.isPACESupported)
|
||||
ret["isChipAuthenticationSupported"] = String(passport.isChipAuthenticationSupported)
|
||||
|
||||
// passportImage
|
||||
// signatureImage
|
||||
|
||||
// activeAuthenticationSupported
|
||||
|
||||
//print("passport.certificateSigningGroups", passport.certificateSigningGroups)
|
||||
|
||||
// ret["certificateSigningGroups"] = passport.certificateSigningGroups.mapKeys(certificateTypeToString).mapValues(encodeX509WrapperToJsonString)
|
||||
// if let passportDataElements = passport.passportDataElements {
|
||||
// ret["passportDataElements"] = passportDataElements
|
||||
// } else {
|
||||
// ret["passportDataElements"] = [:]
|
||||
// }
|
||||
|
||||
do {
|
||||
// although this line won't be reached if there is an error, Its better to handle it here instead of crashing the app
|
||||
if let sod = try passport.getDataGroup(DataGroupId.SOD) as? SOD {
|
||||
// ret["concatenatedDataHashes"] = try sod.getEncapsulatedContent().base64EncodedString() // this is what we call concatenatedDataHashes, not the true eContent
|
||||
ret["eContentBase64"] = try sod.getEncapsulatedContent().base64EncodedString() // this is what we call concatenatedDataHashes, not the true eContent
|
||||
|
||||
ret["signatureAlgorithm"] = try sod.getSignatureAlgorithm()
|
||||
ret["encapsulatedContentDigestAlgorithm"] = try sod.getEncapsulatedContentDigestAlgorithm()
|
||||
|
||||
let messageDigestFromSignedAttributes = try sod.getMessageDigestFromSignedAttributes()
|
||||
let signedAttributes = try sod.getSignedAttributes()
|
||||
//print("messageDigestFromSignedAttributes", messageDigestFromSignedAttributes)
|
||||
|
||||
ret["signedAttributes"] = signedAttributes.base64EncodedString()
|
||||
// if let pubKey = convertOpaquePointerToSecKey(opaquePointer: sod.pubKey),
|
||||
// let serializedPublicKey = serializePublicKey(pubKey) {
|
||||
// ret["publicKeyBase64"] = serializedPublicKey
|
||||
// } else {
|
||||
// // Handle the case where pubKey is nil
|
||||
// }
|
||||
|
||||
if let serializedSignature = serializeSignature(from: sod) {
|
||||
ret["signatureBase64"] = serializedSignature
|
||||
}
|
||||
} else {
|
||||
print("SOD not found or could not be cast to SOD")
|
||||
reject("E_PASSPORT_READ", "SODNotFound : SOD not found or could not be cast to SOD", nil)
|
||||
return
|
||||
}
|
||||
|
||||
} catch {
|
||||
//print("Error serializing SOD data: \(error)")
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
|
||||
let stringified = String(data: try JSONEncoder().encode(ret), encoding: .utf8)
|
||||
|
||||
resolve(stringified)
|
||||
} catch {
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mrz ✅
|
||||
// dataHashes ✅
|
||||
// eContentBytes ✅
|
||||
// pubkey
|
||||
// signature ✅
|
||||
|
||||
// func convertOpaquePointerToSecKey(opaquePointer: OpaquePointer?) -> SecKey? {
|
||||
// guard let opaquePointer = opaquePointer else { return nil }
|
||||
|
||||
// // Assuming the key is in DER format
|
||||
// // Replace with actual code to convert OpaquePointer to Data
|
||||
// let keyData = Data(bytes: opaquePointer, count: keyLength) // Replace `keyLength` with actual length of key data
|
||||
|
||||
// let attributes: [String: Any] = [
|
||||
// kSecAttrKeyType as String: kSecAttrKeyTypeRSA, // or kSecAttrKeyTypeECSECPrimeRandom for ECDSA
|
||||
// kSecAttrKeyClass as String: kSecAttrKeyClassPublic
|
||||
// ]
|
||||
|
||||
// var error: Unmanaged<CFError>?
|
||||
// let secKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error)
|
||||
|
||||
// if let error = error {
|
||||
// //print("Error creating SecKey: \(error.takeRetainedValue())")
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// return secKey
|
||||
// }
|
||||
|
||||
func serializePublicKey(_ publicKey: SecKey) -> String? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
||||
//print("Error serializing public key: \(error!.takeRetainedValue() as Error)")
|
||||
return nil
|
||||
}
|
||||
return publicKeyData.base64EncodedString()
|
||||
}
|
||||
|
||||
func serializeSignature(from sod: SOD) -> String? {
|
||||
do {
|
||||
let signature = try sod.getSignature()
|
||||
return signature.base64EncodedString()
|
||||
} catch {
|
||||
//print("Error extracting signature: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func serializeX509Wrapper(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate = certificate else { return nil }
|
||||
|
||||
let itemsDict = certificate.getItemsAsDict()
|
||||
var certInfoStringKeys = [String: String]()
|
||||
|
||||
// Convert CertificateItem keys to String keys
|
||||
for (key, value) in itemsDict {
|
||||
certInfoStringKeys[key.rawValue] = value
|
||||
}
|
||||
|
||||
// Add PEM representation
|
||||
let certPEM = certificate.certToPEM()
|
||||
certInfoStringKeys["PEM"] = certPEM
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certInfoStringKeys, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
//print("Error serializing X509Wrapper: \(error)")
|
||||
return nil
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
func encodeX509WrapperToJsonString(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate = certificate else { return nil }
|
||||
let certificateItems = certificate.getItemsAsDict()
|
||||
|
||||
// Convert certificate items to JSON
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certificateItems, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
//print("Error serializing certificate items to JSON: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func encodeByteArrayToHexString(_ byteArray: [UInt8]) -> String {
|
||||
return byteArray.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
func encodeErrors(_ errors: [Error]) -> [String] {
|
||||
return errors.map { $0.localizedDescription }
|
||||
}
|
||||
|
||||
func convertDataGroupHashToSerializableFormat(_ dataGroupHash: DataGroupHash) -> [String: Any] {
|
||||
return [
|
||||
"id": dataGroupHash.id,
|
||||
"sodHash": dataGroupHash.sodHash,
|
||||
"computedHash": dataGroupHash.computedHash,
|
||||
"match": dataGroupHash.match
|
||||
]
|
||||
}
|
||||
|
||||
func dataGroupIdToString(_ id: DataGroupId) -> String {
|
||||
return String(id.rawValue) // or any other method to get a string representation
|
||||
}
|
||||
|
||||
func certificateTypeToString(_ type: CertificateType) -> String {
|
||||
return type.stringValue()
|
||||
}
|
||||
|
||||
func convertDataGroupToSerializableFormat(_ dataGroup: DataGroup) -> [String: Any] {
|
||||
return [
|
||||
"datagroupType": dataGroupIdToString(dataGroup.datagroupType),
|
||||
"body": encodeByteArrayToHexString(dataGroup.body),
|
||||
"data": encodeByteArrayToHexString(dataGroup.data)
|
||||
]
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
#else
|
||||
// E2E Testing stub implementation
|
||||
@available(iOS 15, *)
|
||||
@objc(SelfPassportReader)
|
||||
class PassportReader: NSObject {
|
||||
@@ -433,7 +77,6 @@ class PassportReader: NSObject {
|
||||
|
||||
@objc(configure:enableDebugLogs:)
|
||||
func configure(token: String, enableDebugLogs: Bool) {
|
||||
// No-op for E2E testing
|
||||
}
|
||||
|
||||
@objc(scanPassport:dateOfBirth:dateOfExpiry:canNumber:useCan:skipPACE:skipCA:extendedMode:usePacePolling:resolve:reject:)
|
||||
@@ -447,13 +90,15 @@ class PassportReader: NSObject {
|
||||
skipCA: NSNumber,
|
||||
extendedMode: NSNumber,
|
||||
usePacePolling: NSNumber,
|
||||
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
reject("E2E_TESTING", "NFC scanning not available in E2E testing mode", nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
304
packages/mobile-sdk-alpha/ios/SelfSDK/PassportReaderCore.swift
Normal file
304
packages/mobile-sdk-alpha/ios/SelfSDK/PassportReaderCore.swift
Normal file
@@ -0,0 +1,304 @@
|
||||
// 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 Foundation
|
||||
import React
|
||||
#if !E2E_TESTING
|
||||
import NFCPassportReader
|
||||
import Security
|
||||
|
||||
@available(iOS 13, macOS 10.15, *)
|
||||
extension CertificateType {
|
||||
func stringValue() -> String {
|
||||
switch self {
|
||||
case .documentSigningCertificate:
|
||||
return "documentSigningCertificate"
|
||||
case .issuerSigningCertificate:
|
||||
return "issuerSigningCertificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Dictionary {
|
||||
func mapKeys<T: Hashable>(_ transform: (Key) -> T) -> Dictionary<T, Value> {
|
||||
Dictionary<T, Value>(uniqueKeysWithValues: map { (transform($0.key), $0.value) })
|
||||
}
|
||||
}
|
||||
|
||||
#if !E2E_TESTING
|
||||
@available(iOS 15, *)
|
||||
enum PassportReaderCore {
|
||||
static func getMRZKey(passportNumber: String, dateOfBirth: String, dateOfExpiry: String) -> String {
|
||||
let pptNr = pad(passportNumber, fieldLength: 9)
|
||||
let dob = pad(dateOfBirth, fieldLength: 6)
|
||||
let exp = pad(dateOfExpiry, fieldLength: 6)
|
||||
|
||||
let passportNrChksum = calcCheckSum(pptNr)
|
||||
let dateOfBirthChksum = calcCheckSum(dob)
|
||||
let expiryDateChksum = calcCheckSum(exp)
|
||||
|
||||
return "\(pptNr)\(passportNrChksum)\(dob)\(dateOfBirthChksum)\(exp)\(expiryDateChksum)"
|
||||
}
|
||||
|
||||
static func pad(_ value: String, fieldLength: Int) -> String {
|
||||
let paddedValue = (value + String(repeating: "<", count: fieldLength)).prefix(fieldLength)
|
||||
return String(paddedValue)
|
||||
}
|
||||
|
||||
static func calcCheckSum(_ checkString: String) -> Int {
|
||||
let characterDict = [
|
||||
"0": "0", "1": "1", "2": "2", "3": "3", "4": "4",
|
||||
"5": "5", "6": "6", "7": "7", "8": "8", "9": "9",
|
||||
"<": "0", " ": "0",
|
||||
"A": "10", "B": "11", "C": "12", "D": "13", "E": "14",
|
||||
"F": "15", "G": "16", "H": "17", "I": "18", "J": "19",
|
||||
"K": "20", "L": "21", "M": "22", "N": "23", "O": "24",
|
||||
"P": "25", "Q": "26", "R": "27", "S": "28", "T": "29",
|
||||
"U": "30", "V": "31", "W": "32", "X": "33", "Y": "34",
|
||||
"Z": "35",
|
||||
]
|
||||
|
||||
var sum = 0
|
||||
var m = 0
|
||||
let multipliers: [Int] = [7, 3, 1]
|
||||
|
||||
for c in checkString {
|
||||
guard let lookup = characterDict["\(c)"], let number = Int(lookup) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
sum += number * multipliers[m]
|
||||
m = (m + 1) % 3
|
||||
}
|
||||
|
||||
return sum % 10
|
||||
}
|
||||
|
||||
static func scanPassport(
|
||||
reader: NFCPassportReader.PassportReader,
|
||||
passportNumber: String,
|
||||
dateOfBirth: String,
|
||||
dateOfExpiry: String,
|
||||
canNumber: String,
|
||||
useCan: Bool,
|
||||
skipPACE: Bool,
|
||||
skipCA: Bool,
|
||||
extendedMode: Bool,
|
||||
usePacePolling: Bool,
|
||||
onStart: (() -> Void)? = nil,
|
||||
onSuccess: (() -> Void)? = nil,
|
||||
onFailure: ((Error) -> Void)? = nil,
|
||||
resolve: @escaping RCTPromiseResolveBlock,
|
||||
reject: @escaping RCTPromiseRejectBlock
|
||||
) {
|
||||
let customMessageHandler: (NFCViewDisplayMessage) -> String? = { displayMessage in
|
||||
switch displayMessage {
|
||||
case .requestPresentPassport:
|
||||
return "Hold your iPhone against an NFC enabled passport."
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
onStart?()
|
||||
|
||||
Task {
|
||||
do {
|
||||
let password: String
|
||||
let passwordType: PACEPasswordType
|
||||
|
||||
if useCan {
|
||||
if canNumber.count != 6 {
|
||||
reject("E_PASSPORT_READ", "CAN number must be 6 digits", nil)
|
||||
return
|
||||
}
|
||||
|
||||
password = canNumber
|
||||
passwordType = .can
|
||||
} else {
|
||||
password = getMRZKey(
|
||||
passportNumber: passportNumber,
|
||||
dateOfBirth: dateOfBirth,
|
||||
dateOfExpiry: dateOfExpiry
|
||||
)
|
||||
passwordType = .mrz
|
||||
}
|
||||
|
||||
let passport = try await reader.readPassport(
|
||||
password: password,
|
||||
type: passwordType,
|
||||
tags: [.COM, .DG1, .SOD],
|
||||
skipCA: skipCA,
|
||||
skipPACE: skipPACE,
|
||||
useExtendedMode: extendedMode,
|
||||
usePacePolling: usePacePolling,
|
||||
customDisplayMessage: customMessageHandler
|
||||
)
|
||||
|
||||
var ret = [String: String]()
|
||||
ret["documentType"] = passport.documentType
|
||||
ret["documentSubType"] = passport.documentSubType
|
||||
ret["documentNumber"] = passport.documentNumber
|
||||
ret["issuingAuthority"] = passport.issuingAuthority
|
||||
ret["documentExpiryDate"] = passport.documentExpiryDate
|
||||
ret["dateOfBirth"] = passport.dateOfBirth
|
||||
ret["gender"] = passport.gender
|
||||
ret["nationality"] = passport.nationality
|
||||
ret["lastName"] = passport.lastName
|
||||
ret["firstName"] = passport.firstName
|
||||
ret["passportMRZ"] = passport.passportMRZ
|
||||
ret["placeOfBirth"] = passport.placeOfBirth
|
||||
ret["residenceAddress"] = passport.residenceAddress
|
||||
ret["phoneNumber"] = passport.phoneNumber
|
||||
ret["personalNumber"] = passport.personalNumber
|
||||
|
||||
if let serializedDocumentSigningCertificate = serializeX509Wrapper(passport.documentSigningCertificate) {
|
||||
ret["documentSigningCertificate"] = serializedDocumentSigningCertificate
|
||||
}
|
||||
|
||||
if let serializedCountrySigningCertificate = serializeX509Wrapper(passport.countrySigningCertificate) {
|
||||
ret["countrySigningCertificate"] = serializedCountrySigningCertificate
|
||||
}
|
||||
|
||||
ret["LDSVersion"] = passport.LDSVersion
|
||||
ret["dataGroupsPresent"] = passport.dataGroupsPresent.joined(separator: ", ")
|
||||
|
||||
do {
|
||||
let dataGroupHashesDict = passport.dataGroupHashes.mapKeys { "\($0)" }
|
||||
let serializableDataGroupHashes = dataGroupHashesDict.mapValues { convertDataGroupHashToSerializableFormat($0) }
|
||||
let dataGroupHashesData = try JSONSerialization.data(withJSONObject: serializableDataGroupHashes, options: [])
|
||||
let dataGroupHashesJsonString = String(data: dataGroupHashesData, encoding: .utf8) ?? ""
|
||||
ret["dataGroupHashes"] = dataGroupHashesJsonString
|
||||
} catch {
|
||||
}
|
||||
|
||||
ret["passportCorrectlySigned"] = String(passport.passportCorrectlySigned)
|
||||
ret["documentSigningCertificateVerified"] = String(passport.documentSigningCertificateVerified)
|
||||
ret["passportDataNotTampered"] = String(passport.passportDataNotTampered)
|
||||
ret["activeAuthenticationPassed"] = String(passport.activeAuthenticationPassed)
|
||||
ret["activeAuthenticationChallenge"] = encodeByteArrayToHexString(passport.activeAuthenticationChallenge)
|
||||
ret["activeAuthenticationSignature"] = encodeByteArrayToHexString(passport.activeAuthenticationSignature)
|
||||
ret["verificationErrors"] = encodeErrors(passport.verificationErrors).joined(separator: ", ")
|
||||
ret["isPACESupported"] = String(passport.isPACESupported)
|
||||
ret["isChipAuthenticationSupported"] = String(passport.isChipAuthenticationSupported)
|
||||
|
||||
do {
|
||||
if let sod = try passport.getDataGroup(DataGroupId.SOD) as? SOD {
|
||||
ret["eContentBase64"] = try sod.getEncapsulatedContent().base64EncodedString()
|
||||
ret["signatureAlgorithm"] = try sod.getSignatureAlgorithm()
|
||||
ret["encapsulatedContentDigestAlgorithm"] = try sod.getEncapsulatedContentDigestAlgorithm()
|
||||
_ = try sod.getMessageDigestFromSignedAttributes()
|
||||
let signedAttributes = try sod.getSignedAttributes()
|
||||
ret["signedAttributes"] = signedAttributes.base64EncodedString()
|
||||
|
||||
if let serializedSignature = serializeSignature(from: sod) {
|
||||
ret["signatureBase64"] = serializedSignature
|
||||
}
|
||||
} else {
|
||||
print("SOD not found or could not be cast to SOD")
|
||||
reject("E_PASSPORT_READ", "SODNotFound : SOD not found or could not be cast to SOD", nil)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
return
|
||||
}
|
||||
|
||||
let stringified = String(data: try JSONEncoder().encode(ret), encoding: .utf8)
|
||||
onSuccess?()
|
||||
resolve(stringified)
|
||||
} catch {
|
||||
onFailure?(error)
|
||||
reject("E_PASSPORT_READ", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func serializePublicKey(_ publicKey: SecKey) -> String? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
|
||||
return nil
|
||||
}
|
||||
return publicKeyData.base64EncodedString()
|
||||
}
|
||||
|
||||
static func serializeSignature(from sod: SOD) -> String? {
|
||||
do {
|
||||
let signature = try sod.getSignature()
|
||||
return signature.base64EncodedString()
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func serializeX509Wrapper(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let itemsDict = certificate.getItemsAsDict()
|
||||
var certInfoStringKeys = [String: String]()
|
||||
|
||||
for (key, value) in itemsDict {
|
||||
certInfoStringKeys[key.rawValue] = value
|
||||
}
|
||||
|
||||
certInfoStringKeys["PEM"] = certificate.certToPEM()
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certInfoStringKeys, options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeX509WrapperToJsonString(_ certificate: X509Wrapper?) -> String? {
|
||||
guard let certificate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: certificate.getItemsAsDict(), options: [])
|
||||
return String(data: jsonData, encoding: .utf8)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func encodeByteArrayToHexString(_ byteArray: [UInt8]) -> String {
|
||||
byteArray.map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
static func encodeErrors(_ errors: [Error]) -> [String] {
|
||||
errors.map { $0.localizedDescription }
|
||||
}
|
||||
|
||||
static func convertDataGroupHashToSerializableFormat(_ dataGroupHash: DataGroupHash) -> [String: Any] {
|
||||
[
|
||||
"id": dataGroupHash.id,
|
||||
"sodHash": dataGroupHash.sodHash,
|
||||
"computedHash": dataGroupHash.computedHash,
|
||||
"match": dataGroupHash.match,
|
||||
]
|
||||
}
|
||||
|
||||
static func dataGroupIdToString(_ id: DataGroupId) -> String {
|
||||
String(id.rawValue)
|
||||
}
|
||||
|
||||
static func certificateTypeToString(_ type: CertificateType) -> String {
|
||||
type.stringValue()
|
||||
}
|
||||
|
||||
static func convertDataGroupToSerializableFormat(_ dataGroup: DataGroup) -> [String: Any] {
|
||||
[
|
||||
"datagroupType": dataGroupIdToString(dataGroup.datagroupType),
|
||||
"body": encodeByteArrayToHexString(dataGroup.body),
|
||||
"data": encodeByteArrayToHexString(dataGroup.data),
|
||||
]
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -10,6 +10,7 @@
|
||||
91182B0A9E07E7852A7606A7 /* Pods_SelfSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9361AEE8EFAF28843D411F75 /* Pods_SelfSDK.framework */; };
|
||||
BF7273CE2E53412C002FE485 /* PassportReader.m in Sources */ = {isa = PBXBuildFile; fileRef = BF7273CC2E53412C002FE485 /* PassportReader.m */; };
|
||||
BF7273CF2E53412C002FE485 /* PassportReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7273CD2E53412C002FE485 /* PassportReader.swift */; };
|
||||
BF7273D12F00000002FE485 /* PassportReaderCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7273D02F00000002FE485 /* PassportReaderCore.swift */; };
|
||||
BFB0C0F02EA8B57000DBA670 /* NFCPassportReader 2.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFB0C0EF2EA8B57000DBA670 /* NFCPassportReader 2.xcframework */; };
|
||||
BFE3DFC62E4F4E7300195298 /* QKMRZParser in Frameworks */ = {isa = PBXBuildFile; productRef = BFE3DFC52E4F4E7300195298 /* QKMRZParser */; };
|
||||
BFE3DFC92E4F4E7A00195298 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFE3DFC82E4F4E7A00195298 /* AVFoundation.framework */; };
|
||||
@@ -31,6 +32,7 @@
|
||||
9361AEE8EFAF28843D411F75 /* Pods_SelfSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SelfSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF7273CC2E53412C002FE485 /* PassportReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PassportReader.m; sourceTree = "<group>"; };
|
||||
BF7273CD2E53412C002FE485 /* PassportReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportReader.swift; sourceTree = "<group>"; };
|
||||
BF7273D02F00000002FE485 /* PassportReaderCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportReaderCore.swift; sourceTree = "<group>"; };
|
||||
BFB0C0EF2EA8B57000DBA670 /* NFCPassportReader 2.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "NFCPassportReader 2.xcframework"; sourceTree = "<group>"; };
|
||||
BFE3DFB82E4F4E5C00195298 /* SelfSDK.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SelfSDK.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFE3DFC82E4F4E7A00195298 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
@@ -77,6 +79,7 @@
|
||||
BFB0C0EF2EA8B57000DBA670 /* NFCPassportReader 2.xcframework */,
|
||||
BF7273CC2E53412C002FE485 /* PassportReader.m */,
|
||||
BF7273CD2E53412C002FE485 /* PassportReader.swift */,
|
||||
BF7273D02F00000002FE485 /* PassportReaderCore.swift */,
|
||||
BFE3DFCC2E4F4EC400195298 /* SelfCameraView.swift */,
|
||||
BFE3DFCD2E4F4EC400195298 /* SelfLiveMRZScannerView.swift */,
|
||||
BFE3DFCE2E4F4EC400195298 /* MrzScanEngine.swift */,
|
||||
@@ -244,6 +247,7 @@
|
||||
BFE3DFD82E4F4EC400195298 /* SelfLiveMRZScannerView.swift in Sources */,
|
||||
BF7273CE2E53412C002FE485 /* PassportReader.m in Sources */,
|
||||
BF7273CF2E53412C002FE485 /* PassportReader.swift in Sources */,
|
||||
BF7273D12F00000002FE485 /* PassportReaderCore.swift in Sources */,
|
||||
BFE3DFD92E4F4EC400195298 /* MrzScanEngine.swift in Sources */,
|
||||
AA000002000000000000001A /* MrzOcrCorrection.swift in Sources */,
|
||||
AA000003000000000000001A /* MrzResultMapper.swift in Sources */,
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
"import": "./dist/esm/browser.js",
|
||||
"require": "./dist/cjs/browser.cjs"
|
||||
},
|
||||
"./adapters/browser": {
|
||||
"types": "./dist/esm/adapters/browser/index.d.ts",
|
||||
"import": "./dist/esm/adapters/browser/index.js",
|
||||
"require": "./dist/cjs/adapters/browser/index.cjs"
|
||||
},
|
||||
"./onboarding/*": {
|
||||
"types": "./dist/esm/flows/onboarding/*.d.ts",
|
||||
"import": "./dist/esm/flows/onboarding/*.js",
|
||||
@@ -98,21 +103,11 @@
|
||||
"import": "./dist/svgs/icons/*.svg",
|
||||
"require": "./dist/svgs/icons/*.svg"
|
||||
},
|
||||
"./animations/*.lottie": {
|
||||
"react-native": "./dist/animations/*.lottie",
|
||||
"import": "./dist/animations/*.lottie",
|
||||
"require": "./dist/animations/*.lottie"
|
||||
},
|
||||
"./animations/*.json": {
|
||||
"react-native": "./dist/animations/*.json",
|
||||
"import": "./dist/animations/*.json",
|
||||
"require": "./dist/animations/*.json"
|
||||
},
|
||||
"./animations/loading/*.lottie": {
|
||||
"react-native": "./dist/animations/loading/*.lottie",
|
||||
"import": "./dist/animations/loading/*.lottie",
|
||||
"require": "./dist/animations/loading/*.lottie"
|
||||
},
|
||||
"./animations/loading/*.json": {
|
||||
"react-native": "./dist/animations/loading/*.json",
|
||||
"import": "./dist/animations/loading/*.json",
|
||||
@@ -180,7 +175,6 @@
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lottiefiles/dotlottie-react-native": "0.5.0",
|
||||
"@openpassport/zk-kit-lean-imt": "^0.0.6",
|
||||
"@react-native-async-storage/async-storage": "^2.1.2",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
@@ -198,6 +192,7 @@
|
||||
"eslint-plugin-sort-exports": "^0.9.1",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"jsdom": "^25.0.1",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"poseidon-lite": "^0.3.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.3.1",
|
||||
@@ -215,8 +210,8 @@
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lottiefiles/dotlottie-react-native": "*",
|
||||
"@react-native-async-storage/async-storage": ">=1.0.0",
|
||||
"lottie-react-native": "7.2.2",
|
||||
"react": "^18.3.1",
|
||||
"react-native": ">=0.76.0 <0.78.0",
|
||||
"react-native-get-random-values": ">=1.0.0",
|
||||
|
||||
@@ -24,6 +24,18 @@ echo "🔍 Checking for Android build options..."
|
||||
if [ -d "$MOBILE_SDK_NATIVE" ]; then
|
||||
echo "✅ Native modules source submodule found, building from source..."
|
||||
|
||||
# Check if Java is actually available (required for Gradle build)
|
||||
# Note: macOS has a /usr/bin/java stub that passes `command -v` but fails at runtime
|
||||
if ! java -version 2>/dev/null 1>/dev/null; then
|
||||
echo "⚠️ Java not available — skipping Android AAR build"
|
||||
if [ -f "dist/android/mobile-sdk-alpha-release.aar" ]; then
|
||||
echo "📦 Using existing prebuilt AAR: dist/android/mobile-sdk-alpha-release.aar"
|
||||
else
|
||||
echo "⚠️ No prebuilt AAR available. Android native modules will not be included."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if we already have a valid AAR file
|
||||
if [ -f "dist/android/mobile-sdk-alpha-release.aar" ]; then
|
||||
echo "🔍 AAR file found, validating contents..."
|
||||
|
||||
@@ -33,5 +33,19 @@ export function createWebCryptoAdapter(): CryptoAdapter {
|
||||
'Signing requires native keychain access via the bridge.',
|
||||
);
|
||||
},
|
||||
|
||||
async generateKey(_keyRef: string): Promise<{ keyRef: string }> {
|
||||
throw new Error(
|
||||
'Key generation is not implemented in the browser crypto adapter. ' +
|
||||
'Key generation requires native keychain access via the bridge.',
|
||||
);
|
||||
},
|
||||
|
||||
async getPublicKey(_keyRef: string): Promise<Uint8Array> {
|
||||
throw new Error(
|
||||
'Public key retrieval is not implemented in the browser crypto adapter. ' +
|
||||
'Public key retrieval requires native keychain access via the bridge.',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,6 +12,17 @@ const DOCUMENTS_STORE = 'documents';
|
||||
const CATALOG_STORE = 'catalog';
|
||||
const CATALOG_KEY = 'current';
|
||||
|
||||
export function cloneForStorage<T>(value: T): T {
|
||||
if (typeof globalThis.structuredClone === 'function') {
|
||||
try {
|
||||
return globalThis.structuredClone(value);
|
||||
} catch {
|
||||
// Fall through to the JSON clone for WebViews with partial implementations.
|
||||
}
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
@@ -83,7 +94,7 @@ export function createIndexedDBDocumentsAdapter(): DocumentsAdapter {
|
||||
|
||||
async saveDocumentCatalog(catalog: DocumentCatalog): Promise<void> {
|
||||
const db = await getDB();
|
||||
await txPut(db, CATALOG_STORE, CATALOG_KEY, structuredClone(catalog));
|
||||
await txPut(db, CATALOG_STORE, CATALOG_KEY, cloneForStorage(catalog));
|
||||
},
|
||||
|
||||
async loadDocumentById(id: string): Promise<IDDocument | null> {
|
||||
@@ -94,7 +105,7 @@ export function createIndexedDBDocumentsAdapter(): DocumentsAdapter {
|
||||
|
||||
async saveDocument(id: string, passportData: IDDocument): Promise<void> {
|
||||
const db = await getDB();
|
||||
await txPut(db, DOCUMENTS_STORE, id, structuredClone(passportData));
|
||||
await txPut(db, DOCUMENTS_STORE, id, cloneForStorage(passportData));
|
||||
},
|
||||
|
||||
async deleteDocument(id: string): Promise<void> {
|
||||
|
||||
@@ -23,5 +23,19 @@ export function createCryptoAdapter(): CryptoAdapter {
|
||||
'Provide a custom CryptoAdapter with a sign implementation for your platform.',
|
||||
);
|
||||
},
|
||||
|
||||
async generateKey(_keyRef: string): Promise<{ keyRef: string }> {
|
||||
throw new Error(
|
||||
'Key generation is not implemented in the default crypto adapter. ' +
|
||||
'Provide a custom CryptoAdapter with a generateKey implementation for your platform.',
|
||||
);
|
||||
},
|
||||
|
||||
async getPublicKey(_keyRef: string): Promise<Uint8Array> {
|
||||
throw new Error(
|
||||
'Public key retrieval is not implemented in the default crypto adapter. ' +
|
||||
'Provide a custom CryptoAdapter with a getPublicKey implementation for your platform.',
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user