diff --git a/packages/stripe/CHANGELOG.md b/packages/stripe/CHANGELOG.md index add843a00..d614a27c6 100644 --- a/packages/stripe/CHANGELOG.md +++ b/packages/stripe/CHANGELOG.md @@ -8,7 +8,7 @@ - Feat: Add support for ACHv2 - Feat: Add support for setting a card's currency when creating a Token - Feat: Added support for placeholderColor, textErrorColor , borderColor, borderRadius, and borderWidth for AuBECSDebitForm on iOS -- Several fixes by the Stripe sdk [v0.7.0](https://github.com/stripe/stripe-react-native/releases/tag/v0.4.0) +- Several fixes by the Stripe sdk [v0.7.0](https://github.com/stripe/stripe-react-native/releases/tag/v0.7.0) - Updated freezed dependency to allow 2.x ## 2.4.0 diff --git a/packages/stripe_android/android/build.gradle b/packages/stripe_android/android/build.gradle index d192b7e52..3cd14e84a 100644 --- a/packages/stripe_android/android/build.gradle +++ b/packages/stripe_android/android/build.gradle @@ -37,11 +37,19 @@ android { } dependencies { + implementation 'com.github.bumptech.glide:glide:4.12.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1" implementation 'com.stripe:stripe-android:20.1.+' implementation 'com.google.android.material:material:1.6.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' + + // play-services-wallet is already included in stripe-android + compileOnly "com.google.android.gms:play-services-wallet:19.1.0" + + // Users need to declare this dependency on their own, otherwise all methods are a no-op + compileOnly 'com.stripe:stripe-android-issuing-push-provisioning:1.1.0' } diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/BaseActivityEventListener.kt b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/BaseActivityEventListener.kt index 118eba486..07bc099c0 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/BaseActivityEventListener.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/BaseActivityEventListener.kt @@ -6,12 +6,16 @@ import com.flutter.stripe.StripeAndroidPlugin import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import java.lang.ref.WeakReference -abstract class BaseActivityEventListener : ActivityEventListener, ActivityResultListener { +open class BaseActivityEventListener : ActivityEventListener, ActivityResultListener { lateinit var activity: WeakReference override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { - onActivityResult(activity.get(), requestCode, resultCode, data) + onActivityResult(activity.get()!!, requestCode, resultCode, data) return false } + + override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { + TODO("Override") + } } \ No newline at end of file diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReactContextBaseJavaModule.java b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReactContextBaseJavaModule.java index 89b5dcf64..55bcb846a 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReactContextBaseJavaModule.java +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReactContextBaseJavaModule.java @@ -5,6 +5,8 @@ import android.content.Intent; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import io.flutter.embedding.android.FlutterFragmentActivity; import io.flutter.plugin.common.PluginRegistry; @@ -33,6 +35,10 @@ public String getName() { return "StripeSdk"; } + public Map getConstants() { + return new HashMap(); + } + @Override public boolean onActivityResult(int requestCode, int resultCode, Intent data) { for (ActivityEventListener eventListener : eventListeners) { diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReadableMap.java b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReadableMap.java index 40777a966..2e401db80 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReadableMap.java +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/bridge/ReadableMap.java @@ -3,10 +3,13 @@ import androidx.annotation.Nullable; import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -105,5 +108,41 @@ public ReadableArray getArray(@NotNull String key) { return null; } } + + private static HashMap toMap(JSONObject jsonobj) throws JSONException { + HashMap map = new HashMap(); + Iterator keys = jsonobj.keys(); + while(keys.hasNext()) { + String key = keys.next(); + Object value = jsonobj.get(key); + if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } else if (value instanceof JSONObject) { + value = toMap((JSONObject) value); + } + map.put(key, value); + } + return map; + } + + private static List toList(JSONArray array) throws JSONException { + List list = new ArrayList(); + for(int i = 0; i < array.length(); i++) { + Object value = array.get(i); + if (value instanceof JSONArray) { + value = toList((JSONArray) value); + } + else if (value instanceof JSONObject) { + value = toMap((JSONObject) value); + } + list.add(value); + } + return list; + } + + @NotNull + public HashMap toHashMap() throws JSONException { + return toMap(this.map); + } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/ThemedReactContext.kt b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/ThemedReactContext.kt index 8729ef8d0..c2fa51ae3 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/ThemedReactContext.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/ThemedReactContext.kt @@ -2,6 +2,7 @@ package com.facebook.react.uimanager import android.content.Context import android.content.ContextWrapper +import com.facebook.react.bridge.ReactApplicationContext import com.reactnativestripesdk.StripeSdkModule import io.flutter.plugin.common.MethodChannel @@ -9,6 +10,8 @@ class ThemedReactContext(context: Context, private val channel: MethodChannel, private val sdkAccessor: () -> StripeSdkModule): ContextWrapper(context) { + val reactApplicationContext: ReactApplicationContext = context as ReactApplicationContext + fun getNativeModule(clazz: Class): UIManagerModule? { return UIManagerModule(channel) } diff --git a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/events/RCTEventEmitter.kt b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/events/RCTEventEmitter.kt index 5e3e109a8..f0e260a8b 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/events/RCTEventEmitter.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/facebook/react/uimanager/events/RCTEventEmitter.kt @@ -5,7 +5,7 @@ import io.flutter.plugin.common.MethodChannel class RCTEventEmitter(private val channel: MethodChannel) { - fun receiveEvent(viewTag: Any, eventName: String, serializeEventData: WritableMap) { + fun receiveEvent(viewTag: Any, eventName: String, serializeEventData: WritableMap?) { channel.invokeMethod(eventName, serializeEventData) } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt index 8c8f7ddbe..28afa4d4c 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/flutter/stripe/StripeAndroidPlugin.kt @@ -161,6 +161,10 @@ If you continue to have trouble, follow this discussion to get some support http params = call.requiredArgument("params"), promise = Promise(result) ) + "isCardInWallet" -> stripeSdk.isCardInWallet( + params = call.requiredArgument("params"), + promise = Promise(result) + ) else -> result.notImplemented() } } diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFieldViewManager.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFieldViewManager.kt index ea39a5d71..fb5e91096 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFieldViewManager.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFieldViewManager.kt @@ -6,12 +6,11 @@ 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.stripe.android.model.PaymentMethodCreateParams class CardFieldViewManager : SimpleViewManager() { override fun getName() = "CardField" - internal var reactContextRef: ThemedReactContext? = null + private var reactContextRef: ThemedReactContext? = null override fun getExportedCustomDirectEventTypeConstants(): MutableMap { return MapBuilder.of( @@ -47,9 +46,9 @@ class CardFieldViewManager : SimpleViewManager() { view.setCardStyle(cardStyle) } - @ReactProp(name = "placeholder") - fun setPlaceHolders(view: CardFieldView, placeholder: ReadableMap) { - view.setPlaceHolders(placeholder) + @ReactProp(name = "placeholders") + fun setPlaceHolders(view: CardFieldView, placeholders: ReadableMap) { + view.setPlaceHolders(placeholders) } override fun createViewInstance(reactContext: ThemedReactContext): CardFieldView { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormView.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormView.kt index 3fdaab0dd..b4b224bf6 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormView.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormView.kt @@ -2,6 +2,8 @@ package com.reactnativestripesdk import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.Typeface +import android.os.Build import android.view.View import android.view.View.OnFocusChangeListener import android.widget.FrameLayout @@ -9,8 +11,9 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerModule import com.facebook.react.uimanager.events.EventDispatcher -import com.flutter.stripe.R +import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel import com.stripe.android.databinding.CardMultilineWidgetBinding import com.stripe.android.databinding.StripeCardFormViewBinding import com.stripe.android.model.Address @@ -39,35 +42,31 @@ class CardFormView(context: ThemedReactContext) : FrameLayout(context) { } fun setPostalCodeEnabled(value: Boolean) { - val cardFormView = StripeCardFormViewBinding.bind(cardForm) val visibility = if (value) View.VISIBLE else View.GONE - cardFormView.cardMultilineWidget.postalCodeRequired = false - cardFormView.postalCodeContainer.visibility = visibility + cardFormViewBinding.cardMultilineWidget.postalCodeRequired = false + cardFormViewBinding.postalCodeContainer.visibility = visibility } - // TODO: uncomment when ios-sdk allows for this - // fun setPlaceHolders(value: ReadableMap) { - // val cardFormView = StripeCardFormViewBinding.bind(cardForm) - // - // val numberPlaceholder = getValOr(value, "number", null) - // val expirationPlaceholder = getValOr(value, "expiration", null) - // val cvcPlaceholder = getValOr(value, "cvc", null) - // val postalCodePlaceholder = getValOr(value, "postalCode", null) - // - // numberPlaceholder?.let { - //// multilineWidgetBinding.tlCardNumber.hint = it - // } - // expirationPlaceholder?.let { - // multilineWidgetBinding.tlExpiry.hint = it - // } - // cvcPlaceholder?.let { - // multilineWidgetBinding.tlCvc.hint = it - // } - // postalCodePlaceholder?.let { - // cardFormView.postalCodeContainer.hint = it - // } - // } + fun setPlaceHolders(value: ReadableMap) { + val numberPlaceholder = getValOr(value, "number", null) + val expirationPlaceholder = getValOr(value, "expiration", null) + val cvcPlaceholder = getValOr(value, "cvc", null) + val postalCodePlaceholder = getValOr(value, "postalCode", null) + + numberPlaceholder?.let { + multilineWidgetBinding.tlCardNumber.hint = it + } + expirationPlaceholder?.let { + multilineWidgetBinding.tlExpiry.hint = it + } + cvcPlaceholder?.let { + multilineWidgetBinding.tlCvc.hint = it + } + postalCodePlaceholder?.let { + cardFormViewBinding.postalCodeContainer.hint = it + } + } fun setAutofocus(value: Boolean) { if (value) { @@ -102,11 +101,81 @@ class CardFormView(context: ThemedReactContext) : FrameLayout(context) { } fun setCardStyle(value: ReadableMap) { - val binding = StripeCardFormViewBinding.bind(cardForm) val backgroundColor = getValOr(value, "backgroundColor", null) + val textColor = getValOr(value, "textColor", null) + val borderWidth = getIntOrNull(value, "borderWidth") + val borderColor = getValOr(value, "borderColor", null) + val borderRadius = getIntOrNull(value, "borderRadius") ?: 0 + val fontSize = getIntOrNull(value, "fontSize") + val fontFamily = getValOr(value, "fontFamily") + val placeholderColor = getValOr(value, "placeholderColor", null) + val textErrorColor = getValOr(value, "textErrorColor", null) + val cursorColor = getValOr(value, "cursorColor", null) + + val editTextBindings = setOf( + cardFormViewBinding.cardMultilineWidget.cardNumberEditText, + cardFormViewBinding.cardMultilineWidget.cvcEditText, + cardFormViewBinding.cardMultilineWidget.expiryDateEditText, + cardFormViewBinding.postalCode + ) + + textColor?.let { + for (binding in editTextBindings) { + binding.setTextColor(Color.parseColor(it)) + } + cardFormViewBinding.countryLayout.countryAutocomplete.setTextColor(Color.parseColor(it)) + } + textErrorColor?.let { + for (binding in editTextBindings) { + binding.setErrorColor(Color.parseColor(it)) + cardFormViewBinding.postalCode.setErrorColor(Color.parseColor(it)) + } + } + placeholderColor?.let { + multilineWidgetBinding.tlExpiry.defaultHintTextColor = ColorStateList.valueOf(Color.parseColor(it)) + multilineWidgetBinding.tlCardNumber.defaultHintTextColor = ColorStateList.valueOf(Color.parseColor(it)) + multilineWidgetBinding.tlCvc.defaultHintTextColor = ColorStateList.valueOf(Color.parseColor(it)) + cardFormViewBinding.postalCodeContainer.defaultHintTextColor = ColorStateList.valueOf(Color.parseColor(it)) + } + fontSize?.let { + for (binding in editTextBindings) { + binding.textSize = it.toFloat() + } + } + fontFamily?.let { + for (binding in editTextBindings) { + binding.typeface = Typeface.create(it, Typeface.NORMAL) + } + } + cursorColor?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val color = Color.parseColor(it) + for (binding in editTextBindings) { + binding.textCursorDrawable?.setTint(color) + binding.textSelectHandle?.setTint(color) + binding.textSelectHandleLeft?.setTint(color) + binding.textSelectHandleRight?.setTint(color) + binding.highlightColor = color + } + } + } - binding.cardMultilineWidgetContainer.background = MaterialShapeDrawable().also { shape -> + cardFormViewBinding.cardMultilineWidgetContainer.setPadding(40, 0, 40, 0) + cardFormViewBinding.cardMultilineWidgetContainer.background = MaterialShapeDrawable( + ShapeAppearanceModel() + .toBuilder() + .setAllCorners(CornerFamily.ROUNDED, (borderRadius * 2).toFloat()) + .build() + ).also { shape -> + shape.strokeWidth = 0.0f + shape.strokeColor = ColorStateList.valueOf(Color.parseColor("#000000")) shape.fillColor = ColorStateList.valueOf(Color.parseColor("#FFFFFF")) + borderWidth?.let { + shape.strokeWidth = (it * 2).toFloat() + } + borderColor?.let { + shape.strokeColor = ColorStateList.valueOf(Color.parseColor(it)) + } backgroundColor?.let { shape.fillColor = ColorStateList.valueOf(Color.parseColor(it)) } @@ -143,8 +212,7 @@ class CardFormView(context: ThemedReactContext) : FrameLayout(context) { .setCountry(it.address?.country) .build() - val binding = StripeCardFormViewBinding.bind(cardForm) - binding.cardMultilineWidget.paymentMethodCard?.let { params -> cardParams = params } + cardFormViewBinding.cardMultilineWidget.paymentMethodCard?.let { params -> cardParams = params } } } else { cardParams = null diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormViewManager.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormViewManager.kt index 89a2109a5..ce3d793dc 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormViewManager.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/CardFormViewManager.kt @@ -36,10 +36,10 @@ class CardFormViewManager : SimpleViewManager() { view.setPostalCodeEnabled(postalCodeEnabled) } - // @ReactProp(name = "placeholder") - // fun setPlaceHolders(view: CardFormView, placeholder: ReadableMap) { - // view.setPlaceHolders(placeholder) - // } + @ReactProp(name = "placeholders") + fun setPlaceHolders(view: CardFormView, placeholders: ReadableMap) { + view.setPlaceHolders(placeholders); + } @ReactProp(name = "autofocus") fun setAutofocus(view: CardFormView, autofocus: Boolean = false) { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt index 7e3c81fb9..581967054 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/Mappers.kt @@ -358,10 +358,10 @@ internal fun mapFromPaymentMethod(paymentMethod: PaymentMethod): WritableMap { usBankAccount.putString("linkedAccount", paymentMethod.usBankAccount?.linkedAccount) usBankAccount.putString("fingerprint", paymentMethod.usBankAccount?.fingerprint) usBankAccount.putString("preferredNetworks", paymentMethod.usBankAccount?.networks?.preferred) - usBankAccount.putArray("supportedNetworks", paymentMethod.usBankAccount?.networks?.supported) + usBankAccount.putArray("supportedNetworks", paymentMethod.usBankAccount?.networks?.supported as? ReadableArray) pm.putString("id", paymentMethod.id) - pm.putString("type", mapPaymentMethodType(paymentMethod.type)) + pm.putString("paymentMethodType", mapPaymentMethodType(paymentMethod.type)) pm.putBoolean("livemode", paymentMethod.liveMode) pm.putString("customerId", paymentMethod.customerId) pm.putMap("billingDetails", mapFromBillingDetails(paymentMethod.billingDetails)) @@ -498,8 +498,10 @@ internal fun mapFromSetupIntentLastErrorType(errorType: SetupIntent.Error.Type?) } } -fun getValOr(map: ReadableMap, key: String, default: String? = ""): String? { - return if (map.hasKey(key)) map.getString(key) else default +fun getValOr(map: ReadableMap?, key: String, default: String? = ""): String? { + return map?.let { + if (it.hasKey(key)) it.getString(key) else default + } ?: default } internal fun mapToAddress(addressMap: ReadableMap?, cardAddress: Address?): Address? { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt index df5ec3104..4dd04e789 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/PaymentMethodCreateParamsFactory.kt @@ -5,11 +5,12 @@ import com.stripe.android.model.* class PaymentMethodCreateParamsFactory( private val clientSecret: String, - private val params: ReadableMap, + private val paymentMethodData: ReadableMap?, + private val options: ReadableMap, private val cardFieldView: CardFieldView?, private val cardFormView: CardFormView?, ) { - private val billingDetailsParams = mapToBillingDetails(getMapOrNull(params, "billingDetails"), cardFieldView?.cardAddress ?: cardFormView?.cardAddress) + private val billingDetailsParams = mapToBillingDetails(getMapOrNull(paymentMethodData, "billingDetails"), cardFieldView?.cardAddress ?: cardFormView?.cardAddress) @Throws(PaymentMethodCreateParamsException::class) fun createConfirmParams(paymentMethodType: PaymentMethod.Type): ConfirmPaymentIntentParams { @@ -62,18 +63,17 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createIDEALPaymentConfirmParams(): ConfirmPaymentIntentParams { - val bankName = getValOr(params, "bankName", null) + val bankName = getValOr(paymentMethodData, "bankName", null) val idealParams = PaymentMethodCreateParams.Ideal(bankName) val createParams = PaymentMethodCreateParams.create(ideal = idealParams, billingDetails = billingDetailsParams) - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage")) return ConfirmPaymentIntentParams .createWithPaymentMethodCreateParams( paymentMethodCreateParams = createParams, clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), ) } @@ -86,6 +86,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), ) } @@ -94,8 +95,8 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createCardPaymentConfirmParams(): ConfirmPaymentIntentParams { - val paymentMethodId = getValOr(params, "paymentMethodId", null) - val token = getValOr(params, "token", null) + val paymentMethodId = getValOr(paymentMethodData, "paymentMethodId", null) + val token = getValOr(paymentMethodData, "token", null) val cardParams = cardFieldView?.cardParams ?: cardFormView?.cardParams @@ -103,10 +104,10 @@ class PaymentMethodCreateParamsFactory( throw PaymentMethodCreateParamsException("Card details not complete") } - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage")) + val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) if (paymentMethodId != null) { - val cvc = getValOr(params, "cvc", null) + val cvc = getValOr(paymentMethodData, "cvc", null) val paymentMethodOptionParams = if (cvc != null) PaymentMethodOptionsParams.Card(cvc) else null @@ -134,7 +135,7 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createIDEALPaymentSetupParams(): ConfirmSetupIntentParams { - val bankName = getValOr(params, "bankName", null) + val bankName = getValOr(paymentMethodData, "bankName", null) val idealParams = PaymentMethodCreateParams.Ideal(bankName) val createParams = @@ -149,7 +150,7 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createSepaPaymentSetupParams(): ConfirmSetupIntentParams { billingDetailsParams?.let { - val iban = getValOr(params, "iban", null) ?: run { + val iban = getValOr(paymentMethodData, "iban", null) ?: run { throw PaymentMethodCreateParamsException("You must provide IBAN") } @@ -188,10 +189,9 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createSofortPaymentConfirmParams(): ConfirmPaymentIntentParams { - val country = getValOr(params, "country", null) ?: run { + val country = getValOr(paymentMethodData, "country", null) ?: run { throw PaymentMethodCreateParamsException("You must provide bank account country") } - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage")) val params = PaymentMethodCreateParams.create( PaymentMethodCreateParams.Sofort(country = country), @@ -202,13 +202,13 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), ) } @Throws(PaymentMethodCreateParamsException::class) private fun createSofortPaymentSetupParams(): ConfirmSetupIntentParams { - val country = getValOr(params, "country", null) + val country = getValOr(paymentMethodData, "country", null) ?: throw PaymentMethodCreateParamsException("You must provide country") val params = PaymentMethodCreateParams.create( @@ -231,20 +231,20 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), ) } @Throws(PaymentMethodCreateParamsException::class) private fun createBancontactPaymentConfirmParams(): ConfirmPaymentIntentParams { billingDetailsParams?.let { - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage", null)) val params = PaymentMethodCreateParams.createBancontact(it) return ConfirmPaymentIntentParams .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")), ) } @@ -274,6 +274,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -289,6 +290,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -304,6 +306,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -313,10 +316,10 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createSepaPaymentConfirmParams(): ConfirmPaymentIntentParams { billingDetailsParams?.let { - val iban = getValOr(params, "iban", null) ?: run { + val iban = getValOr(paymentMethodData, "iban", null) ?: run { throw PaymentMethodCreateParamsException("You must provide IBAN") } - val setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage")) + val params = PaymentMethodCreateParams.create( sepaDebit = PaymentMethodCreateParams.SepaDebit(iban), billingDetails = it @@ -326,7 +329,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, - setupFutureUsage = setupFutureUsage + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -335,7 +338,7 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createFpxPaymentConfirmParams(): ConfirmPaymentIntentParams { - val bank = getBooleanOrFalse(params, "testOfflineBank").let { "test_offline_bank" } + val bank = getBooleanOrFalse(paymentMethodData, "testOfflineBank").let { "test_offline_bank" } val params = PaymentMethodCreateParams.create( PaymentMethodCreateParams.Fpx(bank) ) @@ -344,6 +347,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -356,6 +360,7 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @@ -364,7 +369,7 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createAuBecsDebitPaymentConfirmParams(): ConfirmPaymentIntentParams { - val formDetails = getMapOrNull(params, "formDetails") ?: run { + val formDetails = getMapOrNull(paymentMethodData, "formDetails") ?: run { throw PaymentMethodCreateParamsException("You must provide form details") } @@ -390,12 +395,13 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @Throws(PaymentMethodCreateParamsException::class) private fun createAuBecsDebitPaymentSetupParams(): ConfirmSetupIntentParams { - val formDetails = getMapOrNull(params, "formDetails") ?: run { + val formDetails = getMapOrNull(paymentMethodData, "formDetails") ?: run { throw PaymentMethodCreateParamsException("You must provide form details") } @@ -426,16 +432,17 @@ class PaymentMethodCreateParamsFactory( @Throws(PaymentMethodCreateParamsException::class) private fun createUSBankAccountPaymentSetupParams(): ConfirmSetupIntentParams { - params.getString("accountNumber")?.let { + // 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 ConfirmSetupIntentParams.create( - paymentMethodCreateParams = createUSBankAccountParams(params), + paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), clientSecret = clientSecret, ) } ?: run { - // Payment method is assumed to be already attached through via collectBankAccountForSetup + // Payment method is assumed to be already attached through via collectBankAccount return ConfirmSetupIntentParams.create( clientSecret = clientSecret, paymentMethodType = PaymentMethod.Type.USBankAccount @@ -459,23 +466,25 @@ class PaymentMethodCreateParamsFactory( .createWithPaymentMethodCreateParams( paymentMethodCreateParams = params, clientSecret = clientSecret, + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } @Throws(PaymentMethodCreateParamsException::class) private fun createUSBankAccountPaymentConfirmParams(): ConfirmPaymentIntentParams { - params.getString("accountNumber")?.let { + // 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(params), + paymentMethodCreateParams = createUSBankAccountParams(paymentMethodData), clientSecret, - setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(params, "setupFutureUsage")) + setupFutureUsage = mapToPaymentIntentFutureUsage(getValOr(options, "setupFutureUsage")) ) } ?: run { - // Payment method is assumed to be already attached through via collectBankAccountForPayment + // Payment method is assumed to be already attached through via collectBankAccount return ConfirmPaymentIntentParams.create( clientSecret = clientSecret, paymentMethodType = PaymentMethod.Type.USBankAccount diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt index 52f3d2348..9d3d7d20d 100644 --- a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/StripeSdkModule.kt @@ -13,6 +13,7 @@ import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule import com.stripe.android.* import com.stripe.android.core.AppInfo +import com.stripe.android.core.ApiVersion import com.stripe.android.googlepaylauncher.GooglePayLauncher import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher import com.stripe.android.model.* @@ -189,6 +190,14 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React } } + override fun getConstants(): MutableMap = + hashMapOf( + "API_VERSIONS" to hashMapOf( + "CORE" to ApiVersion.API_VERSION_CODE, + "ISSUING" to PushProvisioningProxy.getApiVersion(), + ) + ) + @ReactMethod fun initialise(params: ReadableMap, promise: Promise) { val publishableKey = getValOr(params, "publishableKey", null) as String @@ -306,8 +315,8 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React return } val cardAddress = cardFieldView?.cardAddress ?: cardFormView?.cardAddress - - val billingDetailsParams = mapToBillingDetails(getMapOrNull(data, "billingDetails"), cardAddress) + val paymentMethodData = getMapOrNull(data, "paymentMethodData") + val billingDetailsParams = mapToBillingDetails(getMapOrNull(paymentMethodData, "billingDetails"), cardAddress) val paymentMethodCreateParams = PaymentMethodCreateParams.create(cardParams, billingDetailsParams) stripe.createPaymentMethod( @@ -452,7 +461,8 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React @ReactMethod fun confirmPayment(paymentIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise) { - val paymentMethodType = getValOr(params, "type")?.let { mapToPaymentMethodType(it) } ?: run { + 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 } @@ -476,14 +486,14 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React // return // } - val factory = PaymentMethodCreateParamsFactory(paymentIntentClientSecret, params, cardFieldView, cardFormView) + val factory = PaymentMethodCreateParamsFactory(paymentIntentClientSecret, paymentMethodData, options, cardFieldView, cardFormView) try { val confirmParams = factory.createConfirmParams(paymentMethodType) urlScheme?.let { confirmParams.returnUrl = mapToReturnURL(urlScheme) } - confirmParams.shipping = mapToShippingDetails(getMapOrNull(params, "shippingDetails")) + confirmParams.shipping = mapToShippingDetails(getMapOrNull(paymentMethodData, "shippingDetails")) paymentLauncherFragment.confirm( confirmParams, paymentIntentClientSecret, @@ -520,12 +530,12 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React @ReactMethod fun confirmSetupIntent(setupIntentClientSecret: String, params: ReadableMap, options: ReadableMap, promise: Promise) { - val paymentMethodType = getValOr(params, "type")?.let { mapToPaymentMethodType(it) } ?: run { + val paymentMethodType = getValOr(params, "paymentMethodType")?.let { mapToPaymentMethodType(it) } ?: run { promise.resolve(createError(ConfirmPaymentErrorType.Failed.toString(), "You must provide paymentMethodType")) return } - val factory = PaymentMethodCreateParamsFactory(setupIntentClientSecret, params, cardFieldView, cardFormView) + val factory = PaymentMethodCreateParamsFactory(setupIntentClientSecret, getMapOrNull(params, "paymentMethodData"), options, cardFieldView, cardFormView) try { val confirmParams = factory.createSetupParams(paymentMethodType) @@ -606,15 +616,27 @@ class StripeSdkModule(private val reactContext: ReactApplicationContext) : React googlePayFragment?.createPaymentMethod(currencyCode, amount) } + @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, promise) + } + } + @ReactMethod fun collectBankAccount(isPaymentIntent: Boolean, clientSecret: String, params: ReadableMap, promise: Promise) { - val paymentMethodType = mapToPaymentMethodType(getValOr(params, "type", null)) + val paymentMethodData = getMapOrNull(params, "paymentMethodData") + val paymentMethodType = mapToPaymentMethodType(getValOr(params, "paymentMethodType", null)) if (paymentMethodType != PaymentMethod.Type.USBankAccount) { promise.resolve(createError(ErrorType.Failed.toString(), "collectBankAccount currently only accepts the USBankAccount payment method type.")) return } - val billingDetails = getMapOrNull(params, "billingDetails") + val billingDetails = getMapOrNull(paymentMethodData, "billingDetails") val name = billingDetails?.getString("name") if (name.isNullOrEmpty()) { diff --git a/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt new file mode 100644 index 000000000..7c6bb14bf --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonManager.kt @@ -0,0 +1,60 @@ +package com.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 = "cardDescription") + fun cardDescription(view: AddToWalletButtonView, cardDescription: String) { + view.setCardDescription(cardDescription) + } + + @ReactProp(name = "cardLastFour") + fun cardLastFour(view: AddToWalletButtonView, last4: String) { + view.setCardLastFour(last4) + } + + @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/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt new file mode 100644 index 000000000..c048a4475 --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletButtonView.kt @@ -0,0 +1,152 @@ +package com.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 com.reactnativestripesdk.PushProvisioningProxy +import com.reactnativestripesdk.createError + + +class AddToWalletButtonView(private val context: ThemedReactContext, private val requestManager: RequestManager) : AppCompatImageView(context) { + private var cardDescription: String? = null + private var cardLastFour: String? = 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() + cardDescription?.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. `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 setCardDescription(description: String) { + cardDescription = description + } + + fun setCardLastFour(last4: String) { + cardLastFour = last4 + } + + 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/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt new file mode 100644 index 000000000..4219dcde0 --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/AddToWalletCompleteEvent.kt @@ -0,0 +1,22 @@ +package com.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/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt new file mode 100644 index 000000000..604897e2c --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/EphemeralKeyProvider.kt @@ -0,0 +1,35 @@ +package com.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/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt new file mode 100644 index 000000000..5aebb8ee8 --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/PushProvisioningProxy.kt @@ -0,0 +1,117 @@ +package com.reactnativestripesdk + +import android.app.Activity +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.util.Log +import com.facebook.react.bridge.* +import com.reactnativestripesdk.pushprovisioning.AddToWalletButtonView +import com.reactnativestripesdk.pushprovisioning.EphemeralKeyProvider +import com.reactnativestripesdk.pushprovisioning.TapAndPayProxy +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 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, promise: Promise) { + TapAndPayProxy.invoke(activity, cardLastFour, promise) + } + + 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/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt new file mode 100644 index 000000000..807f44032 --- /dev/null +++ b/packages/stripe_android/android/src/main/kotlin/com/reactnativestripesdk/pushprovisioning/TapAndPayProxy.kt @@ -0,0 +1,129 @@ +package com.reactnativestripesdk.pushprovisioning + +import android.app.Activity +import android.util.Log +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import com.reactnativestripesdk.createError +import com.google.android.gms.tasks.Task + +object TapAndPayProxy { + private const val TAG = "StripeTapAndPay" + private var tapAndPayClient: Any? = null + const val REQUEST_CODE_TOKENIZE = 90909 + + fun invoke(activity: Activity, newCardLastFour: String, promise: Promise) { + try { + val tapAndPayClass = Class.forName("com.google.android.gms.tapandpay.TapAndPay") + val getClientMethod = tapAndPayClass.getMethod("getClient", Activity::class.java) + + tapAndPayClient = getClientMethod.invoke(null, activity).also { + val tapAndPayClientClass = Class.forName("com.google.android.gms.tapandpay.TapAndPayClient") + val listTokensMethod = tapAndPayClientClass.getMethod("listTokens") + + val tokens = listTokensMethod.invoke(it) as Task> + tokens.addOnCompleteListener { task -> + if (task.isSuccessful) { + for (token in task.result) { + try { + val getFpanLastFourMethod = Class.forName("com.google.android.gms.tapandpay.issuer.TokenInfo").getMethod("getFpanLastFour") + val existingFpanLastFour = getFpanLastFourMethod.invoke(token) as String + if (existingFpanLastFour == newCardLastFour) { + promise.resolve( + createResult( + true, + token)) + return@addOnCompleteListener + } + } 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.") + } + } + } else { + Log.e(TAG, "Unable to fetch existing tokens from Google TapAndPay.") + } + promise.resolve(createResult(false)) + } + } + } catch (e: Exception) { + Log.e(TAG, "Google TapAndPay dependency not found") + promise.resolve(createError("Failed", "Google TapAndPay dependency not found.")) + } + } + + 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 createResult(cardIsInWallet: Boolean, token: Any? = null): WritableMap { + val result = WritableNativeMap() + result.putBoolean("isInWallet", cardIsInWallet) + result.putMap("token", mapFromTokenInfo(token)) + return result + } + + 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" + } + } +}