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:
Justin Hernandez
2026-03-16 10:23:10 -07:00
committed by GitHub
224 changed files with 6892 additions and 6015 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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:** 1k3k LOC changed. Smaller is fine for focused fixes. If >3k, add a brief justification for why it cant 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.

View File

@@ -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.**

View File

@@ -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)

View File

@@ -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'

View File

@@ -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')

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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 */,

View File

@@ -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

View File

@@ -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/', '');

View File

@@ -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",

View File

@@ -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) {

View 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);
}

View File

@@ -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)`,
);
}

View File

@@ -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,
};

File diff suppressed because one or more lines are too long

View File

@@ -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');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)}

View File

@@ -62,6 +62,7 @@ export const DisclosureItem: React.FC<DisclosureItemProps> = ({
color={proofRequestColors.slate900}
textTransform="uppercase"
letterSpacing={0.48}
allowFontScaling={false}
testID={`${testID}-text`}
>
{text}

View File

@@ -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}

View File

@@ -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>
</>

View File

@@ -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}

View File

@@ -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';

View File

@@ -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) => {

View File

@@ -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}

View File

@@ -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<{

View File

@@ -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}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 = {

View 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,
});
});
});

View File

@@ -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?.();

View File

@@ -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(),
}));

View File

@@ -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

View File

@@ -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",

View File

@@ -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))
}

View File

@@ -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)
}
/**

View File

@@ -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

View File

@@ -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")

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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"))
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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")

View File

@@ -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

View 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

View File

@@ -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 */,

View File

@@ -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",

View File

@@ -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..."

View File

@@ -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.',
);
},
};
}

View File

@@ -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> {

View File

@@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More