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 import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference class MyidSdkModule(reactContext: ReactApplicationContext) : NativeMyidSdkSpec(reactContext) { private val client = MyIdClient() /** * At most one verification can be in flight. Atomic because three threads * touch it: start() on the JS thread, onActivityResult on the UI thread and * invalidate() on the native-modules thread. */ private val pending = AtomicReference(null) /** Keeps a late duplicate callback from settling a newer flow's promise. */ private val flowCounter = AtomicLong(0) private data class PendingFlow(val id: Long, val promise: Promise) private val activityEventListener = object : BaseActivityEventListener() { override fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent? ) { // handleActivityResult ignores the request code, so filter here. if (requestCode != MYID_REQUEST_CODE) return val flowId = pending.get()?.id ?: return client.handleActivityResult(resultCode, resultListenerFor(flowId)) } } private fun resultListenerFor(flowId: Long) = object : MyIdResultListener { override fun onSuccess(result: MyIdResult) { val promise = takePending(flowId) ?: return promise.resolve(result.toPayload()) } override fun onError(exception: MyIdException) { takePending(flowId)?.reject(exception.code.toString(), exception.message) } override fun onUserExited() { takePending(flowId)?.reject(CODE_USER_CANCELLED, "User canceled flow") } override fun onEvent(event: MyIdEvent) = Unit } init { reactContext.addActivityEventListener(activityEventListener) } /** @param appearance unused: Android theming is XML resource overrides. */ override fun start(config: ReadableMap, appearance: ReadableMap, promise: Promise) { // Resolved before claiming the slot so a missing activity cannot wedge it. val activity = currentActivity if (activity == null) { promise.reject(CODE_FAILURE, "Cannot start MyID: no foreground activity") return } val flow = PendingFlow(flowCounter.incrementAndGet(), promise) // Guard and claim in one atomic step. if (!pending.compareAndSet(null, flow)) { promise.reject(CODE_FAILURE, "A MyID verification is already in progress") return } try { val myIdConfig = MyidSdkConfigMapper.build(config, activity) client.startActivityForResult( activity = activity, requestCode = MYID_REQUEST_CODE, config = myIdConfig, listener = resultListenerFor(flow.id) ) } catch (error: Exception) { // Never put the config in the message: it carries clientHash. takePending(flow.id)?.reject(CODE_FAILURE, "Failed to start MyID: ${error.message}") } } override fun invalidate() { reactApplicationContext.removeActivityEventListener(activityEventListener) pending.getAndSet(null) ?.promise ?.reject(CODE_FAILURE, "MyID was interrupted because the app reloaded") super.invalidate() } /** Settles exactly once, and only for the flow that owns the promise. */ private fun takePending(flowId: Long): Promise? { val flow = pending.get() if (flow == null || flow.id != flowId) return null return if (pending.compareAndSet(flow, null)) flow.promise else null } private fun MyIdResult.toPayload(): WritableMap = Arguments.createMap().apply { putString("code", code) // A missing portrait must not fail an otherwise successful verification. val portrait = try { getGraphicFieldImageByType(MyIdGraphicFieldType.FacePortrait) } catch (error: Exception) { 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" } }