package io.getvouch.rn import android.app.Activity import android.content.Intent import android.util.Log import android.webkit.CookieManager import androidx.activity.result.ActivityResultCaller import com.facebook.react.bridge.ActivityEventListener import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Callback import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule import com.vlayer.vouch.sdk.CreateProofRequestParams import com.vlayer.vouch.sdk.HeadlessProgress import com.vlayer.vouch.sdk.ProveResult import com.vlayer.vouch.sdk.VouchSdk import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @ReactModule(name = VouchRNModule.NAME) class VouchRNModule( reactContext: ReactApplicationContext, ) : NativeVouchSpec(reactContext), ActivityEventListener { private var customerId: String? = null private var apiKey: String? = null private var languageCodeOverride: String? = null private var pendingSuccessCallback: Callback? = null private var pendingErrorCallback: Callback? = null init { reactContext.addActivityEventListener(this) } override fun getName(): String = NAME override fun initialize( customerId: String, apiKey: String, languageCodeOverride: String?, promise: Promise, ) { if (this.customerId != null && this.apiKey != null) { Log.w(NAME, "VouchSDK is already initialized") promise.resolve(true) return } try { this.customerId = customerId this.apiKey = apiKey this.languageCodeOverride = languageCodeOverride Log.i(NAME, "VouchSDK initialized with customerId: $customerId") promise.resolve(true) } catch (error: Exception) { this.customerId = null this.apiKey = null this.languageCodeOverride = null promise.reject("initialize_failed", error.message, error) } } override fun isInitialized(): Boolean = customerId != null override fun destroy(promise: Promise) { customerId = null apiKey = null languageCodeOverride = null pendingSuccessCallback = null pendingErrorCallback = null try { UiThreadUtil.runOnUiThread { try { val cookieManager = CookieManager.getInstance() cookieManager.removeAllCookies { try { cookieManager.flush() } catch (error: Exception) { Log.e(NAME, "Failed to flush WebView cookies during destroy", error) } promise.resolve(true) } } catch (error: Exception) { Log.e(NAME, "Failed to clear WebView cookies during destroy", error) promise.resolve(true) } } } catch (error: Exception) { Log.e(NAME, "Failed to clear WebView cookies during destroy", error) promise.resolve(true) } } override fun addListener(eventName: String) { // Required by NativeEventEmitter. } override fun removeListeners(count: Double) { // Required by NativeEventEmitter. } override fun start( dataSourceId: String, webhookUrl: String, inputs: ReadableMap?, metadata: String?, onSuccess: Callback, onError: Callback, ) { val currentCustomerId = customerId if (currentCustomerId == null) { Log.e(NAME, "VouchSDK must be initialized before calling start") val errorMap = Arguments.createMap().apply { putString("proofId", "") putInt("reason", -1) putString("description", "VouchSDK not initialized") } onError.invoke(errorMap) return } val currentApiKey = apiKey if (currentApiKey == null) { Log.e(NAME, "VouchSDK must be initialized before calling start") val errorMap = Arguments.createMap().apply { putString("proofId", "") putInt("reason", -1) putString("description", "VouchSDK not initialized") } onError.invoke(errorMap) return } val activity = currentActivity if (activity == null) { Log.e(NAME, "Could not find current activity to present VouchSDK") val errorMap = Arguments.createMap().apply { putString("proofId", "") putInt("reason", -1) putString("description", "Could not find current activity") } onError.invoke(errorMap) return } // Store callbacks for activity result pendingSuccessCallback = onSuccess pendingErrorCallback = onError // Convert ReadableMap to Map val inputsMap: Map? = inputs?.let { readableMap -> val iterator = readableMap.keySetIterator() val map = mutableMapOf() while (iterator.hasNextKey()) { val key = iterator.nextKey() readableMap.getString(key)?.let { value -> map[key] = value } } if (map.isEmpty()) null else map } // Launch VouchActivity val intent = VouchActivity.createIntent( context = activity, customerId = currentCustomerId, apiKey = currentApiKey, languageCodeOverride = languageCodeOverride, datasourceId = dataSourceId, webhookUrl = webhookUrl, inputs = inputsMap, metadata = metadata, ) activity.startActivityForResult(intent, REQUEST_CODE) } override fun startHeadless( dataSourceId: String, webhookUrl: String, inputs: ReadableMap?, metadata: String?, onProgress: Callback, promise: Promise, ) { CoroutineScope(Dispatchers.Main).launch { val activity = currentActivity if (activity == null) { promise.reject("activity_not_found", "Could not find current activity") return@launch } val activityResultCaller = activity as? ActivityResultCaller if (activityResultCaller == null) { promise.reject("activity_result_caller_not_found", "Current activity cannot launch VouchSDK") return@launch } val sdk = try { createHeadlessSdk(activityResultCaller, promise) ?: return@launch } catch (error: Exception) { promise.reject("initialize_failed", error.message ?: "Failed to initialize VouchSDK", error) return@launch } try { when ( val result = sdk.proveHeadless( context = reactApplicationContext, params = CreateProofRequestParams( id = java.util.UUID.randomUUID().toString(), datasourceId = dataSourceId, webhookUrl = webhookUrl, inputs = inputs.toStringMap(), metadata = metadata, ), onProgress = { progress -> emitHeadlessProgress(onProgress, progress.toReactNativeValue()) }, ) ) { is ProveResult.Success -> { promise.resolve( Arguments.createMap().apply { putString("proofId", result.proofRequest.id.toString()) }, ) } is ProveResult.Failure -> promise.reject( "prove_failed", result.error.message ?: "Failed to prove request", ) } } catch (error: Exception) { promise.reject( "prove_failed", error.message ?: "Failed to prove request", error, ) } finally { runCatching { sdk.close() } } } } override fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent?, ) { if (requestCode != REQUEST_CODE) { return } val requestId = data?.getStringExtra(VouchActivity.EXTRA_REQUEST_ID) ?: "" when (resultCode) { VouchActivity.RESULT_SUCCESS -> { val successMap = Arguments.createMap().apply { putString("proofId", requestId) } pendingSuccessCallback?.invoke(successMap) } VouchActivity.RESULT_FAILURE -> { val errorMessage = data?.getStringExtra(VouchActivity.EXTRA_ERROR_MESSAGE) ?: "Unknown error" val errorMap = Arguments.createMap().apply { putString("proofId", requestId) putInt("reason", 1) putString("description", errorMessage) } pendingErrorCallback?.invoke(errorMap) } VouchActivity.RESULT_CANCELED, Activity.RESULT_CANCELED -> { val errorMap = Arguments.createMap().apply { putString("proofId", requestId) putInt("reason", 0) putString("description", "Flow cancelled by user") } pendingErrorCallback?.invoke(errorMap) } } // Clear pending callbacks pendingSuccessCallback = null pendingErrorCallback = null } private fun createHeadlessSdk( activityResultCaller: ActivityResultCaller, promise: Promise, ): VouchSdk? { val currentCustomerId = customerId val currentApiKey = apiKey if (currentCustomerId == null || currentApiKey == null) { promise.reject("not_initialized", "VouchSDK not initialized") return null } return VouchSdk( activityResultCaller = activityResultCaller, customerId = currentCustomerId, apiKey = currentApiKey, languageCode = languageCodeOverride, ) } private fun emitHeadlessProgress( onProgress: Callback?, progress: String, ) { reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(HEADLESS_PROGRESS_EVENT, progress) } override fun onNewIntent(intent: Intent) { // Not used } companion object { const val NAME = "VouchRN" private const val HEADLESS_PROGRESS_EVENT = "VouchHeadlessProgress" private const val REQUEST_CODE = 9001 } } private fun HeadlessProgress.toReactNativeValue(): String = when (this) { HeadlessProgress.DOWNLOADING_CONFIG -> "downloadingConfig" HeadlessProgress.SNIFFING_REQUESTS -> "sniffingRequests" HeadlessProgress.PROVING -> "proving" HeadlessProgress.FINISHED -> "finished" } private fun ReadableMap?.toStringMap(): Map { if (this == null) return emptyMap() val result = mutableMapOf() val iterator = keySetIterator() while (iterator.hasNextKey()) { val key = iterator.nextKey() getString(key)?.let { result[key] = it } } return result }