package com.bureau.reactnative.otl import android.util.Log import com.bureau.onetaplogin.BureauAuth import com.bureau.onetaplogin.BureauDebug import com.bureau.onetaplogin.DebugEvent import com.bureau.onetaplogin.Environment as OTLEnvironment import com.bureau.onetaplogin.models.AuthCallback import com.bureau.onetaplogin.models.AuthenticationStatus 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.ReadableArray import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import kotlinx.coroutines.runBlocking /** * React Native bridge module for Bureau SDK * Handles device intelligence submission and authentication */ class BureauOtlModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { companion object { const val NAME = "BureauOtlModule" // Debug logging flag - disabled by default @Volatile var debugEnabled = false private set // Log tag for debug logging private const val LOG_TAG = "BureauSDK" // Environment constants private const val ENV_PRODUCTION = "Production" private const val ENV_PRODUCTION_LOWERCASE = "production" private const val ENV_SANDBOX = "Sandbox" private const val ENV_SANDBOX_LOWERCASE = "sandbox" // Response messages private const val RESPONSE_SUCCESS = "Success" private const val ERROR_PREFIX = "Error: Message: " private const val CODE_SEPARATOR = " Code: " // Result keys private const val RESULT_KEY_STATUS = "status" private const val RESULT_KEY_MESSAGE = "message" // Status case names private const val STATUS_NETWORK_AND_OPERATOR_MISMATCH = "networkAndOperatorMismatch" private const val STATUS_AUTH_FAILURE = "authFailure" private const val STATUS_NETWORK_NOT_SUPPORTED = "networkNotSupported" private const val STATUS_OPERATOR_NOT_SUPPORTED = "operatorNotSupported" private const val STATUS_OPERATOR_AND_NETWORK_NOT_SUPPORTED = "operatorAndNetworkNotSupported" private const val STATUS_AWAITING_PROVIDER_ACK = "awaitingProviderAck" private const val STATUS_AUTH_VALIDATION_ERROR = "authValidationError" private const val STATUS_DUPLICATE_CORRELATION_ID = "duplicateCorrelationId" private const val STATUS_INTEGRATION_FAILURE = "integrationFailure" private const val STATUS_AUTH_STATE_EXPIRED = "authStateExpired" private const val STATUS_UNAUTHORIZED = "unauthorized" private const val STATUS_COUNTRY_NOT_SUPPORTED = "countryNotSupported" private const val STATUS_RATE_LIMIT_EXCEEDED = "rateLimitExceeded" private const val STATUS_INTERNAL_SERVER_ERROR = "internalServerError" private const val STATUS_NETWORK_UNAVAILABLE = "networkUnavailable" private const val STATUS_WIFI_DETECTED_AND_NO_DATA_NETWORK = "wifiDetectedAndNoDataNetwork" private const val STATUS_COMPLETED = "completed" private const val STATUS_TIMEOUT = "timeout" private const val STATUS_UNKNOWN = "unknown" } /** * Enum for categorizing authentication status log levels */ private enum class StatusCategory { SUCCESS, // Completed, awaitingProviderAck, networkAndOperatorMismatch INFO, // Informational statuses ERROR, // Error statuses TIMEOUT // Timeout status } /** * Categorizes authentication status for appropriate logging level * * @param statusCase - The status case string * @return StatusCategory enum value */ private fun getStatusCategory(statusCase: String): StatusCategory { return when (statusCase) { STATUS_COMPLETED, STATUS_AWAITING_PROVIDER_ACK, STATUS_NETWORK_AND_OPERATOR_MISMATCH -> StatusCategory.SUCCESS STATUS_TIMEOUT -> StatusCategory.TIMEOUT else -> StatusCategory.ERROR } } /** * Logs authentication result with appropriate log level based on status category * * @param statusCase - The authentication status case name * @param message - The authentication status message */ private fun logAuthenticationResult(statusCase: String, message: String) { when (getStatusCategory(statusCase)) { StatusCategory.SUCCESS -> logInfo("Authentication status: $statusCase - $message") StatusCategory.TIMEOUT -> logError("Authentication status: $statusCase - $message") StatusCategory.ERROR -> logError("Authentication status: $statusCase - $message") StatusCategory.INFO -> logInfo("Authentication status: $statusCase - $message") } } /** * Detects if a timeout occurred based on authentication status and elapsed time * * @param authenticationStatus - The authentication status from SDK * @param startTime - Timestamp when authentication started (milliseconds) * @param timeout - Configured timeout duration in milliseconds * @return True if timeout is detected, false otherwise */ private fun detectTimeout(authenticationStatus: AuthenticationStatus, startTime: Long, timeout: Int): Boolean { val elapsedTime = System.currentTimeMillis() - startTime val statusCase = authenticationStatus.toString() // Timeout detected if: // 1. Status is 'awaitingProviderAck' and elapsed time >= timeout duration // 2. Status is 'unknown' and elapsed time >= timeout duration // 3. Status is 'timeout' and elapsed time >= timeout duration // Android SDK returns 'awaitingProviderAck' when timeout occurs after timeoutInMs + 200ms val isTimeoutCandidate = statusCase == STATUS_AWAITING_PROVIDER_ACK || statusCase == STATUS_UNKNOWN || statusCase == STATUS_TIMEOUT return isTimeoutCandidate && elapsedTime >= timeout } /** * Creates a standardized timeout error log message with context * * @param clientId - Bureau client ID (will be masked) * @param sessionId - Session ID (will be masked) * @param timeout - Configured timeout duration in milliseconds * @param elapsedTime - Actual elapsed time in milliseconds * @param env - Environment string * @return Formatted log message string */ private fun createTimeoutLogContext( clientId: String, sessionId: String, timeout: Int, elapsedTime: Long, env: String ): String { val percentage = String.format("%.1f", (elapsedTime.toDouble() / timeout) * 100) return "Timeout error detected - duration: ${timeout}ms, elapsed: ${elapsedTime}ms ($percentage% of timeout), " + "sessionId: ${maskSensitiveData(sessionId)}, clientId: ${maskSensitiveData(clientId)}, env: $env, platform: Android" } override fun getName(): String { return NAME } /** * Enables or disables debug logging * * @param enabled - Whether to enable debug logging */ @ReactMethod fun setDebugEnabled(enabled: Boolean) { debugEnabled = enabled Log.i(LOG_TAG, "[INFO] Debug logging ${if (enabled) "enabled" else "disabled"}") } /** * Retrieves all native SDK debug logs from BureauDebug * Returns logs in a format compatible with React Native LogEntry structure * * @param promise - Promise to resolve with array of log entries */ @ReactMethod fun getNativeLogs(promise: Promise) { try { // Get current snapshot of debug events from BureauDebug // BureauDebug.events is a StateFlow, so we need to access it properly val debugEvents = BureauDebug.events.value // Convert DebugEvent to WritableMap for React Native bridge val logArray: WritableArray = Arguments.createArray() for (event in debugEvents) { val logMap: WritableMap = Arguments.createMap() logMap.putString("id", "${event.timestampMs}-${event.hashCode()}") logMap.putDouble("timestamp", event.timestampMs.toDouble()) // Map Android log levels to React Native log levels val level = when (event.level.uppercase()) { "D", "DEBUG" -> "DEBUG" "I", "INFO" -> "INFO" "W", "WARN" -> "INFO" // Map WARN to INFO for consistency "E", "ERROR" -> "ERROR" else -> "DEBUG" } logMap.putString("level", level) // Format message with tag prefix for better context val message = if (event.tag.isNotEmpty()) { "[${event.tag}] ${event.message}" } else { event.message } logMap.putString("message", message) logArray.pushMap(logMap) } promise.resolve(logArray) } catch (e: Exception) { promise.reject("GET_NATIVE_LOGS_ERROR", "Failed to retrieve native logs: ${e.message}", e) } } /** * Masks sensitive data for logging (shows first 4 and last 4 characters) */ private fun maskSensitiveData(value: String): String { if (value.length <= 8) { return "***" } return "${value.substring(0, 4)}...${value.substring(value.length - 4)}" } /** * Logs debug message to Logcat (appears in Android logcat and React Native console via Metro) */ private fun logDebug(message: String) { if (!debugEnabled) return Log.d(LOG_TAG, "[DEBUG] $message") } /** * Logs info message to Logcat (appears in Android logcat and React Native console via Metro) */ private fun logInfo(message: String) { if (!debugEnabled) return Log.i(LOG_TAG, "[INFO] $message") } /** * Logs error message to Logcat (appears in Android logcat and React Native console via Metro) */ private fun logError(message: String) { if (!debugEnabled) return Log.e(LOG_TAG, "[ERROR] $message") } /** * Authenticates a user using Bureau's One-Tap Login * * @param clientId - Bureau client ID * @param sessionId - Unique session/correlation ID * @param msisdn - Mobile number with country code * @param env - Environment: "production" or "sandbox" * @param timeout - Timeout in milliseconds * @param allowedCountryCodes - Optional array of allowed country codes (e.g., ["91", "1"]). Defaults to ["91"] if null * @param pspCallbacks - Optional PSP callback keys passed to SDK authenticate (5.0.8+) * @param promise - Promise to resolve with authentication result */ @ReactMethod fun authenticate(clientId: String, sessionId: String, msisdn: String, env: String, timeout: Int, allowedCountryCodes: ReadableArray?, pspCallbacks: ReadableArray?, promise: Promise){ val startTime = System.currentTimeMillis() val functionName = "authenticate" // Step 1: Entry point logging with all parameters val entryLogStart = System.currentTimeMillis() val maskedClientId = maskSensitiveData(clientId) val maskedSessionId = maskSensitiveData(sessionId) val maskedMsisdn = maskSensitiveData(msisdn) val timeoutInSeconds = timeout / 1000 val entryLogTime = System.currentTimeMillis() - entryLogStart BureauDebug.d(LOG_TAG, "$functionName: Entry - Starting authentication") logDebug("$functionName: Entry - clientId: $maskedClientId, sessionId: $maskedSessionId, msisdn: $maskedMsisdn, env: $env, timeout: ${timeout}ms (${timeoutInSeconds}s)") BureauDebug.d(LOG_TAG, "$functionName: Step 1: Entry point - Parameters: clientId type: ${clientId::class.simpleName}, sessionId type: ${sessionId::class.simpleName}, msisdn type: ${msisdn::class.simpleName}, env type: ${env::class.simpleName}, timeout type: ${timeout::class.simpleName}, allowedCountryCodes provided: ${allowedCountryCodes != null}, pspCallbacks provided: ${pspCallbacks != null}, entryLogTime: ${entryLogTime}ms") // Step 2: Determine environment with detailed logging val envDeterminationStart = System.currentTimeMillis() val envInput = env.trim() val envComparisonResult = envInput.equals(ENV_PRODUCTION_LOWERCASE, ignoreCase = true) val environment = if (envComparisonResult) OTLEnvironment.ENV_PRODUCTION else OTLEnvironment.ENV_SANDBOX val envMode = if (environment == OTLEnvironment.ENV_PRODUCTION) "production" else "sandbox" val envDeterminationTime = System.currentTimeMillis() - envDeterminationStart BureauDebug.d(LOG_TAG, "$functionName: Step 2: Environment determination - input: '$envInput', comparison result: $envComparisonResult, selected: $envMode, determinationTime: ${envDeterminationTime}ms, elapsedFromStart: ${System.currentTimeMillis() - startTime}ms") logDebug("$functionName: Step 2: Environment mode selected: $envMode (from input: '$envInput')") // Step 3: Convert allowedCountryCodes from ReadableArray to List val countryCodeConversionStart = System.currentTimeMillis() val countryCodesList: List = if (allowedCountryCodes != null) { val arraySize = allowedCountryCodes.size() BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion - ReadableArray provided, size: $arraySize") val codes = mutableListOf() for (i in 0 until arraySize) { // isNull first, then getString — getString() can be null at runtime despite @NonNull (RN 0.72 vs 0.79+) if (!allowedCountryCodes.isNull(i)) { val code: String? = allowedCountryCodes.getString(i) if (code != null) { val safeCode: String = code codes.add(safeCode) BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion - index $i: '$safeCode'") } else { BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion - index $i: null from getString, skipped") } } else { BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion - index $i: null value skipped") } } codes } else { // Default to ["91"] for backward compatibility BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion - ReadableArray is null, using default: [\"91\"]") listOf("91") } val countryCodeConversionTime = System.currentTimeMillis() - countryCodeConversionStart BureauDebug.d(LOG_TAG, "$functionName: Step 3: Country code conversion complete - final codes: ${countryCodesList.joinToString(", ")}, count: ${countryCodesList.size}, conversionTime: ${countryCodeConversionTime}ms, elapsedFromStart: ${System.currentTimeMillis() - startTime}ms") logDebug("$functionName: Step 3: Allowed country codes: ${countryCodesList.joinToString(", ")}") val pspCallbacksList = mutableListOf() if (pspCallbacks != null) { for (i in 0 until pspCallbacks.size()) { val value = pspCallbacks.getString(i)?.trim() if (value.isNullOrEmpty()) continue pspCallbacksList.add(value) } } val pspCallbacksArray: Array = pspCallbacksList.toTypedArray() BureauDebug.d(LOG_TAG, "$functionName: PSP callbacks count: ${pspCallbacksList.size}") // Step 4: Build BureauAuth instance with detailed Builder logging val builderStart = System.currentTimeMillis() BureauDebug.d(LOG_TAG, "$functionName: Step 4: Building BureauAuth instance - environment: $envMode, timeout: ${timeout}ms (${timeoutInSeconds}s), clientId: $maskedClientId, countryCodes: ${countryCodesList.joinToString(", ")}, pspCallbacksCount: ${pspCallbacksList.size}") val builder = BureauAuth.Builder() BureauDebug.d(LOG_TAG, "$functionName: Step 4.1: Builder created") builder.environment(environment) BureauDebug.d(LOG_TAG, "$functionName: Step 4.2: Builder.environment() called with: $environment") builder.timeOutInMs(timeout) BureauDebug.d(LOG_TAG, "$functionName: Step 4.3: Builder.timeOutInMs() called with: ${timeout}ms") builder.clientId(clientId) BureauDebug.d(LOG_TAG, "$functionName: Step 4.4: Builder.clientId() called with: $maskedClientId") // Use reflection to call allowedCountryCodes if available (for compatibility with older SDK versions) try { val method = builder.javaClass.getMethod("allowedCountryCodes", List::class.java) method.invoke(builder, countryCodesList) BureauDebug.d(LOG_TAG, "$functionName: Step 4.5: Builder.allowedCountryCodes() called with: ${countryCodesList.joinToString(", ")}") } catch (e: NoSuchMethodException) { BureauDebug.d(LOG_TAG, "$functionName: Step 4.5: Builder.allowedCountryCodes() not available, using default country codes") } catch (e: Exception) { BureauDebug.e(LOG_TAG, "$functionName: Step 4.5: Error calling allowedCountryCodes: ${e.message}") } val bureauAuth: BureauAuth = builder.build() val builderTime = System.currentTimeMillis() - builderStart BureauDebug.d(LOG_TAG, "$functionName: Step 4.6: Builder.build() completed - instance created, buildTime: ${builderTime}ms, elapsedFromStart: ${System.currentTimeMillis() - startTime}ms") logDebug("$functionName: Step 4: BureauAuth instance created successfully") // Step 5: Initiate authentication with parameter conversion logging val authCallPrepStart = System.currentTimeMillis() val msisdnLong: Long = try { msisdn.toLong() } catch (e: NumberFormatException) { val errorMsg = "Failed to convert MSISDN to Long: ${e.message}" BureauDebug.e(LOG_TAG, "$functionName: Step 5: MSISDN conversion failed - $errorMsg") logError("$functionName: Step 5: MSISDN conversion error: ${e.message}") promise.reject("MSISDN_CONVERSION_ERROR", errorMsg, e) return } val authCallPrepTime = System.currentTimeMillis() - authCallPrepStart BureauDebug.d(LOG_TAG, "$functionName: Step 5: Authentication call preparation - correlationId: $maskedSessionId, mobileNumber: $maskedMsisdn, msisdnLong: $msisdnLong, conversionTime: ${authCallPrepTime}ms, elapsedFromStart: ${System.currentTimeMillis() - startTime}ms") logDebug("$functionName: Step 5: Making authentication call to SDK") val sdkCallStart = System.currentTimeMillis() BureauDebug.d(LOG_TAG, "$functionName: Step 5.1: Calling bureauAuth.authenticate() - SDK call start time: $sdkCallStart") bureauAuth.authenticate( reactContext, sessionId, msisdnLong, pspCallbacksArray, object : AuthCallback { override fun onResult(authenticationStatus: AuthenticationStatus) { val callbackReceivedTime = System.currentTimeMillis() val sdkCallDuration = callbackReceivedTime - sdkCallStart val elapsedTime = callbackReceivedTime - startTime val elapsedSeconds = String.format("%.4f", elapsedTime / 1000.0) // Step 6: Extract message and status case from authentication status val extractionStart = System.currentTimeMillis() val message = authenticationStatus.message var statusCase: String = authenticationStatus.toString() val extractionTime = System.currentTimeMillis() - extractionStart BureauDebug.d(LOG_TAG, "$functionName: Step 6: SDK callback received - raw status: $statusCase, raw message: $message, extractionTime: ${extractionTime}ms, sdkCallDuration: ${sdkCallDuration}ms, elapsedFromStart: ${elapsedTime}ms") logDebug("$functionName: Step 6: SDK response received - status: $statusCase, message: $message") // Step 7: Detect timeout scenarios with detailed logging val timeoutDetectionStart = System.currentTimeMillis() val isTimeout = detectTimeout(authenticationStatus, startTime, timeout) val timeoutDetectionTime = System.currentTimeMillis() - timeoutDetectionStart BureauDebug.d(LOG_TAG, "$functionName: Step 7: Timeout detection - elapsedTime: ${elapsedTime}ms, timeoutThreshold: ${timeout}ms, statusCase: $statusCase, isTimeout: $isTimeout, detectionTime: ${timeoutDetectionTime}ms") var finalStatus = statusCase var finalMessage = message if (isTimeout) { finalStatus = STATUS_TIMEOUT finalMessage = "Authentication timeout after ${timeout}ms" val timeoutContext = createTimeoutLogContext( clientId, sessionId, timeout, elapsedTime, env ) BureauDebug.e(LOG_TAG, "$functionName: Step 7.1: TIMEOUT detected - $timeoutContext") Log.e(LOG_TAG, "[ERROR] $functionName: TIMEOUT - $timeoutContext") } else { BureauDebug.d(LOG_TAG, "$functionName: Step 7.1: No timeout detected - using original status: $statusCase") } // Step 8: Log final authentication result val resultLogStart = System.currentTimeMillis() if (finalStatus == STATUS_COMPLETED) { BureauDebug.d(LOG_TAG, "$functionName: Step 8: Authentication successful - Status: $finalStatus, Message: $finalMessage, Correlation Id: $maskedSessionId, Duration: ${elapsedSeconds}s, totalTime: ${elapsedTime}ms") } else { BureauDebug.e(LOG_TAG, "$functionName: Step 8: Authentication failed - Status: $finalStatus, Message: $finalMessage, Correlation Id: $maskedSessionId, Duration: ${elapsedSeconds}s, totalTime: ${elapsedTime}ms") } val resultLogTime = System.currentTimeMillis() - resultLogStart // Log authentication result using helper method logAuthenticationResult(finalStatus, finalMessage) // Step 9: Construct result map with detailed logging and latency metrics val resultConstructionStart = System.currentTimeMillis() val totalTime = System.currentTimeMillis() - startTime val result = Arguments.createMap().apply { putString(RESULT_KEY_STATUS, finalStatus) putString(RESULT_KEY_MESSAGE, finalMessage) putInt("totalLatency", totalTime.toInt()) putInt("initLatency", builderTime.toInt()) putInt("authLatency", sdkCallDuration.toInt()) } val resultConstructionTime = System.currentTimeMillis() - resultConstructionStart BureauDebug.d(LOG_TAG, "$functionName: Step 9: Result map constructed - status: $finalStatus, message: $finalMessage, totalLatency: ${totalTime}ms, initLatency: ${builderTime}ms, authLatency: ${sdkCallDuration}ms, constructionTime: ${resultConstructionTime}ms") // Log latency metrics at INFO level for visibility Log.i(LOG_TAG, "[LATENCY] $functionName: Total: ${totalTime}ms, Init: ${builderTime}ms, Auth: ${sdkCallDuration}ms") // Step 10: Resolve promise with timing breakdown val promiseResolveStart = System.currentTimeMillis() BureauDebug.d(LOG_TAG, "$functionName: Step 10: Resolving promise - totalExecutionTime: ${totalTime}ms, breakdown: entryLog=${entryLogTime}ms, envDetermination=${envDeterminationTime}ms, countryCodeConversion=${countryCodeConversionTime}ms, builder=${builderTime}ms, authCallPrep=${authCallPrepTime}ms, sdkCall=${sdkCallDuration}ms, extraction=${extractionTime}ms, timeoutDetection=${timeoutDetectionTime}ms, resultLog=${resultLogTime}ms, resultConstruction=${resultConstructionTime}ms") promise.resolve(result) val promiseResolveTime = System.currentTimeMillis() - promiseResolveStart BureauDebug.d(LOG_TAG, "$functionName: Step 10: Promise resolved - resolveTime: ${promiseResolveTime}ms, totalTime: ${System.currentTimeMillis() - startTime}ms") } }) } }