package expo.modules.esimmanager import android.app.Activity import android.content.Context import android.content.Intent import android.os.Build import android.telephony.euicc.EuiccManager import android.telephony.TelephonyManager import android.telephony.SubscriptionManager import android.telephony.SubscriptionInfo import android.telephony.euicc.EuiccInfo import android.util.Log import androidx.annotation.RequiresApi import androidx.core.content.getSystemService import androidx.core.net.toUri import expo.modules.kotlin.Promise import expo.modules.kotlin.activityresult.AppContextActivityResultContract import expo.modules.kotlin.activityresult.AppContextActivityResultLauncher import expo.modules.kotlin.exception.CodedException import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record import java.io.Serializable data class InstallParams( @Field val activationCode: String ) : Record data class EsimInfo( @Field val displayName: String, @Field val carrierName: String, @Field val isActive: Boolean, @Field val subscriptionId: Int, @Field val countryIso: String ) : Record class ExpoEsimManagerModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoEsimManager") val euiccManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) appContext.reactContext?.getSystemService() else null AsyncFunction("install") { params: InstallParams, promise: Promise -> Log.d("ExpoEsimManager", "install called with activation code") currentPromise = promise try { CarrierEuiccProvisioningService.setActivationCode(params.activationCode) Log.d("ExpoEsimManager", "Activation code set, launching intent") launchEsimManagerIntent( promise = promise, useQrCode = false ) } catch (e: IllegalArgumentException) { Log.e("ExpoEsimManager", "Invalid activation code: ${e.message}") failure( code = "INVALID_ACTIVATION_CODE", message = e.message ?: "Invalid activation code format" ) } catch (e: Exception) { Log.e("ExpoEsimManager", "Error setting activation code: ${e.message}") failure( code = "ACTIVATION_CODE_ERROR", message = "Failed to process activation code" ) } } AsyncFunction("scanQrCode") { promise: Promise -> currentPromise = promise launchEsimManagerIntent( promise = promise, useQrCode = true ) } Function("isEsimSupported") { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { euiccManager?.isEnabled == true } else { TODO("VERSION.SDK_INT < P") } } Function("getInstalledEsims") { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { getInstalledEsimsInternal() } else { emptyList() } } Function("diagnosticInfo") { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { getDiagnosticInfo() } else { mapOf( "supported" to false, "reason" to "Android API level too low (< 28)" ) } } RegisterActivityContracts { activityResultLauncher = registerForActivityResult(activityResultContract) } } private var currentPromise: Promise? = null private var activityResultLauncher: AppContextActivityResultLauncher? = null private val activityResultContract = object : AppContextActivityResultContract { override fun createIntent(context: Context, input: IntentWrapper): Intent = input.intent override fun parseResult( input: IntentWrapper, resultCode: Int, intent: Intent? ): ActivityResult = ActivityResult(resultCode = resultCode, data = intent) } private fun launchEsimManagerIntent( promise: Promise, useQrCode: Boolean = false ) = runCatching { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { unsupportedEsimError() return@runCatching } val intent = try { if (useQrCode) { // For QR code scanning, try Samsung-specific intent first if (isSamsungDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getSamsungEsimIntent(useQrCode = true) } else { // For non-Samsung devices, try ACTION_VIEW with a generic eSIM URI getGenericEsimIntent() } } else { // For activation code, try ACTION_VIEW first (works for most devices) getActionViewIntent().also { intent -> // Check if there's an activity that can handle this intent val resolveInfo = appContext.reactContext?.packageManager?.resolveActivity( intent, 0 ) if (resolveInfo == null) { throw Exception("No activity found for ACTION_VIEW with eSIM URI") } } } } catch (e: Exception) { Log.d("ExpoEsimManager", "Primary intent failed, trying fallback") // Fallback to Samsung-specific intent if primary fails if (isSamsungDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { getSamsungEsimIntent(useQrCode = useQrCode) } else { throw Exception("No suitable eSIM intent available for this device") } } activityResultLauncher?.launch( input = IntentWrapper(intent), callback = ::handleResult ) }.onFailure(::installError) private fun isSamsungDevice(): Boolean { val manufacturer = Build.MANUFACTURER.lowercase() return manufacturer == "samsung" } @RequiresApi(Build.VERSION_CODES.R) private fun getSamsungEsimIntent(useQrCode: Boolean): Intent { return Intent(EuiccManager.ACTION_START_EUICC_ACTIVATION).apply { putExtra(EuiccManager.EXTRA_USE_QR_SCANNER, useQrCode) } } private fun getActionViewIntent(): Intent { val activationCodeUri = CarrierEuiccProvisioningService.activationCode.toUri() return Intent(Intent.ACTION_VIEW, activationCodeUri) } private fun getGenericEsimIntent(): Intent { // For QR code scanning on non-Samsung devices, try a generic eSIM intent return Intent(Intent.ACTION_VIEW).apply { data = "esim://".toUri() } } private fun handleResult(result: ActivityResult) { Log.d("ExpoEsimManager", "handleResult called with resultCode: ${result.resultCode}") when (result.resultCode) { Activity.RESULT_OK -> { Log.d("ExpoEsimManager", "eSIM installation successful") success() } Activity.RESULT_CANCELED -> { Log.d("ExpoEsimManager", "eSIM installation canceled") handleCanceledResult(result) } else -> { Log.d("ExpoEsimManager", "Unknown result code: ${result.resultCode}") unknownError() } } } private fun handleCanceledResult(result: ActivityResult) { val errorInfo = extractErrorInfo(result) logErrorInfo(errorInfo) val errorResult = determineError(errorInfo) if (errorResult != null) { failure( code = errorResult.code, message = errorResult.message ) } // Se errorResult for null, significa que o success já foi chamado (caso não-Samsung) } private data class ErrorInfo( val detailedErrorCode: Int, val operationCode: Int, val errorCode: Int ) private data class ErrorResult( val code: String, val message: String ) private fun extractErrorInfo(result: ActivityResult): ErrorInfo { return ErrorInfo( detailedErrorCode = result.data?.getIntExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, -1) ?: -1, operationCode = result.data?.getIntExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_OPERATION_CODE, -1) ?: -1, errorCode = result.data?.getIntExtra(EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_ERROR_CODE, -1) ?: -1 ) } private fun logErrorInfo(errorInfo: ErrorInfo) { Log.d("ExpoEsimManager", "detailedErrorCode: ${errorInfo.detailedErrorCode}") Log.d("ExpoEsimManager", "operationCode: ${errorInfo.operationCode}") Log.d("ExpoEsimManager", "errorCode: ${errorInfo.errorCode}") Log.d("ExpoEsimManager", "isSamsungDevice: ${isSamsungDevice()}") } private fun determineError(errorInfo: ErrorInfo): ErrorResult? { // 1. Primeira prioridade: operationCode + errorCode if (errorInfo.operationCode != -1 && errorInfo.errorCode != -1) { Log.d("ExpoEsimManager", "Using operation and error codes") return ErrorResult( code = getErrorCodeFromCombination(errorInfo.operationCode, errorInfo.errorCode), message = getOperationErrorMessage(errorInfo.operationCode, errorInfo.errorCode) ) } // 2. Segunda prioridade: detailedErrorCode if (errorInfo.detailedErrorCode != -1) { Log.d("ExpoEsimManager", "Using detailed error code") val errorMessage = getDetailedErrorMessage(errorInfo.detailedErrorCode) if (errorMessage != null) { return ErrorResult( code = getErrorCode(errorInfo.detailedErrorCode), message = errorMessage ) } } // 3. Fallback: comportamento específico por dispositivo return if (isSamsungDevice()) { Log.d("ExpoEsimManager", "Samsung device: RESULT_CANCELED without error codes - treating as ambiguous result") ErrorResult( code = "AMBIGUOUS_RESULT", message = "eSIM installation result is unclear on Samsung device. The user may have canceled or the installation may have completed. Please check your eSIM settings to verify the installation status." ) } else { Log.d("ExpoEsimManager", "Non-Samsung device: RESULT_CANCELED without error codes - treating as ambiguous result") ErrorResult( code = "AMBIGUOUS_RESULT", message = "eSIM installation result is unclear. The user may have canceled or the installation may have completed. Please check your eSIM settings to verify the installation status." ) } } private fun getDetailedErrorMessage(detailedErrorCode: Int): String? { return when (detailedErrorCode) { EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR -> "eSIM installation failed with resolvable error" EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_ERROR -> "eSIM installation failed with general error" // Specific error codes that indicate invalid eSIM 10001, 10002, 10003 -> "Invalid eSIM activation code or QR code" 10004 -> "eSIM profile already exists" 10005 -> "eSIM installation failed - network error" 10006 -> "eSIM installation failed - server error" 10007 -> "eSIM installation failed - invalid server response" 10008 -> "eSIM installation failed - authentication error" 10009 -> "eSIM installation failed - profile not found" 10010 -> "eSIM installation failed - insufficient storage" -1 -> null // No detailed error code available else -> "eSIM installation failed with error code: $detailedErrorCode" } } private fun getOperationErrorMessage(operationCode: Int, errorCode: Int): String { val operationName = when (operationCode) { 1 -> "System" // EuiccManager.OPERATION_SYSTEM 2 -> "SIM Slot" // EuiccManager.OPERATION_SIM_SLOT 3 -> "eUICC Card" // EuiccManager.OPERATION_EUICC_CARD 4 -> "Switch" // EuiccManager.OPERATION_SWITCH 5 -> "Download" // EuiccManager.OPERATION_DOWNLOAD 6 -> "Metadata" // EuiccManager.OPERATION_METADATA 7 -> "eUICC GSMA" // EuiccManager.OPERATION_EUICC_GSMA 8 -> "APDU" // EuiccManager.OPERATION_APDU 11 -> "HTTP" // EuiccManager.OPERATION_HTTP else -> "Unknown Operation ($operationCode)" } val errorName = when (errorCode) { 10000 -> "Device is carrier locked" 10001 -> "Invalid activation code" 10002 -> "Invalid confirmation code" 10003 -> "Incompatible carrier" 10004 -> "Insufficient memory on eUICC" 10005 -> "Operation timed out" 10006 -> "eUICC is missing or defective" 10007 -> "Unsupported eUICC version" 10008 -> "No SIM card available" 10009 -> "Failed to install profile" 10010 -> "Operation disallowed by profile policy rules" 10011 -> "Address is missing" 10012 -> "Certificate error or authentication failed" 10013 -> "No profiles available" 10014 -> "Failed to create connection" 10015 -> "Invalid response format" 10016 -> "Operation is busy, try again later" else -> "Error $errorCode" } return "$operationName error: $errorName" } private fun getErrorCodeFromCombination(operationCode: Int, errorCode: Int): String { val operation = when (operationCode) { 1 -> "SYSTEM" 2 -> "SIM_SLOT" 3 -> "EUICC_CARD" 4 -> "SWITCH" 5 -> "DOWNLOAD" 6 -> "METADATA" 7 -> "EUICC_GSMA" 8 -> "APDU" 11 -> "HTTP" else -> "UNKNOWN_OPERATION" } val error = when (errorCode) { 10000 -> "CARRIER_LOCKED" 10001 -> "INVALID_ACTIVATION_CODE" 10002 -> "INVALID_CONFIRMATION_CODE" 10003 -> "INCOMPATIBLE_CARRIER" 10004 -> "INSUFFICIENT_MEMORY" 10005 -> "TIMEOUT" 10006 -> "EUICC_MISSING" 10007 -> "UNSUPPORTED_VERSION" 10008 -> "SIM_MISSING" 10009 -> "INSTALL_FAILED" 10010 -> "DISALLOWED_BY_PPR" 10011 -> "ADDRESS_MISSING" 10012 -> "CERTIFICATE_ERROR" 10013 -> "NO_PROFILES_AVAILABLE" 10014 -> "CONNECTION_ERROR" 10015 -> "INVALID_RESPONSE" 10016 -> "OPERATION_BUSY" else -> "UNKNOWN_ERROR" } return "${operation}_${error}" } private fun getErrorCode(detailedErrorCode: Int): String { return when (detailedErrorCode) { EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_RESOLVABLE_ERROR -> "RESOLVABLE_ERROR" EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_ERROR -> "GENERAL_ERROR" 10001, 10002, 10003 -> "INVALID_ACTIVATION_CODE" 10004 -> "PROFILE_ALREADY_EXISTS" 10005 -> "NETWORK_ERROR" 10006 -> "SERVER_ERROR" 10007 -> "INVALID_SERVER_RESPONSE" 10008 -> "AUTHENTICATION_ERROR" 10009 -> "PROFILE_NOT_FOUND" 10010 -> "INSUFFICIENT_STORAGE" else -> "ESIM_ERROR" } } private fun success() { CarrierEuiccProvisioningService.clearActivationCode() currentPromise?.resolve( mapOf( "status" to "success", "message" to "eSIM installed successfully" ) ) } private fun unknownError() = failure( code = "UNKNOWN_ERROR", message = "Unknown error occurred during eSIM activation" ) private fun unsupportedEsimError() = failure( code = "UNSUPPORTED_ERROR", message = "Device is not supported eSIM!" ) private fun userCanceledError() = failure( code = "USER_CANCELED", message = "User canceled eSIM activation" ) private fun installError(throwable: Throwable) = failure( code = "INSTALL_ERROR", message = throwable.message ?: "" ) private fun failure(code: String, message: String) { CarrierEuiccProvisioningService.clearActivationCode() currentPromise?.reject( exception = CodedException( code = code, message = message, cause = null ) ) } @RequiresApi(Build.VERSION_CODES.P) private fun getInstalledEsimsInternal(): List { return try { val context = appContext.reactContext ?: return emptyList() val subscriptionManager = context.getSystemService() ?: return emptyList() val telephonyManager = context.getSystemService() ?: return emptyList() Log.d("ExpoEsimManager", "Getting installed eSIMs") Log.d("ExpoEsimManager", "Android SDK: ${Build.VERSION.SDK_INT}") // Check permissions first val hasReadPhoneState = context.checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == android.content.pm.PackageManager.PERMISSION_GRANTED val hasReadPhoneNumbers = context.checkSelfPermission(android.Manifest.permission.READ_PHONE_NUMBERS) == android.content.pm.PackageManager.PERMISSION_GRANTED if (!hasReadPhoneState || !hasReadPhoneNumbers) { Log.w("ExpoEsimManager", "Missing required permissions") return emptyList() } // Get all active subscriptions (this includes both active and inactive eSIMs) val subscriptions = try { Log.d("ExpoEsimManager", "Using activeSubscriptionInfoList") subscriptionManager.activeSubscriptionInfoList ?: emptyList() } catch (e: SecurityException) { Log.e("ExpoEsimManager", "SecurityException with activeSubscriptionInfoList") emptyList() } Log.d("ExpoEsimManager", "Found ${subscriptions.size} total subscriptions") val esimList = mutableListOf() for (subscription in subscriptions) { try { Log.d("ExpoEsimManager", "Processing subscription, isEmbedded: ${subscription.isEmbedded}") if (subscription.isEmbedded) { Log.d("ExpoEsimManager", "Found eSIM") val defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId() val isActive = subscription.subscriptionId == defaultDataSubId val esimInfo = EsimInfo( displayName = subscription.displayName?.toString() ?: "eSIM", carrierName = subscription.carrierName?.toString() ?: "Unknown Carrier", isActive = isActive, subscriptionId = subscription.subscriptionId, countryIso = subscription.countryIso ?: "" ) esimList.add(esimInfo) Log.d("ExpoEsimManager", "Added eSIM: Active=${esimInfo.isActive}") } } catch (e: SecurityException) { Log.w("ExpoEsimManager", "Security exception accessing subscription info") } catch (e: Exception) { Log.w("ExpoEsimManager", "Error processing subscription") } } Log.d("ExpoEsimManager", "Returning ${esimList.size} eSIMs") esimList } catch (e: Exception) { Log.e("ExpoEsimManager", "Error getting installed eSIMs") emptyList() } } @RequiresApi(Build.VERSION_CODES.P) private fun getDiagnosticInfo(): Map { val context = appContext.reactContext val diagnostics = mutableMapOf() try { diagnostics["androidVersion"] = Build.VERSION.SDK_INT diagnostics["manufacturer"] = Build.MANUFACTURER diagnostics["model"] = Build.MODEL // Check EuiccManager val euiccManager = context?.getSystemService() diagnostics["euiccManagerAvailable"] = (euiccManager != null) diagnostics["euiccEnabled"] = euiccManager?.isEnabled ?: false // Check SubscriptionManager val subscriptionManager = context?.getSystemService() diagnostics["subscriptionManagerAvailable"] = (subscriptionManager != null) if (subscriptionManager != null && context != null) { // Check permissions first val hasReadPhoneState = context.checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == android.content.pm.PackageManager.PERMISSION_GRANTED val hasReadPhoneNumbers = context.checkSelfPermission(android.Manifest.permission.READ_PHONE_NUMBERS) == android.content.pm.PackageManager.PERMISSION_GRANTED if (hasReadPhoneState && hasReadPhoneNumbers) { try { val activeSubscriptions = subscriptionManager.activeSubscriptionInfoList diagnostics["activeSubscriptionsCount"] = activeSubscriptions?.size ?: 0 diagnostics["activeSubscriptionsAccessible"] = true // Add detailed subscription info for debugging if (activeSubscriptions != null) { val subscriptionDetails = mutableListOf>() for (subscription in activeSubscriptions) { subscriptionDetails.add(mapOf( "displayName" to (subscription.displayName?.toString() ?: "null"), "carrierName" to (subscription.carrierName?.toString() ?: "null"), "subscriptionId" to subscription.subscriptionId, "countryIso" to (subscription.countryIso ?: "null") )) } diagnostics["subscriptionDetails"] = subscriptionDetails } } catch (e: SecurityException) { diagnostics["activeSubscriptionsAccessible"] = false diagnostics["subscriptionError"] = "Security exception" } } else { diagnostics["activeSubscriptionsAccessible"] = false diagnostics["subscriptionError"] = "Missing required permissions" diagnostics["missingPermissions"] = listOfNotNull( if (!hasReadPhoneState) "READ_PHONE_STATE" else null, if (!hasReadPhoneNumbers) "READ_PHONE_NUMBERS" else null ) } } // Check permissions if (context != null) { val permissions = listOf( "android.permission.READ_PHONE_STATE", "android.permission.READ_PHONE_NUMBERS" ) val permissionStatus = mutableMapOf() for (permission in permissions) { try { val granted = context.checkSelfPermission(permission) == android.content.pm.PackageManager.PERMISSION_GRANTED permissionStatus[permission] = granted } catch (e: Exception) { permissionStatus[permission] = false } } diagnostics["permissions"] = permissionStatus } } catch (e: Exception) { diagnostics["error"] = "Unknown error" } return diagnostics } } private class ActivityResult( val resultCode: Int, val data: Intent? ) : Serializable private data class IntentWrapper(val intent: Intent) : Serializable