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. /// /// 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. private var activeDelegate: MyidSdkDelegateProxy? @objc public func start( config: NSDictionary, appearance: NSDictionary, 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. DispatchQueue.main.async { [weak self] in guard let self else { onError( Self.codeFailure as NSString, "MyID module was released before the flow could start" as NSString ) return } guard self.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 self?.activeDelegate = nil switch outcome { case let .success(payload): onSuccess(payload) case let .failure(code, message): onError(code as NSString, message as NSString) } } self.activeDelegate = delegate let myIdConfig = MyidSdkConfigBuilder.makeConfig( from: config, appearance: appearance ) MyIdClient.start(withConfig: myIdConfig, withDelegate: delegate) } } /// 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. @objc public func cancelActive() { DispatchQueue.main.async { [weak self] in guard let delegate = self?.activeDelegate else { return } self?.activeDelegate = nil delegate.cancel( code: Self.codeFailure, message: "MyID was interrupted because the app reloaded" ) } } } /// 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. /// /// `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. private func settleOnce(_ outcome: MyidSdkOutcome) { guard let callback = settle else { return } settle = nil withExtendedLifetime(self) { callback(outcome) } } func cancel(code: String, message: String) { settleOnce(.failure(code: code, message: message)) } 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 } settleOnce(.success(payload)) } func onError(exception: MyIdException) { settleOnce(.failure(code: String(exception.code), message: exception.message)) } func onUserExited() { settleOnce( .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. } }