package expo.modules.otpretriever import android.app.Activity import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.util.Log import com.google.android.gms.auth.api.phone.SmsRetriever import com.google.android.gms.auth.api.phone.SmsRetrieverClient import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.tasks.Task import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.otpretriever.utils.AppSignatureHelper import java.util.concurrent.Executors import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import java.util.regex.Pattern class ExpoOtpRetrieverModule : Module() { private val TAG = "ExpoOtpRetriever" private var smsReceiver: BroadcastReceiver? = null private var scheduler: ScheduledExecutorService? = null private var timeoutTask: ScheduledFuture<*>? = null // Define the module with exports and events override fun definition() = ModuleDefinition { // Export methods to JavaScript Name("ExpoOtpRetriever") // Export events that can be listened to from JavaScript Events("otpReceived", "otpTimeout", "otpError") // Start listening for OTP SMS messages AsyncFunction("startOtpListener") { timeoutSeconds: Int -> startOtpListener(timeoutSeconds) } // Stop listening for OTP SMS messages AsyncFunction("stopOtpListener") { stopOtpListener() } // Get the app hash for SMS format AsyncFunction("getAppHash") { getAppHash() } } /** * Starts the SMS Retriever to listen for OTP messages */ private fun startOtpListener(timeoutSeconds: Int) { val appContext = appContext.reactContext ?: run { sendEvent("otpError", mapOf( "code" to "LISTENER_ERROR", "message" to "Could not get application context" )) return } // Check if Google Play Services are available val googleApiAvailability = GoogleApiAvailability.getInstance() val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(appContext) if (resultCode != ConnectionResult.SUCCESS) { sendEvent("otpError", mapOf( "code" to "PLAY_SERVICES_UNAVAILABLE", "message" to "Google Play Services are not available on this device" )) return } // Stop any existing listener first stopOtpListener() // Create a new SMS Retriever Client val client: SmsRetrieverClient = SmsRetriever.getClient(appContext) val task: Task = client.startSmsRetriever() // Set up success/failure listeners for the task task.addOnSuccessListener { Log.d(TAG, "SMS Retriever started successfully") registerBroadcastReceiver() startTimeoutTimer(timeoutSeconds) } task.addOnFailureListener { e -> Log.e(TAG, "Failed to start SMS Retriever", e) sendEvent("otpError", mapOf( "code" to "LISTENER_ERROR", "message" to "Failed to start SMS Retriever: ${e.message}" )) } } /** * Stops listening for OTP messages and cleans up resources */ private fun stopOtpListener() { unregisterBroadcastReceiver() cancelTimeoutTimer() } /** * Registers a broadcast receiver to receive SMS retriever events */ private fun registerBroadcastReceiver() { val appContext = appContext.reactContext ?: return // Create a new broadcast receiver if needed if (smsReceiver == null) { smsReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action) { val extras = intent.extras val status = extras?.get(SmsRetriever.EXTRA_STATUS) when (status) { com.google.android.gms.auth.api.phone.SmsRetriever.StatusCodes.SUCCESS -> { // Get the SMS message content val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE) message?.let { // Extract the OTP code from the message val otp = extractOtpFromMessage(it) otp?.let { code -> // Send the OTP code to JavaScript sendEvent("otpReceived", mapOf("otp" to code)) } ?: run { sendEvent("otpError", mapOf( "code" to "LISTENER_ERROR", "message" to "Received SMS but could not extract OTP code" )) } } // Stop listening after receiving a message stopOtpListener() } com.google.android.gms.auth.api.phone.SmsRetriever.StatusCodes.TIMEOUT -> { sendEvent("otpTimeout", mapOf("message" to "SMS retrieval timed out")) stopOtpListener() } else -> { sendEvent("otpError", mapOf( "code" to "LISTENER_ERROR", "message" to "Unknown status code: $status" )) } } } } } } // Register the broadcast receiver val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION) appContext.registerReceiver(smsReceiver, intentFilter) } /** * Unregisters the broadcast receiver if it exists */ private fun unregisterBroadcastReceiver() { val appContext = appContext.reactContext ?: return smsReceiver?.let { try { appContext.unregisterReceiver(it) } catch (e: IllegalArgumentException) { // Receiver not registered, ignore } smsReceiver = null } } /** * Starts a timer that will emit a timeout event if no OTP is received */ private fun startTimeoutTimer(timeoutSeconds: Int) { cancelTimeoutTimer() scheduler = Executors.newScheduledThreadPool(1) timeoutTask = scheduler?.schedule({ sendEvent("otpTimeout", mapOf("message" to "SMS retrieval timed out")) stopOtpListener() }, timeoutSeconds.toLong(), TimeUnit.SECONDS) } /** * Cancels the timeout timer if it exists */ private fun cancelTimeoutTimer() { timeoutTask?.cancel(true) timeoutTask = null scheduler?.shutdown() scheduler = null } /** * Extracts the OTP code from the SMS message using regex */ private fun extractOtpFromMessage(message: String): String? { // This is a simple regex for finding a 4-6 digit number in the message // You might want to customize this based on your OTP format val pattern = Pattern.compile("\\b([0-9]{4,6})\\b") val matcher = pattern.matcher(message) return if (matcher.find()) { matcher.group(0) } else { null } } /** * Gets the app hash for SMS format */ private fun getAppHash(): String { val appContext = appContext.reactContext ?: run { throw Error("Could not get application context") } val appSignatureHelper = AppSignatureHelper(appContext) return appSignatureHelper.getAppSignatures()[0] } }