package com.myidsdk import android.app.Activity import android.content.Intent import android.graphics.Bitmap import android.util.Base64 import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import uz.myid.android.sdk.capture.MyIdClient import uz.myid.android.sdk.capture.MyIdException import uz.myid.android.sdk.capture.MyIdResult import uz.myid.android.sdk.capture.MyIdResultListener import uz.myid.android.sdk.capture.model.MyIdEvent import uz.myid.android.sdk.capture.model.MyIdGraphicFieldType import java.io.ByteArrayOutputStream class MyidSdkModule(reactContext: ReactApplicationContext) : NativeMyidSdkSpec(reactContext) { 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. */ private var pendingPromise: Promise? = null private val activityEventListener = object : BaseActivityEventListener() { override fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent? ) { // handleActivityResult does not check the request code itself, so // filtering here keeps unrelated results from settling our promise. if (requestCode != MYID_REQUEST_CODE) return client.handleActivityResult(resultCode, resultListener) } } private val resultListener = object : MyIdResultListener { override fun onSuccess(result: MyIdResult) { val promise = takePending() ?: return promise.resolve(result.toPayload()) } override fun onError(exception: MyIdException) { takePending()?.reject(exception.code.toString(), exception.message) } override fun onUserExited() { takePending()?.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. } } 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. */ override fun start(config: ReadableMap, appearance: ReadableMap, promise: Promise) { if (pendingPromise != null) { promise.reject(CODE_FAILURE, "A MyID verification is already in progress") return } val activity = currentActivity if (activity == null) { promise.reject(CODE_FAILURE, "Cannot start MyID: no foreground activity") return } pendingPromise = promise try { val myIdConfig = MyidSdkConfigMapper.build(config, activity) client.startActivityForResult( activity = activity, requestCode = MYID_REQUEST_CODE, config = myIdConfig, listener = resultListener ) } catch (error: Exception) { // Never leak the config into the message — it carries clientHash. takePending()?.reject(CODE_FAILURE, "Failed to start MyID: ${error.message}") } } override fun invalidate() { reactApplicationContext.removeActivityEventListener(activityEventListener) takePending()?.reject(CODE_FAILURE, "MyID was interrupted because the app reloaded") super.invalidate() } /** * Claims the pending promise so it can be settled exactly once — the SDK may * invoke a listener callback more than once in edge cases. */ private fun takePending(): Promise? { val promise = pendingPromise pendingPromise = null return promise } private fun MyIdResult.toPayload(): WritableMap = Arguments.createMap().apply { putString("code", code) val portrait = try { getGraphicFieldImageByType(MyIdGraphicFieldType.FacePortrait) } catch (error: Exception) { // A missing portrait must not fail an otherwise successful verification. null } portrait?.toBase64Jpeg()?.let { putString("base64", it) } } private fun Bitmap.toBase64Jpeg(): String? = try { ByteArrayOutputStream().use { stream -> compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, stream) Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP) } } catch (error: Exception) { null } companion object { const val NAME = NativeMyidSdkSpec.NAME private const val MYID_REQUEST_CODE = 1771 private const val JPEG_QUALITY = 100 private const val CODE_USER_CANCELLED = "101" private const val CODE_FAILURE = "103" } }