package com.blaze.rtnblazesdk.shared import android.os.Handler import android.os.Looper import android.util.Log import androidx.annotation.Keep import com.blaze.rtnblazesdk.utils.parsing.BlazeRTNJsonParsingError import com.blaze.rtnblazesdk.utils.parsing.parseJsonWithDetailedErrors import com.blaze.rtnblazesdk.utils.parsing.toJsonStringWithException import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout /** * Public interface for the Blaze Async Bridge Provides async communication between native code and * JavaScript */ @Keep interface BlazeAsyncBridge { sealed class TimeoutType { object DefaultTimeout : TimeoutType() object NoTimeout : TimeoutType() data class Seconds(val seconds: Double) : TimeoutType() internal val timeoutInMillis: Long get() = when (this) { is DefaultTimeout -> 2000L // Default timeout is NoTimeout -> Long.MAX_VALUE // Use Long.MAX_VALUE to indicate no timeout is Seconds -> (seconds * 1000).toLong() // Convert seconds to milliseconds } } } /// Private empty parameters data class for methods with no parameters. private object EmptyParams /** * Call a JavaScript method with parameters and return the result as type T. * * On Android if you would like to call a JS function without any return type you may call it like this: * * ```kotlin * val result: Unit = asyncBridge.callJSMethod("SomeFunction") * ``` * * @param name The name of the JavaScript method to call. * @param params The parameters to pass to the JavaScript method. * @param timeout The timeout in seconds for the method call. Default is [BlazeAsyncBridge.TimeoutType.DefaultTimeout]. */ @Keep @JvmOverloads suspend inline fun BlazeAsyncBridge.callJSMethod( name: String, params: P, timeout: BlazeAsyncBridge.TimeoutType = BlazeAsyncBridge.TimeoutType.DefaultTimeout ): T { // Cast to concrete implementation and call internal method directly val bridge = this as? BlazeAsyncBridgeModule ?: throw IllegalStateException( "BlazeAsyncBridge must be implemented by BlazeAsyncBridgeModule" ) val result: String? = bridge.callJSMethodInternal( name = name, params = params, timeout = timeout ) // Deserialize JSON string to target type - parseJsonWithDetailedErrors handles null case return try { result.parseJsonWithDetailedErrors(methodName = name) } catch (e: BlazeRTNJsonParsingError) { throw BlazeAsyncBridgeModule.BlazeAsyncError.TypeError( "JS method '$name' parsing failed: ${e.message}" ) } } /** * Call a JavaScript method with no parameters and return the result as type T. * * @param name The name of the JavaScript method to call. * @param timeout The timeout in seconds for the JS method call. */ @Keep @JvmOverloads suspend inline fun BlazeAsyncBridge.callJSMethod( name: String, timeout: BlazeAsyncBridge.TimeoutType = BlazeAsyncBridge.TimeoutType.DefaultTimeout ): T { // Use EmptyParams internally for methods with no parameters return callJSMethod( name = name, params = EmptyParams, timeout = timeout ) } @PublishedApi internal class BlazeAsyncBridgeModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context), BlazeAsyncBridge { companion object { const val NAME = "RTNBlazeAsyncBridge" private const val BRIDGE_ASYNC_FUNCTION_NAME = "BlazeAsyncBridge.jsRequest" } override fun getName(): String = NAME // Thread-safe maps for callbacks and timers - matching iOS structure private val callbackMap = ConcurrentHashMap() // Main thread handler for UI operations private val mainHandler = Handler(Looper.getMainLooper()) // Completion handler interface - matching iOS callback pattern private fun interface CompletionHandler { fun invoke(result: String?, error: Throwable?) } // Error types - matching iOS enum sealed class BlazeAsyncError(message: String) : Exception(message) { class TimeoutError(methodName: String) : BlazeAsyncError("JS method call timed out: $methodName") class JSError(errorMessage: String) : BlazeAsyncError(errorMessage) @PublishedApi internal class TypeError(message: String) : BlazeAsyncError(message) } /** Internal method that returns JSON string - core implementation */ @PublishedApi internal suspend fun

callJSMethodInternal( name: String, params: P, timeout: BlazeAsyncBridge.TimeoutType ): String? { val timeoutMillis = timeout.timeoutInMillis val callbackId = UUID.randomUUID().toString() try { return safeWithTimeout(timeoutMillis) { withContext(Dispatchers.Main) { suspendCancellableCoroutine { continuation -> try { storeCallback(callbackId, continuation) sendJSRequest(name, params, callbackId) continuation.invokeOnCancellation { cause -> cleanupCallback(callbackId) Log.d( NAME, "Task cancelled for JS method call: $name, reason: ${cause?.message}" ) } } catch (e: Throwable) { cleanupCallback(callbackId) Log.e(NAME, "Error setting up JS method call: $name", e) continuation.resumeWithException(e) } } } } } catch (e: TimeoutCancellationException) { cleanupCallback(callbackId) throw BlazeAsyncError.TimeoutError(name) } catch (e: Throwable) { cleanupCallback(callbackId) throw e } } // MARK: - Private Helper Methods private fun storeCallback(callbackId: String, continuation: CancellableContinuation) { val completionHandler = CompletionHandler { result, error -> handleCallbackResponse(callbackId, result, error, continuation) } callbackMap[callbackId] = completionHandler } private fun handleCallbackResponse( callbackId: String, result: String?, error: Throwable?, continuation: CancellableContinuation ) { // Resume continuation - matching iOS if (error != null) { continuation.resumeWithException(error) } else { continuation.resume(result) } // Remove callback from map - matching iOS cleanupCallback(callbackId) } /** Send JS request */ private fun

sendJSRequest(name: String, params: P, callbackId: String) { // Serialize params to JSON string using GsonUtils val paramsJson = try { when (params) { null -> "null" is EmptyParams -> "null" // Assuming EmptyParams is your empty object class else -> params.toJsonStringWithException() } } catch (e: Exception) { Log.e(NAME, "Failed to serialize params to JSON for method: $name", e) throw BlazeAsyncError.TypeError("Failed to serialize params: ${e.message}") } val eventParams = Arguments.createMap().apply { putString("methodName", name) putString("params", paramsJson) // Send as JSON string instead of ReadableMap putString("callbackId", callbackId) } sendJsEvent(BRIDGE_ASYNC_FUNCTION_NAME, eventParams) Log.d(NAME, "Sending JS request with JSON params: $name, callbackId: $callbackId") } /** Cleanup callback and timer - used for cancellation */ private fun cleanupCallback(callbackId: String) { callbackMap.remove(callbackId) } /** Method called by JS to resolve/reject a native call - matching iOS resolveJSResponse */ @ReactMethod @DoNotStrip fun resolveJSResponse(response: ReadableMap, promise: Promise) { // Run on main thread to match iOS @MainActor behavior mainHandler.post { handleJSResponse(response, promise) } } /** Handle JS response - matching iOS handleJSResponse */ private fun handleJSResponse(response: ReadableMap, promise: Promise) { val callbackId = response.getString("callbackId") if (callbackId == null) { promise.reject("INVALID_RESPONSE", "Invalid response format: missing callbackId") return } val callback = callbackMap[callbackId] if (callback == null) { // Callback might have already been handled (e.g., due to timeout or cancellation) promise.resolve(null) Log.d( NAME, "Received response for unknown callbackId: $callbackId (likely already handled)" ) return } val success = response.getBoolean("success") if (success) { // Extract JSON string from response - JS should send data as JSON string val jsonString = response.getString("data") callback.invoke(jsonString, null) Log.d(NAME, "Resolved JS method call: callbackId: $callbackId") } else { val errorMessage = response.getString("errorMessage") ?: "Unknown error" val error = BlazeAsyncError.JSError(errorMessage) callback.invoke(null, error) Log.d(NAME, "Rejected JS method call: callbackId: $callbackId, error: $errorMessage") } promise.resolve(null) } /** Send event to JS - helper method */ private fun sendJsEvent(eventName: String, params: WritableMap) { try { context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } catch (e: Exception) { Log.e(NAME, "Error sending event to JS: $eventName", e) } } // Standard React Native event emitter methods for NativeEventEmitter compatibility @ReactMethod @DoNotStrip fun addListener(eventName: String) { // Required for NativeEventEmitter - React Native handles listener management } @ReactMethod @DoNotStrip fun removeListeners(count: Int) { // Required for NativeEventEmitter - React Native handles listener management } } internal suspend fun safeWithTimeout(timeoutMs: Long, block: suspend () -> T): T { return if (timeoutMs != Long.MAX_VALUE) { withTimeout(timeoutMs) { block() } } else { block() // No timeout } }