import Foundation import MyIdSDK 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. /// /// 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`. @objc(MyidSdkImpl) public final class MyidSdkImpl: NSObject { public typealias SuccessHandler = (NSDictionary) -> Void public typealias ErrorHandler = (NSString, NSString) -> Void static let codeUserCancelled = "101" static let codeFailure = "103" /// The SDK presents full screen, so at most one flow can be in flight. /// Held strongly for the duration: MyIdClient does not retain its delegate. private var activeDelegate: MyidSdkDelegateProxy? @objc public func start( config: NSDictionary, appearance: NSDictionary, onSuccess: @escaping SuccessHandler, onError: @escaping ErrorHandler ) { guard activeDelegate == nil else { onError( Self.codeFailure as NSString, "A MyID verification is already in progress" as NSString ) return } let delegate = MyidSdkDelegateProxy { [weak self] outcome in // Released before settling so a repeated SDK callback cannot settle twice. self?.activeDelegate = nil switch outcome { case let .success(payload): onSuccess(payload) case let .failure(code, message): onError(code as NSString, message as NSString) } } activeDelegate = delegate // The SDK presents view controllers, so it must be touched on the main queue. DispatchQueue.main.async { let myIdConfig = MyidSdkConfigBuilder.makeConfig( from: config, appearance: appearance ) MyIdClient.start(withConfig: myIdConfig, withDelegate: delegate) } } } /// 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`. final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate { private var settle: ((MyidSdkOutcome) -> Void)? init(settle: @escaping (MyidSdkOutcome) -> Void) { self.settle = settle } /// Claims the callback so the promise settles exactly once — a delegate /// method can fire more than once in edge cases. private func take() -> ((MyidSdkOutcome) -> Void)? { let callback = settle settle = nil return callback } func onSuccess(result: MyIdResult) { 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. if let base64 = result.image?.jpegData(compressionQuality: 1.0)?.base64EncodedString() { payload["base64"] = base64 } take()?(.success(payload)) } func onError(exception: MyIdException) { take()?(.failure(code: String(exception.code), message: exception.message)) } func onUserExited() { take()?( .failure(code: MyidSdkImpl.codeUserCancelled, message: "User canceled flow") ) } 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. } }