From 228c096eeda8a9817ca6b6a6ceb37b873416f462 Mon Sep 17 00:00:00 2001 From: Tomasz Sapeta Date: Fri, 9 Sep 2022 17:21:20 +0200 Subject: [PATCH] Update stripe-react-native to 0.18.1 to fix compilation errors in Xcode 14 --- CHANGELOG.md | 2 + .../AuBECSDebitFormView.kt | 2 + .../reactnativestripesdk/CardFieldView.kt | 34 +- .../reactnativestripesdk/CardFormView.kt | 38 +- .../CollectBankAccountLauncherFragment.kt | 14 +- .../FinancialConnectionsSheetFragment.kt | 278 +++++++++ .../GooglePayButtonManager.kt | 30 + .../GooglePayButtonView.kt | 38 ++ .../reactnativestripesdk/GooglePayFragment.kt | 8 + .../GooglePayPaymentMethodLauncherFragment.kt | 8 +- .../PaymentLauncherFragment.kt | 23 +- .../PaymentMethodCreateParamsFactory.kt | 580 +++++++----------- .../PaymentSheetAppearance.kt | 1 + .../PaymentSheetFragment.kt | 78 ++- .../reactnativestripesdk/StripeSdkModule.kt | 167 +++-- .../reactnativestripesdk/StripeSdkPackage.kt | 3 + .../AddToWalletButtonManager.kt | 55 ++ .../pushprovisioning/AddToWalletButtonView.kt | 147 +++++ .../AddToWalletCompleteEvent.kt | 22 + .../pushprovisioning/EphemeralKeyProvider.kt | 35 ++ .../pushprovisioning/PushProvisioningProxy.kt | 129 ++++ .../pushprovisioning/TapAndPayProxy.kt | 136 ++++ .../{ => utils}/Errors.kt | 6 +- .../{ => utils}/Extensions.kt | 13 +- .../{ => utils}/Mappers.kt | 29 +- .../utils/PostalCodeUtilities.kt | 18 + ios/Podfile.lock | 64 +- .../ABI45_0_0stripe-react-native.podspec.json | 4 +- .../ABI46_0_0stripe-react-native.podspec.json | 4 +- .../ios/ApplePayUtils.swift | 159 +++++ .../ios/CardFieldView.swift | 19 +- .../ios/CardFormView.swift | 20 + .../stripe-react-native/ios/Errors.swift | 13 +- .../ios/FinancialConnections.swift | 258 ++++++++ .../stripe-react-native/ios/Mappers.swift | 42 +- .../ios/PaymentMethodFactory.swift | 9 + .../stripe-react-native/ios/StripeSdk.m | 16 +- .../stripe-react-native/ios/StripeSdk.swift | 172 ++++-- .../ios/Tests/ApplePayUtilsTests.swift | 214 +++++++ .../ios/Tests/PushProvisioningTests.swift | 49 ++ .../AddToWalletButtonView.swift | 6 +- .../PushProvisioningUtils.swift | 64 ++ .../stripe-react-native.podspec.json | 16 +- packages/expo/bundledNativeModules.json | 2 +- 44 files changed, 2418 insertions(+), 607 deletions(-) create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/FinancialConnectionsSheetFragment.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonManager.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonView.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt rename android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/{ => utils}/Errors.kt (93%) rename android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/{ => utils}/Extensions.kt (58%) rename android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/{ => utils}/Mappers.kt (97%) create mode 100644 android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/utils/PostalCodeUtilities.kt create mode 100644 ios/vendored/unversioned/@stripe/stripe-react-native/ios/ApplePayUtils.swift create mode 100644 ios/vendored/unversioned/@stripe/stripe-react-native/ios/FinancialConnections.swift create mode 100644 ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/ApplePayUtilsTests.swift create mode 100644 ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/PushProvisioningTests.swift create mode 100644 ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/PushProvisioningUtils.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f9502210cd94d..97a4cceeb14c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Package-specific changes not released in any SDK will be added here just before ### 📚 3rd party library updates +- Updated `@stripe/stripe-react-native` from `0.13.1` to `0.18.1`. + ### 🛠 Breaking changes ### 🎉 New features 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..1f63c7ffbcb6a 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 @@ -21,7 +24,6 @@ import com.stripe.android.model.Address import com.stripe.android.model.PaymentMethodCreateParams import com.stripe.android.view.CardFormView import com.stripe.android.view.CardInputListener -import host.exp.expoview.R class CardFormView(context: ThemedReactContext) : FrameLayout(context) { private var cardForm: CardFormView = CardFormView(context, null, R.style.StripeCardFormView_Borderless) @@ -51,10 +53,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 +262,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/GooglePayButtonManager.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonManager.kt new file mode 100644 index 0000000000000..6d4565a0955b1 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonManager.kt @@ -0,0 +1,30 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk + +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp + +class GooglePayButtonManager : SimpleViewManager() { + override fun getName(): String { + return REACT_CLASS + } + + override fun onAfterUpdateTransaction(view: GooglePayButtonView) { + super.onAfterUpdateTransaction(view) + + view.initialize() + } + + @ReactProp(name = "buttonType") + fun buttonType(view: GooglePayButtonView, buttonType: String) { + view.setType(buttonType) + } + + override fun createViewInstance(reactContext: ThemedReactContext): GooglePayButtonView { + return GooglePayButtonView(reactContext) + } + + companion object { + const val REACT_CLASS = "GooglePayButton" + } +} diff --git a/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonView.kt b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonView.kt new file mode 100644 index 0000000000000..416780630e393 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/GooglePayButtonView.kt @@ -0,0 +1,38 @@ +package versioned.host.exp.exponent.modules.api.components.reactnativestripesdk + +import android.content.res.Configuration +import android.view.LayoutInflater +import android.widget.FrameLayout +import com.facebook.react.uimanager.ThemedReactContext + +class GooglePayButtonView(private val context: ThemedReactContext) : FrameLayout(context) { + private var buttonType: String? = null + + fun initialize() { + val type = + when (buttonType) { + "pay" -> R.layout.pay_with_googlepay_button_no_shadow + "pay_dark" -> R.layout.pay_with_googlepay_button_dark + "pay_shadow" -> R.layout.pay_with_googlepay_button + "standard" -> R.layout.googlepay_button_no_shadow + "standard_dark" -> R.layout.googlepay_button_dark + "standard_shadow" -> R.layout.googlepay_button + else -> if (isNightMode()) R.layout.googlepay_button_dark else R.layout.googlepay_button + } + + val button = LayoutInflater.from(context).inflate( + type, null + ) + + addView(button) + } + + fun setType(type: String) { + buttonType = type + } + + private fun isNightMode(): Boolean { + val nightModeFlags: Int = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return nightModeFlags == Configuration.UI_MODE_NIGHT_YES + } +} 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..58b3e69125ee6 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,8 @@ class StripeSdkPackage : ReactPackage { AuBECSDebitFormViewManager(), StripeContainerManager(), CardFormViewManager(), + GooglePayButtonManager(), + 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..706246c90e284 --- /dev/null +++ b/android/expoview/src/main/java/versioned/host/exp/exponent/modules/api/components/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt @@ -0,0 +1,147 @@ +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 + .asDrawable() + .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/ios/Podfile.lock b/ios/Podfile.lock index 5edf5cba28b98..8076424b79cba 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.4.0) - - StripeFinancialConnections (~> 22.4.0) + - Stripe (~> 22.7.0) + - StripeFinancialConnections (~> 22.7.0) - 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.4.0) - - StripeFinancialConnections (~> 22.4.0) + - Stripe (~> 22.7.0) + - StripeFinancialConnections (~> 22.7.0) - ABI46_0_0UMAppLoader (3.1.0) - ABI46_0_0Yoga (1.14.0) - Amplitude (6.0.0) @@ -2059,27 +2059,27 @@ PODS: - RNScreens (3.15.0): - React-Core - React-RCTImage - - Stripe (22.4.0): - - Stripe/Stripe3DS2 (= 22.4.0) - - StripeApplePay (= 22.4.0) - - StripeCore (= 22.4.0) - - StripeUICore (= 22.4.0) - - stripe-react-native (0.13.1): + - 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): - React-Core - - Stripe (~> 22.4.0) - - StripeFinancialConnections (~> 22.4.0) - - Stripe/Stripe3DS2 (22.4.0): - - StripeApplePay (= 22.4.0) - - StripeCore (= 22.4.0) - - StripeUICore (= 22.4.0) - - StripeApplePay (22.4.0): - - StripeCore (= 22.4.0) - - StripeCore (22.4.0) - - StripeFinancialConnections (22.4.0): - - StripeCore (= 22.4.0) - - StripeUICore (= 22.4.0) - - StripeUICore (22.4.0): - - StripeCore (= 22.4.0) + - 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) - UMAppLoader (3.1.0) - Yoga (1.14.0) - ZXingObjC/Core (3.6.5) @@ -3255,7 +3255,7 @@ SPEC CHECKSUMS: ABI45_0_0RNGestureHandler: c6a3399e797896b51aaafa5ffd9ef3217a965554 ABI45_0_0RNReanimated: 7dbc4ffd288bfbb8657e5d28d8c633108a4fbc7a ABI45_0_0RNScreens: 09474fa5b21fada696fcfda7029ea1de53af363b - ABI45_0_0stripe-react-native: 165947b4b857c03da141aae92470620c0d2e742c + ABI45_0_0stripe-react-native: 48734eb853a923bbf041ceab5ac065c9a2c08d49 ABI45_0_0UMAppLoader: 9f4f8ab793e254030a3e1986ce2d75b89308168b ABI45_0_0Yoga: 59ff587f79c3e37f52418bef27055ecf8e0bce1c ABI46_0_0EASClient: c419fef100fbdac83e755c790d6b427594d4263e @@ -3362,7 +3362,7 @@ SPEC CHECKSUMS: ABI46_0_0RNGestureHandler: 42f00c4ce0ac12c4c51081de65e2ba831dfd5cb7 ABI46_0_0RNReanimated: c68969af1eae15d09ee5b8b3b61407feb5db9884 ABI46_0_0RNScreens: f521a4613dc2207aab4fcd72b5bb7cef74104a39 - ABI46_0_0stripe-react-native: 7f12b5c30e713ef7d444295e7021acb13ccff2fb + ABI46_0_0stripe-react-native: 599bc13001fb6eae9a687e1eb587c720d04ee744 ABI46_0_0UMAppLoader: 271ab738d153f711d91da6b1b7a3a981d668dcfe ABI46_0_0Yoga: 898c6ba86e91d19b0810efa11e8a6c7e5559a64d Amplitude: cc34fcd8dfffc3470bc2e05f3a4abb0178f6d963 @@ -3515,12 +3515,12 @@ SPEC CHECKSUMS: RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNReanimated: 5c8c17e26787fd8984cd5accdc70fef2ca70aafd RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 - Stripe: a925dfa9a7e51aa8f782f428abc10cb828b45a85 - stripe-react-native: 01069cba9dfaaf108df9c65a266ff753f311ddc8 - StripeApplePay: 369857bafe8baf02e31b47478db1574febd2a2f3 - StripeCore: a94d2823817c97c79fce60884ab9027a2da798c1 - StripeFinancialConnections: 269658b79f639c2d6b7d513235d469418b04c2dd - StripeUICore: a8e24a6c91a5c99075c7a490d45695fb912876e8 + Stripe: fb29a476e4866fec4ef22fb76207363dd32795aa + stripe-react-native: 5663bf9de94bff6b3d92c16d32784433b5e94cf7 + StripeApplePay: 09955cdf3f49b367af2feadd9c5b3bddb35446c0 + StripeCore: 39ea580c26ccc324fb9671288a2ae21114e54dca + StripeFinancialConnections: 0e1d638388572d52ce829416fbc7b0af2bde3865 + StripeUICore: eed17e95a4517fc02482e250a6422c2a81a14ce8 UMAppLoader: 6185e8c45922f187002b85afae097961c4781df4 Yoga: 7ab6e3ee4ce47d7b789d1cb520163833e515f452 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 dbbdd689a4c77..2605a544fbcf8 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.4.0" + "~> 22.7.0" ], "StripeFinancialConnections": [ - "~> 22.4.0" + "~> 22.7.0" ] }, "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 7a75a480dfc41..fa591732f831f 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.4.0" + "~> 22.7.0" ], "StripeFinancialConnections": [ - "~> 22.4.0" + "~> 22.7.0" ] }, "pod_target_xcconfig": { diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/ApplePayUtils.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/ApplePayUtils.swift new file mode 100644 index 0000000000000..e8dc6fb183f82 --- /dev/null +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/ApplePayUtils.swift @@ -0,0 +1,159 @@ +// +// ApplePayUtils.swift +// stripe-react-native +// +// Created by Charles Cruzan on 6/27/22. +// + +import Foundation +import Stripe + +class ApplePayUtils { + + @available(iOS 15.0, *) + internal class func createDeferredPaymentSummaryItem(item: [String : Any]) throws -> PKPaymentSummaryItem { + let label = item["label"] as? String ?? "" + let amount = NSDecimalNumber(string: item["amount"] as? String ?? "") + + let deferredItem = PKDeferredPaymentSummaryItem( + label: label, + amount: amount + ) + guard let date = item["deferredDate"] as? Double else { + throw ApplePayUtilsError.missingParameter(label, "deferredDate") + } + deferredItem.deferredDate = Date(timeIntervalSince1970: date) + return deferredItem + } + + @available(iOS 15.0, *) + internal class func createRecurringPaymentSummaryItem(item: [String : Any]) throws -> PKPaymentSummaryItem { + let label = item["label"] as? String ?? "" + let amount = NSDecimalNumber(string: item["amount"] as? String ?? "") + + let recurringItem = PKRecurringPaymentSummaryItem( + label: label, + amount: amount + ) + guard let intervalCount = item["intervalCount"] as? Int else { + throw ApplePayUtilsError.missingParameter(label, "intervalCount") + } + recurringItem.intervalCount = intervalCount + recurringItem.intervalUnit = try mapToIntervalUnit(intervalString: item["intervalUnit"] as? String) + if let startDate = item["startDate"] as? Double { + recurringItem.startDate = Date(timeIntervalSince1970: startDate) + } + if let endDate = item["endDate"] as? Double { + recurringItem.endDate = Date(timeIntervalSince1970: endDate) + } + return recurringItem + } + + internal class func mapToIntervalUnit(intervalString: String?) throws -> NSCalendar.Unit { + switch intervalString { + case "minute": + return NSCalendar.Unit.minute + case "hour": + return NSCalendar.Unit.hour + case "day": + return NSCalendar.Unit.day + case "month": + return NSCalendar.Unit.month + case "year": + return NSCalendar.Unit.year + default: + throw ApplePayUtilsError.invalidTimeInterval(intervalString ?? "null") + } + } + + internal class func createImmediatePaymentSummaryItem(item: [String : Any]) -> PKPaymentSummaryItem { + let label = item["label"] as? String ?? "" + let amount = NSDecimalNumber(string: item["amount"] as? String ?? "") + + return PKPaymentSummaryItem( + label: label, + amount: amount, + type: item["isPending"] as? Bool ?? false ? + PKPaymentSummaryItemType.pending : PKPaymentSummaryItemType.final + ) + } + + public class func buildPaymentSummaryItems(items: [[String : Any]]?) throws -> [PKPaymentSummaryItem] { + var paymentSummaryItems: [PKPaymentSummaryItem] = [] + if let items = items { + for item in items { + let paymentSummaryItem = try buildPaymentSummaryItem(item: item) + paymentSummaryItems.append(paymentSummaryItem) + } + } + + return paymentSummaryItems + } + + internal class func buildPaymentSummaryItem(item: [String : Any]) throws -> PKPaymentSummaryItem { + switch item["paymentType"] as? String { + case "Deferred": + if #available(iOS 15.0, *) { + return try createDeferredPaymentSummaryItem(item: item) + } else { + return createImmediatePaymentSummaryItem(item: item) + } + case "Recurring": + if #available(iOS 15.0, *) { + return try createRecurringPaymentSummaryItem(item: item) + } else { + return createImmediatePaymentSummaryItem(item: item) + } + case "Immediate": + return createImmediatePaymentSummaryItem(item: item) + default: + throw ApplePayUtilsError.invalidCartSummaryItemType(item["paymentType"] as? String ?? "null") + } + } + + public class func buildPaymentSheetApplePayConfig( + merchantIdentifier: String?, + merchantCountryCode: String?, + paymentSummaryItems: [[String : Any]]? + ) throws -> PaymentSheet.ApplePayConfiguration { + guard let merchantId = merchantIdentifier else { + throw ApplePayUtilsError.missingMerchantId + } + guard let countryCode = merchantCountryCode else { + throw ApplePayUtilsError.missingCountryCode + } + let paymentSummaryItems = try ApplePayUtils.buildPaymentSummaryItems( + items: paymentSummaryItems + ) + return PaymentSheet.ApplePayConfiguration.init( + merchantId: merchantId, + merchantCountryCode: countryCode, + paymentSummaryItems:paymentSummaryItems.count > 0 ? paymentSummaryItems : nil + ) + } +} + +enum ApplePayUtilsError : Error, Equatable { + case invalidCartSummaryItemType(String) + case missingParameter(String, String) + case invalidTimeInterval(String) + case missingMerchantId + case missingCountryCode +} + +extension ApplePayUtilsError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidCartSummaryItemType(let type): + return "Failed to ceate Apple Pay summary item. Expected `type` to be one of 'Immediate', 'Recurring', or 'Deferred', but received: \(type)" + case .missingParameter(let label, let parameter): + return "Failed to create Apple Pay summary item with label: \(label). The \(parameter) item parameter is required, but none was provided." + case .invalidTimeInterval(let providedInterval): + return "Failed to create Apple Pay summary item. \(providedInterval) is not a valid timeInterval, must be one of: minute, hour, day, month, or year." + case .missingMerchantId: + return "`merchantIdentifier` is required, but none was found. Ensure you are passing this to initStripe your StripeProvider." + case .missingCountryCode: + return "`merchantCountryCode` is a required param, but was not provided." + } + } +} diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFieldView.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFieldView.swift index ebbe37c8f8238..0f9284381c300 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFieldView.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFieldView.swift @@ -132,13 +132,16 @@ class CardFieldView: UIView, STPPaymentCardTextFieldDelegate { func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { if onCardChange != nil { - let brand = STPCardValidator.brand(forNumber: textField.cardParams.number ?? "") - let validExpiryDate = STPCardValidator.validationState(forExpirationYear: textField.cardParams.expYear?.stringValue ?? "", inMonth: textField.cardParams.expMonth?.stringValue ?? "") - let validCVC = STPCardValidator.validationState(forCVC: textField.cardParams.cvc ?? "", cardBrand: brand) - let validNumber = STPCardValidator.validationState(forNumber: textField.cardParams.number ?? "", validatingCardBrand: true) + let brand = STPCardValidator.brand(forNumber: textField.cardNumber ?? "") + let validExpiryDate = STPCardValidator.validationState( + forExpirationYear: textField.formattedExpirationYear ?? "", + inMonth: textField.formattedExpirationMonth ?? "" + ) + let validCVC = STPCardValidator.validationState(forCVC: textField.cvc ?? "", cardBrand: brand) + let validNumber = STPCardValidator.validationState(forNumber: textField.cardNumber ?? "", validatingCardBrand: true) var cardData: [String: Any?] = [ - "expiryMonth": textField.cardParams.expMonth ?? NSNull(), - "expiryYear": textField.cardParams.expYear ?? NSNull(), + "expiryMonth": textField.expirationMonth, + "expiryYear": textField.expirationYear, "complete": textField.isValid, "brand": Mappers.mapFromCardBrand(brand) ?? NSNull(), "last4": textField.cardParams.last4 ?? "", @@ -150,8 +153,8 @@ class CardFieldView: UIView, STPPaymentCardTextFieldDelegate { cardData["postalCode"] = textField.postalCode ?? "" } if (dangerouslyGetFullCardDetails) { - cardData["number"] = textField.cardParams.number ?? "" - cardData["cvc"] = textField.cardParams.cvc ?? "" + cardData["number"] = textField.cardNumber ?? "" + cardData["cvc"] = textField.cvc ?? "" } onCardChange!(cardData as [AnyHashable : Any]) } diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFormView.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFormView.swift index fdc2b4488bdcb..f0bc39b2ad3b2 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFormView.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/CardFormView.swift @@ -75,6 +75,26 @@ class CardFormView: UIView, STPCardFormViewDelegate { if let backgroundColor = cardStyle["backgroundColor"] as? String { cardForm?.backgroundColor = UIColor(hexString: backgroundColor) } + /** + The following reveals a bug in STPCardFormView where there's a extra space in the layer, + and thus must remain commented out for now. + + if let borderWidth = cardStyle["borderWidth"] as? Int { + cardForm?.layer.borderWidth = CGFloat(borderWidth) + } else { + cardForm?.layer.borderWidth = CGFloat(0) + } + + */ + if let borderColor = cardStyle["borderColor"] as? String { + cardForm?.layer.borderColor = UIColor(hexString: borderColor).cgColor + } + if let borderRadius = cardStyle["borderRadius"] as? Int { + cardForm?.layer.cornerRadius = CGFloat(borderRadius) + } + if let cursorColor = cardStyle["cursorColor"] as? String { + cardForm?.tintColor = UIColor(hexString: cursorColor) + } // if let disabledBackgroundColor = cardStyle["disabledBackgroundColor"] as? String { // cardForm?.disabledBackgroundColor = UIColor(hexString: disabledBackgroundColor) // } 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 66083042b52dc..056c58dc16ef1 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Errors.swift @@ -1,4 +1,5 @@ import Stripe +@_spi(STP) import StripeCore enum ErrorType { static let Failed = "Failed" @@ -50,7 +51,7 @@ class Errors { class func createError (_ code: String, _ error: NSError?) -> NSDictionary { let value: NSDictionary = [ "code": code, - "message": error?.userInfo[STPError.errorMessageKey] ?? NSNull(), + "message": error?.userInfo[STPError.errorMessageKey] ?? error?.localizedDescription ?? NSNull(), "localizedMessage": error?.localizedDescription ?? NSNull(), "declineCode": error?.userInfo[STPError.stripeDeclineCodeKey] ?? NSNull(), "stripeErrorCode": error?.userInfo[STPError.stripeErrorCodeKey] ?? NSNull(), @@ -84,5 +85,15 @@ class Errors { return ["error": value] } + + class func createError(_ code: String, _ error: Error) -> NSDictionary { + if let stripeError = error as? StripeError { + return createError(code, NSError.stp_error(from: stripeError)) + } + + return createError(code, error as NSError) + } + + 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/FinancialConnections.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/FinancialConnections.swift new file mode 100644 index 0000000000000..8b44a8866e030 --- /dev/null +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/FinancialConnections.swift @@ -0,0 +1,258 @@ +// +// FinancialConnections.swift +// stripe-react-native +// +// Created by Charles Cruzan on 7/12/22. +// + +import Foundation +import StripeFinancialConnections +import Stripe + +class FinancialConnections { + + internal static func present( + withClientSecret: String, + resolve: @escaping RCTPromiseResolveBlock + ) -> Void { + DispatchQueue.main.async { + FinancialConnectionsSheet(financialConnectionsSessionClientSecret: withClientSecret).present( + from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), + completion: { result in + switch result { + case .completed(session: let session): + resolve([ "session": mapFromSessionResult(session) ]) + case .canceled: + resolve(Errors.createError(ErrorType.Canceled, "The flow has been canceled.")) + case .failed(let error): + resolve(Errors.createError(ErrorType.Failed, error)) + } + }) + } + } + + internal static func presentForToken( + withClientSecret: String, + resolve: @escaping RCTPromiseResolveBlock + ) -> Void { + DispatchQueue.main.async { + FinancialConnectionsSheet(financialConnectionsSessionClientSecret: withClientSecret).presentForToken( + from: findViewControllerPresenter(from: UIApplication.shared.delegate?.window??.rootViewController ?? UIViewController()), + completion: { result in + switch result { + case .completed(result: let result): + resolve( + [ + "session": mapFromSessionResult(result.session), + "token" : mapFromTokenResult(result.token) + ] + ) + case .canceled: + resolve(Errors.createError(ErrorType.Canceled, "The flow has been canceled.")) + case .failed(let error): + resolve(Errors.createError(ErrorType.Failed, error)) + } + }) + } + } + + internal static func mapFromSessionResult( + _ session: StripeAPI.FinancialConnectionsSession + ) -> NSDictionary { + return [ + "id": session.id, + "clientSecret": session.clientSecret, + "livemode": session.livemode, + "accounts": mapFromAccountsList(accounts: session.accounts) + ] + } + + internal static func mapFromTokenResult( + _ token: StripeAPI.BankAccountToken? + ) -> NSDictionary { + return [ + "bankAccount": mapFromBankAccount(bankAccount: token?.bankAccount) ?? NSNull(), + "livemode": token?.livemode ?? false, + "id": token?.id ?? NSNull(), + "used": token?.used ?? false, + "type": Mappers.mapFromTokenType(STPTokenType.bankAccount) ?? NSNull(), + "created": NSNull(), // Doesn't exist on StripeAPI.BankAccountToken + ] + } + + internal static func mapFromBankAccount( + bankAccount: StripeAPI.BankAccountToken.BankAccount? + ) -> NSDictionary? { + guard let bankAccount = bankAccount else { + return nil + } + // return Mappers.mapFromBankAccount(bankAccount) Cannot use this since it expects an STPBankAccount + return [ + "id": bankAccount.id, + "bankName": bankAccount.bankName ?? NSNull(), + "accountHolderName": bankAccount.accountHolderName ?? NSNull(), + "accountHolderType": NSNull(), // Doesn't exist on StripeAPI.BankAccountToken + "currency": bankAccount.currency, + "country": bankAccount.country, + "routingNumber": bankAccount.routingNumber ?? NSNull(), + "fingerprint": bankAccount.fingerprint ?? NSNull(), + "last4": bankAccount.last4, + "status": bankAccount.status.prefix(1).uppercased() + bankAccount.status.lowercased().dropFirst(), // stripe-ios returns a string, not STPBankAccountStatus + ] + } + + internal static func mapFromAccountsList( + accounts: StripeAPI.FinancialConnectionsSession.AccountList + ) -> [[String: Any]] { + var result = [[String: Any]]() + + for account in accounts.data { + result.append([ + "id": account.id, + "livemode": account.livemode, + "displayName": account.displayName ?? NSNull(), + "status": mapFromStatus(account.status), + "institutionName": account.institutionName, + "last4": account.last4 ?? NSNull(), + "created": account.created * 1000, + "balance": mapFromAccountBalance(balance: account.balance) ?? NSNull(), + "balanceRefresh": mapFromAccountBalanceRefresh(balanceRefresh: account.balanceRefresh) ?? NSNull(), + "category": mapFromCategory(account.category), + "subcategory": mapFromSubcategory(account.subcategory), + "permissions": account.permissions?.map { mapFromPermission($0) } ?? NSNull(), + "supportedPaymentMethodTypes": account.supportedPaymentMethodTypes.map { mapFromSupportedPaymentMethodTypes($0) }, + ]) + } + + return result + } + + internal static func mapFromAccountBalance( + balance: StripeAPI.FinancialConnectionsAccount.Balance? + ) -> NSDictionary? { + guard let balance = balance else { + return nil + } + + return [ + "asOf": balance.asOf * 1000, + "type": mapFromBalanceType(balance.type), +// TODO: Protected by internal on iOS only. PR is out to fix + "cash": ["available": NSNull()], // balance.cash?.available + "credit": ["used": NSNull()], // balance.credit?.used + "current": balance.current, + ] + } + + internal static func mapFromAccountBalanceRefresh( + balanceRefresh: StripeAPI.FinancialConnectionsAccount.BalanceRefresh? + ) -> NSDictionary? { + guard let balanceRefresh = balanceRefresh else { + return nil + } + + return [ + "status": mapFromBalanceRefreshStatus(balanceRefresh.status), + "lastAttemptedAt": balanceRefresh.lastAttemptedAt * 1000, + ] + } + + internal static func mapFromStatus( _ status: StripeAPI.FinancialConnectionsAccount.Status) -> String { + switch status { + case .active: + return "active" + case .inactive: + return "inactive" + case .disconnected: + return "disconnected" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromCategory( _ category: StripeAPI.FinancialConnectionsAccount.Category) -> String { + switch category { + case .cash: + return "cash" + case .credit: + return "credit" + case .investment: + return "investment" + case .other: + return "other" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromSubcategory( _ subcategory: StripeAPI.FinancialConnectionsAccount.Subcategory) -> String { + switch subcategory { + case .savings: + return "savings" + case .mortgage: + return "mortgage" + case .checking: + return "checking" + case .creditCard: + return "creditCard" + case .lineOfCredit: + return "lineOfCredit" + case .other: + return "other" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromPermission( _ permission: StripeAPI.FinancialConnectionsAccount.Permissions) -> String { + switch permission { + case .transactions: + return "transactions" + case .ownership: + return "ownership" + case .paymentMethod: + return "paymentMethod" + case .accountNumbers: + return "accountNumbers" + case .balances: + return "balances" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromSupportedPaymentMethodTypes( _ type: StripeAPI.FinancialConnectionsAccount.SupportedPaymentMethodTypes) -> String { + switch type { + case .usBankAccount: + return "usBankAccount" + case .link: + return "link" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromBalanceType( _ type: StripeAPI.FinancialConnectionsAccount.Balance.ModelType) -> String { + switch type { + case .cash: + return "cash" + case .credit: + return "credit" + case .unparsable: + return "unparsable" + } + } + + internal static func mapFromBalanceRefreshStatus( _ status: StripeAPI.FinancialConnectionsAccount.BalanceRefresh.Status) -> String { + switch status { + case .succeeded: + return "succeeded" + case .pending: + return "pending" + case .failed: + return "failed" + case .unparsable: + return "unparsable" + } + } +} diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Mappers.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Mappers.swift index 483300415663a..25af05059d9f1 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Mappers.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Mappers.swift @@ -16,17 +16,6 @@ class Mappers { } } - class func mapToPaymentSummaryItemType(type: String?) -> PKPaymentSummaryItemType { - if let type = type { - switch type { - case "pending": return PKPaymentSummaryItemType.pending - case "final": return PKPaymentSummaryItemType.final - default: return PKPaymentSummaryItemType.final - } - } - return PKPaymentSummaryItemType.final - } - class func mapFromBankAccountHolderType(_ type: STPBankAccountHolderType?) -> String? { if let type = type { switch type { @@ -61,18 +50,21 @@ class Mappers { } class func mapFromBankAccount(_ bankAccount: STPBankAccount?) -> NSDictionary? { - if (bankAccount == nil) { + guard let bankAccount = bankAccount else { return nil } + let result: NSDictionary = [ - "id": bankAccount?.stripeID ?? NSNull(), - "bankName": bankAccount?.bankName ?? NSNull(), - "accountHolderName": bankAccount?.accountHolderName ?? NSNull(), - "accountHolderType": mapFromBankAccountHolderType(bankAccount?.accountHolderType) ?? NSNull(), - "country": bankAccount?.country ?? NSNull(), - "currency": bankAccount?.currency ?? NSNull(), - "routingNumber": bankAccount?.routingNumber ?? NSNull(), - "status": mapFromBankAccountStatus(bankAccount?.status) ?? NSNull(), + "id": bankAccount.stripeID, + "bankName": bankAccount.bankName ?? NSNull(), + "accountHolderName": bankAccount.accountHolderName ?? NSNull(), + "accountHolderType": mapFromBankAccountHolderType(bankAccount.accountHolderType) ?? NSNull(), + "country": bankAccount.country ?? NSNull(), + "currency": bankAccount.currency ?? NSNull(), + "routingNumber": bankAccount.routingNumber ?? NSNull(), + "status": mapFromBankAccountStatus(bankAccount.status) ?? NSNull(), + "fingerprint": bankAccount.fingerprint ?? NSNull(), + "last4": bankAccount.last4 ?? NSNull() ] return result } @@ -171,8 +163,12 @@ class Mappers { let amount = NSDecimalNumber(string: method["amount"] as? String ?? "") let identifier = method["identifier"] as! String let detail = method["detail"] as? String ?? "" - let type = Mappers.mapToPaymentSummaryItemType(type: method["type"] as? String) - let pm = PKShippingMethod.init(label: label, amount: amount, type: type) + let pm = PKShippingMethod.init( + label: label, + amount: amount, + type: method["isPending"] as? Bool ?? false + ? PKPaymentSummaryItemType.pending : PKPaymentSummaryItemType.final + ) pm.identifier = identifier pm.detail = detail shippingMethodsList.append(pm) @@ -283,6 +279,7 @@ class Mappers { case STPPaymentMethodType.klarna: return "Klarna" case STPPaymentMethodType.USBankAccount: return "USBankAccount" case STPPaymentMethodType.payPal: return "PayPal" + case STPPaymentMethodType.affirm: return "Affirm" case STPPaymentMethodType.unknown: return "Unknown" default: return "Unknown" } @@ -312,6 +309,7 @@ class Mappers { case "WeChatPay": return STPPaymentMethodType.weChatPay case "USBankAccount": return STPPaymentMethodType.USBankAccount case "PayPal": return STPPaymentMethodType.payPal + case "Affirm": return STPPaymentMethodType.affirm default: return STPPaymentMethodType.unknown } } diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/PaymentMethodFactory.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/PaymentMethodFactory.swift index f78b838d9c7ae..4370cbb17b728 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/PaymentMethodFactory.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/PaymentMethodFactory.swift @@ -53,6 +53,8 @@ class PaymentMethodFactory { return try createUSBankAccountPaymentMethodParams() case STPPaymentMethodType.payPal: return try createPayPalPaymentMethodParams() + case STPPaymentMethodType.affirm: + return try createAffirmPaymentMethodParams() // case STPPaymentMethodType.weChatPay: // return try createWeChatPayPaymentMethodParams() default: @@ -102,6 +104,8 @@ class PaymentMethodFactory { return try createUSBankAccountPaymentMethodOptions() case STPPaymentMethodType.payPal: return nil + case STPPaymentMethodType.affirm: + return nil default: throw PaymentMethodError.paymentNotSupported } @@ -361,6 +365,11 @@ class PaymentMethodFactory { private func createPayPalPaymentMethodParams() throws -> STPPaymentMethodParams { return STPPaymentMethodParams(payPal: STPPaymentMethodPayPalParams(), billingDetails: billingDetailsParams, metadata: nil) } + + private func createAffirmPaymentMethodParams() throws -> STPPaymentMethodParams { + let params = STPPaymentMethodAffirmParams() + return STPPaymentMethodParams(affirm: params, metadata: nil) + } } enum PaymentMethodError: Error { 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 74c4ccd940e04..a8c2f16c5dbe8 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.m @@ -121,10 +121,24 @@ @interface RCT_EXTERN_MODULE(StripeSdk, RCTEventEmitter) resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + canAddCardToWallet:(NSDictionary *)params + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) RCT_EXTERN_METHOD( isCardInWallet:(NSDictionary *)params resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) - +RCT_EXTERN_METHOD( + collectBankAccountToken:(NSString *)clientSecret + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) +RCT_EXTERN_METHOD( + collectFinancialConnectionsAccounts:(NSString *)clientSecret + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) @end 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 cb03088f0984b..41ddbcfcc14e8 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/StripeSdk.swift @@ -1,5 +1,6 @@ import PassKit import Stripe +import StripeFinancialConnections @objc(StripeSdk) class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionViewControllerDelegate, UIAdaptivePresentationControllerDelegate { @@ -85,12 +86,15 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi } } - if params["applePay"] as? Bool == true { - if let merchantIdentifier = self.merchantIdentifier, let merchantCountryCode = params["merchantCountryCode"] as? String { - configuration.applePay = .init(merchantId: merchantIdentifier, - merchantCountryCode: merchantCountryCode) - } else { - resolve(Errors.createError(ErrorType.Failed, "Either merchantIdentifier or merchantCountryCode is missing")) + if let applePayParams = params["applePay"] as? NSDictionary { + do { + configuration.applePay = try ApplePayUtils.buildPaymentSheetApplePayConfig( + merchantIdentifier: self.merchantIdentifier, + merchantCountryCode: applePayParams["merchantCountryCode"] as? String, + paymentSummaryItems: applePayParams["paymentSummaryItems"] as? [[String : Any]] + ) + } catch { + resolve(Errors.createError(ErrorType.Failed, error.localizedDescription)) return } } @@ -145,15 +149,14 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi resolve(Errors.createError(ErrorType.Failed, error as NSError)) case .success(let paymentSheetFlowController): self.paymentSheetFlowController = paymentSheetFlowController + var result: NSDictionary? = nil if let paymentOption = stripeSdk?.paymentSheetFlowController?.paymentOption { - let option: NSDictionary = [ + result = [ "label": paymentOption.label, "image": paymentOption.image.pngData()?.base64EncodedString() ?? "" ] - resolve(Mappers.createResult("paymentOption", option)) - } else { - resolve(Mappers.createResult("paymentOption", nil)) } + resolve(Mappers.createResult("paymentOption", result)) } } @@ -285,6 +288,7 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi if (paymentMethodType == .payPal) { resolve(Errors.createError(ErrorType.Failed, "PayPal is not yet supported through SetupIntents.")) + return } var err: NSDictionary? = nil @@ -344,17 +348,16 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi resolve(Errors.createError(ErrorType.Failed, "You can use this method only after either onDidSetShippingMethod or onDidSetShippingContact events emitted")) return } - var paymentSummaryItems: [PKPaymentSummaryItem] = [] - if let items = summaryItems as? [[String : Any]] { - for item in items { - let label = item["label"] as? String ?? "" - let amount = NSDecimalNumber(string: item["amount"] as? String ?? "") - let type = Mappers.mapToPaymentSummaryItemType(type: item["type"] as? String) - paymentSummaryItems.append(PKPaymentSummaryItem(label: label, amount: amount, type: type)) - } + + var paymentSummaryItems : [PKPaymentSummaryItem] = [] + do { + paymentSummaryItems = try ApplePayUtils.buildPaymentSummaryItems(items: summaryItems as? [[String : Any]]) + } catch { + resolve(Errors.createError(ErrorType.Failed, error.localizedDescription)) + return } + var shippingAddressErrors: [Error] = [] - for item in errorAddressFields { let field = item["field"] as! String let message = item["message"] as? String ?? field + " error" @@ -513,18 +516,14 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi paymentRequest.shippingMethods = Mappers.mapToShippingMethods(shippingMethods: shippingMethods) - var paymentSummaryItems: [PKPaymentSummaryItem] = [] - - if let items = summaryItems as? [[String : Any]] { - for item in items { - let label = item["label"] as? String ?? "" - let amount = NSDecimalNumber(string: item["amount"] as? String ?? "") - let type = Mappers.mapToPaymentSummaryItemType(type: item["type"] as? String) - paymentSummaryItems.append(PKPaymentSummaryItem(label: label, amount: amount, type: type)) - } + do { + paymentRequest.paymentSummaryItems = try ApplePayUtils + .buildPaymentSummaryItems(items: summaryItems as? [[String : Any]]) + } catch { + resolve(Errors.createError(ErrorType.Failed, error.localizedDescription)) + return } - paymentRequest.paymentSummaryItems = paymentSummaryItems if let applePayContext = STPApplePayContext(paymentRequest: paymentRequest, delegate: self) { DispatchQueue.main.async { applePayContext.presentApplePay(completion: nil) @@ -572,12 +571,10 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi STPAPIClient.shared.createPaymentMethod(with: paymentMethodParams) { paymentMethod, error in if let createError = error { resolve(Errors.createError(ErrorType.Failed, createError.localizedDescription)) - return - } - - if let paymentMethod = paymentMethod { - let method = Mappers.mapFromPaymentMethod(paymentMethod) - resolve(Mappers.createResult("paymentMethod", method)) + } else { + resolve( + Mappers.createResult("paymentMethod", Mappers.mapFromPaymentMethod(paymentMethod)) + ) } } } else { @@ -763,10 +760,11 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi if let intent = intent { if (intent.status == .requiresPaymentMethod) { resolve(Errors.createError(ErrorType.Canceled, "Bank account collection was canceled.")) + } else { + resolve( + Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: intent)) + ) } - resolve( - Mappers.createResult("paymentIntent", Mappers.mapFromPaymentIntent(paymentIntent: intent)) - ) } else { resolve(Errors.createError(ErrorType.Unknown, "There was unexpected error while collecting bank account information.")) } @@ -787,10 +785,11 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi if let intent = intent { if (intent.status == .requiresPaymentMethod) { resolve(Errors.createError(ErrorType.Canceled, "Bank account collection was canceled.")) + } else { + resolve( + Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: intent)) + ) } - resolve( - Mappers.createResult("setupIntent", Mappers.mapFromSetupIntent(setupIntent: intent)) - ) } else { resolve(Errors.createError(ErrorType.Unknown, "There was unexpected error while collecting bank account information.")) } @@ -802,7 +801,7 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi @objc(confirmPayment:data:options:resolver:rejecter:) func confirmPayment( paymentIntentClientSecret: String, - params: NSDictionary, + params: NSDictionary?, options: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock @@ -810,13 +809,13 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi self.confirmPaymentResolver = resolve self.confirmPaymentClientSecret = paymentIntentClientSecret - let paymentMethodData = params["paymentMethodData"] as? NSDictionary - let type = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String) - guard let paymentMethodType = type else { - resolve(Errors.createError(ErrorType.Failed, "You must provide paymentMethodType")) + let paymentMethodData = params?["paymentMethodData"] as? NSDictionary + let (missingPaymentMethodError, paymentMethodType) = getPaymentMethodType(params: params) + if (missingPaymentMethodError != nil) { + resolve(missingPaymentMethodError) return } - + if (paymentMethodType == .FPX) { let testOfflineBank = paymentMethodData?["testOfflineBank"] as? Bool if (testOfflineBank == false || testOfflineBank == nil) { @@ -833,10 +832,24 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi STPPaymentHandler.shared().confirmPayment(paymentIntentParams, with: self, completion: onCompleteConfirmPayment) } } + + func getPaymentMethodType( + params: NSDictionary? + ) -> (NSDictionary?, STPPaymentMethodType?) { + if let params = params { + guard let paymentMethodType = Mappers.mapToPaymentMethodType(type: params["paymentMethodType"] as? String) else { + return (Errors.createError(ErrorType.Failed, "You must provide paymentMethodType"), nil) + } + return (nil, paymentMethodType) + } else { + // If params aren't provided, it means we expect that the payment method was attached on the server side + return (nil, nil) + } + } func createPaymentIntentParams( paymentIntentClientSecret: String, - paymentMethodType: STPPaymentMethodType, + paymentMethodType: STPPaymentMethodType?, paymentMethodData: NSDictionary?, options: NSDictionary ) -> (NSDictionary?, STPPaymentIntentParams) { @@ -848,6 +861,8 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi if (paymentMethodType == .USBankAccount && paymentMethodData == nil) { return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret, paymentMethodType: .USBankAccount) } else { + guard let paymentMethodType = paymentMethodType else { return STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) } + let paymentMethodId = paymentMethodData?["paymentMethodId"] as? String let parameters = STPPaymentIntentParams(clientSecret: paymentIntentClientSecret) @@ -891,7 +906,6 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi } else { resolve(Errors.createError(ErrorType.Unknown, error?.localizedDescription)) } - return } @@ -916,7 +930,6 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi } else { resolve(Errors.createError(ErrorType.Unknown, error?.localizedDescription)) } - return } @@ -995,7 +1008,26 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi } } } - + + @objc(canAddCardToWallet:resolver:rejecter:) + func canAddCardToWallet( + params: NSDictionary, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) -> Void { + guard let last4 = params["cardLastFour"] as? String else { + resolve(Errors.createError(ErrorType.Failed, "You must provide `cardLastFour`")) + return + } + let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: last4, + primaryAccountIdentifier: params["primaryAccountIdentifier"] as? String ?? "", + testEnv: params["testEnv"] as? Bool ?? false) + resolve([ + "canAddCard": canAddCard, + "details": ["status": status?.rawValue], + ]) + } + @objc(isCardInWallet:resolver:rejecter:) func isCardInWallet( params: NSDictionary, @@ -1006,17 +1038,35 @@ class StripeSdk: RCTEventEmitter, STPApplePayContextDelegate, STPBankSelectionVi resolve(Errors.createError(ErrorType.Failed, "You must provide `cardLastFour`")) return } - - let existingPass: PKPass? = { - if #available(iOS 13.4, *) { - return PKPassLibrary().passes(of: PKPassType.secureElement).first(where: {$0.secureElementPass?.primaryAccountNumberSuffix == last4}) - } else { - return PKPassLibrary().passes(of: PKPassType.payment).first(where: {$0.paymentPass?.primaryAccountNumberSuffix == last4}) - } - }() - resolve(["isInWallet": existingPass != nil]) + resolve(["isInWallet": PushProvisioningUtils.passExistsWith(last4: last4)]) } - + + @objc(collectBankAccountToken:resolver:rejecter:) + func collectBankAccountToken( + clientSecret: String, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) -> Void { + if (STPAPIClient.shared.publishableKey == nil) { + resolve(Errors.MISSING_INIT_ERROR) + return + } + FinancialConnections.presentForToken(withClientSecret: clientSecret, resolve: resolve) + } + + @objc(collectFinancialConnectionsAccounts:resolver:rejecter:) + func collectFinancialConnectionsAccounts( + clientSecret: String, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) -> Void { + if (STPAPIClient.shared.publishableKey == nil) { + resolve(Errors.MISSING_INIT_ERROR) + return + } + FinancialConnections.present(withClientSecret: clientSecret, resolve: resolve) + } + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { confirmPaymentResolver?(Errors.createError(ErrorType.Canceled, "FPX Payment has been canceled")) } diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/ApplePayUtilsTests.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/ApplePayUtilsTests.swift new file mode 100644 index 0000000000000..4be1ffa22c947 --- /dev/null +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/ApplePayUtilsTests.swift @@ -0,0 +1,214 @@ +// +// Tests.swift +// Tests +// +// Created by Charles Cruzan on 6/21/22. +// Copyright © 2022 Facebook. All rights reserved. +// + +import XCTest +@testable import stripe_react_native +import PassKit + +@available(iOS 15.0, *) +class ApplePayUtilsTests: XCTestCase { + + func test_buildPaymentSheetApplePayConfig_FailsWithoutMerchantIdentifier() throws { + XCTAssertThrowsError( + try ApplePayUtils.buildPaymentSheetApplePayConfig(merchantIdentifier: nil, merchantCountryCode: "", paymentSummaryItems: nil) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.missingMerchantId + ) + } + } + + func test_buildPaymentSheetApplePayConfig_FailsWithoutCountryCode() throws { + XCTAssertThrowsError( + try ApplePayUtils.buildPaymentSheetApplePayConfig(merchantIdentifier: TestFixtures.MERCHANT_ID, merchantCountryCode: nil, paymentSummaryItems: nil) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.missingCountryCode + ) + } + } + + func test_buildPaymentSheetApplePayConfig_withNilAndEmptyArray_shouldBeEqual() throws { + let resultWithItemsAsNil = try ApplePayUtils.buildPaymentSheetApplePayConfig(merchantIdentifier: TestFixtures.MERCHANT_ID, merchantCountryCode: TestFixtures.COUNTRY_CODE, paymentSummaryItems: nil) + let resultWithItemsAsEmptyArray = try ApplePayUtils.buildPaymentSheetApplePayConfig(merchantIdentifier: TestFixtures.MERCHANT_ID, merchantCountryCode: TestFixtures.COUNTRY_CODE, paymentSummaryItems: []) + XCTAssertEqual(resultWithItemsAsNil.paymentSummaryItems, resultWithItemsAsEmptyArray.paymentSummaryItems) + } + + func test_buildPaymentSheetApplePayConfig_withItems_shouldMatchExpected() throws { + let result = try ApplePayUtils.buildPaymentSheetApplePayConfig(merchantIdentifier: TestFixtures.MERCHANT_ID, merchantCountryCode: TestFixtures.COUNTRY_CODE, paymentSummaryItems: TestFixtures.CART_ITEM_DICTIONARY) + + let deferredItemResult = PKDeferredPaymentSummaryItem(label: "deferred label", amount: 1.00) + deferredItemResult.deferredDate = Date(timeIntervalSince1970: 123456789) + let immediateItemResult = PKPaymentSummaryItem(label: "immediate label", amount: 2.00, type: .pending) + let recurringResult = PKRecurringPaymentSummaryItem(label: "recurring label", amount: 1.00) + recurringResult.intervalUnit = .minute + recurringResult.intervalCount = 2 + recurringResult.startDate = Date(timeIntervalSince1970: 123456789) + recurringResult.endDate = Date(timeIntervalSince1970: 234567890) + + XCTAssertEqual( + result.paymentSummaryItems, + [deferredItemResult, immediateItemResult, recurringResult] + ) + XCTAssertEqual( + result.merchantId, + TestFixtures.MERCHANT_ID + ) + XCTAssertEqual( + result.merchantCountryCode, + TestFixtures.COUNTRY_CODE + ) + } + + func test_createDeferredPaymentSummaryItem() throws { + let result = try ApplePayUtils.createDeferredPaymentSummaryItem(item: TestFixtures.DEFERRED_CART_ITEM_DICTIONARY) + + let expectedResult = PKDeferredPaymentSummaryItem(label: "deferred label", amount: 1.00) + expectedResult.deferredDate = Date(timeIntervalSince1970: 123456789) + + XCTAssertEqual( + result, + expectedResult + ) + } + + func test_createRecurringPaymentSummaryItem() throws { + let result = try ApplePayUtils.createRecurringPaymentSummaryItem(item: TestFixtures.RECURRING_CART_ITEM_DICTIONARY) + + let expectedResult = PKRecurringPaymentSummaryItem(label: "recurring label", amount: 1.00) + expectedResult.intervalUnit = .minute + expectedResult.intervalCount = 2 + expectedResult.startDate = Date(timeIntervalSince1970: 123456789) + expectedResult.endDate = Date(timeIntervalSince1970: 234567890) + + XCTAssertEqual( + result, + expectedResult + ) + } + + func test_createRecurringPaymentSummaryItem_withUnexpectedIntervalUnit_fails() throws { + XCTAssertThrowsError( + try ApplePayUtils.createRecurringPaymentSummaryItem(item: [ + "paymentType":"Recurring", + "intervalUnit": "decade", + "intervalCount": 1, + ] as [String : Any]) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.invalidTimeInterval("decade") + ) + } + + XCTAssertThrowsError( + try ApplePayUtils.createRecurringPaymentSummaryItem(item: [ + "paymentType":"Recurring", + "intervalCount": 1, + ] as [String : Any]) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.invalidTimeInterval("null") + ) + } + } + + func test_createImmediatePaymentSummaryItem() throws { + let result = ApplePayUtils.createImmediatePaymentSummaryItem(item: TestFixtures.IMMEDIATE_CART_ITEM_DICTIONARY_NOT_PENDING) + + let expectedResult = PKPaymentSummaryItem(label: "immediate label", amount: 2.00, type: .final) + + XCTAssertEqual( + result, + expectedResult + ) + } + + func test_buildPaymentSummaryItems() throws { + let result = try ApplePayUtils.buildPaymentSummaryItems(items: TestFixtures.CART_ITEM_DICTIONARY) + let deferredItemResult = PKDeferredPaymentSummaryItem(label: "deferred label", amount: 1.00) + deferredItemResult.deferredDate = Date(timeIntervalSince1970: 123456789) + let immediateItemResult = PKPaymentSummaryItem(label: "immediate label", amount: 2.00, type: .pending) + let recurringResult = PKRecurringPaymentSummaryItem(label: "recurring label", amount: 1.00) + recurringResult.intervalUnit = .minute + recurringResult.intervalCount = 2 + recurringResult.startDate = Date(timeIntervalSince1970: 123456789) + recurringResult.endDate = Date(timeIntervalSince1970: 234567890) + + XCTAssertEqual( + result, + [deferredItemResult, immediateItemResult, recurringResult] + ) + } + + func test_buildPaymentSummaryItems_unexpectedType_fails() throws { + XCTAssertThrowsError( + try ApplePayUtils.buildPaymentSummaryItems(items: [[ + "paymentType":"wrong type", + ]] as [[String : Any]]) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.invalidCartSummaryItemType("wrong type") + ) + } + + XCTAssertThrowsError( + try ApplePayUtils.buildPaymentSummaryItems(items: [[ + "paymentType":"", + ]] as [[String : Any]]) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.invalidCartSummaryItemType("") + ) + } + + XCTAssertThrowsError( + try ApplePayUtils.buildPaymentSummaryItems(items: [[ + "label":"my labal", + ]] as [[String : Any]]) + ) { error in + XCTAssertEqual( + error as! ApplePayUtilsError, ApplePayUtilsError.invalidCartSummaryItemType("null") + ) + } + } + + + private struct TestFixtures { + static let MERCHANT_ID = "merchant.com.id" + static let COUNTRY_CODE = "US" + static let DEFERRED_CART_ITEM_DICTIONARY = [ + "paymentType":"Deferred", + "deferredDate": 123456789 as NSNumber, + "label": "deferred label", + "amount": "1.00" + ] as [String : Any] + static let RECURRING_CART_ITEM_DICTIONARY = [ + "paymentType":"Recurring", + "intervalUnit": "minute", + "intervalCount": 2, + "startDate": 123456789 as NSNumber, + "endDate": 234567890 as NSNumber, + "label": "recurring label", + "amount": "1.00" + ] as [String : Any] + static let IMMEDIATE_CART_ITEM_DICTIONARY = [ + "paymentType":"Immediate", + "isPending": true, + "label": "immediate label", + "amount": "2.00" + ] as [String : Any] + static let CART_ITEM_DICTIONARY = [ + DEFERRED_CART_ITEM_DICTIONARY, IMMEDIATE_CART_ITEM_DICTIONARY, RECURRING_CART_ITEM_DICTIONARY + ] + static let IMMEDIATE_CART_ITEM_DICTIONARY_NOT_PENDING = [ + "paymentType":"Immediate", + "label": "immediate label", + "amount": "2.00" + ] as [String : Any] + } +} diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/PushProvisioningTests.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/PushProvisioningTests.swift new file mode 100644 index 0000000000000..a95d06385edcd --- /dev/null +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/Tests/PushProvisioningTests.swift @@ -0,0 +1,49 @@ +// +// Tests.swift +// Tests +// +// Created by Charles Cruzan on 6/21/22. +// Copyright © 2022 Facebook. All rights reserved. +// + +import XCTest +@testable import stripe_react_native + +class PushProvisioningTests: XCTestCase { + func testCanAddCardToWalletInTestMode() throws { + let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: "4242", + primaryAccountIdentifier: "", + testEnv: true) + XCTAssertEqual(canAddCard, true) + XCTAssertEqual(status, nil) + } + + func testCanAddCardToWalletInLiveMode() throws { + let (canAddCard, status) = PushProvisioningUtils.canAddCardToWallet(last4: "4242", + primaryAccountIdentifier: "", + testEnv: false) + XCTAssertEqual(canAddCard, false) + XCTAssertEqual(status, PushProvisioningUtils.AddCardToWalletStatus.MISSING_CONFIGURATION) + } + + func testCanAddPaymentPassInTestMode() throws { + XCTAssertEqual( + PushProvisioningUtils.canAddPaymentPass(primaryAccountIdentifier: "", isTestMode: true), + true + ) + } + + func testCanAddPaymentPassInLiveMode() throws { + XCTAssertEqual( + PushProvisioningUtils.canAddPaymentPass(primaryAccountIdentifier: "", isTestMode: false), + false + ) + } + + func testCheckIfPassExists() throws { + XCTAssertEqual( + PushProvisioningUtils.passExistsWith(last4: "4242"), + false + ) + } +} diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/AddToWalletButtonView.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/AddToWalletButtonView.swift index 1fb8470d4e300..59b9dc6b2c4fc 100644 --- a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/AddToWalletButtonView.swift +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/AddToWalletButtonView.swift @@ -33,10 +33,6 @@ class AddToWalletButtonView: UIView { } } - func canAddPaymentPass() -> Bool { - return self.testEnv ? STPFakeAddPaymentPassViewController.canAddPaymentPass() : PKAddPaymentPassViewController.canAddPaymentPass() - } - override func didSetProps(_ changedProps: [String]!) { if let addToWalletButton = addToWalletButton { addToWalletButton.removeFromSuperview() @@ -53,7 +49,7 @@ class AddToWalletButtonView: UIView { } @objc func beginPushProvisioning() { - if (!canAddPaymentPass()) { + if (!PushProvisioningUtils.canAddPaymentPass(primaryAccountIdentifier: cardDetails?["primaryAccountIdentifier"] as? String ?? "", isTestMode: self.testEnv)) { onCompleteAction!( Errors.createError( ErrorType.Failed, diff --git a/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/PushProvisioningUtils.swift b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/PushProvisioningUtils.swift new file mode 100644 index 0000000000000..ea5569b76e261 --- /dev/null +++ b/ios/vendored/unversioned/@stripe/stripe-react-native/ios/pushprovisioning/PushProvisioningUtils.swift @@ -0,0 +1,64 @@ +// +// PushProvisioningUtils.swift +// stripe-react-native +// +// Created by Charles Cruzan on 6/9/22. +// + +import Foundation +import Stripe + +internal class PushProvisioningUtils { + class func canAddCardToWallet( + last4: String, + primaryAccountIdentifier: String, + testEnv: Bool + ) -> (canAddCard: Bool, status: AddCardToWalletStatus?) { + if (!PKAddPassesViewController.canAddPasses()) { + return (false, AddCardToWalletStatus.UNSUPPORTED_DEVICE) + } + + var status : AddCardToWalletStatus? = nil + var canAddCard = PushProvisioningUtils.canAddPaymentPass( + primaryAccountIdentifier: primaryAccountIdentifier, + isTestMode: testEnv) + + if (!canAddCard) { + status = AddCardToWalletStatus.MISSING_CONFIGURATION + } else if (PushProvisioningUtils.passExistsWith(last4: last4)) { + canAddCard = false + status = AddCardToWalletStatus.CARD_ALREADY_EXISTS + } + + return (canAddCard, status) + } + + class func canAddPaymentPass(primaryAccountIdentifier: String, isTestMode: Bool) -> Bool { + if (isTestMode) { + return STPFakeAddPaymentPassViewController.canAddPaymentPass() + } + + if #available(iOS 13.4, *) { + return PKPassLibrary().canAddSecureElementPass(primaryAccountIdentifier: primaryAccountIdentifier) + } else { + return PKAddPaymentPassViewController.canAddPaymentPass() + } + } + + class func passExistsWith(last4: String) -> Bool { + let existingPass: PKPass? = { + if #available(iOS 13.4, *) { + return PKPassLibrary().passes(of: PKPassType.secureElement).first(where: {$0.secureElementPass?.primaryAccountNumberSuffix == last4}) + } else { + return PKPassLibrary().passes(of: PKPassType.payment).first(where: {$0.paymentPass?.primaryAccountNumberSuffix == last4}) + } + }() + return existingPass != nil + } + + enum AddCardToWalletStatus: String { + case UNSUPPORTED_DEVICE + case MISSING_CONFIGURATION + case CARD_ALREADY_EXISTS + } +} 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 46a1642dc376b..941e8b5e1ee69 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.13.1", + "version": "0.18.1", "summary": "Stripe SDK for React Native", "homepage": "https://github.com/stripe/stripe-react-native/#readme", "license": "MIT", @@ -10,18 +10,26 @@ }, "source": { "git": "https://github.com/stripe/stripe-react-native.git", - "tag": "0.13.1" + "tag": "0.18.1" }, "source_files": "ios/**/*.{h,m,mm,swift}", + "exclude_files": "ios/Tests/", "dependencies": { "React-Core": [], "Stripe": [ - "~> 22.4.0" + "~> 22.7.0" ], "StripeFinancialConnections": [ - "~> 22.4.0" + "~> 22.7.0" ] }, + "testspecs": [ + { + "name": "Tests", + "test_type": "unit", + "source_files": "ios/Tests/**/*.{m,swift}" + } + ], "pod_target_xcconfig": { "HEADER_SEARCH_PATHS": "\"${PODS_ROOT}/Stripe/Stripe3DS2\" \"${PODS_ROOT}/Headers/Public/Stripe\"" } diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 661ffb1cea93a..47c456806e65e 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.2", "@react-native-segmented-control/segmented-control": "2.4.0", - "@stripe/stripe-react-native": "0.13.1", + "@stripe/stripe-react-native": "0.18.1", "expo-analytics-amplitude": "~11.3.0", "expo-app-auth": "~11.1.0", "expo-app-loader-provider": "~8.0.0",