Commit befb2c86 authored by Javokhir's avatar Javokhir
Browse files

refactor: tighten inline comments across the native bridge

Keeps the non-obvious rationale (pod naming, repository injection, one-shot
promise settling, optional guards) and drops restatements of what the code
already shows.
parent 954742e1
......@@ -87,3 +87,6 @@ nitrogen/
# local demo credentials
example/.env*
# Local notes, not part of the package
plans/
......@@ -2,11 +2,9 @@ require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
# The pod is deliberately NOT named "MyidSdk": that differs from the vendor pod
# "MyIdSDK" only by case, and on a case-insensitive filesystem the two targets
# collide on the same intermediate build directory, so this pod's generated
# Swift header lands in the vendor's build dir and the ObjC++ layer cannot
# import it. The JS module name is unaffected and stays "MyidSdk".
# Not named "MyidSdk": that differs from the vendor pod "MyIdSDK" only by case,
# and on a case-insensitive filesystem both targets share one build directory,
# leaving this pod's generated Swift header where ObjC++ cannot import it.
Pod::Spec.new do |s|
s.name = "RNMyidSdk"
s.version = package["version"]
......@@ -21,12 +19,11 @@ Pod::Spec.new do |s|
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
s.private_header_files = "ios/**/*.h"
# Binary XCFramework distributed on the public CocoaPods trunk.
s.dependency "MyIdSDK", "3.1.3"
s.swift_version = "5.0"
# DEFINES_MODULE lets the ObjC++ layer import the generated RNMyidSdk-Swift.h.
# Lets the ObjC++ layer import the generated RNMyidSdk-Swift.h.
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
install_modules_dependencies(s)
......
......@@ -3,8 +3,7 @@ buildscript {
kotlinVersion: "2.0.21",
minSdkVersion: 24,
compileSdkVersion: 36,
// Override from the app's root build.gradle:
// ext { myIdCaptureSdkVersion = "3.1.9" }
// Override with: ext { myIdCaptureSdkVersion = "3.1.9" }
myIdCaptureSdkVersion: "3.1.9"
]
......@@ -53,20 +52,12 @@ android {
}
}
// MyID artifacts live outside Maven Central:
// artifactory.aigroup.uz - the SDK itself, public read access, no credentials
// developer.huawei.com - com.huawei.hms:safetydetect, pulled in by
// myid-integrity-sdk, which the capture SDK depends on
// MyID artifacts live outside Maven Central. Registered on every project, not
// just this one: a consuming app resolves our transitive dependencies against
// its own repository set, so a module-level block here is not enough.
//
// These are registered on every project, not just this one, because a consuming
// app resolves our transitive dependencies against ITS OWN repository set when
// building its runtime classpath. A module-level repositories block here is
// enough to compile this library in isolation but still fails the app build
// with "Could not find uz.myid.sdk.capture:myid-capture-sdk".
//
// Apps that manage repositories centrally (dependencyResolutionManagement with
// FAIL_ON_PROJECT_REPOS) must set myIdSkipRepositoryInjection=true in
// gradle.properties and declare the two URLs themselves — see the README.
// Apps managing repositories centrally (FAIL_ON_PROJECT_REPOS) should set
// myIdSkipRepositoryInjection=true and declare the URLs themselves.
if (!rootProject.hasProperty("myIdSkipRepositoryInjection")) {
rootProject.allprojects {
repositories {
......@@ -86,9 +77,8 @@ repositories {
dependencies {
implementation "com.facebook.react:react-android"
// The debug artifact is a separate, non-obfuscated build of the same SDK.
// Apps that define build types other than debug/release must declare
// matchingFallbacks for them, otherwise resolution fails — see the README.
// The debug artifact is a non-obfuscated build of the same SDK. Apps with
// build types beyond debug/release need matchingFallbacks — see the README.
debugImplementation "uz.myid.sdk.capture:myid-capture-sdk-debug:${getExtOrDefault('myIdCaptureSdkVersion')}"
releaseImplementation "uz.myid.sdk.capture:myid-capture-sdk:${getExtOrDefault('myIdCaptureSdkVersion')}"
}
......@@ -17,13 +17,8 @@ import uz.myid.android.sdk.capture.model.MyIdScreenOrientation
/**
* Translates the JS config payload into a [MyIdConfig].
*
* A `withX(...)` call only happens when the key is actually present, so an
* omitted option keeps the SDK's own default instead of being overwritten with
* a value invented here.
*
* An unrecognised enum string skips its setter rather than throwing — a typo
* should not hard-fail a verification the user already started, and skipping is
* what actually leaves the SDK default in place.
* A setter is only called when its key is present, so an omitted option — or an
* unrecognised enum string — leaves the SDK default in place.
*/
internal object MyidSdkConfigMapper {
......@@ -40,10 +35,6 @@ internal object MyidSdkConfigMapper {
config.optBoolean("showErrorScreen")?.let { builder.withErrorScreen(it) }
config.optString("huaweiAppId")?.let { builder.withHuaweiAppId(it) }
// An unrecognised value maps to null and the setter is skipped, so the SDK
// keeps its own default. Substituting a guess here would silently change
// behaviour on a typo — and the guess is not always right: the SDK defaults
// imageFormat to JPEG, not PNG.
config.optEnum("environment")?.let {
when (it) {
"DEBUG" -> MyIdEnvironment.Debug
......@@ -123,12 +114,8 @@ internal object MyidSdkConfigMapper {
}
}?.let(builder::withScreenOrientation)
// Only forwarded when the caller actually supplied something. withOrganizationDetails
// copies the object verbatim, so an empty one would replace the SDK default
// (which carries MyID's call-centre number) with blanks.
//
// Note the same copy semantics mean supplying only `logo` clears the default
// phone number; the SDK exposes no way to read it back. Documented in README.
// withOrganizationDetails copies the object verbatim, so an empty one would
// blank out the SDK default carrying MyID's call-centre number.
config.optMap("organizationDetails")
?.takeIf { it.hasKey("phone") || it.hasKey("logo") }
?.let { details ->
......@@ -149,7 +136,7 @@ internal object MyidSdkConfigMapper {
private fun ReadableMap.optString(key: String): String? =
if (hasKey(key) && !isNull(key)) getString(key) else null
/** Enum values travel as strings; normalise case so 'debug' works like 'DEBUG'. */
/** Normalise case so 'debug' works like 'DEBUG'. */
private fun ReadableMap.optEnum(key: String): String? = optString(key)?.uppercase()
private fun ReadableMap.optInt(key: String): Int? =
......@@ -164,7 +151,7 @@ internal object MyidSdkConfigMapper {
private fun ReadableMap.optMap(key: String): ReadableMap? =
if (hasKey(key) && !isNull(key)) getMap(key) else null
/** Resolves a drawable by name; a missing asset degrades to no logo. */
/** A missing asset degrades to no logo. */
private fun Context.drawableIdOrNull(name: String): Int? =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
}
......@@ -26,21 +26,13 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
private val client = MyIdClient()
/**
* The SDK owns the screen for the whole flow, so at most one verification can
* be in flight. Guarded rather than queued: a second concurrent call is a
* caller bug, not something to silently serialise.
*
* Atomic because three threads touch it: start() runs on the JS thread,
* onActivityResult on the UI thread, and invalidate() on the native-modules
* thread. A plain field gives neither visibility nor an atomic check-then-set.
* At most one verification can be in flight. Atomic because three threads
* touch it: start() on the JS thread, onActivityResult on the UI thread and
* invalidate() on the native-modules thread.
*/
private val pending = AtomicReference<PendingFlow?>(null)
/**
* Distinguishes one flow from the next, so a late duplicate callback from a
* finished flow cannot settle the promise of a newer one. The SDK can invoke a
* listener callback more than once.
*/
/** Keeps a late duplicate callback from settling a newer flow's promise. */
private val flowCounter = AtomicLong(0)
private data class PendingFlow(val id: Long, val promise: Promise)
......@@ -52,8 +44,7 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
resultCode: Int,
data: Intent?
) {
// handleActivityResult does not check the request code itself, so
// filtering here keeps unrelated results from settling our promise.
// handleActivityResult ignores the request code, so filter here.
if (requestCode != MYID_REQUEST_CODE) return
val flowId = pending.get()?.id ?: return
......@@ -76,22 +67,16 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
takePending(flowId)?.reject(CODE_USER_CANCELLED, "User canceled flow")
}
override fun onEvent(event: MyIdEvent) {
// Progress telemetry (camera opened, face captured, ...). Not surfaced:
// the JS API is a single promise, and the SDK renders its own progress.
}
override fun onEvent(event: MyIdEvent) = Unit
}
init {
reactContext.addActivityEventListener(activityEventListener)
}
/**
* @param appearance accepted for bridge symmetry; theming on Android is done
* by overriding the SDK's XML resources, not at runtime.
*/
/** @param appearance unused: Android theming is XML resource overrides. */
override fun start(config: ReadableMap, appearance: ReadableMap, promise: Promise) {
// Resolved before claiming the slot, so a missing activity cannot wedge it.
// Resolved before claiming the slot so a missing activity cannot wedge it.
val activity = currentActivity
if (activity == null) {
promise.reject(CODE_FAILURE, "Cannot start MyID: no foreground activity")
......@@ -100,7 +85,7 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
val flow = PendingFlow(flowCounter.incrementAndGet(), promise)
// compareAndSet is both the single-flight guard and the claim, atomically.
// Guard and claim in one atomic step.
if (!pending.compareAndSet(null, flow)) {
promise.reject(CODE_FAILURE, "A MyID verification is already in progress")
return
......@@ -115,7 +100,7 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
listener = resultListenerFor(flow.id)
)
} catch (error: Exception) {
// Never leak the config into the message — it carries clientHash.
// Never put the config in the message: it carries clientHash.
takePending(flow.id)?.reject(CODE_FAILURE, "Failed to start MyID: ${error.message}")
}
}
......@@ -128,11 +113,7 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
super.invalidate()
}
/**
* Claims the pending promise so it settles exactly once, and only for the flow
* that owns it — a duplicate callback from an earlier flow finds a different
* id and is dropped.
*/
/** Settles exactly once, and only for the flow that owns the promise. */
private fun takePending(flowId: Long): Promise? {
val flow = pending.get()
......@@ -144,10 +125,10 @@ class MyidSdkModule(reactContext: ReactApplicationContext) :
private fun MyIdResult.toPayload(): WritableMap = Arguments.createMap().apply {
putString("code", code)
// A missing portrait must not fail an otherwise successful verification.
val portrait = try {
getGraphicFieldImageByType(MyIdGraphicFieldType.FacePortrait)
} catch (error: Exception) {
// A missing portrait must not fail an otherwise successful verification.
null
}
......
#import "MyidSdk.h"
// Generated by the Swift compiler from MyidSdkImpl.swift. The bracketed form is
// used when the pod is built as a framework; the quoted fallback covers static
// linkage, where the header is copied next to the sources instead.
// Framework linkage resolves the bracketed form; static linkage the quoted one.
#if __has_include(<RNMyidSdk/RNMyidSdk-Swift.h>)
#import <RNMyidSdk/RNMyidSdk-Swift.h>
#else
......@@ -10,12 +8,11 @@
#endif
/**
* Codegen hands this layer C++ structs (JS::NativeMyidSdk::*Input), which Swift
* cannot see. So the only job here is to flatten them into NSDictionary and
* hand off to MyidSdkImpl, which owns all the actual SDK interaction.
* Codegen hands this layer C++ structs that Swift cannot see, so the only job
* here is to flatten them into NSDictionary for MyidSdkImpl.
*
* Optional fields are inserted only when they carry a value, so an omitted
* option stays absent all the way down and the SDK keeps its own default.
* Optional fields are inserted only when set, so an omitted option stays absent
* and the SDK keeps its default.
*/
@implementation MyidSdk {
MyidSdkImpl *_impl;
......@@ -30,7 +27,7 @@
return self;
}
/** Inserts the string under `key`, skipping nil (an absent JS key). */
/** Skips nil, which means the JS key was absent. */
static void MyidSdkPutString(NSMutableDictionary *target, NSString *key, NSString *value)
{
if (value != nil) {
......@@ -71,8 +68,7 @@ static NSDictionary *MyidSdkConfigToDictionary(JS::NativeMyidSdk::MyIdConfigInpu
MyidSdkPutBool(result, @"showErrorScreen", config.showErrorScreen());
// cameraResolution, imageFormat, screenOrientation, withSoundGuides and
// huaweiAppId are Android-only and have no counterpart in MyIdConfig here,
// so they are intentionally not forwarded.
// huaweiAppId are Android-only and deliberately not forwarded.
auto organizationDetails = config.organizationDetails();
if (organizationDetails.has_value()) {
......@@ -121,8 +117,7 @@ static NSDictionary *MyidSdkAppearanceToDictionary(JS::NativeMyidSdk::MyIdAppear
}];
}
/// Called when the module is torn down, e.g. a reload mid-flow. Mirrors the
/// Android module: settle the in-flight promise instead of leaving it hanging.
/// Settles an in-flight promise when the module is torn down, e.g. a reload.
- (void)invalidate
{
[_impl cancelActive];
......
......@@ -4,13 +4,9 @@ import UIKit
/// Translates the JS payload into the SDK's own config objects.
///
/// A property is assigned only when its key is present, so an omitted option
/// keeps whatever default `MyIdConfig()` / `MyIdAppearance()` already set rather
/// than being overwritten with a value invented here.
///
/// An unrecognised enum string skips its assignment rather than throwing — a
/// typo should not hard-fail a verification the user already started, and
/// skipping is what actually leaves the SDK default in place.
/// A property is assigned only when its key is present, so an omitted option —
/// or an unrecognised enum string — keeps the default `MyIdConfig()` and
/// `MyIdAppearance()` already set.
enum MyidSdkConfigBuilder {
static func makeConfig(
......@@ -35,9 +31,6 @@ enum MyidSdkConfigBuilder {
config.showErrorScreen = showErrorScreen.boolValue
}
// An unrecognised value maps to nil and the assignment is skipped, so the
// SDK keeps its own default. Substituting a guess would silently change
// behaviour on a typo.
if let environment = dictionary.enumValue(for: "environment") {
switch environment {
case "DEBUG": config.environment = .debug
......@@ -68,8 +61,7 @@ enum MyidSdkConfigBuilder {
switch locale {
case "ENGLISH": config.locale = .english
case "RUSSIAN": config.locale = .russian
// Documented platform fallback, not an unknown value: iOS has no
// Cyrillic, Karakalpak or Tajik locale.
// iOS has no Cyrillic, Karakalpak or Tajik locale.
case "UZBEK", "UZBEK_CYRILLIC", "KARAKALPAK", "TAJIK": config.locale = .uzbek
default: break
}
......@@ -99,9 +91,8 @@ enum MyidSdkConfigBuilder {
}
}
// Only forwarded when the caller supplied something: an empty details object
// would replace the SDK's own default, which carries MyID's call-centre
// number. Each field is likewise assigned only when present.
// An empty details object would replace the SDK default, which carries
// MyID's call-centre number.
if let details = dictionary["organizationDetails"] as? NSDictionary, details.count > 0 {
let organizationDetails = MyIdOrganizationDetails()
......@@ -123,10 +114,8 @@ enum MyidSdkConfigBuilder {
return config
}
/// Every assignment is guarded: `MyIdAppearance()` seeds its own values, so
/// writing nil for a key the caller omitted would strip the SDK's defaults
/// rather than leave them alone. The same guard makes an unparseable color a
/// no-op instead of a wipe.
/// Assignments are guarded: `MyIdAppearance()` seeds its own values, so
/// writing nil for an omitted key would strip them.
private static func makeAppearance(from dictionary: NSDictionary) -> MyIdAppearance {
let appearance = MyIdAppearance()
......@@ -169,7 +158,7 @@ private extension NSDictionary {
self[key] as? NSNumber
}
/// Enum values travel as strings; normalise case so "debug" works like "DEBUG".
/// Normalise case so "debug" works like "DEBUG".
func enumValue(for key: String) -> String? {
string(for: key)?.uppercased()
}
......@@ -183,8 +172,7 @@ private extension NSDictionary {
extension UIColor {
/// Accepts `#RRGGBB` and `RRGGBB`. Returns nil for anything unparseable so a
/// bad value leaves the SDK's own color in place.
/// Accepts `#RRGGBB` and `RRGGBB`; nil for anything unparseable.
convenience init?(myIdHex hex: String) {
var value = hex.trimmingCharacters(in: .whitespacesAndNewlines)
......
......@@ -4,15 +4,13 @@ import UIKit
/// Entry point used by the ObjC++ turbo module.
///
/// Deliberately imports no React headers: the ObjC++ layer passes plain
/// closures instead of RCTPromise blocks, which keeps this compiling the same
/// way under static and framework linkage.
/// Imports no React headers: the bridge passes plain closures rather than
/// RCTPromise blocks, so this compiles the same under static and framework
/// linkage.
///
/// The `MyIdClientDelegate` conformance lives on an internal proxy rather than
/// on this class. Anything public and `@objc` here is re-emitted into the
/// generated `RNMyidSdk-Swift.h`, and that header is parsed by ObjC++ that has
/// no visibility of MyIdSDK's Swift types — exposing the conformance makes the
/// bridge fail to compile on `MyIdClientDelegate` and `MyIdEvent`.
/// `MyIdClientDelegate` conformance lives on an internal proxy. Anything public
/// and `@objc` here is re-emitted into the generated Swift header, which ObjC++
/// parses without visibility of MyIdSDK's Swift types.
@objc(MyidSdkImpl)
public final class MyidSdkImpl: NSObject {
......@@ -22,11 +20,8 @@ public final class MyidSdkImpl: NSObject {
static let codeUserCancelled = "101"
static let codeFailure = "103"
/// The SDK presents full screen, so at most one flow can be in flight.
///
/// Confined to the main queue: `start` is invoked on the JS thread while the
/// SDK's delegate callbacks arrive on the main thread, so an unsynchronised
/// field would race between the guard, the assignment and the clear.
/// At most one flow can be in flight. Confined to the main queue: `start`
/// runs on the JS thread while delegate callbacks arrive on the main thread.
private var activeDelegate: MyidSdkDelegateProxy?
@objc
......@@ -36,8 +31,8 @@ public final class MyidSdkImpl: NSObject {
onSuccess: @escaping SuccessHandler,
onError: @escaping ErrorHandler
) {
// The SDK presents view controllers, so it must be touched on the main
// queue anyway; doing the whole state transition here keeps it race-free.
// Main queue: the SDK presents view controllers, and confining the whole
// state transition here keeps it race-free.
DispatchQueue.main.async { [weak self] in
guard let self else {
onError(
......@@ -75,9 +70,8 @@ public final class MyidSdkImpl: NSObject {
}
}
/// Settles an in-flight flow, mirroring the Android module's `invalidate`.
/// Without this a reload mid-flow leaves the promise hanging forever and the
/// proxy deallocated while the SDK still holds it.
/// Settles an in-flight flow on teardown; without it a reload mid-flow leaves
/// the promise hanging and the proxy deallocated while the SDK holds it.
@objc
public func cancelActive() {
DispatchQueue.main.async { [weak self] in
......@@ -92,15 +86,12 @@ public final class MyidSdkImpl: NSObject {
}
}
/// Outcome of a single verification attempt.
enum MyidSdkOutcome {
case success(NSDictionary)
case failure(code: String, message: String)
}
/// Bridges `MyIdClientDelegate` to a one-shot callback.
///
/// Internal on purpose — see the note on `MyidSdkImpl`.
/// Bridges `MyIdClientDelegate` to a one-shot callback. Internal on purpose.
final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate {
private var settle: ((MyidSdkOutcome) -> Void)?
......@@ -109,12 +100,10 @@ final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate {
self.settle = settle
}
/// Claims the callback so the promise settles exactly once — a delegate
/// method can fire more than once in edge cases.
/// Settles exactly once; a delegate method can fire more than once.
///
/// `withExtendedLifetime` because the callback clears the only strong
/// reference to this proxy, which would otherwise deallocate it while a
/// delegate method is still on the stack.
/// reference to this proxy while a delegate method is still on the stack.
private func settleOnce(_ outcome: MyidSdkOutcome) {
guard let callback = settle else { return }
settle = nil
......@@ -130,8 +119,7 @@ final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate {
let payload = NSMutableDictionary()
payload["code"] = result.code
// JPEG rather than PNG keeps the base64 small enough to cross the bridge
// comfortably, and matches what the Android side returns.
// JPEG keeps the base64 small and matches the Android side.
if let base64 = result.image?.jpegData(compressionQuality: 1.0)?.base64EncodedString() {
payload["base64"] = base64
}
......@@ -149,8 +137,5 @@ final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate {
)
}
func onEvent(event: MyIdEvent) {
// Progress telemetry (camera opened, face captured, ...). Not surfaced:
// the JS API is a single promise and the SDK renders its own progress.
}
func onEvent(event: MyIdEvent) {}
}
......@@ -2,19 +2,16 @@ import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
/**
* Codegen spec. This is the raw bridge contract, NOT the public API.
* Codegen spec — the raw bridge contract, not the public API.
*
* Every enum crosses as a plain `string` because codegen cannot express TS
* string unions — the narrowing lives in `myid-types.ts`, and the native side
* uppercases and falls back to the SDK default for anything it does not know.
*
* Nested object params must be *named* exported type aliases; an inline object
* literal degrades to `AnyTypeAnnotation` and loses type safety on both platforms.
* Enums cross as plain strings because codegen cannot express TS string unions.
* Nested params must be named type aliases; an inline object literal degrades to
* `AnyTypeAnnotation`.
*/
export type MyIdOrganizationDetailsInput = {
phone?: string;
/** Native asset name: Android drawable / iOS image-set. Not a file path or URL. */
/** Native asset name: Android drawable / iOS image set. */
logo?: string;
};
......@@ -62,10 +59,6 @@ export type MyIdResultPayload = {
};
export interface Spec extends TurboModule {
/**
* Both params are required at the bridge level — the public wrapper always
* sends an appearance object, passing `{}` when the caller omits it.
*/
start(
config: MyIdConfigInput,
appearance: MyIdAppearanceInput
......
/**
* Wire values for every MyID enum.
*
* These strings are the bridge format and match the MyID Flutter plugin
* exactly, so backend docs and existing integration knowledge transfer
* unchanged. The native layer uppercases the incoming value and falls back to
* the SDK's own default when it does not recognise it.
* Wire values for every MyID enum. Matches the MyID Flutter plugin, so backend
* docs transfer unchanged. Native uppercases the value and skips unknown ones.
*/
export type MyIdEnvironment = 'PRODUCTION' | 'DEBUG';
......
......@@ -8,12 +8,8 @@ import { MyIdError, MyIdErrorCode } from './myid-error';
import type { MyIdResult, StartMyIdOptions } from './myid-types';
/**
* Drops keys whose value is `undefined`.
*
* This matters: the native layer only applies a setting when its key is
* present, so an absent key preserves the SDK's own default. Forwarding
* `undefined` would instead push a null through the bridge and could override a
* default with an empty value.
* Drops undefined keys. Native only applies a setting when its key is present,
* so an absent key preserves the SDK default.
*/
function stripUndefined<T extends object>(value: T): T {
const entries = Object.entries(value).filter(
......@@ -23,11 +19,7 @@ function stripUndefined<T extends object>(value: T): T {
return Object.fromEntries(entries) as T;
}
/**
* Reads the `code` a native rejection carries. React Native surfaces both
* platforms' reject(code, message) as an Error with a `code` property, but it
* arrives as a string on one platform and can be a number on the other.
*/
/** Arrives as a string on one platform and a number on the other. */
function readErrorCode(error: unknown): string {
if (typeof error === 'object' && error !== null && 'code' in error) {
const { code } = error as { code: unknown };
......@@ -53,36 +45,24 @@ function readErrorMessage(error: unknown): string {
}
/**
* Launches the MyID verification flow and resolves once the user completes it.
* Launches the MyID verification flow.
*
* Rejects with a {@link MyIdError} for every failure path — including the user
* simply backing out, which arrives as code `101`.
* Rejects with a {@link MyIdError} on every failure path, including the user
* dismissing the flow (code `101`).
*
* @example
* ```ts
* try {
* const result = await start({
* config: { sessionId, clientHash, clientHashId },
* });
* console.log(result.code, result.base64);
* } catch (error) {
* if (isMyIdError(error) && error.code === MyIdErrorCode.USER_CANCELLED) {
* // user backed out — usually not worth surfacing
* }
* }
* ```
*/
export async function start(options: StartMyIdOptions): Promise<MyIdResult> {
const { config, iosAppearance } = options;
// An organizationDetails that strips to {} must not be forwarded: native would
// build an empty details object and overwrite the SDK's own default, which
// carries MyID's call-centre number.
//
// The ternary also neutralises an explicit null. That matters more than it
// looks: the generated iOS accessor only checks for nil, not NSNull, so a null
// here would construct the param struct around NSNull and crash on first
// access.
// An empty details object would replace the SDK default, which carries MyID's
// call-centre number. The ternary also blocks an explicit null: the generated
// iOS accessor checks for nil but not NSNull, and would crash on first access.
const organizationDetails = config.organizationDetails
? stripUndefined<MyIdOrganizationDetailsInput>(config.organizationDetails)
: undefined;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment