package com.useideem.zsm.reactnative import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.module.annotations.ReactModule import com.useideem.zsm.core.ZSMConfig import com.useideem.zsm.core.ZSMError import com.useideem.zsm.core.ZSMLogger import com.useideem.zsm.core.LogLevel import com.useideem.zsm.fido2.FIDO2Client import org.json.JSONObject import org.json.JSONArray import com.useideem.zsm.reactnative.generated @ReactModule(name = ZSMNativeModule.NAME) class ZSMNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { companion object { const val NAME = "ZSM" } @Volatile private var zsmInstance: FIDO2Client? = null override fun getName(): String = NAME @ReactMethod(isBlockingSynchronousMethod = true) fun getVersionString(): String { return generated.ZSM_VERSION } @ReactMethod fun create(config: ReadableMap, promise: Promise) { try { val jsonConfig = convertReadableMapToJson(config) val zsmConfig = ZSMConfig(jsonConfig) zsmInstance = FIDO2Client(reactApplicationContext, zsmConfig) if (zsmInstance == null) { promise.reject("zsm_instance_error", "Failed to create instance") } else { val result = Arguments.createMap() result.putBoolean("success", true) promise.resolve(result) } } catch (e: Exception) { promise.reject("zsm_config_error", e.message) } } @ReactMethod fun configure(config: ReadableMap, promise: Promise) { val instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") try { val jsonConfig = convertReadableMapToJson(config) val zsmConfig = ZSMConfig(jsonConfig) instance.configure(zsmConfig) val result = Arguments.createMap() result.putBoolean("success", true) promise.resolve(result) } catch (e: Exception) { promise.reject("configure error", e.message) } } @ReactMethod fun webauthn_create(options: ReadableMap, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("webauthn_create called", level = LogLevel.TRACE, traceId = actualTraceId) val instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") try { val jsonOptions = convertReadableMapToJson(options) ZSMLogger.log("Calling webauthnCreate with options", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnCreate(jsonOptions) { result: JSONObject?, metadata: Map?, error: ZSMError? -> ZSMLogger.log("webauthnCreate callback - result: $result, error: $error", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, error) } } catch (e: Exception) { ZSMLogger.log("webauthn_create exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("webauthn_create_error", e.message) } } @ReactMethod fun webauthn_get(options: ReadableMap, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("webauthn_get called", level = LogLevel.TRACE, traceId = actualTraceId) val instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") try { val jsonOptions = convertReadableMapToJson(options) ZSMLogger.log("Calling webauthnGet with options", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnGet(jsonOptions) { result: JSONObject?, metadata: Map?, error: ZSMError? -> ZSMLogger.log("webauthnGet callback - result: $result, error: $error", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, error) } } catch (e: Exception) { ZSMLogger.log("webauthn_get exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("webauthn_get_error", e.message) } } @ReactMethod fun webauthn_retrieve(identityId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("webauthn_retrieve called with identityId: '$identityId'", level = LogLevel.TRACE, traceId = actualTraceId) val instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") try { // webauthnRetrieve only reads from local storage, so use original userIdentifier for storage access // The identity_id will be used later during webauthn_get for MPC operations ZSMLogger.log("Calling webauthnRetrieve() without changing consumerId", level = LogLevel.TRACE, traceId = actualTraceId) ZSMLogger.log("Current consumerId (for storage): '${instance.config.consumerId}'", level = LogLevel.TRACE, traceId = actualTraceId) ZSMLogger.log("identityId (for later MPC): '$identityId'", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnRetrieve() { result: JSONObject?, metadata: Map?, error: ZSMError? -> ZSMLogger.log("webauthnRetrieve callback - result: $result, error: $error", level = LogLevel.TRACE, traceId = actualTraceId) if (result != null) { ZSMLogger.log("Found credential in storage for consumerId: '${instance.config.consumerId}'", level = LogLevel.TRACE, traceId = actualTraceId) } else { ZSMLogger.log("No credential found in storage for consumerId: '${instance.config.consumerId}'", level = LogLevel.TRACE, traceId = actualTraceId) } handleCallback(promise, result, metadata, error) } } catch (e: Exception) { ZSMLogger.log("webauthn_retrieve exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("webauthn_retrieve_error", e.message) } } private fun convertReadableMapToJson(map: ReadableMap): JSONObject { @Suppress("UNCHECKED_CAST") return JSONObject(map.toHashMap() as Map) } @ReactMethod fun logToReactNative(level: String, message: String) { val reactContext = reactApplicationContext reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(ZSMNativeModule.NAME, mapOf("level" to level, "message" to message)) } // Helper to convert Map to WritableMap // todo: remove the need for these manual conversions private fun convertMapToWritableMap(map: Map<*, *>?): WritableMap { val writableMap = Arguments.createMap() map?.forEach { (key, value) -> if (key is String) { // Ensure the key is of type String when (value) { is Map<*, *> -> writableMap.putMap(key, convertMapToWritableMap(value)) is List<*> -> writableMap.putArray(key, convertListToWritableArray(value)) is Boolean -> writableMap.putBoolean(key, value) is Int -> writableMap.putInt(key, value) is Double -> writableMap.putDouble(key, value) is String -> writableMap.putString(key, value) else -> writableMap.putString(key, value?.toString() ?: "null") } } } return writableMap } // Helper to convert List to WritableArray // todo: remove the need for these manual conversions private fun convertListToWritableArray(list: List<*>?): WritableArray { val writableArray = Arguments.createArray() list?.forEach { value -> when (value) { is Map<*, *> -> writableArray.pushMap(convertMapToWritableMap(value)) is List<*> -> writableArray.pushArray(convertListToWritableArray(value)) is Boolean -> writableArray.pushBoolean(value) is Int -> writableArray.pushInt(value) is Double -> writableArray.pushDouble(value) is String -> writableArray.pushString(value) else -> writableArray.pushString(value?.toString() ?: "null") } } return writableArray } fun convertReadableArrayToJson(array: ReadableArray): JSONArray { val jsonArray = JSONArray() for (i in 0 until array.size()) { when (array.getType(i)) { ReadableType.Null -> jsonArray.put(JSONObject.NULL) ReadableType.Boolean -> jsonArray.put(array.getBoolean(i)) ReadableType.Number -> jsonArray.put(array.getDouble(i)) ReadableType.String -> jsonArray.put(array.getString(i)) ReadableType.Map -> array.getMap(i)?.let { jsonArray.put(convertReadableMapToJson(it)) } ?: jsonArray.put(JSONObject.NULL) ReadableType.Array -> array.getArray(i)?.let { jsonArray.put(convertReadableArrayToJson(it)) } ?: jsonArray.put(JSONObject.NULL) } } return jsonArray } // Handle ZSMError only - no more Throwable anywhere private fun handleCallback(promise: Promise, result: Any?, metadata: Map?, error: ZSMError?) { if (error != null) { // Create rich error object with all ZSMError properties val errorMap = Arguments.createMap() errorMap.putString("message", error.message ?: "Unknown error") errorMap.putInt("code", error.code.ordinal) errorMap.putString("codeString", error.code.name) errorMap.putString("traceId", error.traceId) errorMap.putBoolean("isRecoverable", error.code.isRecoverable) errorMap.putString("recommendedAction", error.code.recommendedAction) errorMap.putString("details", error.toString()) // Reject with structured error information for consistency with iOS promise.reject( "ZSM_${error.code.ordinal}", "[ZSM ${error.code.ordinal}] ${error.message ?: "Unknown error"} (Trace: ${error.traceId})", Exception("${error.code.name}: ${error.message}").apply { // Additional error context available in stack trace } ) } else { val resultMap = Arguments.createMap() resultMap.putMap("result", convertJsonToWritableMap(result as? JSONObject ?: JSONObject())) resultMap.putMap("metadata", metadata?.let { convertMapToWritableMap(it) }) promise.resolve(resultMap) } } // Improved convertJsonToWritableMap to remove unnecessary safe calls // todo: remove the need for these manual conversions private fun convertJsonToWritableMap(json: JSONObject?): WritableMap { val writableMap = Arguments.createMap() json?.keys()?.forEach { key -> val value = json.get(key) when (value) { is JSONObject -> writableMap.putMap(key, convertJsonToWritableMap(value)) is org.json.JSONArray -> writableMap.putArray(key, convertJsonToWritableArray(value)) is Boolean -> writableMap.putBoolean(key, value) is Int -> writableMap.putInt(key, value) is Double -> writableMap.putDouble(key, value) is String -> writableMap.putString(key, value) else -> writableMap.putString(key, "$value") } } return writableMap } // Improved convertJsonToWritableArray // todo: remove the need for these manual conversions private fun convertJsonToWritableArray(jsonArray: org.json.JSONArray?): WritableArray { val writableArray = Arguments.createArray() jsonArray?.let { for (i in 0 until it.length()) { val value = it.get(i) when (value) { is JSONObject -> writableArray.pushMap(convertJsonToWritableMap(value)) is org.json.JSONArray -> writableArray.pushArray(convertJsonToWritableArray(value)) is Boolean -> writableArray.pushBoolean(value) is Int -> writableArray.pushInt(value) is Double -> writableArray.pushDouble(value) is String -> writableArray.pushString(value) else -> writableArray.pushString("$value") } } } return writableArray } }