diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a4d7a19cc6b..560fe309128f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ Package-specific changes not released in any SDK will be added here just before ### 📚 3rd party library updates - Updated `react-native-safe-area-context` from `4.3.1` to `4.4.1`. ([#19055](https://github.com/expo/expo/pull/19401) by [@brentvatne](https://github.com/brentvatne)) -- Updated `@stripe/stripe-react-native` from `0.13.1` to `0.18.1` on iOS. ([#19055](https://github.com/expo/expo/pull/19055) by [@tsapeta](https://github.com/tsapeta)) +- Updated `@stripe/stripe-react-native` from `0.13.1` to `0.19.0`. ([#19055](https://github.com/expo/expo/pull/19055) by [@tsapeta](https://github.com/tsapeta), [#19432](https://github.com/expo/expo/pull/19432) by [@kudo](https://github.com/kudo)) - Updated `@shopify/flash-list` from `1.1.0` to `1.3.0`. ([#19317](https://github.com/expo/expo/pull/19317) by [@kudo](https://github.com/kudo)) - Updated `react-native-view-shot` from `3.3.0` to `3.4.0`. ([#19405](https://github.com/expo/expo/pull/19405) by [@douglowder](https://github.com/douglowder)) - Updated `react-native-webview` from `11.23.0` to `11.23.1`. ([#19375](https://github.com/expo/expo/pull/19375) by [@aleqsio](https://github.com/aleqsio)) diff --git a/android/app/build.gradle b/android/app/build.gradle index 462a82a232437..e0a1186e503bd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -17,7 +17,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion safeExtGet("compileSdkVersion", 31) + compileSdkVersion safeExtGet("compileSdkVersion", 33) compileOptions { sourceCompatibility JavaVersion.VERSION_11 diff --git a/android/build.gradle b/android/build.gradle index c81389c4a8360..edf6721bed98a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,7 +4,7 @@ buildscript { ext { minSdkVersion = 21 targetSdkVersion = 31 - compileSdkVersion = 31 + compileSdkVersion = 33 dbFlowVersion = '4.2.4' buildToolsVersion = '31.0.0' diff --git a/android/expoview/build.gradle b/android/expoview/build.gradle index 9f2def7f1201b..631e3d4190018 100644 --- a/android/expoview/build.gradle +++ b/android/expoview/build.gradle @@ -110,7 +110,7 @@ repositories { } android { - compileSdkVersion safeExtGet("compileSdkVersion", 31) + compileSdkVersion safeExtGet("compileSdkVersion", 33) // Used to override the NDK path/version on internal CI or by allowing // users to customize the NDK path/version from their root project (e.g. for M1 support) @@ -422,9 +422,10 @@ dependencies { api 'com.github.troZee:ViewPager2:v1.0.6' // stripe-react-native - implementation('com.stripe:stripe-android:20.5.+') - implementation('com.stripe:financial-connections:20.5.+') - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' + implementation('com.stripe:stripe-android:20.12.+') + implementation('com.stripe:financial-connections:20.12.+') + compileOnly 'com.stripe:stripe-android-issuing-push-provisioning:1.1.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" implementation 'com.google.android.material:material:1.3.0' diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/AuBECSDebitFormView.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/AuBECSDebitFormView.kt index 106229e2ad102..4c93a8df97463 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/AuBECSDebitFormView.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/AuBECSDebitFormView.kt @@ -10,6 +10,8 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.getIntOrNull +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.getValOr import com.stripe.android.databinding.BecsDebitWidgetBinding import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.view.BecsDebitWidget diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt index 671a6a372db87..13fc89a51e70f 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt @@ -5,6 +5,7 @@ import android.graphics.Color import android.graphics.Typeface import android.os.Build import android.text.Editable +import android.text.InputFilter import android.text.TextWatcher import android.util.Log import android.widget.FrameLayout @@ -16,6 +17,8 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapCardBrand import com.stripe.android.core.model.CountryCode import com.stripe.android.core.model.CountryUtils import com.stripe.android.databinding.CardInputWidgetBinding @@ -25,7 +28,6 @@ import com.stripe.android.view.CardInputListener import com.stripe.android.view.CardInputWidget import com.stripe.android.view.CardValidCallback import com.stripe.android.view.StripeEditText -import java.lang.Exception class CardFieldView(context: ThemedReactContext) : FrameLayout(context) { private var mCardWidget: CardInputWidget = CardInputWidget(context) @@ -205,12 +207,11 @@ class CardFieldView(context: ThemedReactContext) : FrameLayout(context) { * We can reliable assume that setPostalCodeEnabled is called before * setCountryCode because of the order of the props in CardField.tsx */ - fun setCountryCode(countryCode: String?) { + fun setCountryCode(countryString: String?) { if (mCardWidget.postalCodeEnabled) { - val doesCountryUsePostalCode = CountryUtils.doesCountryUsePostalCode( - CountryCode.create(value = countryCode ?: LocaleListCompat.getAdjustedDefault()[0].country) - ) - mCardWidget.postalCodeRequired = doesCountryUsePostalCode + val countryCode = CountryCode.create(value = countryString ?: LocaleListCompat.getAdjustedDefault()[0]?.country ?: "US") + mCardWidget.postalCodeRequired = CountryUtils.doesCountryUsePostalCode(countryCode) + setPostalCodeFilter(countryCode) } } @@ -275,6 +276,7 @@ class CardFieldView(context: ThemedReactContext) : FrameLayout(context) { cardDetails["validNumber"] = getCardValidationState(CardValidCallback.Fields.Number, cardInputWidgetBinding.cardNumberEditText) cardDetails["validCVC"] = getCardValidationState(CardValidCallback.Fields.Cvc, cardInputWidgetBinding.cvcEditText) cardDetails["validExpiryDate"] = getCardValidationState(CardValidCallback.Fields.Expiry, cardInputWidgetBinding.expiryDateEditText) + cardDetails["brand"] = mapCardBrand(cardInputWidgetBinding.cardNumberEditText.cardBrand) if (isValid) { onValidCardChange() @@ -335,6 +337,26 @@ class CardFieldView(context: ThemedReactContext) : FrameLayout(context) { }) } + private fun setPostalCodeFilter(countryCode: CountryCode) { + cardInputWidgetBinding.postalCodeEditText.filters = arrayOf( + *cardInputWidgetBinding.postalCodeEditText.filters, + createPostalCodeInputFilter(countryCode) + ) + } + + private fun createPostalCodeInputFilter(countryCode: CountryCode): InputFilter { + return InputFilter { charSequence, start, end, _, _, _ -> + for (i in start until end) { + val isValidCharacter = (countryCode == CountryCode.US && PostalCodeUtilities.isValidUsPostalCodeCharacter(charSequence[i])) || + (countryCode != CountryCode.US && PostalCodeUtilities.isValidGlobalPostalCodeCharacter(charSequence[i])) + if (!isValidCharacter) { + return@InputFilter "" + } + } + return@InputFilter null + } + } + override fun requestLayout() { super.requestLayout() post(mLayoutRunnable) diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFormView.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFormView.kt index 67ef53c2e61f0..7ddd2f0ccb11c 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFormView.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFormView.kt @@ -4,6 +4,7 @@ import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Typeface import android.os.Build +import android.text.InputFilter import android.view.View import android.view.View.OnFocusChangeListener import android.widget.FrameLayout @@ -14,6 +15,8 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapCardBrand import com.stripe.android.core.model.CountryCode import com.stripe.android.databinding.CardMultilineWidgetBinding import com.stripe.android.databinding.StripeCardFormViewBinding @@ -51,10 +54,15 @@ class CardFormView(context: ThemedReactContext) : FrameLayout(context) { } fun setDefaultValues(defaults: ReadableMap) { - defaults.getString("countryCode")?.let { - cardFormViewBinding.countryLayout.setSelectedCountryCode(CountryCode(it)) - cardFormViewBinding.countryLayout.updateUiForCountryEntered(CountryCode(it)) + setCountry(defaults.getString("countryCode")) + } + + private fun setCountry(countryString: String?) { + if (countryString != null) { + cardFormViewBinding.countryLayout.setSelectedCountryCode(CountryCode(countryString)) + cardFormViewBinding.countryLayout.updateUiForCountryEntered(CountryCode(countryString)) } + setPostalCodeFilter() } fun setPlaceHolders(value: ReadableMap) { @@ -255,6 +263,29 @@ class CardFormView(context: ThemedReactContext) : FrameLayout(context) { } } + private fun setPostalCodeFilter() { + cardFormViewBinding.postalCode.filters = arrayOf( + *cardFormViewBinding.postalCode.filters, + createPostalCodeInputFilter() + ) + } + + private fun createPostalCodeInputFilter(): InputFilter { + return InputFilter { charSequence, start, end, _, _, _ -> + if (cardFormViewBinding.countryLayout.getSelectedCountryCode() == CountryCode.US) { + // Rely on CardFormView's built-in US postal code filter + return@InputFilter null + } + + for (i in start until end) { + if (!PostalCodeUtilities.isValidGlobalPostalCodeCharacter(charSequence[i])) { + return@InputFilter "" + } + } + return@InputFilter null + } + } + override fun requestLayout() { super.requestLayout() post(mLayoutRunnable) diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt index 3c05074265a8b..7ac63d26a638e 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt @@ -9,6 +9,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createResult +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapFromPaymentIntentResult +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapFromSetupIntentResult import com.stripe.android.model.PaymentIntent import com.stripe.android.model.SetupIntent import com.stripe.android.model.StripeIntent @@ -19,6 +24,7 @@ import com.stripe.android.payments.bankaccount.navigation.CollectBankAccountResu class CollectBankAccountLauncherFragment( private val context: ReactApplicationContext, private val publishableKey: String, + private val stripeAccountId: String?, private val clientSecret: String, private val isPaymentIntent: Boolean, private val collectParams: CollectBankAccountConfiguration.USBankAccount, @@ -43,12 +49,14 @@ class CollectBankAccountLauncherFragment( if (isPaymentIntent) { collectBankAccountLauncher.presentWithPaymentIntent( publishableKey, + stripeAccountId, clientSecret, collectParams ) } else { collectBankAccountLauncher.presentWithSetupIntent( publishableKey, + stripeAccountId, clientSecret, collectParams ) @@ -78,7 +86,11 @@ class CollectBankAccountLauncherFragment( promise.resolve(createError(ErrorType.Failed.toString(), result.error)) } } - (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() + removeFragment(context) } } + + companion object { + const val TAG = "collect_bank_account_launcher_fragment" + } } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/FinancialConnectionsSheetFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/FinancialConnectionsSheetFragment.kt new file mode 100644 index 0000000000000..c39a11b351588 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/FinancialConnectionsSheetFragment.kt @@ -0,0 +1,278 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.facebook.react.bridge.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createMissingActivityError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapFromToken +import com.stripe.android.financialconnections.FinancialConnectionsSheet +import com.stripe.android.financialconnections.FinancialConnectionsSheetForTokenResult +import com.stripe.android.financialconnections.FinancialConnectionsSheetResult +import com.stripe.android.financialconnections.model.* + +class FinancialConnectionsSheetFragment : Fragment() { + enum class Mode { + ForToken, ForSession + } + + private lateinit var promise: Promise + private lateinit var context: ReactApplicationContext + private lateinit var configuration: FinancialConnectionsSheet.Configuration + private lateinit var mode: Mode + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + return FrameLayout(requireActivity()).also { + it.visibility = View.GONE + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + when (mode) { + Mode.ForToken -> { + FinancialConnectionsSheet.createForBankAccountToken( + this, + ::onFinancialConnectionsSheetForTokenResult + ).present( + configuration = configuration + ) + } + Mode.ForSession -> { + FinancialConnectionsSheet.create( + this, + ::onFinancialConnectionsSheetForDataResult + ).present( + configuration = configuration + ) + } + } + } + + private fun onFinancialConnectionsSheetForTokenResult(result: FinancialConnectionsSheetForTokenResult) { + when(result) { + is FinancialConnectionsSheetForTokenResult.Canceled -> { + promise.resolve( + createError(ErrorType.Canceled.toString(), "The flow has been canceled") + ) + } + is FinancialConnectionsSheetForTokenResult.Failed -> { + promise.resolve( + createError(ErrorType.Failed.toString(), result.error) + ) + } + is FinancialConnectionsSheetForTokenResult.Completed -> { + promise.resolve(createTokenResult(result)) + (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() + } + } + } + + private fun onFinancialConnectionsSheetForDataResult(result: FinancialConnectionsSheetResult) { + when(result) { + is FinancialConnectionsSheetResult.Canceled -> { + promise.resolve( + createError(ErrorType.Canceled.toString(), "The flow has been canceled") + ) + } + is FinancialConnectionsSheetResult.Failed -> { + promise.resolve( + createError(ErrorType.Failed.toString(), result.error) + ) + } + is FinancialConnectionsSheetResult.Completed -> { + promise.resolve( + WritableNativeMap().also { + it.putMap("session", mapFromSession(result.financialConnectionsSession)) + } + ) + (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() + } + } + } + + fun presentFinancialConnectionsSheet(clientSecret: String, mode: Mode, publishableKey: String, stripeAccountId: String?, promise: Promise, context: ReactApplicationContext) { + this.promise = promise + this.context = context + this.mode = mode + this.configuration = FinancialConnectionsSheet.Configuration( + financialConnectionsSessionClientSecret = clientSecret, + publishableKey = publishableKey, + stripeAccountId = stripeAccountId, + ) + + (context.currentActivity as? AppCompatActivity)?.let { + attemptToCleanupPreviousFragment(it) + commitFragmentAndStartFlow(it) + } ?: run { + promise.resolve(createMissingActivityError()) + return + } + } + + private fun attemptToCleanupPreviousFragment(currentActivity: AppCompatActivity) { + currentActivity.supportFragmentManager.beginTransaction() + .remove(this) + .commitAllowingStateLoss() + } + + private fun commitFragmentAndStartFlow(currentActivity: AppCompatActivity) { + try { + currentActivity.supportFragmentManager.beginTransaction() + .add(this, "financial_connections_sheet_launch_fragment") + .commit() + } catch (error: IllegalStateException) { + promise.resolve(createError(ErrorType.Failed.toString(), error.message)) + } + } + + companion object { + private fun createTokenResult(result: FinancialConnectionsSheetForTokenResult.Completed): WritableMap { + return WritableNativeMap().also { + it.putMap("session", mapFromSession(result.financialConnectionsSession)) + it.putMap("token", mapFromToken(result.token)) + } + } + + private fun mapFromSession(financialConnectionsSession: FinancialConnectionsSession): WritableMap { + val session = WritableNativeMap() + session.putString("id", financialConnectionsSession.id) + session.putString("clientSecret", financialConnectionsSession.clientSecret) + session.putBoolean("livemode", financialConnectionsSession.livemode) + session.putArray("accounts", mapFromAccountsList(financialConnectionsSession.accounts)) + return session + } + + private fun mapFromAccountsList(accounts: FinancialConnectionsAccountList): ReadableArray { + val results: WritableArray = Arguments.createArray() + for (account in accounts.data) { + val map = WritableNativeMap() + map.putString("id", account.id) + map.putBoolean("livemode", account.livemode) + map.putString("displayName", account.displayName) + map.putString("status", mapFromStatus(account.status)) + map.putString("institutionName", account.institutionName) + map.putString("last4", account.last4) + map.putDouble("created", account.created * 1000.0) + map.putMap("balance", mapFromAccountBalance(account.balance)) + map.putMap("balanceRefresh", mapFromAccountBalanceRefresh(account.balanceRefresh)) + map.putString("category", mapFromCategory(account.category)) + map.putString("subcategory", mapFromSubcategory(account.subcategory)) + map.putArray("permissions", (account.permissions?.map { permission -> mapFromPermission(permission) })?.toReadableArray()) + map.putArray("supportedPaymentMethodTypes", (account.supportedPaymentMethodTypes.map { type -> mapFromSupportedPaymentMethodTypes(type) }).toReadableArray()) + results.pushMap(map) + } + return results + } + + private fun mapFromAccountBalance(balance: Balance?): WritableMap? { + if (balance == null) { + return null + } + val map = WritableNativeMap() + map.putDouble("asOf", balance.asOf * 1000.0) + map.putString("type", mapFromBalanceType(balance.type)) + map.putMap("current", balance.current as ReadableMap) + WritableNativeMap().also { + it.putMap("available", balance.cash?.available as ReadableMap) + map.putMap("cash", it) + } + WritableNativeMap().also { + it.putMap("used", balance.credit?.used as ReadableMap) + map.putMap("credit", it) + } + return map + } + + private fun mapFromAccountBalanceRefresh(balanceRefresh: BalanceRefresh?): WritableMap? { + if (balanceRefresh == null) { + return null + } + val map = WritableNativeMap() + map.putString("status", mapFromBalanceRefreshStatus(balanceRefresh.status)) + map.putDouble("lastAttemptedAt", balanceRefresh.lastAttemptedAt * 1000.0) + return map + } + + private fun mapFromStatus(status: FinancialConnectionsAccount.Status): String { + return when (status) { + FinancialConnectionsAccount.Status.ACTIVE -> "active" + FinancialConnectionsAccount.Status.DISCONNECTED -> "disconnected" + FinancialConnectionsAccount.Status.INACTIVE -> "inactive" + FinancialConnectionsAccount.Status.UNKNOWN -> "unparsable" + } + } + + private fun mapFromCategory(category: FinancialConnectionsAccount.Category): String { + return when (category) { + FinancialConnectionsAccount.Category.CASH -> "cash" + FinancialConnectionsAccount.Category.CREDIT -> "credit" + FinancialConnectionsAccount.Category.INVESTMENT -> "investment" + FinancialConnectionsAccount.Category.OTHER -> "other" + FinancialConnectionsAccount.Category.UNKNOWN -> "unparsable" + } + } + + private fun mapFromSubcategory(subcategory: FinancialConnectionsAccount.Subcategory): String { + return when (subcategory) { + FinancialConnectionsAccount.Subcategory.CHECKING -> "checking" + FinancialConnectionsAccount.Subcategory.CREDIT_CARD -> "creditCard" + FinancialConnectionsAccount.Subcategory.LINE_OF_CREDIT -> "lineOfCredit" + FinancialConnectionsAccount.Subcategory.MORTGAGE -> "mortgage" + FinancialConnectionsAccount.Subcategory.OTHER -> "other" + FinancialConnectionsAccount.Subcategory.SAVINGS -> "savings" + FinancialConnectionsAccount.Subcategory.UNKNOWN -> "unparsable" + } + } + + private fun mapFromPermission(permission: FinancialConnectionsAccount.Permissions): String { + return when (permission) { + FinancialConnectionsAccount.Permissions.PAYMENT_METHOD -> "paymentMethod" + FinancialConnectionsAccount.Permissions.BALANCES -> "balances" + FinancialConnectionsAccount.Permissions.OWNERSHIP -> "ownership" + FinancialConnectionsAccount.Permissions.TRANSACTIONS -> "transactions" + FinancialConnectionsAccount.Permissions.UNKNOWN -> "unparsable" + } + } + + private fun mapFromSupportedPaymentMethodTypes(type: FinancialConnectionsAccount.SupportedPaymentMethodTypes): String { + return when (type) { + FinancialConnectionsAccount.SupportedPaymentMethodTypes.US_BANK_ACCOUNT -> "usBankAccount" + FinancialConnectionsAccount.SupportedPaymentMethodTypes.LINK -> "link" + FinancialConnectionsAccount.SupportedPaymentMethodTypes.UNKNOWN -> "unparsable" + } + } + + private fun mapFromBalanceType(type: Balance.Type): String { + return when (type) { + Balance.Type.CASH -> "cash" + Balance.Type.CREDIT -> "credit" + Balance.Type.UNKNOWN -> "unparsable" + } + } + + private fun mapFromBalanceRefreshStatus(status: BalanceRefresh.BalanceRefreshStatus?): String { + return when (status) { + BalanceRefresh.BalanceRefreshStatus.SUCCEEDED -> "succeeded" + BalanceRefresh.BalanceRefreshStatus.FAILED -> "failed" + BalanceRefresh.BalanceRefreshStatus.PENDING -> "pending" + BalanceRefresh.BalanceRefreshStatus.UNKNOWN -> "unparsable" + null -> "null" + } + } + } +} + +fun List.toReadableArray(): ReadableArray { + val results: WritableArray = Arguments.createArray() + for (s in this) { + results.pushString(s) + } + return results +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayFragment.kt index 7dbc520cb78a5..df97038e3d649 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayFragment.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayFragment.kt @@ -8,6 +8,10 @@ import android.widget.FrameLayout import androidx.fragment.app.Fragment import com.facebook.react.bridge.Promise import com.facebook.react.bridge.WritableNativeMap +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.GooglePayErrorType +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createResult +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapFromPaymentMethod import com.stripe.android.googlepaylauncher.GooglePayEnvironment import com.stripe.android.googlepaylauncher.GooglePayLauncher import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher @@ -200,4 +204,8 @@ class GooglePayFragment(private val initPromise: Promise) : Fragment() { isPhoneNumberRequired = isPhoneNumberRequired ) } + + companion object { + const val TAG = "google_pay_launch_fragment" + } } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayPaymentMethodLauncherFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayPaymentMethodLauncherFragment.kt index d8f70355701c6..1f6b6fa2aff22 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayPaymentMethodLauncherFragment.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayPaymentMethodLauncherFragment.kt @@ -5,10 +5,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.removeFragment import com.stripe.android.googlepaylauncher.GooglePayEnvironment import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher @@ -37,9 +37,13 @@ class GooglePayPaymentMethodLauncherFragment( ), readyCallback = { promise.resolve(it) - (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() + removeFragment(context) }, resultCallback = {} ) } + + companion object { + const val TAG = "google_pay_support_fragment" + } } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentLauncherFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentLauncherFragment.kt index 9e85ae11b0972..ed547470db162 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentLauncherFragment.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentLauncherFragment.kt @@ -9,6 +9,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createMissingActivityError import com.stripe.android.ApiResultCallback import com.stripe.android.Stripe import com.stripe.android.model.* @@ -107,7 +110,7 @@ class PaymentLauncherFragment( (context.currentActivity as? AppCompatActivity)?.let { try { it.supportFragmentManager.beginTransaction() - .add(fragment, "payment_launcher_fragment") + .add(fragment, TAG) .commit() } catch (error: IllegalStateException) { promise.resolve(createError(ErrorType.Failed.toString(), error.message)) @@ -116,6 +119,8 @@ class PaymentLauncherFragment( promise.resolve(createMissingActivityError()) } } + + const val TAG = "payment_launcher_fragment" } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, @@ -151,25 +156,21 @@ class PaymentLauncherFragment( } is PaymentResult.Canceled -> { promise.resolve(createError(ConfirmPaymentErrorType.Canceled.toString(), message = null)) - cleanup() + removeFragment(context) } is PaymentResult.Failed -> { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), paymentResult.throwable)) - cleanup() + removeFragment(context) } } } } - private fun cleanup() { - (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() - } - private fun retrieveSetupIntent(clientSecret: String, stripeAccountId: String?) { stripe.retrieveSetupIntent(clientSecret, stripeAccountId, object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError(ConfirmSetupIntentErrorType.Failed.toString(), e)) - cleanup() + removeFragment(context) } override fun onSuccess(result: SetupIntent) { @@ -201,7 +202,7 @@ class PaymentLauncherFragment( promise.resolve(createError(ConfirmSetupIntentErrorType.Unknown.toString(), "unhandled error: ${result.status}")) } } - cleanup() + removeFragment(context) } }) } @@ -210,7 +211,7 @@ class PaymentLauncherFragment( stripe.retrievePaymentIntent(clientSecret, stripeAccountId, object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), e)) - cleanup() + removeFragment(context) } override fun onSuccess(result: PaymentIntent) { @@ -242,7 +243,7 @@ class PaymentLauncherFragment( promise.resolve(createError(ConfirmPaymentErrorType.Unknown.toString(), "unhandled error: ${result.status}")) } } - cleanup() + removeFragment(context) } }) } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt index 18eadf637706b..47bdafb4269d4 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt @@ -1,10 +1,13 @@ package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk import com.facebook.react.bridge.ReadableMap +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapToBillingDetails +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapToUSBankAccountHolderType +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapToUSBankAccountType import com.stripe.android.model.* class PaymentMethodCreateParamsFactory( - private val clientSecret: String, private val paymentMethodData: ReadableMap?, private val options: ReadableMap, private val cardFieldView: CardFieldView?, @@ -13,26 +16,27 @@ class PaymentMethodCreateParamsFactory( private val billingDetailsParams = mapToBillingDetails(getMapOrNull(paymentMethodData, "billingDetails"), cardFieldView?.cardAddress ?: cardFormView?.cardAddress) @Throws(PaymentMethodCreateParamsException::class) - fun createConfirmParams(paymentMethodType: PaymentMethod.Type): ConfirmPaymentIntentParams { + fun createPaymentMethodParams(paymentMethodType: PaymentMethod.Type): PaymentMethodCreateParams { try { return when (paymentMethodType) { - PaymentMethod.Type.Card -> createCardPaymentConfirmParams() - PaymentMethod.Type.Ideal -> createIDEALPaymentConfirmParams() - PaymentMethod.Type.Alipay -> createAlipayPaymentConfirmParams() - PaymentMethod.Type.Sofort -> createSofortPaymentConfirmParams() - PaymentMethod.Type.Bancontact -> createBancontactPaymentConfirmParams() - PaymentMethod.Type.SepaDebit -> createSepaPaymentConfirmParams() - PaymentMethod.Type.Oxxo -> createOXXOPaymentConfirmParams() - PaymentMethod.Type.Giropay -> createGiropayPaymentConfirmParams() - PaymentMethod.Type.Eps -> createEPSPaymentConfirmParams() - PaymentMethod.Type.GrabPay -> createGrabPayPaymentConfirmParams() - PaymentMethod.Type.P24 -> createP24PaymentConfirmParams() - PaymentMethod.Type.Fpx -> createFpxPaymentConfirmParams() - PaymentMethod.Type.AfterpayClearpay -> createAfterpayClearpayPaymentConfirmParams() - PaymentMethod.Type.AuBecsDebit -> createAuBecsDebitPaymentConfirmParams() - PaymentMethod.Type.Klarna -> createKlarnaPaymentConfirmParams() - PaymentMethod.Type.USBankAccount -> createUSBankAccountPaymentConfirmParams() - PaymentMethod.Type.PayPal -> createPayPalPaymentConfirmParams() + PaymentMethod.Type.Card -> createCardPaymentMethodParams() + PaymentMethod.Type.Ideal -> createIDEALParams() + PaymentMethod.Type.Alipay -> createAlipayParams() + PaymentMethod.Type.Sofort -> createSofortParams() + PaymentMethod.Type.Bancontact -> createBancontactParams() + PaymentMethod.Type.SepaDebit -> createSepaParams() + PaymentMethod.Type.Oxxo -> createOXXOParams() + PaymentMethod.Type.Giropay -> createGiropayParams() + PaymentMethod.Type.Eps -> createEPSParams() + PaymentMethod.Type.GrabPay -> createGrabPayParams() + PaymentMethod.Type.P24 -> createP24Params() + PaymentMethod.Type.Fpx -> createFpxParams() + PaymentMethod.Type.AfterpayClearpay -> createAfterpayClearpayParams() + PaymentMethod.Type.AuBecsDebit -> createAuBecsDebitParams() + PaymentMethod.Type.Klarna -> createKlarnaParams() + PaymentMethod.Type.USBankAccount -> createUSBankAccountParams(paymentMethodData) + PaymentMethod.Type.PayPal -> createPayPalParams() + PaymentMethod.Type.Affirm -> createAffirmParams() else -> { throw Exception("This paymentMethodType is not supported yet") } @@ -43,351 +47,116 @@ class PaymentMethodCreateParamsFactory( } @Throws(PaymentMethodCreateParamsException::class) - fun createSetupParams(paymentMethodType: PaymentMethod.Type): ConfirmSetupIntentParams { - try { - return when (paymentMethodType) { - PaymentMethod.Type.Card -> createCardPaymentSetupParams() - PaymentMethod.Type.Ideal -> createIDEALPaymentSetupParams() - PaymentMethod.Type.Sofort -> createSofortPaymentSetupParams() - PaymentMethod.Type.Bancontact -> createBancontactPaymentSetupParams() - PaymentMethod.Type.SepaDebit -> createSepaPaymentSetupParams() - PaymentMethod.Type.AuBecsDebit -> createAuBecsDebitPaymentSetupParams() - PaymentMethod.Type.USBankAccount -> createUSBankAccountPaymentSetupParams() - PaymentMethod.Type.PayPal -> createPayPalPaymentSetupParams() - else -> { - throw Exception("This paymentMethodType is not supported yet") - } - } - } catch (error: PaymentMethodCreateParamsException) { - throw error - } - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createIDEALPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createIDEALParams(): PaymentMethodCreateParams { val bankName = getValOr(paymentMethodData, "bankName", null) val idealParams = PaymentMethodCreateParams.Ideal(bankName) - val createParams = - PaymentMethodCreateParams.create(ideal = idealParams, billingDetails = billingDetailsParams) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = createParams, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), - ) - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createP24PaymentConfirmParams(): ConfirmPaymentIntentParams { - billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createP24(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), - ) - } - - throw PaymentMethodCreateParamsException("You must provide billing details") - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createCardPaymentConfirmParams(): ConfirmPaymentIntentParams { - val paymentMethodId = getValOr(paymentMethodData, "paymentMethodId", null) - val token = getValOr(paymentMethodData, "token", null) - - val cardParams = cardFieldView?.cardParams ?: cardFormView?.cardParams - - if (cardParams == null && paymentMethodId == null && token == null) { - throw PaymentMethodCreateParamsException("Card details not complete") - } - - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - - if (paymentMethodId != null) { - val cvc = getValOr(paymentMethodData, "cvc", null) - val paymentMethodOptionParams = - if (cvc != null) PaymentMethodOptionsParams.Card(cvc) else null - - return ConfirmPaymentIntentParams.createWithPaymentMethodId( - paymentMethodId = paymentMethodId, - paymentMethodOptions = paymentMethodOptionParams, - clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage, - ) - } else { - var card = cardParams - if (token != null) { - card = PaymentMethodCreateParams.Card.create(token) - } - - val paymentMethodCreateParams = PaymentMethodCreateParams.create(card!!, billingDetailsParams) - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = paymentMethodCreateParams, - clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage, - ) - } + return PaymentMethodCreateParams.create(ideal = idealParams, billingDetails = billingDetailsParams) } @Throws(PaymentMethodCreateParamsException::class) - private fun createIDEALPaymentSetupParams(): ConfirmSetupIntentParams { - val bankName = getValOr(paymentMethodData, "bankName", null) - - val idealParams = PaymentMethodCreateParams.Ideal(bankName) - val createParams = - PaymentMethodCreateParams.create(ideal = idealParams, billingDetails = billingDetailsParams) - - return ConfirmSetupIntentParams.create( - paymentMethodCreateParams = createParams, - clientSecret = clientSecret, - ) + private fun createAlipayParams(): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createAlipay() } @Throws(PaymentMethodCreateParamsException::class) - private fun createSepaPaymentSetupParams(): ConfirmSetupIntentParams { - billingDetailsParams?.let { - val iban = getValOr(paymentMethodData, "iban", null) ?: run { - throw PaymentMethodCreateParamsException("You must provide IBAN") - } - - val sepaParams = PaymentMethodCreateParams.SepaDebit(iban) - val createParams = - PaymentMethodCreateParams.create( - sepaDebit = sepaParams, - billingDetails = it - ) - - return ConfirmSetupIntentParams.create( - paymentMethodCreateParams = createParams, - clientSecret = clientSecret - ) - } - - throw PaymentMethodCreateParamsException("You must provide billing details") - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createCardPaymentSetupParams(): ConfirmSetupIntentParams { - val paymentMethodId = getValOr(paymentMethodData, "paymentMethodId", null) - val token = getValOr(paymentMethodData, "token", null) - val cardParams = cardFieldView?.cardParams ?: cardFormView?.cardParams - - if (paymentMethodId != null) { - return ConfirmSetupIntentParams.create( - paymentMethodId, - clientSecret - ) - } - - val paymentMethodCreateParams = - if (token != null) - PaymentMethodCreateParams.create(PaymentMethodCreateParams.Card.create(token), billingDetailsParams) - else if (cardParams != null) - PaymentMethodCreateParams.create(cardParams, billingDetailsParams) - else - null - - if (paymentMethodCreateParams != null) { - return ConfirmSetupIntentParams - .create(paymentMethodCreateParams, clientSecret) - } else { - throw PaymentMethodCreateParamsException("Card details not complete") - } - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createAlipayPaymentConfirmParams(): ConfirmPaymentIntentParams { - return ConfirmPaymentIntentParams.createAlipay(clientSecret) - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createSofortPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createSofortParams(): PaymentMethodCreateParams { val country = getValOr(paymentMethodData, "country", null) ?: run { throw PaymentMethodCreateParamsException("You must provide bank account country") } - val params = PaymentMethodCreateParams.create( + return PaymentMethodCreateParams.create( PaymentMethodCreateParams.Sofort(country = country), billingDetailsParams ) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), - ) } @Throws(PaymentMethodCreateParamsException::class) - private fun createSofortPaymentSetupParams(): ConfirmSetupIntentParams { - val country = getValOr(paymentMethodData, "country", null) - ?: throw PaymentMethodCreateParamsException("You must provide country") - - val params = PaymentMethodCreateParams.create( - PaymentMethodCreateParams.Sofort(country = country), - billingDetailsParams - ) - - return ConfirmSetupIntentParams.create( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - ) - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createGrabPayPaymentConfirmParams(): ConfirmPaymentIntentParams { - val billingDetails = billingDetailsParams ?: PaymentMethod.BillingDetails() - val params = PaymentMethodCreateParams.createGrabPay(billingDetails) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), - ) - } - - @Throws(PaymentMethodCreateParamsException::class) - private fun createBancontactPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createBancontactParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createBancontact(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), - ) + return PaymentMethodCreateParams.createBancontact(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } - private fun createBancontactPaymentSetupParams(): ConfirmSetupIntentParams { + @Throws(PaymentMethodCreateParamsException::class) + private fun createSepaParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createBancontact(it) + val iban = getValOr(paymentMethodData, "iban", null) ?: run { + throw PaymentMethodCreateParamsException("You must provide IBAN") + } - return ConfirmSetupIntentParams - .create( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - ) + return PaymentMethodCreateParams.create( + sepaDebit = PaymentMethodCreateParams.SepaDebit(iban), + billingDetails = it + ) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createOXXOPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createOXXOParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createOxxo(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return PaymentMethodCreateParams.createOxxo(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createEPSPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createGiropayParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createEps(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return PaymentMethodCreateParams.createGiropay(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createGiropayPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createEPSParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createGiropay(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return PaymentMethodCreateParams.createEps(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createSepaPaymentConfirmParams(): ConfirmPaymentIntentParams { - billingDetailsParams?.let { - val iban = getValOr(paymentMethodData, "iban", null) ?: run { - throw PaymentMethodCreateParamsException("You must provide IBAN") - } - - val params = PaymentMethodCreateParams.create( - sepaDebit = PaymentMethodCreateParams.SepaDebit(iban), - billingDetails = it - ) + private fun createGrabPayParams(): PaymentMethodCreateParams { + val billingDetails = billingDetailsParams ?: PaymentMethod.BillingDetails() + return PaymentMethodCreateParams.createGrabPay(billingDetails) + } - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + @Throws(PaymentMethodCreateParamsException::class) + private fun createP24Params(): PaymentMethodCreateParams { + billingDetailsParams?.let { + return PaymentMethodCreateParams.createP24(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createFpxPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createFpxParams(): PaymentMethodCreateParams { val bank = getBooleanOrFalse(paymentMethodData, "testOfflineBank").let { "test_offline_bank" } - val params = PaymentMethodCreateParams.create( + return PaymentMethodCreateParams.create( PaymentMethodCreateParams.Fpx(bank) ) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) } @Throws(PaymentMethodCreateParamsException::class) - private fun createAfterpayClearpayPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createAfterpayClearpayParams(): PaymentMethodCreateParams { billingDetailsParams?.let { - val params = PaymentMethodCreateParams.createAfterpayClearpay(it) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return PaymentMethodCreateParams.createAfterpayClearpay(it) } throw PaymentMethodCreateParamsException("You must provide billing details") } @Throws(PaymentMethodCreateParamsException::class) - private fun createAuBecsDebitPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createAuBecsDebitParams(): PaymentMethodCreateParams { val formDetails = getMapOrNull(paymentMethodData, "formDetails") ?: run { throw PaymentMethodCreateParamsException("You must provide form details") } @@ -402,129 +171,206 @@ class PaymentMethodCreateParamsFactory( .setEmail(email) .build() - val params = PaymentMethodCreateParams.create( + return PaymentMethodCreateParams.create( auBecsDebit = PaymentMethodCreateParams.AuBecsDebit( bsbNumber = bsbNumber, accountNumber = accountNumber ), billingDetails = billingDetails ) - - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) } @Throws(PaymentMethodCreateParamsException::class) - private fun createAuBecsDebitPaymentSetupParams(): ConfirmSetupIntentParams { - val formDetails = getMapOrNull(paymentMethodData, "formDetails") ?: run { - throw PaymentMethodCreateParamsException("You must provide form details") + private fun createKlarnaParams(): PaymentMethodCreateParams { + if (billingDetailsParams == null || + billingDetailsParams.address?.country.isNullOrBlank() || + billingDetailsParams.email.isNullOrBlank() + ) { + throw PaymentMethodCreateParamsException("Klarna requires that you provide the following billing details: email, country") } - val bsbNumber = getValOr(formDetails, "bsbNumber") as String - val accountNumber = getValOr(formDetails, "accountNumber") as String - val name = getValOr(formDetails, "name") as String - val email = getValOr(formDetails, "email") as String - - val billingDetails = PaymentMethod.BillingDetails.Builder() - .setName(name) - .setEmail(email) - .build() + return PaymentMethodCreateParams.createKlarna(billingDetailsParams) + } - val params = PaymentMethodCreateParams.create( - auBecsDebit = PaymentMethodCreateParams.AuBecsDebit( - bsbNumber = bsbNumber, - accountNumber = accountNumber - ), - billingDetails = billingDetails - ) + @Throws(PaymentMethodCreateParamsException::class) + private fun createPayPalParams(): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createPayPal(null) + } - return ConfirmSetupIntentParams - .create( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - ) + @Throws(PaymentMethodCreateParamsException::class) + private fun createAffirmParams(): PaymentMethodCreateParams { + return PaymentMethodCreateParams.createAffirm(billingDetailsParams) } @Throws(PaymentMethodCreateParamsException::class) - private fun createUSBankAccountPaymentSetupParams(): ConfirmSetupIntentParams { - // If payment method data is supplied, assume they are passing in the bank details manually - paymentMethodData?.let { - if (billingDetailsParams?.name.isNullOrBlank()) { - throw PaymentMethodCreateParamsException("When creating a US bank account payment method, you must provide the following billing details: name") + fun createParams(clientSecret: String, paymentMethodType: PaymentMethod.Type?, isPaymentIntent: Boolean): ConfirmStripeIntentParams { + try { + return when (paymentMethodType) { + PaymentMethod.Type.Card -> createCardStripeIntentParams(clientSecret, isPaymentIntent) + PaymentMethod.Type.USBankAccount -> createUSBankAccountStripeIntentParams(clientSecret, isPaymentIntent) + PaymentMethod.Type.PayPal -> createPayPalStripeIntentParams(clientSecret, isPaymentIntent) + PaymentMethod.Type.Affirm -> createAffirmStripeIntentParams(clientSecret, isPaymentIntent) + PaymentMethod.Type.Ideal, + PaymentMethod.Type.Alipay, + PaymentMethod.Type.Sofort, + PaymentMethod.Type.Bancontact, + PaymentMethod.Type.SepaDebit, + PaymentMethod.Type.Oxxo, + PaymentMethod.Type.Giropay, + PaymentMethod.Type.Eps, + PaymentMethod.Type.GrabPay, + PaymentMethod.Type.P24, + PaymentMethod.Type.Fpx, + PaymentMethod.Type.AfterpayClearpay, + PaymentMethod.Type.AuBecsDebit, + PaymentMethod.Type.Klarna -> { + val params = createPaymentMethodParams(paymentMethodType) + + return if (isPaymentIntent) { + ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams( + paymentMethodCreateParams = params, + clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), + ) + } else { + ConfirmSetupIntentParams.create( + paymentMethodCreateParams = params, + clientSecret = clientSecret, + ) + } + } + null -> ConfirmPaymentIntentParams.create(clientSecret) + else -> { + throw Exception("This paymentMethodType is not supported yet") + } } - return ConfirmSetupIntentParams.create( - paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), - clientSecret = clientSecret, - ) - } ?: run { - // Payment method is assumed to be already attached through via collectBankAccount - return ConfirmSetupIntentParams.create( - clientSecret = clientSecret, - paymentMethodType = PaymentMethod.Type.USBankAccount - ) + } catch (error: PaymentMethodCreateParamsException) { + throw error } } @Throws(PaymentMethodCreateParamsException::class) - private fun createPayPalPaymentSetupParams(): ConfirmSetupIntentParams { - throw PaymentMethodCreateParamsException("PayPal is not yet supported through SetupIntents.") + private fun createCardPaymentMethodParams(): PaymentMethodCreateParams { + val token = getValOr(paymentMethodData, "token", null) + var cardParams = cardFieldView?.cardParams ?: cardFormView?.cardParams + + if (token != null) { + cardParams = PaymentMethodCreateParams.Card.create(token) + } + + if (cardParams == null) { + throw PaymentMethodCreateParamsException("Card details not complete") + } + + return PaymentMethodCreateParams.create(cardParams, billingDetailsParams) } @Throws(PaymentMethodCreateParamsException::class) - private fun createKlarnaPaymentConfirmParams(): ConfirmPaymentIntentParams { - if (billingDetailsParams == null || - billingDetailsParams.address?.country.isNullOrBlank() || - billingDetailsParams.email.isNullOrBlank() - ) { - throw PaymentMethodCreateParamsException("Klarna requires that you provide the following billing details: email, country") - } + private fun createCardStripeIntentParams(clientSecret: String, isPaymentIntent: Boolean): ConfirmStripeIntentParams { + val paymentMethodId = getValOr(paymentMethodData, "paymentMethodId", null) + val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - val params = PaymentMethodCreateParams.createKlarna(billingDetailsParams) + if (paymentMethodId != null) { + val cvc = getValOr(paymentMethodData, "cvc", null) + val paymentMethodOptionParams = + if (cvc != null) PaymentMethodOptionsParams.Card(cvc) else null - return ConfirmPaymentIntentParams - .createWithPaymentMethodCreateParams( - paymentMethodCreateParams = params, - clientSecret = clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return ( + if (isPaymentIntent) + ConfirmPaymentIntentParams.createWithPaymentMethodId( + paymentMethodId, + paymentMethodOptions = paymentMethodOptionParams, + clientSecret = clientSecret, + setupFutureUsage = setupFutureUsage) + else + ConfirmSetupIntentParams.create( + paymentMethodId, + clientSecret) + ) + } else { + val paymentMethodCreateParams = createCardPaymentMethodParams() + return ( + if (isPaymentIntent) + ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams( + paymentMethodCreateParams, + clientSecret, + setupFutureUsage = setupFutureUsage) + else + ConfirmSetupIntentParams + .create(paymentMethodCreateParams, clientSecret) + ) + } } @Throws(PaymentMethodCreateParamsException::class) - private fun createUSBankAccountPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createUSBankAccountStripeIntentParams(clientSecret: String, isPaymentIntent: Boolean): ConfirmStripeIntentParams { // If payment method data is supplied, assume they are passing in the bank details manually paymentMethodData?.let { if (billingDetailsParams?.name.isNullOrBlank()) { throw PaymentMethodCreateParamsException("When creating a US bank account payment method, you must provide the following billing details: name") } - - return ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams( - paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), - clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) - ) + return if (isPaymentIntent) { + ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams( + paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), + clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) + ) + } else { + ConfirmSetupIntentParams.create( + paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), + clientSecret = clientSecret, + ) + } } ?: run { // Payment method is assumed to be already attached through via collectBankAccount - return ConfirmPaymentIntentParams.create( - clientSecret = clientSecret, - paymentMethodType = PaymentMethod.Type.USBankAccount - ) + return if (isPaymentIntent) { + ConfirmPaymentIntentParams.create( + clientSecret = clientSecret, + paymentMethodType = PaymentMethod.Type.USBankAccount + ) + } else { + ConfirmSetupIntentParams.create( + clientSecret = clientSecret, + paymentMethodType = PaymentMethod.Type.USBankAccount + ) + } } } @Throws(PaymentMethodCreateParamsException::class) - private fun createPayPalPaymentConfirmParams(): ConfirmPaymentIntentParams { + private fun createPayPalStripeIntentParams(clientSecret: String, isPaymentIntent: Boolean): ConfirmStripeIntentParams { + if (!isPaymentIntent) { + throw PaymentMethodCreateParamsException("PayPal is not yet supported through SetupIntents.") + } + + val params = createPayPalParams() + return ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams( - paymentMethodCreateParams = PaymentMethodCreateParams.createPayPal(null), + paymentMethodCreateParams = params, clientSecret = clientSecret, ) } @Throws(PaymentMethodCreateParamsException::class) - private fun createUSBankAccountParams(params: ReadableMap): PaymentMethodCreateParams { + private fun createAffirmStripeIntentParams(clientSecret: String, isPaymentIntent: Boolean): ConfirmStripeIntentParams { + if (!isPaymentIntent) { + throw PaymentMethodCreateParamsException("Affirm is not yet supported through SetupIntents.") + } + + val params = createAffirmParams() + + return ConfirmPaymentIntentParams + .createWithPaymentMethodCreateParams( + paymentMethodCreateParams = params, + clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), + ) + } + + @Throws(PaymentMethodCreateParamsException::class) + private fun createUSBankAccountParams(params: ReadableMap?): PaymentMethodCreateParams { val accountNumber = getValOr(params, "accountNumber", null) val routingNumber = getValOr(params, "routingNumber", null) diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetAppearance.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetAppearance.kt index 9fed47a04dba0..e945091802990 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetAppearance.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetAppearance.kt @@ -2,6 +2,7 @@ package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk import android.graphics.Color import android.os.Bundle +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.PaymentSheetAppearanceException import com.stripe.android.paymentsheet.PaymentSheet fun PaymentSheetFragment.buildPaymentSheetAppearance(userParams: Bundle?): PaymentSheet.Appearance { diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetFragment.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetFragment.kt index a3b5d563b193a..0494e8f13d91e 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetFragment.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/PaymentSheetFragment.kt @@ -18,6 +18,9 @@ import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createResult import com.stripe.android.paymentsheet.PaymentOptionCallback import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.PaymentSheetResult @@ -49,12 +52,13 @@ class PaymentSheetFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val merchantDisplayName = arguments?.getString("merchantDisplayName").orEmpty() + if (merchantDisplayName.isEmpty()) { + initPromise.resolve(createError(ErrorType.Failed.toString(), "merchantDisplayName cannot be empty or null.")) + return + } val customerId = arguments?.getString("customerId").orEmpty() val customerEphemeralKeySecret = arguments?.getString("customerEphemeralKeySecret").orEmpty() - val countryCode = arguments?.getString("merchantCountryCode").orEmpty() - val currencyCode = arguments?.getString("currencyCode").orEmpty() - val googlePayEnabled = arguments?.getBoolean("googlePay") - val testEnv = arguments?.getBoolean("testEnv") + val googlePayConfig = buildGooglePayConfig(arguments?.getBundle("googlePay")) val allowsDelayedPaymentMethods = arguments?.getBoolean("allowsDelayedPaymentMethods") val billingDetailsBundle = arguments?.getBundle("defaultBillingDetails") paymentIntentClientSecret = arguments?.getString("paymentIntentClientSecret").orEmpty() @@ -67,40 +71,33 @@ class PaymentSheetFragment( } val paymentOptionCallback = PaymentOptionCallback { paymentOption -> - if (paymentOption != null) { - val bitmap = getBitmapFromVectorDrawable(context, paymentOption.drawableResourceId) + val result = paymentOption?.let { + val bitmap = getBitmapFromVectorDrawable(context, it.drawableResourceId) val imageString = getBase64FromBitmap(bitmap) val option: WritableMap = WritableNativeMap() - option.putString("label", paymentOption.label) + option.putString("label", it.label) option.putString("image", imageString) - presentPromise?.resolve(createResult("paymentOption", option)) - } else { - presentPromise?.resolve(createError(PaymentSheetErrorType.Canceled.toString(), "The payment option selection flow has been canceled")) + createResult("paymentOption", option) + } ?: run { + createError(PaymentSheetErrorType.Canceled.toString(), "The payment option selection flow has been canceled") } + presentPromise?.resolve(result) } val paymentResultCallback = PaymentSheetResultCallback { paymentResult -> when (paymentResult) { is PaymentSheetResult.Canceled -> { - val message = "The payment flow has been canceled" - confirmPromise?.resolve(createError(PaymentSheetErrorType.Canceled.toString(), message)) - ?: run { - presentPromise?.resolve(createError(PaymentSheetErrorType.Canceled.toString(), message)) - } + resolvePaymentResult(createError(PaymentSheetErrorType.Canceled.toString(), "The payment flow has been canceled")) } is PaymentSheetResult.Failed -> { - confirmPromise?.resolve(createError(PaymentSheetErrorType.Failed.toString(), paymentResult.error)) - ?: run { - presentPromise?.resolve(createError(PaymentSheetErrorType.Failed.toString(), paymentResult.error)) - } + resolvePaymentResult(createError(PaymentSheetErrorType.Failed.toString(), paymentResult.error)) } is PaymentSheetResult.Completed -> { - confirmPromise?.resolve(WritableNativeMap()) ?: run { - presentPromise?.resolve(WritableNativeMap()) - } + resolvePaymentResult(WritableNativeMap()) + // Remove the fragment now, we can be sure it won't be needed again if an intent is successful + removeFragment(context) } } - (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss() } var defaultBillingDetails: PaymentSheet.BillingDetails? = null @@ -128,11 +125,7 @@ class PaymentSheetFragment( id = customerId, ephemeralKeySecret = customerEphemeralKeySecret ) else null, - googlePay = if (googlePayEnabled == true) PaymentSheet.GooglePayConfiguration( - environment = if (testEnv == true) PaymentSheet.GooglePayConfiguration.Environment.Test else PaymentSheet.GooglePayConfiguration.Environment.Production, - countryCode = countryCode, - currencyCode = currencyCode - ) else null, + googlePay = googlePayConfig, appearance = appearance ) @@ -192,6 +185,35 @@ class PaymentSheetFragment( ) } } + + private fun resolvePaymentResult(map: WritableMap) { + confirmPromise?.let { + it.resolve(map) + confirmPromise = null + } ?: run { + presentPromise?.resolve(map) + } + } + + companion object { + const val TAG = "payment_sheet_launch_fragment" + + internal fun buildGooglePayConfig(params: Bundle?): PaymentSheet.GooglePayConfiguration? { + if (params == null) { + return null + } + + val countryCode = params.getString("merchantCountryCode").orEmpty() + val currencyCode = params.getString("currencyCode").orEmpty() + val testEnv = params.getBoolean("testEnv") + + return PaymentSheet.GooglePayConfiguration( + environment = if (testEnv) PaymentSheet.GooglePayConfiguration.Environment.Test else PaymentSheet.GooglePayConfiguration.Environment.Production, + countryCode = countryCode, + currencyCode = currencyCode + ) + } + } } fun getBitmapFromVectorDrawable(context: Context?, drawableId: Int): Bitmap? { diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt index 3b3e8d9bfeda8..9030b38ea4940 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt @@ -5,11 +5,16 @@ import android.content.Intent import android.os.Parcelable import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning.PushProvisioningProxy +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.* +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createMissingActivityError import com.stripe.android.* -import com.stripe.android.core.AppInfo import com.stripe.android.core.ApiVersion +import com.stripe.android.core.AppInfo import com.stripe.android.model.* import com.stripe.android.payments.bankaccount.CollectBankAccountConfiguration import com.stripe.android.view.AddPaymentMethodActivityStarter @@ -17,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch + @ReactModule(name = StripeSdkModule.NAME) class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName(): String { @@ -31,21 +37,27 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ private var stripeAccountId: String? = null private var urlScheme: String? = null + private var confirmPromise: Promise? = null + private var confirmPaymentClientSecret: String? = null + private var paymentSheetFragment: PaymentSheetFragment? = null private var googlePayFragment: GooglePayFragment? = null private var paymentLauncherFragment: PaymentLauncherFragment? = null - - private var confirmPromise: Promise? = null - private var confirmPaymentClientSecret: String? = null + private var collectBankAccountLauncherFragment: CollectBankAccountLauncherFragment? = null + private var financialConnectionsSheetFragment: FinancialConnectionsSheetFragment? = null + private val allFragments: List + get() = listOf( + paymentSheetFragment, + googlePayFragment, + paymentLauncherFragment, + collectBankAccountLauncherFragment, + financialConnectionsSheetFragment + ) private val mActivityEventListener = object : BaseActivityEventListener() { override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { if (::stripe.isInitialized) { - // BEGIN - Necessary on older versions of React Native (~0.64 and below) - paymentSheetFragment?.activity?.activityResultRegistry?.dispatchResult(requestCode, resultCode, data) - googlePayFragment?.activity?.activityResultRegistry?.dispatchResult(requestCode, resultCode, data) - paymentLauncherFragment?.activity?.activityResultRegistry?.dispatchResult(requestCode, resultCode, data) - // END + dispatchActivityResultsToFragments(requestCode, resultCode, data) try { val result = AddPaymentMethodActivityStarter.Result.fromIntent(data) if (data?.getParcelableExtra("extra_activity_result") != null) { @@ -62,6 +74,13 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ reactContext.addActivityEventListener(mActivityEventListener) } + // Necessary on older versions of React Native (~0.65 and below) + private fun dispatchActivityResultsToFragments(requestCode: Int, resultCode: Int, data: Intent?) { + for (fragment in allFragments) { + fragment?.activity?.activityResultRegistry?.dispatchResult(requestCode, resultCode, data) + } + } + private fun configure3dSecure(params: ReadableMap) { val stripe3dsConfigBuilder = PaymentAuthConfig.Stripe3ds2Config.Builder() if (params.hasKey("timeout")) stripe3dsConfigBuilder.setTimeout(params.getInt("timeout")) @@ -82,6 +101,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ hashMapOf( "API_VERSIONS" to hashMapOf( "CORE" to ApiVersion.API_VERSION_CODE, + "ISSUING" to PushProvisioningProxy.getApiVersion(), ) ) @@ -115,13 +135,14 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ @ReactMethod fun initPaymentSheet(params: ReadableMap, promise: Promise) { getCurrentActivityOrResolveWithError(promise)?.let { activity -> + paymentSheetFragment?.removeFragment(reactApplicationContext) paymentSheetFragment = PaymentSheetFragment(reactApplicationContext, promise).also { val bundle = toBundleObject(params) it.arguments = bundle } try { activity.supportFragmentManager.beginTransaction() - .add(paymentSheetFragment!!, "payment_sheet_launch_fragment") + .add(paymentSheetFragment!!, PaymentSheetFragment.TAG) .commit() } catch (error: IllegalStateException) { promise.resolve(createError(ErrorType.Failed.toString(), error.message)) @@ -167,7 +188,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ ) } else { Log.e("StripeReactNative", "FPX payment failed. Promise and/or client secret is not set.") - confirmPromise?.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), "FPX payment failed.")) + confirmPromise?.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), "FPX payment failed. Client secret is not set.")) } } is AddPaymentMethodActivityStarter.Result.Failure -> { @@ -183,27 +204,30 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ @ReactMethod fun createPaymentMethod(data: ReadableMap, options: ReadableMap, promise: Promise) { - val cardParams = (cardFieldView?.cardParams ?: cardFormView?.cardParams) ?: run { - promise.resolve(createError("Failed", "Card details not complete")) + val paymentMethodType = getValOr(data, "paymentMethodType")?.let { mapToPaymentMethodType(it) } ?: run { + promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType")) return } - val cardAddress = cardFieldView?.cardAddress ?: cardFormView?.cardAddress val paymentMethodData = getMapOrNull(data, "paymentMethodData") - val billingDetailsParams = mapToBillingDetails(getMapOrNull(paymentMethodData, "billingDetails"), cardAddress) - - val paymentMethodCreateParams = PaymentMethodCreateParams.create(cardParams, billingDetailsParams) - stripe.createPaymentMethod( - paymentMethodCreateParams, - callback = object : ApiResultCallback { - override fun onError(e: Exception) { - promise.resolve(createError("Failed", e)) - } + 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)) + 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 @@ -355,12 +379,15 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ // } @ReactMethod - fun confirmPayment(paymentIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise) { + fun confirmPayment(paymentIntentClientSecret: String, params: ReadableMap?, options: ReadableMap, promise: Promise) { val paymentMethodData = getMapOrNull(params, "paymentMethodData") - val paymentMethodType = getValOr(params, "paymentMethodType")?.let { mapToPaymentMethodType(it) } ?: run { - promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType")) - return - } + 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 val testOfflineBank = getBooleanOrFalse(params, "testOfflineBank") @@ -381,10 +408,10 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ // return // } - val factory = PaymentMethodCreateParamsFactory(paymentIntentClientSecret, paymentMethodData, options, cardFieldView, cardFormView) + val factory = PaymentMethodCreateParamsFactory(paymentMethodData, options, cardFieldView, cardFormView) try { - val confirmParams = factory.createConfirmParams(paymentMethodType) + val confirmParams = factory.createParams(paymentIntentClientSecret, paymentMethodType, isPaymentIntent = true) as ConfirmPaymentIntentParams urlScheme?.let { confirmParams.returnUrl = mapToReturnURL(urlScheme) } @@ -434,10 +461,10 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ return } - val factory = PaymentMethodCreateParamsFactory(setupIntentClientSecret, getMapOrNull(params, "paymentMethodData"), options, cardFieldView, cardFormView) + val factory = PaymentMethodCreateParamsFactory(getMapOrNull(params, "paymentMethodData"), options, cardFieldView, cardFormView) try { - val confirmParams = factory.createSetupParams(paymentMethodType) + val confirmParams = factory.createParams(setupIntentClientSecret, paymentMethodType, isPaymentIntent = false) as ConfirmSetupIntentParams urlScheme?.let { confirmParams.returnUrl = mapToReturnURL(urlScheme) } @@ -467,7 +494,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ getCurrentActivityOrResolveWithError(promise)?.let { try { it.supportFragmentManager.beginTransaction() - .add(fragment, "google_pay_support_fragment") + .add(fragment, GooglePayPaymentMethodLauncherFragment.TAG) .commit() } catch (error: IllegalStateException) { promise.resolve(createError(ErrorType.Failed.toString(), error.message)) @@ -485,7 +512,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ getCurrentActivityOrResolveWithError(promise)?.let { try { it.supportFragmentManager.beginTransaction() - .add(googlePayFragment!!, "google_pay_launch_fragment") + .add(googlePayFragment!!, GooglePayFragment.TAG) .commit() } catch (error: IllegalStateException) { promise.resolve(createError(ErrorType.Failed.toString(), error.message)) @@ -525,15 +552,47 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ } @ReactMethod - fun isCardInWallet(params: ReadableMap, promise: Promise) { + fun canAddCardToWallet(params: ReadableMap, promise: Promise) { val last4 = getValOr(params, "cardLastFour", null) ?: run { promise.resolve(createError("Failed", "You must provide cardLastFour")) return } + + if (!PushProvisioningProxy.isNFCEnabled(reactApplicationContext)) { + promise.resolve(createCanAddCardResult(false, "UNSUPPORTED_DEVICE")) + return + } + getCurrentActivityOrResolveWithError(promise)?.let { - promise.resolve(false) + PushProvisioningProxy.isCardInWallet(it, last4) { isCardInWallet, token, error -> + val result = error?.let { + createCanAddCardResult(false, "MISSING_CONFIGURATION", null) + } ?: run { + val status = if (isCardInWallet) "CARD_ALREADY_EXISTS" else null + createCanAddCardResult(!isCardInWallet, status, token) + } + promise.resolve(result) + } + } + } + + @ReactMethod + 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 = WritableNativeMap() + map.putBoolean("isInWallet", isCardInWallet) + map.putMap("token", token) + map + } + promise.resolve(result) + } + } } @ReactMethod @@ -558,9 +617,10 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ billingDetails.getString("email") ) - val fragment = CollectBankAccountLauncherFragment( + collectBankAccountLauncherFragment = CollectBankAccountLauncherFragment( reactApplicationContext, publishableKey, + stripeAccountId, clientSecret, isPaymentIntent, collectParams, @@ -569,7 +629,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ getCurrentActivityOrResolveWithError(promise)?.let { try { it.supportFragmentManager.beginTransaction() - .add(fragment, "collect_bank_account_launcher_fragment") + .add(collectBankAccountLauncherFragment!!, "collect_bank_account_launcher_fragment") .commit() } catch (error: IllegalStateException) { promise.resolve(createError(ErrorType.Failed.toString(), error.message)) @@ -596,6 +656,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(result))) } } + val setupCallback = object : ApiResultCallback { override fun onError(e: Exception) { promise.resolve(createError(ErrorType.Failed.toString(), e)) @@ -644,6 +705,28 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ } } + @ReactMethod + fun collectBankAccountToken(clientSecret: String, promise: Promise) { + if (!::stripe.isInitialized) { + promise.resolve(createMissingInitError()) + return + } + financialConnectionsSheetFragment = FinancialConnectionsSheetFragment().also { + it.presentFinancialConnectionsSheet(clientSecret, FinancialConnectionsSheetFragment.Mode.ForToken, publishableKey, stripeAccountId, promise, reactApplicationContext) + } + } + + @ReactMethod + fun collectFinancialConnectionsAccounts(clientSecret: String, promise: Promise) { + if (!::stripe.isInitialized) { + promise.resolve(createMissingInitError()) + return + } + financialConnectionsSheetFragment = FinancialConnectionsSheetFragment().also { + it.presentFinancialConnectionsSheet(clientSecret, FinancialConnectionsSheetFragment.Mode.ForSession, publishableKey, stripeAccountId, promise, reactApplicationContext) + } + } + /** * 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. diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkPackage.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkPackage.kt index f33701c25a09d..aea2a649afef7 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkPackage.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkPackage.kt @@ -4,6 +4,7 @@ import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ViewManager +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning.AddToWalletButtonManager class StripeSdkPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List { @@ -16,6 +17,7 @@ class StripeSdkPackage : ReactPackage { AuBECSDebitFormViewManager(), StripeContainerManager(), CardFormViewManager(), + AddToWalletButtonManager(reactContext) ) } } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt new file mode 100644 index 0000000000000..f0d72d4acb4fa --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt @@ -0,0 +1,55 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning + +import com.bumptech.glide.Glide +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.common.MapBuilder +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.bridge.ReactApplicationContext + + +class AddToWalletButtonManager(applicationContext: ReactApplicationContext) : SimpleViewManager() { + private val requestManager = Glide.with(applicationContext) + override fun getName() = "AddToWalletButton" + + override fun onDropViewInstance(view: AddToWalletButtonView) { + view.onDropViewInstance() + super.onDropViewInstance(view) + } + + override fun onAfterUpdateTransaction(view: AddToWalletButtonView) { + super.onAfterUpdateTransaction(view) + view.onAfterUpdateTransaction() + } + + override fun createViewInstance(reactContext: ThemedReactContext): AddToWalletButtonView { + return AddToWalletButtonView(reactContext, requestManager) + } + + override fun getExportedCustomDirectEventTypeConstants(): MutableMap { + return MapBuilder.of( + AddToWalletCompleteEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCompleteAction") + ) + } + + @ReactProp(name = "androidAssetSource") + fun source(view: AddToWalletButtonView, source: ReadableMap) { + view.setSourceMap(source) + } + + @ReactProp(name = "cardDetails") + fun cardDetails(view: AddToWalletButtonView, cardDetails: ReadableMap) { + view.setCardDetails(cardDetails) + } + + @ReactProp(name = "ephemeralKey") + fun ephemeralKey(view: AddToWalletButtonView, ephemeralKey: ReadableMap) { + view.setEphemeralKey(ephemeralKey) + } + + @ReactProp(name = "token") + fun token(view: AddToWalletButtonView, token: ReadableMap?) { + view.setToken(token) + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt new file mode 100644 index 0000000000000..a91b55f0dbed4 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt @@ -0,0 +1,146 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning + +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.RippleDrawable +import android.view.MotionEvent +import androidx.appcompat.widget.AppCompatImageView +import com.bumptech.glide.RequestManager +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerModule +import com.facebook.react.uimanager.events.EventDispatcher +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError + + +class AddToWalletButtonView(private val context: ThemedReactContext, private val requestManager: RequestManager) : AppCompatImageView(context) { + private var cardDetails: ReadableMap? = null + private var ephemeralKey: String? = null + private var sourceMap: ReadableMap? = null + private var token: ReadableMap? = null + + private var eventDispatcher: EventDispatcher? = context.getNativeModule(UIManagerModule::class.java)?.eventDispatcher + private var loadedSource: GlideUrl? = null + private var heightOverride: Int = 0 + private var widthOverride: Int = 0 + + override fun performClick(): Boolean { + super.performClick() + + cardDetails?.getString("description")?.let { cardDescription -> + ephemeralKey?.let { ephemeralKey -> + PushProvisioningProxy.invoke( + context.reactApplicationContext, + this, + cardDescription, + ephemeralKey, + token) + } ?: run { + dispatchEvent( + createError("Failed", "Missing parameters. `ephemeralKey` must be supplied in the props to ") + ) + } + } ?: run { + dispatchEvent( + createError("Failed", "Missing parameters. `cardDetails.cardDescription` must be supplied in the props to ") + ) + } + return true + } + + init { + this.setOnTouchListener { view, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + view.performClick() + return@setOnTouchListener true + } + return@setOnTouchListener false + } + } + + fun onAfterUpdateTransaction() { + val sourceToLoad = createUrlFromSourceMap(sourceMap) + if (sourceToLoad == null) { + requestManager.clear(this) + setImageDrawable(null) + loadedSource = null + } else if (sourceToLoad != loadedSource || (heightOverride > 0 || widthOverride > 0)) { + loadedSource = sourceToLoad + val scale = sourceMap?.getDouble("scale") ?: 1.0 + + requestManager + .load(sourceToLoad) + .addListener(object : RequestListener { + override fun onLoadFailed(e: GlideException?, model: Any?, target: Target?, isFirstResource: Boolean): Boolean { + dispatchEvent( + createError("Failed", "Failed to load the source from $sourceToLoad") + ) + return true + } + override fun onResourceReady(resource: Drawable?, model: Any?, target: Target?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { + setImageDrawable( + RippleDrawable( + ColorStateList.valueOf(Color.parseColor("#e0e0e0")), + resource, + null)) + return true + } + }) + .centerCrop() + .override((widthOverride * scale).toInt(), (heightOverride * scale).toInt()) + .into(this) + } + } + + private fun createUrlFromSourceMap(sourceMap: ReadableMap?): GlideUrl? { + val uriKey = sourceMap?.getString("uri") + return uriKey?.let { GlideUrl(uriKey) } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + if (w > 0 && h > 0) { + heightOverride = h + widthOverride = w + onAfterUpdateTransaction() + heightOverride = 0 + widthOverride = 0 + } + } + + fun onDropViewInstance() { + requestManager.clear(this) + } + + fun setSourceMap(map: ReadableMap) { + sourceMap = map + } + + fun setCardDetails(detailsMap: ReadableMap) { + cardDetails = detailsMap + } + + fun setEphemeralKey(map: ReadableMap) { + ephemeralKey = map.toHashMap().toString() + } + + fun setToken(map: ReadableMap?) { + token = map + } + + fun dispatchEvent(error: WritableMap?) { + eventDispatcher?.dispatchEvent( + AddToWalletCompleteEvent( + id, + error + ) + ) + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt new file mode 100644 index 0000000000000..04842843ba6fc --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt @@ -0,0 +1,22 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +internal class AddToWalletCompleteEvent constructor(viewTag: Int, private val error: WritableMap?) : Event(viewTag) { + override fun getEventName(): String { + return EVENT_NAME + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + rctEventEmitter.receiveEvent(viewTag, eventName, serializeEventData()) + } + + private fun serializeEventData(): WritableMap? { + return error + } + + companion object { + const val EVENT_NAME = "onCompleteAction" + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt new file mode 100644 index 0000000000000..df493ab8734a5 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt @@ -0,0 +1,35 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning + +import android.os.Parcel +import android.os.Parcelable +import com.stripe.android.pushProvisioning.PushProvisioningEphemeralKeyProvider + + +class EphemeralKeyProvider(private val ephemeralKey: String) : PushProvisioningEphemeralKeyProvider { + + private constructor(parcel: Parcel) : this( + ephemeralKey = parcel.readString() ?: "" + ) + + override fun describeContents(): Int { + return hashCode() + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(ephemeralKey) + } + + override fun createEphemeralKey(apiVersion: String, keyUpdateListener: com.stripe.android.pushProvisioning.EphemeralKeyUpdateListener) { + keyUpdateListener.onKeyUpdate(ephemeralKey) + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): EphemeralKeyProvider { + return EphemeralKeyProvider(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt new file mode 100644 index 0000000000000..40cb018937b2d --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt @@ -0,0 +1,129 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning + +import android.app.Activity +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.content.pm.PackageManager +import android.nfc.NfcAdapter +import android.util.Log +import com.facebook.react.bridge.BaseActivityEventListener +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.mapError +import com.stripe.android.pushProvisioning.PushProvisioningActivity +import com.stripe.android.pushProvisioning.PushProvisioningActivityStarter + + +object PushProvisioningProxy { + private const val TAG = "StripePushProvisioning" + private var description = "Added by Stripe" + private var tokenRequiringTokenization: ReadableMap? = null + + fun getApiVersion(): String { + return try { + Class.forName("com.stripe.android.pushProvisioning.PushProvisioningActivity") + PushProvisioningActivity.API_VERSION + } catch (e: Exception) { + Log.e(TAG, "PushProvisioning dependency not found") + "" + } + } + + fun isNFCEnabled(context: ReactApplicationContext): Boolean { + return if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC)) { + val adapter = NfcAdapter.getDefaultAdapter(context) + adapter.isEnabled + } else { + false + } + } + + fun invoke( + context: ReactApplicationContext, + view: AddToWalletButtonView, + cardDescription: String, + ephemeralKey: String, + token: ReadableMap? + ) { + try { + Class.forName("com.stripe.android.pushProvisioning.PushProvisioningActivityStarter") + description = cardDescription + tokenRequiringTokenization = token + createActivityEventListener(context, view) + context.currentActivity?.let { + DefaultPushProvisioningProxy().beginPushProvisioning( + it, + description, + EphemeralKeyProvider(ephemeralKey) + ) + } ?: run { + view.dispatchEvent( + createError( + "Failed", + "Activity doesn't exist yet. You can safely retry.") + ) + } + } catch (e: Exception) { + Log.e(TAG, "PushProvisioning dependency not found") + } + } + + fun isCardInWallet(activity: Activity, cardLastFour: String, callback: TokenCheckHandler) { + TapAndPayProxy.findExistingToken(activity, cardLastFour, callback) + } + + private fun createActivityEventListener(context: ReactApplicationContext, view: AddToWalletButtonView) { + val listener = object : BaseActivityEventListener() { + override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(activity, requestCode, resultCode, data) + if (requestCode == TapAndPayProxy.REQUEST_CODE_TOKENIZE) { + view.dispatchEvent( + if (resultCode == RESULT_OK) null else mapError("Failed", "Failed to verify identity.", null, null, null, null) + ) + } else if (requestCode == PushProvisioningActivityStarter.REQUEST_CODE) { + if (resultCode == PushProvisioningActivity.RESULT_OK) { + tokenRequiringTokenization?.let { tokenRequiringTokenization -> + val tokenReferenceId = tokenRequiringTokenization.getString("id") + if (tokenReferenceId.isNullOrBlank()) { + view.dispatchEvent( + mapError("Failed", "Token object passed to `` is missing the `id` field.", null, null, null, null) + ) + } else { + TapAndPayProxy.tokenize( + activity, + tokenReferenceId, + tokenRequiringTokenization, + description + ) + } + } ?: run { + view.dispatchEvent(null) + } + } else if (resultCode == PushProvisioningActivity.RESULT_ERROR) { + data?.let { + val error: PushProvisioningActivityStarter.Error = PushProvisioningActivityStarter.Error.fromIntent(data) + view.dispatchEvent( + mapError(error.code.toString(), error.message, null, null, null, null) + ) + } + } + } + } + } + context.addActivityEventListener(listener) + } +} + +class DefaultPushProvisioningProxy { + fun beginPushProvisioning( + activity: Activity, + description: String, + provider: EphemeralKeyProvider + ) { + PushProvisioningActivityStarter( + activity, + PushProvisioningActivityStarter.Args(description, provider, false) + ).startForResult() + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt new file mode 100644 index 0000000000000..9d08603c71edb --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt @@ -0,0 +1,136 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.pushprovisioning + +import android.app.Activity +import android.util.Log +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils.createError +import com.google.android.gms.tasks.Task + +typealias TokenCheckHandler = (isCardInWallet: Boolean, token: WritableMap?, error: WritableMap?) -> Unit + +object TapAndPayProxy { + private const val TAG = "StripeTapAndPay" + private var tapAndPayClient: Any? = null + const val REQUEST_CODE_TOKENIZE = 90909 + + private fun getTapandPayTokens(activity: Activity): Task>? { + return try { + val tapAndPayClass = Class.forName("com.google.android.gms.tapandpay.TapAndPay") + val getClientMethod = tapAndPayClass.getMethod( + "getClient", + Activity::class.java) + val client = getClientMethod.invoke(null, activity) + + val tapAndPayClientClass = Class.forName("com.google.android.gms.tapandpay.TapAndPayClient") + val listTokensMethod = tapAndPayClientClass.getMethod("listTokens") + + listTokensMethod.invoke(client) as Task> + } catch (e: Exception) { + Log.e(TAG, "Google TapAndPay dependency not found") + null + } + } + + internal fun isTokenInWallet(token: Any, newLastFour: String): Boolean { + return try { + val getFpanLastFourMethod = Class.forName("com.google.android.gms.tapandpay.issuer.TokenInfo").getMethod("getFpanLastFour") + val existingFpanLastFour = getFpanLastFourMethod.invoke(token) as String + existingFpanLastFour == newLastFour + } catch (e: Exception) { + Log.e(TAG, "There was a problem finding the class com.google.android.gms.tapandpay.issuer.TokenInfo. Make sure you've included Google's TapAndPay dependency.") + false + } + } + + + fun findExistingToken(activity: Activity, newCardLastFour: String, callback: TokenCheckHandler) { + val tokens = getTapandPayTokens(activity) + if (tokens == null) { + callback(false, null, createError("Failed", "Google TapAndPay dependency not found.")) + return + } + + tokens.addOnCompleteListener { task -> + if (task.isSuccessful) { + for (token in task.result) { + if (isTokenInWallet(token, newCardLastFour)) { + callback(true, mapFromTokenInfo(token), null) + return@addOnCompleteListener + } + } + } else { + Log.e(TAG, "Unable to fetch existing tokens from Google TapAndPay.") + } + callback(false, null, null) + } + } + + fun tokenize(activity: Activity, tokenReferenceId: String, token: ReadableMap, cardDescription: String) { + try { + val tapAndPayClientClass = Class.forName("com.google.android.gms.tapandpay.TapAndPayClient") + val tokenizeMethod = tapAndPayClientClass::class.java.getMethod("tokenize", Activity::class.java, String::class.java, Int::class.java, String::class.java, Int::class.java, Int::class.java) + tokenizeMethod.invoke(tapAndPayClient, + activity, + tokenReferenceId, + token.getInt("serviceProvider"), + cardDescription, + token.getInt("network"), + REQUEST_CODE_TOKENIZE) + } catch (e: Exception) { + Log.e(TAG, "Google TapAndPay dependency not found.") + } + } + + private fun mapFromTokenInfo(token: Any?): WritableMap? { + if (token == null) { + return null + } + val result = WritableNativeMap() + try { + val tokenInfoClass = Class.forName("com.google.android.gms.tapandpay.issuer.TokenInfo") + result.putString( + "id", + tokenInfoClass.getMethod("getIssuerTokenId").invoke(token) as String) + result.putString( + "cardLastFour", + tokenInfoClass.getMethod("getFpanLastFour").invoke(token) as String) + result.putString( + "issuer", + tokenInfoClass.getMethod("getIssuerName").invoke(token) as String) + result.putString( + "status", + mapFromTokenState(tokenInfoClass.getMethod("getTokenState").invoke(token) as Int)) + result.putInt( + "network", + tokenInfoClass.getMethod("getNetwork").invoke(token) as Int) + result.putInt( + "serviceProvider", + tokenInfoClass.getMethod("getTokenServiceProvider").invoke(token) as Int) + } catch (e: Exception) { + Log.e(TAG, + "There was a problem finding the class com.google.android.gms.tapandpay.issuer.TokenInfo. Make sure you've included Google's TapAndPay dependency.") + } + return result + } + + private fun mapFromTokenState(status: Int): String { + try { + val tapAndPayClass = Class.forName("com.google.android.gms.tapandpay.TapAndPay") + return when (status) { + tapAndPayClass.getField("TOKEN_STATE_NEEDS_IDENTITY_VERIFICATION").get(tapAndPayClass) -> "TOKEN_STATE_NEEDS_IDENTITY_VERIFICATION" + tapAndPayClass.getField("TOKEN_STATE_PENDING").get(tapAndPayClass) -> "TOKEN_STATE_PENDING" + tapAndPayClass.getField("TOKEN_STATE_SUSPENDED").get(tapAndPayClass) -> "TOKEN_STATE_SUSPENDED" + tapAndPayClass.getField("TOKEN_STATE_ACTIVE").get(tapAndPayClass) -> "TOKEN_STATE_ACTIVE" + tapAndPayClass.getField("TOKEN_STATE_FELICA_PENDING_PROVISIONING").get(tapAndPayClass) -> "TOKEN_STATE_FELICA_PENDING_PROVISIONING" + tapAndPayClass.getField("TOKEN_STATE_UNTOKENIZED").get(tapAndPayClass) -> "TOKEN_STATE_UNTOKENIZED" + else -> "UNKNOWN" + } + } catch (e: Exception) { + Log.e(TAG, + "There was a problem finding Google's TapAndPay dependency.") + return "UNKNOWN" + } + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Errors.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Errors.kt similarity index 93% rename from android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Errors.kt rename to android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Errors.kt index 2b70c87f8a0a0..76604f6473bf9 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Errors.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Errors.kt @@ -1,4 +1,4 @@ -package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap @@ -111,3 +111,7 @@ internal fun createError(code: String, error: Throwable): WritableMap { null, null) } + +internal fun createMissingInitError(): WritableMap { + return createError(ErrorType.Failed.toString(), "Stripe has not been initialized. Initialize Stripe in your app with the StripeProvider component or the initStripe method.") +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Extensions.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Extensions.kt similarity index 58% rename from android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Extensions.kt rename to android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Extensions.kt index 814dc2d2d58f9..122164a3e05db 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Extensions.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Extensions.kt @@ -1,8 +1,11 @@ -package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils import android.content.Context import android.view.View import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.facebook.react.bridge.ReactApplicationContext fun View.showSoftKeyboard() { post { @@ -19,3 +22,11 @@ fun View.hideSoftKeyboard() { imm?.hideSoftInputFromWindow(windowToken, 0) } } + +fun Fragment.removeFragment(context: ReactApplicationContext) { + (context.currentActivity as? AppCompatActivity)?.supportFragmentManager?.let { + if (it.findFragmentByTag(this.tag) != null) { + it.beginTransaction().remove(this).commitAllowingStateLoss() + } + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Mappers.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Mappers.kt similarity index 97% rename from android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Mappers.kt rename to android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Mappers.kt index b2814d510fb37..c4da8907c69a5 100644 --- a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/Mappers.kt +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/Mappers.kt @@ -1,4 +1,4 @@ -package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils import android.os.Bundle import android.util.Log @@ -14,6 +14,22 @@ internal fun createResult(key: String, value: WritableMap): WritableMap { return map } +internal fun createCanAddCardResult(canAddCard: Boolean, status: String? = null, token: WritableMap? = null): WritableNativeMap { + val result = WritableNativeMap() + val details = WritableNativeMap() + if (status != null) { + result.putBoolean("canAddCard", false) + details.putString("status", status) + } else { + result.putBoolean("canAddCard", canAddCard) + if (token != null) { + details.putMap("token", token) + } + } + result.putMap("details", details) + return result +} + internal fun mapIntentStatus(status: StripeIntent.Status?): String { return when (status) { StripeIntent.Status.Succeeded -> "Succeeded" @@ -108,6 +124,7 @@ internal fun mapPaymentMethodType(type: PaymentMethod.Type?): String { PaymentMethod.Type.Klarna -> "Klarna" PaymentMethod.Type.USBankAccount -> "USBankAccount" PaymentMethod.Type.PayPal -> "PayPal" + PaymentMethod.Type.Affirm -> "Affirm" else -> "Unknown" } } @@ -136,6 +153,7 @@ internal fun mapToPaymentMethodType(type: String?): PaymentMethod.Type? { "Klarna" -> PaymentMethod.Type.Klarna "USBankAccount" -> PaymentMethod.Type.USBankAccount "PayPal" -> PaymentMethod.Type.PayPal + "Affirm" -> PaymentMethod.Type.Affirm else -> null } } @@ -199,12 +217,11 @@ internal fun mapFromBankAccountStatus(status: BankAccount.Status?): String { } internal fun mapFromBankAccount(bankAccount: BankAccount?): WritableMap? { - val bankAccountMap: WritableMap = WritableNativeMap() - if (bankAccount == null) { return null } + val bankAccountMap: WritableMap = WritableNativeMap() bankAccountMap.putString("id", bankAccount.id) bankAccountMap.putString("bankName", bankAccount.bankName) bankAccountMap.putString("accountHolderName", bankAccount.accountHolderName) @@ -213,6 +230,8 @@ internal fun mapFromBankAccount(bankAccount: BankAccount?): WritableMap? { bankAccountMap.putString("country", bankAccount.countryCode) bankAccountMap.putString("routingNumber", bankAccount.routingNumber) bankAccountMap.putString("status", mapFromBankAccountStatus(bankAccount.status)) + bankAccountMap.putString("fingerprint", bankAccount.fingerprint) + bankAccountMap.putString("last4", bankAccount.last4) return bankAccountMap } @@ -294,13 +313,13 @@ internal fun mapFromCard(card: Card?): WritableMap? { internal fun mapFromToken(token: Token): WritableMap { val tokenMap: WritableMap = WritableNativeMap() - tokenMap.putString("id", token.id) - tokenMap.putString("created", token.created.time.toString()) + tokenMap.putDouble("created", token.created.time.toDouble()) tokenMap.putString("type", mapTokenType(token.type)) tokenMap.putBoolean("livemode", token.livemode) tokenMap.putMap("bankAccount", mapFromBankAccount(token.bankAccount)) tokenMap.putMap("card", mapFromCard(token.card)) + tokenMap.putBoolean("used", token.used) return tokenMap } diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/PostalCodeUtilities.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/PostalCodeUtilities.kt new file mode 100644 index 0000000000000..03ce1b8590f17 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/PostalCodeUtilities.kt @@ -0,0 +1,18 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk.utils + +class PostalCodeUtilities { + + companion object { + internal fun isValidGlobalPostalCodeCharacter(c: Char): Boolean { + return Character.isLetterOrDigit(c) + || c.isWhitespace() + || c == '-' + } + + internal fun isValidUsPostalCodeCharacter(c: Char): Boolean { + return Character.isDigit(c) + || c.isWhitespace() + || c == '-' + } + } +} diff --git a/android/versioned-abis/expoview-abi45_0_0/build.gradle b/android/versioned-abis/expoview-abi45_0_0/build.gradle index 7ec94d6493201..587344d799926 100644 --- a/android/versioned-abis/expoview-abi45_0_0/build.gradle +++ b/android/versioned-abis/expoview-abi45_0_0/build.gradle @@ -207,9 +207,9 @@ dependencies { api 'com.github.troZee:ViewPager2:v1.0.6' // stripe-react-native - implementation('com.stripe:stripe-android:20.5.+') - implementation('com.stripe:financial-connections:20.5.+') - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' + implementation('com.stripe:stripe-android:20.12.+') + implementation('com.stripe:financial-connections:20.12.+') + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" implementation 'com.google.android.material:material:1.3.0' diff --git a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/expo/modules/notifications/service/NotificationsService.kt b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/expo/modules/notifications/service/NotificationsService.kt index 04e6d5a9ff98c..71d2415d8254b 100644 --- a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/expo/modules/notifications/service/NotificationsService.kt +++ b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/expo/modules/notifications/service/NotificationsService.kt @@ -509,7 +509,7 @@ open class NotificationsService : BroadcastReceiver() { val notification = intent.getParcelableExtra(NOTIFICATION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.") val action = intent.getParcelableExtra(NOTIFICATION_ACTION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_ACTION_KEY not found in the intent extras.") val response = if (action is TextInputNotificationAction) { - val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent).getString(USER_TEXT_RESPONSE_KEY) + val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent)?.getString(USER_TEXT_RESPONSE_KEY) ?: "" TextInputNotificationResponse(action, notification, userText) } else { NotificationResponse(action, notification) diff --git a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/gesturehandler/PanGestureHandler.kt b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/gesturehandler/PanGestureHandler.kt index bb669a7ae756d..f48b8e9baa245 100644 --- a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/gesturehandler/PanGestureHandler.kt +++ b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/gesturehandler/PanGestureHandler.kt @@ -54,7 +54,7 @@ class PanGestureHandler(context: Context?) : GestureHandler() * position of all the fingers will remain still while doing a rotation gesture. */ init { - val vc = ViewConfiguration.get(context) + val vc = ViewConfiguration.get(context!!) val touchSlop = vc.scaledTouchSlop defaultMinDistSq = (touchSlop * touchSlop).toFloat() minDistSq = defaultMinDistSq diff --git a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java index d851b9af4ec23..9350237053d82 100644 --- a/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java +++ b/android/versioned-abis/expoview-abi45_0_0/src/main/java/abi45_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java @@ -292,13 +292,10 @@ public void setCacheEnabled(WebView view, boolean enabled) { if (enabled) { Context ctx = view.getContext(); if (ctx != null) { - view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath()); view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); - view.getSettings().setAppCacheEnabled(true); } } else { view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.getSettings().setAppCacheEnabled(false); } } @@ -497,7 +494,6 @@ public void setIncognito(WebView view, boolean enabled) { // Disable caching view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.getSettings().setAppCacheEnabled(false); view.clearHistory(); view.clearCache(true); diff --git a/android/versioned-abis/expoview-abi46_0_0/build.gradle b/android/versioned-abis/expoview-abi46_0_0/build.gradle index c4ca05e8342c1..7997f68f9f1c7 100644 --- a/android/versioned-abis/expoview-abi46_0_0/build.gradle +++ b/android/versioned-abis/expoview-abi46_0_0/build.gradle @@ -197,9 +197,9 @@ dependencies { api 'com.github.troZee:ViewPager2:v1.0.6' // stripe-react-native - implementation('com.stripe:stripe-android:20.5.+') - implementation('com.stripe:financial-connections:20.5.+') - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0' + implementation('com.stripe:stripe-android:20.12.+') + implementation('com.stripe:financial-connections:20.12.+') + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" implementation 'com.google.android.material:material:1.3.0' diff --git a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/expo/modules/notifications/service/NotificationsService.kt b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/expo/modules/notifications/service/NotificationsService.kt index 3983f4c3969c9..9228c5424fc78 100644 --- a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/expo/modules/notifications/service/NotificationsService.kt +++ b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/expo/modules/notifications/service/NotificationsService.kt @@ -502,7 +502,7 @@ open class NotificationsService : BroadcastReceiver() { val notification = intent.getParcelableExtra(NOTIFICATION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.") val action = intent.getParcelableExtra(NOTIFICATION_ACTION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_ACTION_KEY not found in the intent extras.") val response = if (action is TextInputNotificationAction) { - val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent).getString(USER_TEXT_RESPONSE_KEY) + val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent)?.getString(USER_TEXT_RESPONSE_KEY) ?: "" TextInputNotificationResponse(action, notification, userText) } else { NotificationResponse(action, notification) diff --git a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt index 4f9c51f997cd3..b7ed9681f1d84 100644 --- a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt +++ b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CardFieldView.kt @@ -210,7 +210,7 @@ class CardFieldView(context: ThemedReactContext) : FrameLayout(context) { fun setCountryCode(countryCode: String?) { if (mCardWidget.postalCodeEnabled) { val doesCountryUsePostalCode = CountryUtils.doesCountryUsePostalCode( - CountryCode.create(value = countryCode ?: LocaleListCompat.getAdjustedDefault()[0].country) + CountryCode.create(value = countryCode ?: LocaleListCompat.getAdjustedDefault()[0]?.country ?: "US") ) mCardWidget.postalCodeRequired = doesCountryUsePostalCode } diff --git a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt index 83b04e43eaee4..40ba08748076e 100644 --- a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt +++ b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/CollectBankAccountLauncherFragment.kt @@ -19,6 +19,7 @@ import com.stripe.android.payments.bankaccount.navigation.CollectBankAccountResu class CollectBankAccountLauncherFragment( private val context: ReactApplicationContext, private val publishableKey: String, + private val stripeAccountId: String?, private val clientSecret: String, private val isPaymentIntent: Boolean, private val collectParams: CollectBankAccountConfiguration.USBankAccount, @@ -47,12 +48,14 @@ class CollectBankAccountLauncherFragment( if (isPaymentIntent) { collectBankAccountLauncher.presentWithPaymentIntent( publishableKey, + stripeAccountId, clientSecret, collectParams ) } else { collectBankAccountLauncher.presentWithSetupIntent( publishableKey, + stripeAccountId, clientSecret, collectParams ) diff --git a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt index 0f2f98c3661f7..76937df6450cf 100644 --- a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt +++ b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/reactnativestripesdk/StripeSdkModule.kt @@ -562,6 +562,7 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ val fragment = CollectBankAccountLauncherFragment( reactApplicationContext, publishableKey, + stripeAccountId, clientSecret, isPaymentIntent, collectParams, diff --git a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java index 0d602a3893a9c..3d64b63f91262 100644 --- a/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java +++ b/android/versioned-abis/expoview-abi46_0_0/src/main/java/abi46_0_0/host/exp/exponent/modules/api/components/webview/RNCWebViewManager.java @@ -316,13 +316,10 @@ public void setCacheEnabled(WebView view, boolean enabled) { if (enabled) { Context ctx = view.getContext(); if (ctx != null) { - view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath()); view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); - view.getSettings().setAppCacheEnabled(true); } } else { view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.getSettings().setAppCacheEnabled(false); } } @@ -521,7 +518,6 @@ public void setIncognito(WebView view, boolean enabled) { // Disable caching view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - view.getSettings().setAppCacheEnabled(false); view.clearHistory(); view.clearCache(true); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5a14867e2a35a..8107290c3e305 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -623,8 +623,8 @@ PODS: - ABI45_0_0React-RCTImage - ABI45_0_0stripe-react-native (0.6.0): - ABI45_0_0React-Core - - Stripe (~> 22.7.0) - - StripeFinancialConnections (~> 22.7.0) + - Stripe (~> 22.8.1) + - StripeFinancialConnections (~> 22.8.1) - ABI45_0_0UMAppLoader (3.1.0) - ABI45_0_0Yoga (1.14.0) - ABI46_0_0EASClient (0.3.0): @@ -1269,8 +1269,8 @@ PODS: - ABI46_0_0React-RCTImage - ABI46_0_0stripe-react-native (0.13.1): - ABI46_0_0React-Core - - Stripe (~> 22.7.0) - - StripeFinancialConnections (~> 22.7.0) + - Stripe (~> 22.8.1) + - StripeFinancialConnections (~> 22.8.1) - ABI46_0_0UMAppLoader (3.1.0) - ABI46_0_0Yoga (1.14.0) - Amplitude (6.0.0) @@ -2051,27 +2051,27 @@ PODS: - RNScreens (3.18.0): - React-Core - React-RCTImage - - Stripe (22.7.1): - - Stripe/Stripe3DS2 (= 22.7.1) - - StripeApplePay (= 22.7.1) - - StripeCore (= 22.7.1) - - StripeUICore (= 22.7.1) - - stripe-react-native (0.18.1): + - Stripe (22.8.3): + - Stripe/Stripe3DS2 (= 22.8.3) + - StripeApplePay (= 22.8.3) + - StripeCore (= 22.8.3) + - StripeUICore (= 22.8.3) + - stripe-react-native (0.19.0): - React-Core - - Stripe (~> 22.7.0) - - StripeFinancialConnections (~> 22.7.0) - - Stripe/Stripe3DS2 (22.7.1): - - StripeApplePay (= 22.7.1) - - StripeCore (= 22.7.1) - - StripeUICore (= 22.7.1) - - StripeApplePay (22.7.1): - - StripeCore (= 22.7.1) - - StripeCore (22.7.1) - - StripeFinancialConnections (22.7.1): - - StripeCore (= 22.7.1) - - StripeUICore (= 22.7.1) - - StripeUICore (22.7.1): - - StripeCore (= 22.7.1) + - Stripe (~> 22.8.1) + - StripeFinancialConnections (~> 22.8.1) + - Stripe/Stripe3DS2 (22.8.3): + - StripeApplePay (= 22.8.3) + - StripeCore (= 22.8.3) + - StripeUICore (= 22.8.3) + - StripeApplePay (22.8.3): + - StripeCore (= 22.8.3) + - StripeCore (22.8.3) + - StripeFinancialConnections (22.8.3): + - StripeCore (= 22.8.3) + - StripeUICore (= 22.8.3) + - StripeUICore (22.8.3): + - StripeCore (= 22.8.3) - UMAppLoader (3.1.0) - Yoga (1.14.0) - ZXingObjC/Core (3.6.5) @@ -3257,7 +3257,7 @@ SPEC CHECKSUMS: ABI45_0_0RNGestureHandler: c6a3399e797896b51aaafa5ffd9ef3217a965554 ABI45_0_0RNReanimated: 7dbc4ffd288bfbb8657e5d28d8c633108a4fbc7a ABI45_0_0RNScreens: 09474fa5b21fada696fcfda7029ea1de53af363b - ABI45_0_0stripe-react-native: 48734eb853a923bbf041ceab5ac065c9a2c08d49 + ABI45_0_0stripe-react-native: d7346ed066d4ef66def5e132789555422e1c9ea2 ABI45_0_0UMAppLoader: 9f4f8ab793e254030a3e1986ce2d75b89308168b ABI45_0_0Yoga: 59ff587f79c3e37f52418bef27055ecf8e0bce1c ABI46_0_0EASClient: c419fef100fbdac83e755c790d6b427594d4263e @@ -3364,7 +3364,7 @@ SPEC CHECKSUMS: ABI46_0_0RNGestureHandler: 42f00c4ce0ac12c4c51081de65e2ba831dfd5cb7 ABI46_0_0RNReanimated: c68969af1eae15d09ee5b8b3b61407feb5db9884 ABI46_0_0RNScreens: f521a4613dc2207aab4fcd72b5bb7cef74104a39 - ABI46_0_0stripe-react-native: 599bc13001fb6eae9a687e1eb587c720d04ee744 + ABI46_0_0stripe-react-native: 608f940005eaa49b26166ddb85d1b5f996e962a2 ABI46_0_0UMAppLoader: 271ab738d153f711d91da6b1b7a3a981d668dcfe ABI46_0_0Yoga: 898c6ba86e91d19b0810efa11e8a6c7e5559a64d Amplitude: cc34fcd8dfffc3470bc2e05f3a4abb0178f6d963 @@ -3521,12 +3521,12 @@ SPEC CHECKSUMS: RNGestureHandler: 7673697e7c0e9391adefae4faa087442bc04af33 RNReanimated: 5c8c17e26787fd8984cd5accdc70fef2ca70aafd RNScreens: f3230dd008a7d0ce5c0a8bc78ff12cf2315bda24 - Stripe: fb29a476e4866fec4ef22fb76207363dd32795aa - stripe-react-native: 5663bf9de94bff6b3d92c16d32784433b5e94cf7 - StripeApplePay: 09955cdf3f49b367af2feadd9c5b3bddb35446c0 - StripeCore: 39ea580c26ccc324fb9671288a2ae21114e54dca - StripeFinancialConnections: 0e1d638388572d52ce829416fbc7b0af2bde3865 - StripeUICore: eed17e95a4517fc02482e250a6422c2a81a14ce8 + Stripe: 90c12b350ae9e8a262efa87d230c00110c741f88 + stripe-react-native: 001c3d794839be02fd3857f1284adfc0c7dea5c7 + StripeApplePay: ce1edb7b3cb8b1abe29734f0fb887ee839a08043 + StripeCore: 7197196aa2344c5c5eaf4a1ba406950c06b1c01c + StripeFinancialConnections: ff08ef2f655b8f577f0bf70f10b588517dfa667e + StripeUICore: 5b635db49163d62f9aa831269031de382bcb2bbd UMAppLoader: 90a8e7f4b09b576588e7c2939321860fba47343e Yoga: 043f8eb97345d0171f27fead4d1849cacf0472a5 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb diff --git a/ios/vendored/sdk45/@stripe/stripe-react-native/ABI45_0_0stripe-react-native.podspec.json b/ios/vendored/sdk45/@stripe/stripe-react-native/ABI45_0_0stripe-react-native.podspec.json index 2605a544fbcf8..4190d2382c497 100644 --- a/ios/vendored/sdk45/@stripe/stripe-react-native/ABI45_0_0stripe-react-native.podspec.json +++ b/ios/vendored/sdk45/@stripe/stripe-react-native/ABI45_0_0stripe-react-native.podspec.json @@ -16,10 +16,10 @@ "dependencies": { "ABI45_0_0React-Core": [], "Stripe": [ - "~> 22.7.0" + "~> 22.8.1" ], "StripeFinancialConnections": [ - "~> 22.7.0" + "~> 22.8.1" ] }, "pod_target_xcconfig": { diff --git a/ios/vendored/sdk46/@stripe/stripe-react-native/ABI46_0_0stripe-react-native.podspec.json b/ios/vendored/sdk46/@stripe/stripe-react-native/ABI46_0_0stripe-react-native.podspec.json index fa591732f831f..b4ba774d96689 100644 --- a/ios/vendored/sdk46/@stripe/stripe-react-native/ABI46_0_0stripe-react-native.podspec.json +++ b/ios/vendored/sdk46/@stripe/stripe-react-native/ABI46_0_0stripe-react-native.podspec.json @@ -16,10 +16,10 @@ "dependencies": { "ABI46_0_0React-Core": [], "Stripe": [ - "~> 22.7.0" + "~> 22.8.1" ], "StripeFinancialConnections": [ - "~> 22.7.0" + "~> 22.8.1" ] }, "pod_target_xcconfig": { diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift index 056c58dc16ef1..c0faba822789b 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift @@ -48,14 +48,17 @@ class Errors { return ["error": value] } + class func createError (_ code: String, _ error: NSError?) -> NSDictionary { + let rootError = getRootError(error) + let value: NSDictionary = [ "code": code, - "message": error?.userInfo[STPError.errorMessageKey] ?? error?.localizedDescription ?? NSNull(), - "localizedMessage": error?.localizedDescription ?? NSNull(), - "declineCode": error?.userInfo[STPError.stripeDeclineCodeKey] ?? NSNull(), - "stripeErrorCode": error?.userInfo[STPError.stripeErrorCodeKey] ?? NSNull(), - "type": error?.userInfo[STPError.stripeErrorTypeKey] ?? NSNull(), + "message": rootError?.userInfo[STPError.errorMessageKey] ?? rootError?.localizedDescription ?? NSNull(), + "localizedMessage": rootError?.localizedDescription ?? NSNull(), + "declineCode": rootError?.userInfo[STPError.stripeDeclineCodeKey] ?? NSNull(), + "stripeErrorCode": rootError?.userInfo[STPError.stripeErrorCodeKey] ?? NSNull(), + "type": rootError?.userInfo[STPError.stripeErrorTypeKey] ?? NSNull(), ] return ["error": value] @@ -91,7 +94,15 @@ class Errors { return createError(code, NSError.stp_error(from: stripeError)) } - return createError(code, error as NSError) + return createError(code, error as NSError?) + } + + class func getRootError(_ error: NSError?) -> NSError? { + // Dig and find the underlying error, otherwise we'll throw errors like "Try again" + if let underlyingError = error?.userInfo[NSUnderlyingErrorKey] as? NSError { + return getRootError(underlyingError) + } + return error } static let MISSING_INIT_ERROR = Errors.createError(ErrorType.Failed, "Stripe has not been initialized. Initialize Stripe in your app with the StripeProvider component or the initStripe method.") diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m index a8c2f16c5dbe8..a1676e9b50e73 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m @@ -71,6 +71,7 @@ @interface RCT_EXTERN_MODULE(StripeSdk, RCTEventEmitter) RCT_EXTERN_METHOD( handleNextAction:(NSString *)paymentIntentClientSecret + returnURL:(NSString *)returnURL resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift index 41ddbcfcc14e8..ad329d23e0b08 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift @@ -684,14 +684,15 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi } } - @objc(handleNextAction:resolver:rejecter:) + @objc(handleNextAction:returnURL:resolver:rejecter:) func handleNextAction( paymentIntentClientSecret: String, + returnURL: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock ){ let paymentHandler = STPPaymentHandler.shared() - paymentHandler.handleNextAction(forPayment: paymentIntentClientSecret, with: self, returnURL: nil) { status, paymentIntent, handleActionError in + paymentHandler.handleNextAction(forPayment: paymentIntentClientSecret, with: self, returnURL: returnURL) { status, paymentIntent, handleActionError in switch (status) { case .failed: resolve(Errors.createError(ErrorType.Failed, handleActionError)) diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/stripe-react-native.podspec.json b/ios/vendored/unversioned/@stripe/stripe-react-native/stripe-react-native.podspec.json index 941e8b5e1ee69..9be3e95b61bd9 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/stripe-react-native.podspec.json +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/stripe-react-native.podspec.json @@ -1,6 +1,6 @@ { "name": "stripe-react-native", - "version": "0.18.1", + "version": "0.19.0", "summary": "Stripe SDK for React Native", "homepage": "https://github.com/stripe/stripe-react-native/#readme", "license": "MIT", @@ -10,17 +10,17 @@ }, "source": { "git": "https://github.com/stripe/stripe-react-native.git", - "tag": "0.18.1" + "tag": "0.19.0" }, "source_files": "ios/**/*.{h,m,mm,swift}", "exclude_files": "ios/Tests/", "dependencies": { "React-Core": [], "Stripe": [ - "~> 22.7.0" + "~> 22.8.1" ], "StripeFinancialConnections": [ - "~> 22.7.0" + "~> 22.8.1" ] }, "testspecs": [ diff --git a/packages/expo-notifications/CHANGELOG.md b/packages/expo-notifications/CHANGELOG.md index 7fb739a5a836b..33aaf04a4e09a 100644 --- a/packages/expo-notifications/CHANGELOG.md +++ b/packages/expo-notifications/CHANGELOG.md @@ -11,6 +11,8 @@ ### 🐛 Bug fixes +- Fixed build error for setting `compileSdkVersion` to 33. ([#19432](https://github.com/expo/expo/pull/19432) by [@kudo](https://github.com/kudo)) + ### 💡 Others - [plugin] Migrate import from @expo/config-plugins to expo/config-plugins and @expo/config-types to expo/config. ([#18855](https://github.com/expo/expo/pull/18855) by [@brentvatne](https://github.com/brentvatne)) diff --git a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt index 7556f8af468f4..c957a9e94e57a 100644 --- a/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt +++ b/packages/expo-notifications/android/src/main/java/expo/modules/notifications/service/NotificationsService.kt @@ -502,7 +502,7 @@ open class NotificationsService : BroadcastReceiver() { val notification = intent.getParcelableExtra(NOTIFICATION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_KEY not found in the intent extras.") val action = intent.getParcelableExtra(NOTIFICATION_ACTION_KEY) ?: throw IllegalArgumentException("$NOTIFICATION_ACTION_KEY not found in the intent extras.") val response = if (action is TextInputNotificationAction) { - val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent).getString(USER_TEXT_RESPONSE_KEY) + val userText = action.placeholder ?: RemoteInput.getResultsFromIntent(intent)?.getString(USER_TEXT_RESPONSE_KEY) ?: "" TextInputNotificationResponse(action, notification, userText) } else { NotificationResponse(action, notification) diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 5c30796395a46..bc1bd0439c8b4 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -9,7 +9,7 @@ "@react-native-firebase/app": "~15.4.0", "@react-native-picker/picker": "2.4.6", "@react-native-segmented-control/segmented-control": "2.4.0", - "@stripe/stripe-react-native": "0.18.1", + "@stripe/stripe-react-native": "0.19.0", "expo-analytics-amplitude": "~11.3.0", "expo-app-auth": "~11.1.0", "expo-app-loader-provider": "~8.0.0",