package com.reactnativestripesdk import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.facebook.react.ReactActivity import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.systeminfo.ReactNativeVersion import com.reactnativestripesdk.addresssheet.AddressLauncherManager import com.reactnativestripesdk.customersheet.CustomerSheetManager import com.reactnativestripesdk.pushprovisioning.PushProvisioningProxy import com.reactnativestripesdk.pushprovisioning.TapAndPayProxy import com.reactnativestripesdk.utils.ConfirmPaymentErrorType import com.reactnativestripesdk.utils.CreateTokenErrorType import com.reactnativestripesdk.utils.DefaultActivityLifecycleCallbacks import com.reactnativestripesdk.utils.ErrorType import com.reactnativestripesdk.utils.GooglePayErrorType import com.reactnativestripesdk.utils.RetrievePaymentIntentErrorType import com.reactnativestripesdk.utils.RetrieveSetupIntentErrorType import com.reactnativestripesdk.utils.StripeUIManager import com.reactnativestripesdk.utils.createCanAddCardResult import com.reactnativestripesdk.utils.createError import com.reactnativestripesdk.utils.createMissingActivityError import com.reactnativestripesdk.utils.createMissingInitError import com.reactnativestripesdk.utils.createResult import com.reactnativestripesdk.utils.getBooleanOr import com.reactnativestripesdk.utils.getIntOrNull import com.reactnativestripesdk.utils.getLongOrNull import com.reactnativestripesdk.utils.getValOr import com.reactnativestripesdk.utils.mapFromPaymentIntentResult import com.reactnativestripesdk.utils.mapFromPaymentMethod import com.reactnativestripesdk.utils.mapFromSetupIntentResult import com.reactnativestripesdk.utils.mapFromToken import com.reactnativestripesdk.utils.mapToAddress import com.reactnativestripesdk.utils.mapToBankAccountType import com.reactnativestripesdk.utils.mapToPaymentMethodType import com.reactnativestripesdk.utils.mapToReturnURL import com.reactnativestripesdk.utils.mapToShippingDetails import com.reactnativestripesdk.utils.mapToUICustomization import com.stripe.android.ApiResultCallback import com.stripe.android.GooglePayJsonFactory import com.stripe.android.PaymentAuthConfig import com.stripe.android.PaymentConfiguration import com.stripe.android.Stripe import com.stripe.android.core.ApiVersion import com.stripe.android.core.AppInfo import com.stripe.android.core.reactnative.ReactNativeAnalytics import com.stripe.android.core.reactnative.ReactNativeSdkInternal import com.stripe.android.customersheet.CustomerSheet import com.stripe.android.googlepaylauncher.GooglePayLauncher import com.stripe.android.model.BankAccountTokenParams import com.stripe.android.model.CardParams import com.stripe.android.model.ConfirmPaymentIntentParams import com.stripe.android.model.ConfirmSetupIntentParams import com.stripe.android.model.PaymentIntent import com.stripe.android.model.PaymentMethod import com.stripe.android.model.RadarSession import com.stripe.android.model.SetupIntent import com.stripe.android.model.Token import com.stripe.android.payments.bankaccount.CollectBankAccountConfiguration import com.stripe.android.paymentsheet.PaymentSheet import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.json.JSONObject internal const val CHECKOUT_UNAVAILABLE_MESSAGE = "Checkout Sessions are temporarily unavailable while the native integration is being rebuilt." private fun rejectCheckoutUnavailable(promise: Promise) { promise.reject(ErrorType.Failed.toString(), CHECKOUT_UNAVAILABLE_MESSAGE) } internal fun createCheckoutUnavailableError(): WritableMap = createError(ErrorType.Failed.toString(), CHECKOUT_UNAVAILABLE_MESSAGE) @ReactModule(name = StripeSdkModule.NAME) @OptIn(ReactNativeSdkInternal::class) class StripeSdkModule( reactContext: ReactApplicationContext, ) : NativeStripeSdkModuleSpec(reactContext) { var cardFieldView: CardFieldView? = null var cardFormView: CardFormView? = null private lateinit var stripe: Stripe private lateinit var publishableKey: String private var stripeAccountId: String? = null private var urlScheme: String? = null private var createPlatformPayPaymentMethodPromise: Promise? = null private var platformPayUsesDeprecatedTokenFlow = false private val stripeUIManagers = mutableListOf() private var paymentSheetManager: PaymentSheetManager? = null private var paymentLauncherManager: PaymentLauncherManager? = null private var collectBankAccountLauncherManager: CollectBankAccountLauncherManager? = null private var financialConnectionsSheetManager: FinancialConnectionsSheetManager? = null private var googlePayLauncherManager: GooglePayLauncherManager? = null private var googlePayPaymentMethodLauncherManager: GooglePayPaymentMethodLauncherManager? = null private var customerSheetManager: CustomerSheetManager? = null private var linkControllerManager: LinkControllerManager? = null internal var embeddedIntentCreationCallback = CompletableDeferred() internal var embeddedConfirmationTokenCreationCallback = CompletableDeferred() internal var customPaymentMethodResultCallback = CompletableDeferred() internal var paymentSheetConfirmationTokenCreationCallback = CompletableDeferred() internal var composeCompatView: StripeAbstractComposeView.CompatView? = null // Storage for pending stripe-connect:// deep link URLs to prevent Expo Router from receiving them private val pendingStripeConnectUrls = mutableListOf() private val pendingUrlsLock = Any() val eventEmitter: EventEmitterCompat by lazy { EventEmitterCompat(reactApplicationContext) } private val mActivityEventListener = object : BaseActivityEventListener() { override fun onActivityResult( activity: Activity, requestCode: Int, resultCode: Int, data: Intent?, ) { if (::stripe.isInitialized) { when (requestCode) { GooglePayRequestHelper.LOAD_PAYMENT_DATA_REQUEST_CODE -> { createPlatformPayPaymentMethodPromise?.let { GooglePayRequestHelper.handleGooglePaymentMethodResult( resultCode, data, stripe, platformPayUsesDeprecatedTokenFlow, it, ) createPlatformPayPaymentMethodPromise = null } } } } } } init { reactContext.addActivityEventListener(mActivityEventListener) } override fun invalidate() { super.invalidate() stripeUIManagers.forEach { it.destroy() } stripeUIManagers.clear() linkControllerManager?.destroy() linkControllerManager = null } private fun registerStripeUIManager(uiManager: StripeUIManager) { uiManager.create() stripeUIManagers.add(uiManager) } private fun unregisterStripeUIManager(uiManager: StripeUIManager?) { val uiManager = uiManager ?: return uiManager.destroy() stripeUIManagers.remove(uiManager) } private fun configure3dSecure(params: ReadableMap) { val stripe3dsConfigBuilder = PaymentAuthConfig.Stripe3ds2Config.Builder() params.getIntOrNull("timeout")?.let { stripe3dsConfigBuilder.setTimeout(it) } val uiCustomization = mapToUICustomization(params) PaymentAuthConfig.init( PaymentAuthConfig .Builder() .set3ds2Config( stripe3dsConfigBuilder .setUiCustomization(uiCustomization) .build(), ).build(), ) } @SuppressLint("RestrictedApi") override fun getTypedExportedConstants(): Map { val packageInfo = try { reactApplicationContext.packageManager.getPackageInfo( reactApplicationContext.packageName, 0, ) } catch (e: Exception) { null } return mapOf( "API_VERSIONS" to mapOf( "CORE" to ApiVersion.API_VERSION_CODE, "ISSUING" to PushProvisioningProxy.getApiVersion(), ), "SYSTEM_INFO" to mapOf( "sdkVersion" to STRIPE_ANDROID_SDK_VERSION, "osVersion" to android.os.Build.VERSION.RELEASE, "deviceType" to "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}", "appName" to ( reactApplicationContext.applicationInfo .loadLabel( reactApplicationContext.packageManager, ).toString() ), "appVersion" to (packageInfo?.versionName ?: ""), "isNewArchitecture" to BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, "reactNativeVersion" to with(ReactNativeVersion.VERSION) { "${get("major")}.${get("minor")}.${get("patch")}" }, ), ) } @SuppressLint("RestrictedApi") @ReactMethod override fun initialise( params: ReadableMap, promise: Promise, ) { val publishableKey = getValOr(params, "publishableKey", null) as String val appInfo = params.getMap("appInfo") as ReadableMap this.stripeAccountId = getValOr(params, "stripeAccountId", null) val urlScheme = getValOr(params, "urlScheme", null) val setReturnUrlSchemeOnAndroid = params.getBooleanOr("setReturnUrlSchemeOnAndroid", false) this.urlScheme = if (setReturnUrlSchemeOnAndroid) urlScheme else null params.getMap("threeDSecureParams")?.let { configure3dSecure(it) } this.publishableKey = publishableKey AddressLauncherManager.publishableKey = publishableKey val name = getValOr(appInfo, "name", "") as String val partnerId = getValOr(appInfo, "partnerId", "") val version = getValOr(appInfo, "version", "") val url = getValOr(appInfo, "url", "") Stripe.appInfo = AppInfo.create(name, version, url, partnerId) stripe = Stripe(reactApplicationContext, publishableKey, stripeAccountId) PaymentConfiguration.init(reactApplicationContext, publishableKey, stripeAccountId) ReactNativeAnalytics.isNewArchitecture = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED ReactNativeAnalytics.reactNativeVersion = with(ReactNativeVersion.VERSION) { "${get("major")}.${get("minor")}.${get("patch")}" } preventActivityRecreation() setupComposeCompatView() promise.resolve(null) } @ReactMethod override fun initPaymentSheet( params: ReadableMap, promise: Promise, ) { if (paymentSheetManager != null) { UiThreadUtil.runOnUiThread { paymentSheetManager?.configure(params, promise) } } else { paymentSheetManager = PaymentSheetManager(reactApplicationContext, params, promise).also { registerStripeUIManager(it) } } } @ReactMethod override fun presentPaymentSheet( options: ReadableMap, promise: Promise, ) { if (paymentSheetManager == null) { promise.resolve(PaymentSheetManager.createMissingInitError()) return } val timeout = options.getLongOrNull("timeout") if (timeout != null) { paymentSheetManager?.presentWithTimeout( timeout, promise, ) } else { paymentSheetManager?.present(promise) } } @ReactMethod override fun confirmPaymentSheetPayment(promise: Promise) { if (paymentSheetManager == null) { promise.resolve(PaymentSheetManager.createMissingInitError()) return } paymentSheetManager?.confirmPayment(promise) } @ReactMethod override fun resetPaymentSheetCustomer(promise: Promise) { PaymentSheet.resetCustomer(context = reactApplicationContext) promise.resolve(null) } @ReactMethod override fun intentCreationCallback( params: ReadableMap, promise: Promise, ) { embeddedIntentCreationCallback.complete(params) if (paymentSheetManager == null) { promise.resolve(PaymentSheetManager.createMissingInitError()) return } paymentSheetManager?.paymentSheetIntentCreationCallback?.complete(params) } @ReactMethod override fun customPaymentMethodResultCallback( result: ReadableMap?, promise: Promise?, ) { // Complete the deferred with the result from JavaScript customPaymentMethodResultCallback.complete(result ?: Arguments.createMap()) // Reset for next use customPaymentMethodResultCallback = CompletableDeferred() promise?.resolve(null) } @ReactMethod override fun confirmationTokenCreationCallback( params: ReadableMap, promise: Promise, ) { embeddedConfirmationTokenCreationCallback.complete(params) paymentSheetConfirmationTokenCreationCallback.complete(params) if (paymentSheetManager == null) { promise.resolve(PaymentSheetManager.createMissingInitError()) return } paymentSheetManager?.paymentSheetConfirmationTokenCreationCallback?.complete(params) } @ReactMethod override fun createPaymentMethod( data: ReadableMap, options: ReadableMap, promise: Promise, ) { val paymentMethodType = getValOr(data, "paymentMethodType")?.let { mapToPaymentMethodType(it) } ?: run { promise.resolve( createError( ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType", ), ) return } val paymentMethodData = data.getMap("paymentMethodData") val factory = PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView) try { val paymentMethodCreateParams = factory.createPaymentMethodParams(paymentMethodType) stripe.createPaymentMethod( paymentMethodCreateParams, callback = object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError("Failed", e)) } override fun onSuccess(result: PaymentMethod) { val paymentMethodMap: WritableMap = mapFromPaymentMethod(result) promise.resolve(createResult("paymentMethod", paymentMethodMap)) } }, ) } catch (error: PaymentMethodCreateParamsException) { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), error)) } } @ReactMethod override fun createToken( params: ReadableMap, promise: Promise, ) { val type = getValOr(params, "type", null) if (type == null) { promise.resolve( createError( CreateTokenErrorType.Failed.toString(), "type parameter is required", ), ) return } when (type) { "BankAccount" -> { createTokenFromBankAccount(params, promise) } "Card" -> { createTokenFromCard(params, promise) } "Pii" -> { createTokenFromPii(params, promise) } else -> { promise.resolve( createError( CreateTokenErrorType.Failed.toString(), "$type type is not supported yet", ), ) } } } private fun createTokenFromPii( params: ReadableMap, promise: Promise, ) { getValOr(params, "personalId", null)?.let { CoroutineScope(Dispatchers.IO).launch { runCatching { val token = stripe.createPiiTokenSynchronous(it, null, stripeAccountId) promise.resolve(createResult("token", mapFromToken(token))) }.onFailure { promise.resolve(createError(CreateTokenErrorType.Failed.toString(), it.message)) } } } ?: run { promise.resolve( createError( CreateTokenErrorType.Failed.toString(), "personalId parameter is required", ), ) } } private fun createTokenFromBankAccount( params: ReadableMap, promise: Promise, ) { val accountHolderName = getValOr(params, "accountHolderName", null) val accountHolderType = getValOr(params, "accountHolderType", null) val accountNumber = getValOr(params, "accountNumber", null) val country = getValOr(params, "country", null) val currency = getValOr(params, "currency", null) val routingNumber = getValOr(params, "routingNumber", null) val bankAccountParams = BankAccountTokenParams( country = country!!, currency = currency!!, accountNumber = accountNumber!!, accountHolderName = accountHolderName, routingNumber = routingNumber, accountHolderType = mapToBankAccountType(accountHolderType), ) CoroutineScope(Dispatchers.IO).launch { runCatching { val token = stripe.createBankAccountTokenSynchronous(bankAccountParams, null, stripeAccountId) promise.resolve(createResult("token", mapFromToken(token))) }.onFailure { promise.resolve(createError(CreateTokenErrorType.Failed.toString(), it.message)) } } } private fun createTokenFromCard( params: ReadableMap, promise: Promise, ) { val cardParamsMap = (cardFieldView?.cardParams ?: cardFormView?.cardParams)?.toParamMap() ?: run { promise.resolve( createError( CreateTokenErrorType.Failed.toString(), "Card details not complete", ), ) return } val cardAddress = cardFieldView?.cardAddress ?: cardFormView?.cardAddress val address = params.getMap("address") val cardParams = CardParams( number = cardParamsMap["number"] as String, expMonth = cardParamsMap["exp_month"] as Int, expYear = cardParamsMap["exp_year"] as Int, cvc = cardParamsMap["cvc"] as String, address = mapToAddress(address, cardAddress), name = getValOr(params, "name", null), currency = getValOr(params, "currency", null), ) CoroutineScope(Dispatchers.IO).launch { try { val token = stripe.createCardTokenSynchronous( cardParams = cardParams, stripeAccountId = stripeAccountId, ) promise.resolve(createResult("token", mapFromToken(token))) } catch (e: Exception) { promise.resolve(createError(CreateTokenErrorType.Failed.toString(), e.message)) } } } @ReactMethod override fun createTokenForCVCUpdate( cvc: String, promise: Promise, ) { stripe.createCvcUpdateToken( cvc, callback = object : ApiResultCallback { override fun onSuccess(result: Token) { val tokenId = result.id val res = Arguments.createMap() res.putString("tokenId", tokenId) promise.resolve(res) } override fun onError(e: Exception) { promise.resolve(createError("Failed", e)) } }, ) } @ReactMethod override fun handleNextAction( paymentIntentClientSecret: String, returnUrl: String?, promise: Promise, ) { unregisterStripeUIManager(paymentSheetManager) paymentLauncherManager = PaymentLauncherManager .forNextActionPayment( context = reactApplicationContext, stripe, publishableKey, stripeAccountId, paymentIntentClientSecret, ).also { registerStripeUIManager(it) it.present(promise) } } @ReactMethod override fun handleNextActionForSetup( setupIntentClientSecret: String, returnUrl: String?, promise: Promise, ) { unregisterStripeUIManager(paymentLauncherManager) paymentLauncherManager = PaymentLauncherManager .forNextActionSetup( context = reactApplicationContext, stripe, publishableKey, stripeAccountId, setupIntentClientSecret, ).also { registerStripeUIManager(it) it.present(promise) } } // TODO: Uncomment when WeChat is re-enabled in stripe-ios // private fun payWithWeChatPay(paymentIntentClientSecret: String, appId: String) { // val activity = currentActivity as ComponentActivity // // activity.lifecycleScope.launch { // stripe.createPaymentMethod(PaymentMethodCreateParams.createWeChatPay()).id?.let { paymentMethodId -> // val confirmPaymentIntentParams = // ConfirmPaymentIntentParams.createWithPaymentMethodId( // paymentMethodId = paymentMethodId, // clientSecret = paymentIntentClientSecret, // paymentMethodOptions = PaymentMethodOptionsParams.WeChatPay( // appId // ) // ) // paymentLauncherFragment.paymentLauncher.confirm(confirmPaymentIntentParams) // } // } // } @ReactMethod override fun confirmPayment( paymentIntentClientSecret: String, params: ReadableMap?, options: ReadableMap?, promise: Promise, ) { val paymentMethodData = params?.getMap("paymentMethodData") val paymentMethodType = if (params != null) { mapToPaymentMethodType(params.getString("paymentMethodType")) ?: run { promise.resolve( createError( ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType", ), ) return } } else { null // Expect that payment method was attached on the server } // if (paymentMethodType == PaymentMethod.Type.WeChatPay) { // val appId = getValOr(params, "appId") ?: run { // promise.resolve(createError("Failed", "You must provide appId")) // return // } // payWithWeChatPay(paymentIntentClientSecret, appId, promise) // // return // } val factory = PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView) try { val confirmParams = factory.createParams( paymentIntentClientSecret, paymentMethodType, isPaymentIntent = true, ) as ConfirmPaymentIntentParams urlScheme?.let { confirmParams.returnUrl = mapToReturnURL(urlScheme) } confirmParams.shipping = mapToShippingDetails(paymentMethodData?.getMap("shippingDetails")) unregisterStripeUIManager(paymentLauncherManager) paymentLauncherManager = PaymentLauncherManager .forPayment( context = reactApplicationContext, stripe, publishableKey, stripeAccountId, paymentIntentClientSecret, confirmParams, ).also { registerStripeUIManager(it) it.present(promise) } } catch (error: PaymentMethodCreateParamsException) { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), error)) } } @ReactMethod override fun retrievePaymentIntent( clientSecret: String, promise: Promise, ) { CoroutineScope(Dispatchers.IO).launch { try { val paymentIntent = stripe.retrievePaymentIntentSynchronous(clientSecret) promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(paymentIntent))) } catch (e: Exception) { promise.resolve(createError(RetrievePaymentIntentErrorType.Unknown.toString(), e)) } } } @ReactMethod override fun retrieveSetupIntent( clientSecret: String, promise: Promise, ) { CoroutineScope(Dispatchers.IO).launch { try { val setupIntent = stripe.retrieveSetupIntentSynchronous(clientSecret) promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(setupIntent))) } catch (e: Exception) { promise.resolve(createError(RetrieveSetupIntentErrorType.Unknown.toString(), e)) } } } @ReactMethod override fun confirmSetupIntent( setupIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise, ) { val paymentMethodType = getValOr(params, "paymentMethodType")?.let { mapToPaymentMethodType(it) } ?: run { promise.resolve( createError( ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType", ), ) return } val factory = PaymentMethodCreateParamsFactory( params.getMap("paymentMethodData"), options, cardFieldView, cardFormView, ) try { val confirmParams = factory.createParams( setupIntentClientSecret, paymentMethodType, isPaymentIntent = false, ) as ConfirmSetupIntentParams urlScheme?.let { confirmParams.returnUrl = mapToReturnURL(urlScheme) } unregisterStripeUIManager(paymentLauncherManager) paymentLauncherManager = PaymentLauncherManager .forSetup( context = reactApplicationContext, stripe, publishableKey, stripeAccountId, setupIntentClientSecret, confirmParams, ).also { registerStripeUIManager(it) it.present(promise) } } catch (error: PaymentMethodCreateParamsException) { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), error)) } } @ReactMethod override fun isPlatformPaySupported( params: ReadableMap?, promise: Promise, ) { val googlePayParams = params?.getMap("googlePay") unregisterStripeUIManager(googlePayPaymentMethodLauncherManager) googlePayPaymentMethodLauncherManager = GooglePayPaymentMethodLauncherManager( reactApplicationContext, googlePayParams?.getBooleanOr("testEnv", false) ?: false, googlePayParams?.getBooleanOr("existingPaymentMethodRequired", false) ?: false, ).also { registerStripeUIManager(it) it.present(promise) } } @ReactMethod override fun confirmPlatformPay( clientSecret: String, params: ReadableMap, isPaymentIntent: Boolean, promise: Promise, ) { if (!::stripe.isInitialized) { promise.resolve(createMissingInitError()) return } val googlePayParams: ReadableMap = params.getMap("googlePay") ?: run { promise.resolve( createError( GooglePayErrorType.Failed.toString(), "You must provide the `googlePay` parameter.", ), ) return } unregisterStripeUIManager(googlePayLauncherManager) googlePayLauncherManager = GooglePayLauncherManager( reactApplicationContext, clientSecret, if (isPaymentIntent) GooglePayLauncherManager.Mode.ForPayment else GooglePayLauncherManager.Mode.ForSetup, googlePayParams, ) { launcherResult, errorMap -> if (errorMap != null) { promise.resolve(errorMap) } else if (launcherResult != null) { when (launcherResult) { GooglePayLauncher.Result.Completed -> { if (isPaymentIntent) { stripe.retrievePaymentIntent( clientSecret, stripeAccountId, expand = listOf("payment_method"), object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createResult("paymentIntent", Arguments.createMap())) } override fun onSuccess(result: PaymentIntent) { promise.resolve( createResult( "paymentIntent", mapFromPaymentIntentResult(result), ), ) } }, ) } else { stripe.retrieveSetupIntent( clientSecret, stripeAccountId, expand = listOf("payment_method"), object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createResult("setupIntent", Arguments.createMap())) } override fun onSuccess(result: SetupIntent) { promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(result))) } }, ) } } GooglePayLauncher.Result.Canceled -> { promise.resolve( createError( GooglePayErrorType.Canceled.toString(), "Google Pay has been canceled", ), ) } is GooglePayLauncher.Result.Failed -> { promise.resolve( createError( GooglePayErrorType.Failed.toString(), launcherResult.error, ), ) } } } }.also { registerStripeUIManager(it) it.present() } } @ReactMethod override fun createPlatformPayPaymentMethod( params: ReadableMap, usesDeprecatedTokenFlow: Boolean, promise: Promise, ) { val googlePayParams: ReadableMap = params.getMap("googlePay") ?: run { promise.resolve( createError( GooglePayErrorType.Failed.toString(), "You must provide the `googlePay` parameter.", ), ) return } platformPayUsesDeprecatedTokenFlow = usesDeprecatedTokenFlow createPlatformPayPaymentMethodPromise = promise getCurrentActivityOrResolveWithError(promise)?.let { val request = GooglePayRequestHelper.createPaymentRequest( it, GooglePayJsonFactory(reactApplicationContext), googlePayParams, ) GooglePayRequestHelper.createPaymentMethod(request, it) } } @ReactMethod override fun canAddCardToWallet( params: ReadableMap, promise: Promise, ) { val last4 = getValOr(params, "cardLastFour", null) ?: run { promise.resolve(createError("Failed", "You must provide cardLastFour")) return } val cardBrand = getValOr(params, "cardBrand", null) ?: run { promise.resolve(createError("Failed", "You must provide cardBrand")) return } if (params.getBooleanOr("supportsTapToPay", true) && !PushProvisioningProxy.isNFCEnabled( reactApplicationContext, ) ) { promise.resolve(createCanAddCardResult(false, "UNSUPPORTED_DEVICE")) return } getCurrentActivityOrResolveWithError(promise)?.let { activity -> TapAndPayProxy.checkEligibility(activity, last4, cardBrand) { canAdd, token, error -> val result = error?.let { createCanAddCardResult(false, "MISSING_CONFIGURATION") } ?: run { val status = if (!canAdd) "CARD_ALREADY_EXISTS" else null createCanAddCardResult(canAdd, status, token) } promise.resolve(result) } } } @ReactMethod override fun isCardInWallet( params: ReadableMap, promise: Promise, ) { val last4 = getValOr(params, "cardLastFour", null) ?: run { promise.resolve(createError("Failed", "You must provide cardLastFour")) return } getCurrentActivityOrResolveWithError(promise)?.let { PushProvisioningProxy.isCardInWallet(it, last4) { isCardInWallet, token, error -> val result: WritableMap = error ?: run { val map = Arguments.createMap() map.putBoolean("isInWallet", isCardInWallet) map.putMap("token", token) map } promise.resolve(result) } } } @ReactMethod override fun collectBankAccount( isPaymentIntent: Boolean, clientSecret: String, params: ReadableMap, promise: Promise, ) { val paymentMethodData = params.getMap("paymentMethodData") val paymentMethodType = mapToPaymentMethodType(getValOr(params, "paymentMethodType", null)) if (paymentMethodType != PaymentMethod.Type.USBankAccount) { promise.resolve( createError( ErrorType.Failed.toString(), "collectBankAccount currently only accepts the USBankAccount payment method type.", ), ) return } val billingDetails = paymentMethodData?.getMap("billingDetails") val name = billingDetails?.getString("name") if (name.isNullOrEmpty()) { promise.resolve( createError( ErrorType.Failed.toString(), "You must provide a name when collecting US bank account details.", ), ) return } val collectParams = CollectBankAccountConfiguration.USBankAccount( name, billingDetails.getString("email"), ) unregisterStripeUIManager(collectBankAccountLauncherManager) collectBankAccountLauncherManager = CollectBankAccountLauncherManager( reactApplicationContext, publishableKey, stripeAccountId, clientSecret, isPaymentIntent, collectParams, ).also { registerStripeUIManager(it) it.present(promise) } } @ReactMethod override fun verifyMicrodeposits( isPaymentIntent: Boolean, clientSecret: String, params: ReadableMap, promise: Promise, ) { val amounts = params.getArray("amounts") val descriptorCode = params.getString("descriptorCode") if ((amounts != null && descriptorCode != null) || (amounts == null && descriptorCode == null)) { promise.resolve( createError( ErrorType.Failed.toString(), "You must provide either amounts OR descriptorCode, not both.", ), ) return } val paymentCallback = object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError(ErrorType.Failed.toString(), e)) } override fun onSuccess(result: PaymentIntent) { promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(result))) } } val setupCallback = object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError(ErrorType.Failed.toString(), e)) } override fun onSuccess(result: SetupIntent) { promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(result))) } } amounts?.let { if (it.size() != 2) { promise.resolve( createError( ErrorType.Failed.toString(), "Expected 2 integers in the amounts array, but received ${it.size()}", ), ) return } if (isPaymentIntent) { stripe.verifyPaymentIntentWithMicrodeposits( clientSecret, it.getInt(0), it.getInt(1), paymentCallback, ) } else { stripe.verifySetupIntentWithMicrodeposits( clientSecret, it.getInt(0), it.getInt(1), setupCallback, ) } } ?: descriptorCode?.let { if (isPaymentIntent) { stripe.verifyPaymentIntentWithMicrodeposits( clientSecret, it, paymentCallback, ) } else { stripe.verifySetupIntentWithMicrodeposits( clientSecret, it, setupCallback, ) } } } @ReactMethod override fun collectBankAccountToken( clientSecret: String, params: ReadableMap, promise: Promise, ) { if (!::stripe.isInitialized) { promise.resolve(createMissingInitError()) return } // Use connectedAccountId from params if provided, otherwise fall back to global stripeAccountId val accountId = getValOr(params, "connectedAccountId", null) ?: stripeAccountId unregisterStripeUIManager(financialConnectionsSheetManager) financialConnectionsSheetManager = FinancialConnectionsSheetManager( reactApplicationContext, clientSecret, FinancialConnectionsSheetManager.Mode.ForToken, publishableKey, accountId, ).also { registerStripeUIManager(it) it.present(promise) } } @ReactMethod override fun collectFinancialConnectionsAccounts( clientSecret: String, params: ReadableMap, promise: Promise, ) { if (!::stripe.isInitialized) { promise.resolve(createMissingInitError()) return } // Use connectedAccountId from params if provided, otherwise fall back to global stripeAccountId val accountId = getValOr(params, "connectedAccountId", null) ?: stripeAccountId unregisterStripeUIManager(financialConnectionsSheetManager) financialConnectionsSheetManager = FinancialConnectionsSheetManager( reactApplicationContext, clientSecret, FinancialConnectionsSheetManager.Mode.ForSession, publishableKey, accountId, ).also { registerStripeUIManager(it) it.present(promise) } } @ReactMethod override fun initCustomerSheet( params: ReadableMap, customerAdapterOverrides: ReadableMap, promise: Promise, ) { if (!::stripe.isInitialized) { promise.resolve(createMissingInitError()) return } unregisterStripeUIManager(customerSheetManager) customerSheetManager = CustomerSheetManager(reactApplicationContext, params, customerAdapterOverrides, promise).also { registerStripeUIManager(it) } } @ReactMethod override fun presentCustomerSheet( params: ReadableMap, promise: Promise, ) { val timeout = params.getLongOrNull("timeout") customerSheetManager?.present(promise, timeout) ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) } } @ReactMethod override fun retrieveCustomerSheetPaymentOptionSelection(promise: Promise) { customerSheetManager?.retrievePaymentOptionSelection(promise) ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) } } @ReactMethod override fun customerAdapterFetchPaymentMethodsCallback( paymentMethodJsonObjects: ReadableArray, promise: Promise, ) { customerSheetManager?.let { fragment -> val paymentMethods = mutableListOf() for (paymentMethodJson in paymentMethodJsonObjects.toArrayList()) { PaymentMethod.fromJson(JSONObject((paymentMethodJson as HashMap<*, *>)))?.let { paymentMethods.add(it) } ?: run { Log.e( "StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method", ) } } fragment.customerAdapter?.fetchPaymentMethodsCallback?.complete(paymentMethods) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun customerAdapterAttachPaymentMethodCallback( paymentMethodJson: ReadableMap, promise: Promise, ) { customerSheetManager?.let { val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap() as HashMap<*, *>)) if (paymentMethod == null) { Log.e( "StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method", ) return } it.customerAdapter?.attachPaymentMethodCallback?.complete(paymentMethod) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun customerAdapterDetachPaymentMethodCallback( paymentMethodJson: ReadableMap, promise: Promise, ) { customerSheetManager?.let { val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap() as HashMap<*, *>)) if (paymentMethod == null) { Log.e( "StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method", ) return } it.customerAdapter?.detachPaymentMethodCallback?.complete(paymentMethod) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun customerAdapterSetSelectedPaymentOptionCallback(promise: Promise) { customerSheetManager?.let { it.customerAdapter?.setSelectedPaymentOptionCallback?.complete(Unit) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun customerAdapterFetchSelectedPaymentOptionCallback( paymentOption: String?, promise: Promise, ) { customerSheetManager?.let { it.customerAdapter?.fetchSelectedPaymentOptionCallback?.complete(paymentOption) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun customerAdapterSetupIntentClientSecretForCustomerAttachCallback( clientSecret: String, promise: Promise, ) { customerSheetManager?.let { it.customerAdapter?.setupIntentClientSecretForCustomerAttachCallback?.complete(clientSecret) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun clientSecretProviderSetupIntentClientSecretCallback( setupIntentClientSecret: String, promise: Promise, ) { customerSheetManager?.let { it.customerSessionProvider?.provideSetupIntentClientSecretCallback?.complete(setupIntentClientSecret) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun clientSecretProviderCustomerSessionClientSecretCallback( customerSessionClientSecretJson: ReadableMap, promise: Promise, ) { val clientSecret = customerSessionClientSecretJson.getString("clientSecret") val customerId = customerSessionClientSecretJson.getString("customerId") if (clientSecret.isNullOrEmpty() || customerId.isNullOrEmpty()) { Log.e( "StripeReactNative", "Invalid CustomerSessionClientSecret format", ) return } customerSheetManager?.let { it.customerSessionProvider?.providesCustomerSessionClientSecretCallback?.complete( CustomerSheet.CustomerSessionClientSecret.create( customerId = customerId, clientSecret = clientSecret, ), ) } ?: run { promise.resolve(CustomerSheetManager.createMissingInitError()) return } } @ReactMethod override fun createRadarSession(promise: Promise) { if (!::stripe.isInitialized) { promise.resolve(createMissingInitError()) return } stripe.createRadarSession( stripeAccountId = stripeAccountId, callback = object : com.stripe.android.ApiResultCallback { override fun onSuccess(result: RadarSession) { val response = WritableNativeMap() response.putString("id", result.id) promise.resolve(response) } override fun onError(e: Exception) { promise.resolve(createError(ErrorType.Failed.toString(), e)) } }, activity = getCurrentActivityOrResolveWithError(promise) as? AppCompatActivity, ) } // Android owns EmbeddedPaymentElement through its native view. Configuration, // update, confirm, and clear commands are handled by EmbeddedPaymentElementViewManager // so they can target the mounted Compose view instance. iOS stores its // EmbeddedPaymentElement on StripeSdkImpl instead, so the shared TurboModule // spec includes these module methods for the iOS implementation. @ReactMethod override fun createEmbeddedPaymentElement( intentConfig: ReadableMap, configuration: ReadableMap, promise: Promise, ) { promise.resolve(null) } @ReactMethod override fun createEmbeddedPaymentElementWithCheckout( sessionKey: String, configuration: ReadableMap, promise: Promise, ) { promise.resolve(null) } @ReactMethod override fun confirmEmbeddedPaymentElement( viewTag: Double, promise: Promise, ) { // No-op on Android. JS dispatches confirm through the view command instead. } @ReactMethod override fun updateEmbeddedPaymentElement( intentConfig: ReadableMap, promise: Promise, ) { // TODO: } @ReactMethod override fun clearEmbeddedPaymentOption( viewTag: Double, promise: Promise, ) { // No-op on Android. JS dispatches clear through the view command instead. } @ReactMethod override fun setFinancialConnectionsForceNativeFlow( enabled: Boolean, promise: Promise, ) { // noop, iOS only } @ReactMethod override fun openAuthenticatedWebView( id: String, url: String, promise: Promise, ) { isAuthWebViewActive = true val activity = getCurrentActivityOrResolveWithError(promise) ?: return UiThreadUtil.runOnUiThread { try { val uri = url.toUri() val builder = androidx.browser.customtabs.CustomTabsIntent .Builder() // Set toolbar color for better UX builder.setShowTitle(true) builder.setUrlBarHidingEnabled(true) val customTabsIntent = builder.build() // NOTE: We intentionally do NOT use FLAG_ACTIVITY_NO_HISTORY here. // That flag can cause React Native's state restoration to fail when returning from Custom Tabs, // resulting in the navigation stack being reset and the previous screen being dismissed. // Custom Tabs will be properly cleaned up when the user navigates back or the session completes. // Note: Custom Tabs doesn't have built-in redirect handling like iOS ASWebAuthenticationSession. // The redirect will be handled via deep linking when the auth server redirects to stripe-connect:// // The React Native Linking module will capture the deep link and pass it back to the JS layer. customTabsIntent.launchUrl(activity, uri) // Fallback: Reset after timeout if JavaScript doesn't call authWebViewDeepLinkHandled Handler(Looper.getMainLooper()).postDelayed({ if (isAuthWebViewActive) { isAuthWebViewActive = false } }, AUTH_WEBVIEW_FALLBACK_TIMEOUT_MS) promise.resolve(null) } catch (e: Exception) { isAuthWebViewActive = false promise.resolve(createError("Failed", e)) } } } @ReactMethod override fun presentExternalWebPage( url: String, promise: Promise, ) { val activity = getCurrentActivityOrResolveWithError(promise) ?: return val uri = url.toUri() if (uri.scheme?.lowercase() !in setOf("http", "https")) { promise.reject(ErrorType.Failed.toString(), "Invalid web URL") return } UiThreadUtil.runOnUiThread { try { androidx.browser.customtabs.CustomTabsIntent .Builder() .setShowTitle(true) .setUrlBarHidingEnabled(true) .build() .launchUrl(activity, uri) promise.resolve(null) } catch (e: Exception) { promise.reject(ErrorType.Failed.toString(), "Failed to open web URL", e) } } } @ReactMethod override fun downloadAndShareFile( url: String, filename: String?, promise: Promise, ) { CoroutineScope(Dispatchers.IO).launch { try { // Download file val client = okhttp3.OkHttpClient() val request = okhttp3.Request .Builder() .url(url) .build() val response = client.newCall(request).execute() if (!response.isSuccessful) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", "NetworkError") putString("message", "HTTP ${response.code}") }, ) return@launch } // Save to cache directory val exportsDir = java.io.File(reactApplicationContext.cacheDir, "stripe-exports") exportsDir.mkdirs() val file = java.io.File(exportsDir, "export-${java.util.UUID.randomUUID()}.csv") response.body?.byteStream()?.use { input -> file.outputStream().use { output -> input.copyTo(output) } } // Share on main thread UiThreadUtil.runOnUiThread { shareFile(file, promise) } } catch (e: Exception) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", "DownloadFailed") putString("message", e.message ?: "Unknown error") }, ) } } } private fun shareFile( file: java.io.File, promise: Promise, ) { val activity = reactApplicationContext.getCurrentActivity() if (activity == null) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", "NoActivity") putString("message", "No activity available") }, ) return } try { val uri = androidx.core.content.FileProvider.getUriForFile( reactApplicationContext, "${reactApplicationContext.packageName}.fileprovider", file, ) val shareIntent = Intent(Intent.ACTION_SEND).apply { type = "text/csv" putExtra(Intent.EXTRA_STREAM, uri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } val chooser = Intent.createChooser(shareIntent, "Share CSV Export") activity.startActivity(chooser) // Schedule cleanup android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ file.delete() }, FILE_CLEANUP_DELAY_MS) promise.resolve( Arguments.createMap().apply { putBoolean("success", true) }, ) } catch (e: Exception) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", "ShareFailed") putString("message", e.message ?: "Unknown error") }, ) } } @ReactMethod override fun authWebViewDeepLinkHandled( id: String, promise: Promise, ) { isAuthWebViewActive = false promise.resolve(null) } /** * Store a stripe-connect:// deep link URL for later retrieval. * This prevents the URL from being broadcast to Expo Router. */ @ReactMethod override fun storeStripeConnectDeepLink( url: String, promise: Promise, ) { synchronized(pendingUrlsLock) { pendingStripeConnectUrls.add(url) } promise.resolve(null) } /** * Poll for pending stripe-connect:// deep link URLs. * Returns all pending URLs and clears the queue. * * URLs are captured by StripeConnectDeepLinkInterceptor Activity, which prevents * Expo Router from receiving the URLs and dismissing the current screen. */ @ReactMethod override fun pollAndClearPendingStripeConnectUrls(promise: Promise) { try { val urlsArray = Arguments.createArray() // Get URLs from SDK's internal storage (set by StripeConnectDeepLinkInterceptor) val sdkUrls = retrieveAndClearPendingUrls() sdkUrls.forEach { url -> urlsArray.pushString(url) } // Legacy: Support old MainActivity pattern (deprecated, will be removed in future version) // This maintains backward compatibility for apps that implemented the manual pattern try { val mainActivityClass = Class.forName("com.stripe.examplestripeconnect.MainActivity") val getPendingUrlsMethod = mainActivityClass.getMethod("getPendingUrls") @Suppress("UNCHECKED_CAST") val mainActivityUrls = getPendingUrlsMethod.invoke(null) as? List if (!mainActivityUrls.isNullOrEmpty()) { Log.w( TAG, "Using deprecated MainActivity.getPendingUrls() pattern. " + "This pattern is no longer needed and will be removed in a future version. " + "The SDK now handles stripe-connect:// URLs automatically.", ) mainActivityUrls.forEach { url -> urlsArray.pushString(url) } } } catch (e: Exception) { // Expected when not using deprecated pattern - this is fine } promise.resolve(urlsArray) } catch (e: Exception) { Log.e(TAG, "Error polling URLs", e) promise.reject("PollError", "Failed to poll pending Stripe Connect URLs: ${e.message}", e) } } override fun addListener(eventType: String?) { // noop, iOS only } override fun removeListeners(count: Double) { // noop, iOS only } override fun handleURLCallback( url: String, promise: Promise, ) { // noop, iOS only. } override fun openApplePaySetup(promise: Promise) { // noop, iOS only. } override fun configureOrderTracking( orderTypeIdentifier: String, orderIdentifier: String, webServiceUrl: String, authenticationToken: String, promise: Promise, ) { // noop, iOS only. } override fun dismissPlatformPay(promise: Promise?) { // noop, iOS only. } override fun updatePlatformPaySheet( summaryItems: ReadableArray?, shippingMethods: ReadableArray?, errors: ReadableArray?, promise: Promise?, ) { // noop, iOS only. } override fun initCheckoutSession( clientSecret: String, configuration: ReadableMap, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutUpdateShippingAddress( sessionKey: String, address: ReadableMap, name: String?, phone: String?, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutApplyPromotionCode( sessionKey: String, code: String, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutRemovePromotionCode( sessionKey: String, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutUpdateLineItemQuantity( sessionKey: String, lineItemId: String, quantity: Double, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutSelectShippingOption( sessionKey: String, id: String, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutRunServerUpdateStart( sessionKey: String, promise: Promise, ) { rejectCheckoutUnavailable(promise) } override fun checkoutRunServerUpdateComplete( sessionKey: String, error: String?, promise: Promise, ) { rejectCheckoutUnavailable(promise) } /** * Safely get and cast the current activity as an AppCompatActivity. If that fails, the promise * provided will be resolved with an error message instructing the user to retry the method. */ private fun getCurrentActivityOrResolveWithError(promise: Promise?): FragmentActivity? { (reactApplicationContext.currentActivity as? FragmentActivity)?.let { return it } promise?.resolve(createMissingActivityError()) return null } private var isRecreatingReactActivity = false private var isAuthWebViewActive = false private val activityLifecycleCallbacks = object : DefaultActivityLifecycleCallbacks() { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle?, ) { // Only set flag when ReactActivity is actually being recreated (savedInstanceState != null) // savedInstanceState != null means this is a recreation, not first creation if (activity is ReactActivity && savedInstanceState != null) { isRecreatingReactActivity = true } // Don't finish Stripe activities during auth webview flow to prevent dismissing the previous screen val isStripeActivity = activity.javaClass.name.startsWith("com.stripe.android") val shouldFinish = isRecreatingReactActivity && isStripeActivity && !isAuthWebViewActive if (shouldFinish) { activity.finish() } // Reset flag after finishing Stripe activities if (isRecreatingReactActivity && shouldFinish) { Handler(Looper.getMainLooper()).post { isRecreatingReactActivity = false } } } } // LinkController - Private Preview @ReactMethod override fun initLinkController( params: ReadableMap, promise: Promise, ) { linkControllerManager?.destroy() linkControllerManager = LinkControllerManager( context = reactApplicationContext, publishableKey = publishableKey, stripeAccountId = stripeAccountId, ) linkControllerManager?.configure(params, promise) } @ReactMethod override fun presentLinkController(promise: Promise) { val manager = linkControllerManager if (manager == null) { promise.resolve(createError(ErrorType.Failed.toString(), LINK_CONTROLLER_NOT_INITIALIZED_ERROR)) return } UiThreadUtil.runOnUiThread { manager.present(promise) } } @ReactMethod override fun confirmLinkControllerSetupIntent( params: ReadableMap, promise: Promise, ) { val manager = linkControllerManager if (manager == null) { promise.resolve(createError(ErrorType.Failed.toString(), LINK_CONTROLLER_NOT_INITIALIZED_ERROR)) return } val clientSecret = params.getString("clientSecret") if (clientSecret == null) { promise.resolve(createError(ErrorType.Failed.toString(), "clientSecret is required.")) return } manager.confirmSetupIntent(clientSecret, promise) } /** * React native apps do not properly handle activity re-creation so make * sure to dismiss any stripe ui when that happens to make sure apps stay * in a consistent state. * * Note that because of some restrictions on some system ui like google * pay this might not always work. */ private fun preventActivityRecreation() { isRecreatingReactActivity = false reactApplicationContext.currentActivity?.application?.unregisterActivityLifecycleCallbacks( activityLifecycleCallbacks ) reactApplicationContext.currentActivity?.application?.registerActivityLifecycleCallbacks(activityLifecycleCallbacks) } private fun setupComposeCompatView() { UiThreadUtil.runOnUiThread { composeCompatView = composeCompatView ?: StripeAbstractComposeView.CompatView( context = reactApplicationContext ).also { reactApplicationContext.currentActivity?.findViewById(android.R.id.content)?.addView( it, ) } } } companion object { const val NAME = NativeStripeSdkModuleSpec.NAME private const val TAG = "StripeSdkModule" // Read the Stripe Android SDK version from gradle.properties at build time private val STRIPE_ANDROID_SDK_VERSION = BuildConfig.STRIPE_ANDROID_SDK_VERSION // Timeout for auth webview fallback (if JavaScript doesn't call authWebViewDeepLinkHandled) private const val AUTH_WEBVIEW_FALLBACK_TIMEOUT_MS = 60_000L private const val FILE_CLEANUP_DELAY_MS = 3_000L // SDK-managed storage for pending stripe-connect:// URLs // This is static because deep links can arrive before ReactContext is available private val pendingConnectUrls = mutableListOf() private val urlsLock = Any() /** * Store a stripe-connect:// deep link URL. * Called automatically by StripeConnectDeepLinkInterceptor. * Can also be called manually from MainActivity if users implement custom handling. * * This method is thread-safe and can be called from any thread. * * @param url The stripe-connect:// URL to store */ @JvmStatic fun storeStripeConnectDeepLink(url: String) { synchronized(urlsLock) { pendingConnectUrls.add(url) } } /** * Retrieve and clear pending URLs. * Internal method used by pollAndClearPendingStripeConnectUrls() bridge method. * * @return List of pending stripe-connect:// URLs */ @JvmStatic internal fun retrieveAndClearPendingUrls(): List { synchronized(urlsLock) { val urls = pendingConnectUrls.toList() pendingConnectUrls.clear() return urls } } } }