Commit 1b21f2dd authored by Javokhir's avatar Javokhir
Browse files

feat(ios): implement MyID turbo module against MyIdSDK 3.1.3

ObjC++ flattens the codegen C++ param structs into NSDictionary, since Swift
cannot see them, and delegates to a Swift layer that owns all SDK interaction.

The pod is named RNMyidSdk rather than MyidSdk: the latter differs from the
vendor pod MyIdSDK only by case, and on a case-insensitive filesystem the two
targets share one intermediate build directory, so the generated Swift header
lands in the vendor's directory and the bridge fails to import it.

MyIdClientDelegate conformance is kept on an internal proxy so it is not
re-emitted into the generated Swift header, which ObjC++ parses without any
visibility of MyIdSDK's Swift types.
parent f790805c
......@@ -2,8 +2,13 @@ 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".
Pod::Spec.new do |s|
s.name = "MyidSdk"
s.name = "RNMyidSdk"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
......@@ -16,5 +21,13 @@ 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.
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
install_modules_dependencies(s)
end
......@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
0AC0420A268FB30DE19318B5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
0C80B921A6F3F58F76C31292 /* libPods-MyidSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MyidSdkExample.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
......@@ -159,6 +160,7 @@
files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
0AC0420A268FB30DE19318B5 /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
......@@ -271,7 +273,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "myidsdk.example";
PRODUCT_BUNDLE_IDENTIFIER = myidsdk.example;
PRODUCT_NAME = MyidSdkExample;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
......@@ -300,7 +302,7 @@
"-ObjC",
"-lc++",
);
PRODUCT_BUNDLE_IDENTIFIER = "myidsdk.example";
PRODUCT_BUNDLE_IDENTIFIER = myidsdk.example;
PRODUCT_NAME = MyidSdkExample;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_VERSION = 5.0;
......@@ -370,6 +372,10 @@
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
......@@ -377,8 +383,14 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
PODFILE_DIR = "$(SRCROOT)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
};
name = Debug;
};
......@@ -435,6 +447,10 @@
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
......@@ -442,8 +458,13 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
PODFILE_DIR = "$(SRCROOT)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES;
};
name = Release;
......
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:MyidSdkExample.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
......@@ -28,14 +28,17 @@
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>Camera is used to verify your identity.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>RCTNewArchEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
......
This diff is collapsed.
#import "MyidSdk.h"
@implementation MyidSdk
- (NSNumber *)multiply:(double)a b:(double)b {
NSNumber *result = @(a * b);
// 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.
#if __has_include(<RNMyidSdk/RNMyidSdk-Swift.h>)
#import <RNMyidSdk/RNMyidSdk-Swift.h>
#else
#import "RNMyidSdk-Swift.h"
#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.
*
* 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.
*/
@implementation MyidSdk {
MyidSdkImpl *_impl;
}
- (instancetype)init
{
if (self = [super init]) {
_impl = [MyidSdkImpl new];
}
return self;
}
/** Inserts the string under `key`, skipping nil (an absent JS key). */
static void MyidSdkPutString(NSMutableDictionary *target, NSString *key, NSString *value)
{
if (value != nil) {
target[key] = value;
}
}
static void MyidSdkPutNumber(NSMutableDictionary *target, NSString *key, std::optional<double> value)
{
if (value.has_value()) {
target[key] = @(value.value());
}
}
static void MyidSdkPutBool(NSMutableDictionary *target, NSString *key, std::optional<bool> value)
{
if (value.has_value()) {
target[key] = @(value.value());
}
}
static NSDictionary *MyidSdkConfigToDictionary(JS::NativeMyidSdk::MyIdConfigInput &config)
{
NSMutableDictionary *result = [NSMutableDictionary new];
MyidSdkPutString(result, @"sessionId", config.sessionId());
MyidSdkPutString(result, @"clientHash", config.clientHash());
MyidSdkPutString(result, @"clientHashId", config.clientHashId());
MyidSdkPutNumber(result, @"minAge", config.minAge());
MyidSdkPutNumber(result, @"distance", config.distance());
MyidSdkPutString(result, @"residency", config.residency());
MyidSdkPutString(result, @"environment", config.environment());
MyidSdkPutString(result, @"entryType", config.entryType());
MyidSdkPutString(result, @"locale", config.locale());
MyidSdkPutString(result, @"cameraShape", config.cameraShape());
MyidSdkPutString(result, @"cameraSelector", config.cameraSelector());
MyidSdkPutString(result, @"presentationStyle", config.presentationStyle());
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.
auto organizationDetails = config.organizationDetails();
if (organizationDetails.has_value()) {
NSMutableDictionary *details = [NSMutableDictionary new];
MyidSdkPutString(details, @"phone", organizationDetails.value().phone());
MyidSdkPutString(details, @"logo", organizationDetails.value().logo());
result[@"organizationDetails"] = details;
}
return result;
}
static NSDictionary *MyidSdkAppearanceToDictionary(JS::NativeMyidSdk::MyIdAppearanceInput &appearance)
{
NSMutableDictionary *result = [NSMutableDictionary new];
MyidSdkPutString(result, @"colorPrimary", appearance.colorPrimary());
MyidSdkPutString(result, @"colorOnPrimary", appearance.colorOnPrimary());
MyidSdkPutString(result, @"colorError", appearance.colorError());
MyidSdkPutString(result, @"colorOnError", appearance.colorOnError());
MyidSdkPutString(result, @"colorOutline", appearance.colorOutline());
MyidSdkPutString(result, @"colorDivider", appearance.colorDivider());
MyidSdkPutString(result, @"colorSuccess", appearance.colorSuccess());
MyidSdkPutString(result, @"colorButtonContainer", appearance.colorButtonContainer());
MyidSdkPutString(result, @"colorButtonContainerDisabled", appearance.colorButtonContainerDisabled());
MyidSdkPutString(result, @"colorButtonContent", appearance.colorButtonContent());
MyidSdkPutString(result, @"colorButtonContentDisabled", appearance.colorButtonContentDisabled());
MyidSdkPutString(result, @"colorScanButtonContainer", appearance.colorScanButtonContainer());
MyidSdkPutNumber(result, @"buttonCornerRadius", appearance.buttonCornerRadius());
return result;
}
- (void)start:(JS::NativeMyidSdk::MyIdConfigInput &)config
appearance:(JS::NativeMyidSdk::MyIdAppearanceInput &)appearance
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[_impl startWithConfig:MyidSdkConfigToDictionary(config)
appearance:MyidSdkAppearanceToDictionary(appearance)
onSuccess:^(NSDictionary *payload) {
resolve(payload);
}
onError:^(NSString *code, NSString *message) {
reject(code, message, nil);
}];
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
......
import Foundation
import MyIdSDK
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.
///
/// Unrecognised enum strings fall back to the SDK default instead of throwing —
/// a typo should not hard-fail a verification the user already started.
enum MyidSdkConfigBuilder {
static func makeConfig(
from dictionary: NSDictionary,
appearance appearanceDictionary: NSDictionary
) -> MyIdConfig {
let config = MyIdConfig()
config.sessionId = dictionary.string(for: "sessionId")
config.clientHash = dictionary.string(for: "clientHash")
config.clientHashId = dictionary.string(for: "clientHashId")
if let minAge = dictionary.number(for: "minAge") {
config.minAge = minAge.intValue
}
if let distance = dictionary.number(for: "distance") {
config.distance = distance.floatValue
}
if let showErrorScreen = dictionary.number(for: "showErrorScreen") {
config.showErrorScreen = showErrorScreen.boolValue
}
if let environment = dictionary.enumValue(for: "environment") {
config.environment = environment == "DEBUG" ? .debug : .production
}
if let entryType = dictionary.enumValue(for: "entryType") {
switch entryType {
case "FACE_DETECTION": config.entryType = .faceDetection
case "VIDEO_IDENTIFICATION": config.entryType = .videoIdentification
default: config.entryType = .identification
}
}
if let residency = dictionary.enumValue(for: "residency") {
switch residency {
case "USER_DEFINED": config.residency = .userDefined
case "NON_RESIDENT": config.residency = .nonResident
default: config.residency = .resident
}
}
if let locale = dictionary.enumValue(for: "locale") {
switch locale {
case "ENGLISH": config.locale = .english
case "RUSSIAN": config.locale = .russian
// UZBEK_CYRILLIC, KARAKALPAK and TAJIK exist on Android only.
default: config.locale = .uzbek
}
}
if let cameraShape = dictionary.enumValue(for: "cameraShape") {
config.cameraShape = cameraShape == "ELLIPSE" ? .ellipse : .circle
}
if let cameraSelector = dictionary.enumValue(for: "cameraSelector") {
config.cameraSelector = cameraSelector == "BACK" ? .back : .front
}
if let presentationStyle = dictionary.enumValue(for: "presentationStyle") {
config.presentationStyle = presentationStyle == "SHEET" ? .sheet : .full
}
if let details = dictionary["organizationDetails"] as? NSDictionary {
let organizationDetails = MyIdOrganizationDetails()
organizationDetails.phoneNumber = details.string(for: "phone")
if let logoName = details.string(for: "logo") {
organizationDetails.logo = UIImage(named: logoName)
}
config.organizationDetails = organizationDetails
}
if appearanceDictionary.count > 0 {
config.appearance = makeAppearance(from: appearanceDictionary)
}
return config
}
private static func makeAppearance(from dictionary: NSDictionary) -> MyIdAppearance {
let appearance = MyIdAppearance()
appearance.colorPrimary = dictionary.color(for: "colorPrimary")
appearance.colorOnPrimary = dictionary.color(for: "colorOnPrimary")
appearance.colorError = dictionary.color(for: "colorError")
appearance.colorOnError = dictionary.color(for: "colorOnError")
appearance.colorOutline = dictionary.color(for: "colorOutline")
appearance.colorDivider = dictionary.color(for: "colorDivider")
appearance.colorSuccess = dictionary.color(for: "colorSuccess")
appearance.colorButtonContainer = dictionary.color(for: "colorButtonContainer")
appearance.colorButtonContainerDisabled = dictionary.color(for: "colorButtonContainerDisabled")
appearance.colorButtonContent = dictionary.color(for: "colorButtonContent")
appearance.colorButtonContentDisabled = dictionary.color(for: "colorButtonContentDisabled")
appearance.colorScanButtonContainer = dictionary.color(for: "colorScanButtonContainer")
if let cornerRadius = dictionary.number(for: "buttonCornerRadius") {
appearance.buttonCornerRadius = cornerRadius.floatValue
}
return appearance
}
}
private extension NSDictionary {
func string(for key: String) -> String? {
self[key] as? String
}
func number(for key: String) -> NSNumber? {
self[key] as? NSNumber
}
/// Enum values travel as strings; normalise case so "debug" works like "DEBUG".
func enumValue(for key: String) -> String? {
string(for: key)?.uppercased()
}
func color(for key: String) -> UIColor? {
guard let hex = string(for: key) else { return nil }
return UIColor(myIdHex: hex)
}
}
extension UIColor {
/// Accepts `#RRGGBB` and `RRGGBB`. Returns nil for anything unparseable so a
/// bad value leaves the SDK's own color in place.
convenience init?(myIdHex hex: String) {
var value = hex.trimmingCharacters(in: .whitespacesAndNewlines)
if value.hasPrefix("#") {
value.removeFirst()
}
guard value.count == 6 else { return nil }
var rgb: UInt64 = 0
guard Scanner(string: value).scanHexInt64(&rgb) else { return nil }
self.init(
red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgb & 0x0000FF) / 255.0,
alpha: 1.0
)
}
}
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.
}
}
......@@ -56,8 +56,8 @@ export type MyIdConfig = {
environment?: MyIdEnvironment;
/**
* `FACE_DETECTION` only captures a face and skips MyID verification.
* `VIDEO_IDENTIFICATION` is experimental and Android-only — iOS falls back to
* `IDENTIFICATION`.
* `VIDEO_IDENTIFICATION` exists on both platforms but is marked experimental
* by the Android SDK, so treat it as unstable.
*/
entryType?: MyIdEntryType;
/**
......
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