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.core.ZSMJSONCallback import com.useideem.zsm.fido2.FIDO2Client import com.useideem.zsm.umfa.UMFAClient 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 @Volatile private var umfaInstance: UMFAClient? = null private var currentConfig: ZSMConfig? = 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) // Inject React Native SDK version into config headers val headers = if (jsonConfig.has("headers")) jsonConfig.getJSONObject("headers") else JSONObject() headers.put("X-SDK-Version", generated.ZSM_VERSION) jsonConfig.put("headers", headers) val zsmConfig = ZSMConfig(jsonConfig) currentConfig = zsmConfig zsmInstance = FIDO2Client(reactApplicationContext, zsmConfig) umfaInstance = UMFAClient(reactApplicationContext, zsmConfig) if (zsmInstance == null || umfaInstance == 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 fido2Instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") val umfaInstanceLocal = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { val jsonConfig = convertReadableMapToJson(config) // Ensure X-SDK-Version header persists across reconfigures val headers = if (jsonConfig.has("headers")) jsonConfig.getJSONObject("headers") else JSONObject() if (!headers.has("X-SDK-Version")) { headers.put("X-SDK-Version", generated.ZSM_VERSION) jsonConfig.put("headers", headers) } val zsmConfig = ZSMConfig(jsonConfig) currentConfig = zsmConfig fido2Instance.configure(zsmConfig) umfaInstanceLocal.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) // Get consumer_id from current config - use empty string if not set (for new users) val consumerId = currentConfig?.consumerId ?: "" ZSMLogger.log("Calling webauthnCreate with consumerId: $consumerId", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnCreate(consumerId, jsonOptions, object : ZSMJSONCallback { override fun onComplete(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) // Get consumer_id from current config - use empty string if not set val consumerId = currentConfig?.consumerId ?: "" ZSMLogger.log("Calling webauthnGet with consumerId: $consumerId", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnGet(consumerId, jsonOptions, object : ZSMJSONCallback { override fun onComplete(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(traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("webauthn_retrieve called", level = LogLevel.TRACE, traceId = actualTraceId) val instance = zsmInstance ?: return promise.reject("zsm_not_initialized", "ZSM instance is not initialized") try { // Get consumer_id from current config - use empty string if not set val consumerId = currentConfig?.consumerId ?: "" ZSMLogger.log("Calling webauthnRetrieve with consumerId: $consumerId", level = LogLevel.TRACE, traceId = actualTraceId) instance.webauthnRetrieve(consumerId, object : ZSMJSONCallback { override fun onComplete(result: JSONObject?, metadata: Map?, error: ZSMError?) { ZSMLogger.log("webauthnRetrieve callback - result: $result, error: $error", 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 unenroll(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("unenroll called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { ZSMLogger.log("Calling UMFAClient.unenroll", level = LogLevel.TRACE, traceId = actualTraceId) instance.unenroll(userId) { success: Boolean, metadata: Map?, error: ZSMError? -> ZSMLogger.log("unenroll callback - success: $success, error: $error", level = LogLevel.TRACE, traceId = actualTraceId) if (error != null) { promise.reject("unenroll_error", error.localizedMessage) } else { val result = Arguments.createMap() result.putBoolean("success", success) promise.resolve(result) } } } catch (e: Exception) { ZSMLogger.log("unenroll exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("unenroll_error", e.message) } } @ReactMethod fun listRegisteredUsers(traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("listRegisteredUsers called", level = LogLevel.TRACE, traceId = actualTraceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { val users = instance.listRegisteredUsers() ZSMLogger.log("listRegisteredUsers returned ${users.size} users", level = LogLevel.TRACE, traceId = actualTraceId) val result = Arguments.createArray() users.forEach { user -> result.pushString(user) } promise.resolve(result) } catch (e: Exception) { ZSMLogger.log("listRegisteredUsers exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("list_users_error", e.message) } } @ReactMethod fun getCredentialState(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("getCredentialState called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { val state = instance.getCredentialState(userId) val stateValue = state.ordinal val stateString = when (state) { com.useideem.zsm.umfa.CredentialManager.CredentialState.BOTH -> "both" com.useideem.zsm.umfa.CredentialManager.CredentialState.PASSKEY_ONLY -> "passkey-only" com.useideem.zsm.umfa.CredentialManager.CredentialState.MPC_ONLY -> "mpc-only" else -> "none" } ZSMLogger.log("getCredentialState result: $stateString ($stateValue)", level = LogLevel.TRACE, traceId = actualTraceId) val result = Arguments.createMap() result.putInt("state", stateValue) result.putString("stateString", stateString) promise.resolve(result) } catch (e: Exception) { ZSMLogger.log("getCredentialState exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("get_credential_state_error", e.message) } } /** * Check all enrollment information for a specific user. * Returns complete enrollment data including identity_id, credential IDs, and state. * This is used to sync native enrollment storage with JS AsyncStorage. * * @param userId The user identifier * @param promise Promise to resolve with enrollment info or null if not found */ @ReactMethod fun checkAllEnrollments(userId: String, promise: Promise) { val traceId = ZSMLogger.generateTraceId() ZSMLogger.log("checkAllEnrollments called with userId: '$userId'", level = LogLevel.TRACE, traceId = traceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { val enrollmentJson = instance.checkAllEnrollments(userId) ZSMLogger.log("checkAllEnrollments localInfo: $enrollmentJson", level = LogLevel.TRACE, traceId = traceId) // Check if there's an error (user not found) if (enrollmentJson.has("error")) { // No enrollment found - return null (not an error) promise.resolve(null) return } promise.resolve(convertJsonToWritableMap(enrollmentJson)) } catch (e: Exception) { ZSMLogger.log("checkAllEnrollments exception: ${e.message}", level = LogLevel.ERROR, traceId = traceId) promise.reject("check_enrollments_error", e.message) } } @ReactMethod fun checkAllEnrollmentsWithRemoteCheck(userId: String, forceRemoteCheck: Boolean, promise: Promise) { val traceId = ZSMLogger.generateTraceId() ZSMLogger.log( "checkAllEnrollmentsWithRemoteCheck called with userId: '$userId', forceRemoteCheck=$forceRemoteCheck", level = LogLevel.TRACE, traceId = traceId ) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") instance.checkAllEnrollments(userId, forceRemoteCheck) { result, _, error -> if (error != null) { ZSMLogger.log("checkAllEnrollmentsWithRemoteCheck error: ${error.message}", level = LogLevel.ERROR, traceId = traceId) promise.reject("check_enrollments_remote_error", error.message) return@checkAllEnrollments } ZSMLogger.log("checkAllEnrollments localInfo: $result", level = LogLevel.INFO, traceId = traceId) ZSMLogger.log( "checkAllEnrollments serverState={hasMpcCredential=${result?.optBoolean("hasMpcRemote")}, hasPasskeyCredential=${result?.optBoolean("hasPasskeyRemote")}, identityId=${result?.optString("identity_id")}, hasRcr=${result?.optBoolean("hasRcr")}}", level = LogLevel.INFO, traceId = traceId ) ZSMLogger.log("checkAllEnrollments mergedInfo: $result", level = LogLevel.INFO, traceId = traceId) promise.resolve(convertJsonToWritableMap(result)) } } /** * Register a user enrollment in the native credential store. * This should be called after successful webauthn_create to sync the enrollment state * so that getCredentialState() and listRegisteredUsers() work correctly. * * @param userId The user identifier * @param identityId The identity_id from the server * @param credentialId The credential ID from webauthn_create * @param credentialType The type of credential: "mpc" or "passkey" (defaults to "mpc") * @param traceId Optional trace ID for logging * @param promise Promise to resolve/reject */ @ReactMethod fun registerEnrollment(userId: String, identityId: String, credentialId: String, credentialType: String?, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("registerEnrollment called with userId: '$userId', identityId: '$identityId', credentialId: '$credentialId', type: '$credentialType'", level = LogLevel.TRACE, traceId = actualTraceId) if (userId.isBlank()) { ZSMLogger.log("registerEnrollment error: userId is blank", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("invalid_user_id", "User ID is required") return } if (credentialId.isBlank()) { ZSMLogger.log("registerEnrollment error: credentialId is blank", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("invalid_credential_id", "Credential ID is required") return } try { val context = reactApplicationContext val type = if (credentialType == "passkey") { com.useideem.zsm.umfa.CredentialManager.CredentialType.PASSKEY } else { com.useideem.zsm.umfa.CredentialManager.CredentialType.MPC } // Create CredentialManager instance and store the credential val credentialManager = com.useideem.zsm.umfa.CredentialManager(context) credentialManager.storeCredential(userId, credentialId, type) ZSMLogger.log("registerEnrollment: stored credential for userId: $userId, type: $type", level = LogLevel.TRACE, traceId = actualTraceId) // Update EnrollmentMapper with the identity_id and credential info com.useideem.zsm.umfa.EnrollmentMapper.updateEnrollment(context, userId) { current -> when (type) { com.useideem.zsm.umfa.CredentialManager.CredentialType.MPC -> current.copy( identityId = identityId.takeIf { it.isNotEmpty() } ?: current.identityId, mpcCredentialId = credentialId, state = if (current.passkeyCredentialId != null) { com.useideem.zsm.umfa.CredentialManager.CredentialState.BOTH } else { com.useideem.zsm.umfa.CredentialManager.CredentialState.MPC_ONLY } ) com.useideem.zsm.umfa.CredentialManager.CredentialType.PASSKEY -> current.copy( identityId = identityId.takeIf { it.isNotEmpty() } ?: current.identityId, passkeyCredentialId = credentialId, state = if (current.mpcCredentialId != null) { com.useideem.zsm.umfa.CredentialManager.CredentialState.BOTH } else { com.useideem.zsm.umfa.CredentialManager.CredentialState.PASSKEY_ONLY } ) } } ZSMLogger.log("registerEnrollment: updated EnrollmentMapper for userId: $userId", level = LogLevel.TRACE, traceId = actualTraceId) val result = Arguments.createMap() result.putBoolean("success", true) result.putString("userId", userId) result.putString("credentialId", credentialId) result.putString("credentialType", credentialType ?: "mpc") promise.resolve(result) } catch (e: Exception) { ZSMLogger.log("registerEnrollment exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("register_enrollment_error", e.message) } } @ReactMethod fun isPasskeySupported(promise: Promise) { val instance = umfaInstance if (instance == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } promise.resolve(instance.isPasskeysSupported()) } @ReactMethod fun passkeyNotSupportedReason(promise: Promise) { val instance = umfaInstance if (instance == null) { promise.resolve("UMFA client is not initialized") return } val status = instance.getPasskeySupportStatus() promise.resolve(status.reason) } @ReactMethod fun getPasskeySupportStatus(promise: Promise) { val instance = umfaInstance if (instance == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } val status = instance.getPasskeySupportStatus() val result = Arguments.createMap() result.putBoolean("supported", status.supported) result.putInt("apiLevel", status.apiLevel) result.putInt("requiredApiLevel", status.requiredApiLevel) result.putBoolean("passkeyServiceConfigured", status.passkeyServiceConfigured) result.putString("reason", status.reason) promise.resolve(result) } @ReactMethod fun checkEnrollment(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("checkEnrollment called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val config = currentConfig val activity = currentActivity if (config == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } try { val context = activity ?: reactApplicationContext val instance = UMFAClient(context, config) instance.checkEnrollment(userId) { result, metadata, error -> if (error != null) { ZSMLogger.log("checkEnrollment error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) // For checkEnrollment, return null instead of rejecting (like web SDK) promise.resolve(null) } else { ZSMLogger.log("checkEnrollment result: $result", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, null) } } } catch (e: Exception) { ZSMLogger.log("checkEnrollment exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.resolve(null) } } @ReactMethod fun isUserEnrolled(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("isUserEnrolled called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { val enrolled = instance.isUserEnrolled(userId) ZSMLogger.log("isUserEnrolled result: $enrolled", level = LogLevel.TRACE, traceId = actualTraceId) promise.resolve(enrolled) } catch (e: Exception) { ZSMLogger.log("isUserEnrolled exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("is_user_enrolled_error", e.message) } } @ReactMethod fun getServerCredentialState(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("getServerCredentialState called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val instance = umfaInstance ?: return promise.reject("umfa_not_initialized", "UMFA instance is not initialized") try { instance.getServerCredentialState(userId) { state, error -> if (error != null) { ZSMLogger.log("getServerCredentialState error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("server_credential_state_error", error.message) } else { val stateValue = state?.ordinal ?: 0 val stateString = when (state) { com.useideem.zsm.umfa.CredentialManager.CredentialState.BOTH -> "both" com.useideem.zsm.umfa.CredentialManager.CredentialState.PASSKEY_ONLY -> "passkey-only" com.useideem.zsm.umfa.CredentialManager.CredentialState.MPC_ONLY -> "mpc-only" else -> "none" } ZSMLogger.log("getServerCredentialState result: $stateString ($stateValue)", level = LogLevel.TRACE, traceId = actualTraceId) val result = Arguments.createMap() result.putInt("state", stateValue) result.putString("stateString", stateString) promise.resolve(result) } } } catch (e: Exception) { ZSMLogger.log("getServerCredentialState exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("server_credential_state_error", e.message) } } @ReactMethod fun addPasskeyToExistingIdentity(userId: String, userVerification: String?, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("addPasskeyToExistingIdentity called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val config = currentConfig val activity = currentActivity if (config == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } if (activity == null) { promise.reject("no_activity", "No foreground activity available. This method must be called when the app is in the foreground.") return } try { val actualUserVerification = userVerification ?: "required" // Re-create UMFAClient with Activity context for this call val tempInstance = UMFAClient(activity, config) tempInstance.addPasskeyToExistingIdentity(userId, actualUserVerification) { result, metadata, error -> if (error != null) { ZSMLogger.log("addPasskeyToExistingIdentity error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("add_passkey_error", error.message) } else { ZSMLogger.log("addPasskeyToExistingIdentity success", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, null) } } } catch (e: Exception) { ZSMLogger.log("addPasskeyToExistingIdentity exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("add_passkey_error", e.message) } } @ReactMethod fun addMpcToPasskeyUser(userId: String, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() ZSMLogger.log("addMpcToPasskeyUser called with userId: '$userId'", level = LogLevel.TRACE, traceId = actualTraceId) val config = currentConfig val activity = currentActivity if (config == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } if (activity == null) { promise.reject("no_activity", "No foreground activity available.") return } try { val tempInstance = UMFAClient(activity, config) tempInstance.addMpcToPasskeyUser(userId) { result, metadata, error -> if (error != null) { ZSMLogger.log("addMpcToPasskeyUser error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("add_mpc_error", error.message) } else { ZSMLogger.log("addMpcToPasskeyUser success", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, null) } } } catch (e: Exception) { ZSMLogger.log("addMpcToPasskeyUser exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("add_mpc_error", e.message) } } /** * Enroll a user using UMFAClient.enroll() - unified enrollment method. * This handles both passkey and MPC enrollment based on userVerification. * * @param userId The user identifier * @param userVerification Controls passkey behavior: * - "preferred": Create PasskeysPlus (passkey + MPC) if supported * - "prevented": Create MPC-only (no passkey) * @param traceId Optional trace ID for logging * @param promise Promise to resolve/reject */ @ReactMethod fun umfaEnroll(userId: String, userVerification: String?, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() val actualUserVerification = userVerification ?: "prevented" ZSMLogger.log("umfaEnroll called with userId: '$userId', userVerification: '$actualUserVerification'", level = LogLevel.TRACE, traceId = actualTraceId) val config = currentConfig if (config == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } // Use Activity context for passkey operations (required for CredentialManager) val context = currentActivity ?: reactApplicationContext try { // Create UMFAClient with appropriate context val instance = UMFAClient(context, config) instance.enroll(userId, actualUserVerification) { result, metadata, error -> if (error != null) { ZSMLogger.log("umfaEnroll error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject( "ZSM_${error.code.ordinal}", "[ZSM ${error.code.ordinal}] ${error.message ?: "Unknown error"} (Trace: ${error.traceId})", Exception("${error.code.name}: ${error.message}") ) } else { ZSMLogger.log("umfaEnroll success - result: $result, metadata: $metadata", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, null) } } } catch (e: Exception) { ZSMLogger.log("umfaEnroll exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("umfa_enroll_error", e.message) } } /** * Authenticate a user using UMFAClient.authenticate() - unified authentication method. * This handles both passkey and MPC authentication based on userVerification. * * @param userId The user identifier * @param userVerification Controls passkey behavior: * - "preferred": Use passkey if available, otherwise MPC * - "prevented": Force MPC-only, never use passkey * @param traceId Optional trace ID for logging * @param promise Promise to resolve/reject */ @ReactMethod fun umfaAuthenticate(userId: String, userVerification: String?, traceId: String?, promise: Promise) { val actualTraceId = traceId ?: ZSMLogger.generateTraceId() val actualUserVerification = userVerification ?: "preferred" ZSMLogger.log("umfaAuthenticate called with userId: '$userId', userVerification: '$actualUserVerification'", level = LogLevel.TRACE, traceId = actualTraceId) val config = currentConfig if (config == null) { promise.reject("umfa_not_initialized", "UMFA instance is not initialized") return } // Use Activity context for passkey operations (required for CredentialManager) val context = currentActivity ?: reactApplicationContext try { // Create UMFAClient with appropriate context val instance = UMFAClient(context, config) instance.authenticate(userId, actualUserVerification) { result, metadata, error -> if (error != null) { ZSMLogger.log("umfaAuthenticate error: ${error.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject( "ZSM_${error.code.ordinal}", "[ZSM ${error.code.ordinal}] ${error.message ?: "Unknown error"} (Trace: ${error.traceId})", Exception("${error.code.name}: ${error.message}") ) } else { ZSMLogger.log("umfaAuthenticate success - result: $result, metadata: $metadata", level = LogLevel.TRACE, traceId = actualTraceId) handleCallback(promise, result, metadata, null) } } } catch (e: Exception) { ZSMLogger.log("umfaAuthenticate exception: ${e.message}", level = LogLevel.ERROR, traceId = actualTraceId) promise.reject("umfa_authenticate_error", e.message) } } @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 == null -> writableMap.putNull(key) value is Map<*, *> -> writableMap.putMap(key, convertMapToWritableMap(value)) value is List<*> -> writableMap.putArray(key, convertListToWritableArray(value)) value is Boolean -> writableMap.putBoolean(key, value) value is Int -> writableMap.putInt(key, value) value is Double -> writableMap.putDouble(key, value) value is Long -> writableMap.putDouble(key, value.toDouble()) value is String -> writableMap.putString(key, value) else -> writableMap.putString(key, value.toString()) } } } 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 == null -> writableArray.pushNull() value is Map<*, *> -> writableArray.pushMap(convertMapToWritableMap(value)) value is List<*> -> writableArray.pushArray(convertListToWritableArray(value)) value is Boolean -> writableArray.pushBoolean(value) value is Int -> writableArray.pushInt(value) value is Double -> writableArray.pushDouble(value) value is Long -> writableArray.pushDouble(value.toDouble()) value is String -> writableArray.pushString(value) else -> writableArray.pushString(value.toString()) } } 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 === JSONObject.NULL || value == null -> writableMap.putNull(key) value is JSONObject -> writableMap.putMap(key, convertJsonToWritableMap(value)) value is org.json.JSONArray -> writableMap.putArray(key, convertJsonToWritableArray(value)) value is Boolean -> writableMap.putBoolean(key, value) value is Int -> writableMap.putInt(key, value) value is Double -> writableMap.putDouble(key, value) value is Long -> writableMap.putDouble(key, value.toDouble()) 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 === JSONObject.NULL || value == null -> writableArray.pushNull() value is JSONObject -> writableArray.pushMap(convertJsonToWritableMap(value)) value is org.json.JSONArray -> writableArray.pushArray(convertJsonToWritableArray(value)) value is Boolean -> writableArray.pushBoolean(value) value is Int -> writableArray.pushInt(value) value is Double -> writableArray.pushDouble(value) value is Long -> writableArray.pushDouble(value.toDouble()) value is String -> writableArray.pushString(value) else -> writableArray.pushString("$value") } } } return writableArray } }