import Foundation import MyIdSDK import UIKit /// Entry point used by the ObjC++ turbo module. /// /// Imports no React headers: the bridge passes plain closures rather than /// RCTPromise blocks, so this compiles the same under static and framework /// linkage. /// /// `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 { public typealias SuccessHandler = (NSDictionary) -> Void public typealias ErrorHandler = (NSString, NSString) -> Void static let codeUserCancelled = "101" static let codeFailure = "103" /// 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 public func start( config: NSDictionary, appearance: NSDictionary, onSuccess: @escaping SuccessHandler, onError: @escaping ErrorHandler ) { // 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( 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 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 guard let delegate = self?.activeDelegate else { return } self?.activeDelegate = nil delegate.cancel( code: Self.codeFailure, message: "MyID was interrupted because the app reloaded" ) } } } enum MyidSdkOutcome { case success(NSDictionary) case failure(code: String, message: String) } /// Bridges `MyIdClientDelegate` to a one-shot callback. Internal on purpose. final class MyidSdkDelegateProxy: NSObject, MyIdClientDelegate { private var settle: ((MyidSdkOutcome) -> Void)? init(settle: @escaping (MyidSdkOutcome) -> Void) { self.settle = settle } /// Settles exactly once; a delegate method can fire more than once. /// /// `withExtendedLifetime` because the callback clears the only strong /// 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 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 keeps the base64 small and matches the Android side. 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) {} }