Skip to content

Commit

Permalink
feat: BETA CustomerSheet APIs (stripe#1491)
Browse files Browse the repository at this point in the history
* upgrade native libaries

* basic example

* types and js wrap

* ios except customer adapter

* android

* move behind explicit beta import

* fix babel error

* changleog

* retrieveCustomerSheetPaymentOptionSelection

* better example

* rename methods

* add e2e test

* doc strings

* add types, JS, and iOS for customer adapter

* remove unused property

* fixes on ios and type changes to prepare for android

* android implementation

* better support for customadapter

* better example

* remove args from fragment constructor

* fix e2e test and changelog
  • Loading branch information
charliecruzan-stripe authored Sep 7, 2023
1 parent d565a0b commit 168771c
Show file tree
Hide file tree
Showing 29 changed files with 2,168 additions and 70 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

**Features**

- **[BETA]** Added [CustomerSheet](https://stripe.com/docs/elements/customer-sheet?platform=react-native) API, a prebuilt UI component that lets your customers manage their saved payment methods. [#1491](https://github.com/stripe/stripe-react-native/pull/1491)
- [PaymentSheet] Added support for AmazonPay (private beta), BLIK (iOS only), GrabPay, and FPX with PaymentIntents. [#1491](https://github.com/stripe/stripe-react-native/pull/1491)

**Fixes**

- Fixed font scaling on Android PaymentSheet not respecting floating-point number values. [#1469](https://github.com/stripe/stripe-react-native/pull/1469)
Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
StripeSdk_kotlinVersion=1.8.0
# Keep StripeSdk_stripeVersion in sync with https://github.com/stripe/stripe-identity-react-native/blob/main/android/gradle.properties
StripeSdk_stripeVersion=20.28.+
StripeSdk_stripeVersion=20.29.+
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
Expand Down Expand Up @@ -399,10 +400,16 @@ class PaymentSheetFragment(
}

fun getBitmapFromVectorDrawable(context: Context?, drawableId: Int): Bitmap? {
var drawable = AppCompatResources.getDrawable(context!!, drawableId) ?: return null
val drawable = AppCompatResources.getDrawable(context!!, drawableId) ?: return null
return getBitmapFromDrawable(drawable)
}

drawable = DrawableCompat.wrap(drawable).mutate()
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
fun getBitmapFromDrawable(drawable: Drawable): Bitmap? {
val drawableCompat = DrawableCompat.wrap(drawable).mutate()
if (drawableCompat.intrinsicWidth <= 0 || drawableCompat.intrinsicHeight <= 0) {
return null
}
val bitmap = Bitmap.createBitmap(drawableCompat.intrinsicWidth, drawableCompat.intrinsicHeight, Bitmap.Config.ARGB_8888)
bitmap.eraseColor(Color.WHITE)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
Expand Down
140 changes: 129 additions & 11 deletions android/src/main/java/com/reactnativestripesdk/StripeSdkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.stripe.android.view.AddPaymentMethodActivityStarter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject


@ReactModule(name = StripeSdkModule.NAME)
Expand All @@ -47,6 +48,8 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
private var paymentLauncherFragment: PaymentLauncherFragment? = null
private var collectBankAccountLauncherFragment: CollectBankAccountLauncherFragment? = null

private var customerSheetFragment: CustomerSheetFragment? = null

internal var eventListenerCount = 0

// If you create a new Fragment, you must put the tag here, otherwise result callbacks for that
Expand All @@ -58,7 +61,8 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
CollectBankAccountLauncherFragment.TAG,
FinancialConnectionsSheetFragment.TAG,
AddressLauncherFragment.TAG,
GooglePayLauncherFragment.TAG
GooglePayLauncherFragment.TAG,
CustomerSheetFragment.TAG
)

private val mActivityEventListener = object : BaseActivityEventListener() {
Expand Down Expand Up @@ -501,23 +505,15 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
fun retrievePaymentIntent(clientSecret: String, promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
val paymentIntent = stripe.retrievePaymentIntentSynchronous(clientSecret)
paymentIntent?.let {
promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(it)))
} ?: run {
promise.resolve(createError(RetrievePaymentIntentErrorType.Unknown.toString(), "Failed to retrieve the PaymentIntent"))
}
promise.resolve(createResult("paymentIntent", mapFromPaymentIntentResult(paymentIntent)))
}
}

@ReactMethod
fun retrieveSetupIntent(clientSecret: String, promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
val setupIntent = stripe.retrieveSetupIntentSynchronous(clientSecret)
setupIntent?.let {
promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(it)))
} ?: run {
promise.resolve(createError(RetrieveSetupIntentErrorType.Unknown.toString(), "Failed to retrieve the SetupIntent"))
}
promise.resolve(createResult("setupIntent", mapFromSetupIntentResult(setupIntent)))
}
}

Expand Down Expand Up @@ -820,6 +816,128 @@ class StripeSdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
}
}

@ReactMethod
fun initCustomerSheet(params: ReadableMap, customerAdapterOverrides: ReadableMap, promise: Promise) {
if (!::stripe.isInitialized) {
promise.resolve(createMissingInitError())
return
}

getCurrentActivityOrResolveWithError(promise)?.let { activity ->
customerSheetFragment?.removeFragment(reactApplicationContext)
customerSheetFragment = CustomerSheetFragment().also {
it.context = reactApplicationContext
it.initPromise = promise
val bundle = toBundleObject(params)
bundle.putBundle("customerAdapter", toBundleObject(customerAdapterOverrides))
it.arguments = bundle
}
try {
activity.supportFragmentManager.beginTransaction()
.add(customerSheetFragment!!, CustomerSheetFragment.TAG)
.commit()
} catch (error: IllegalStateException) {
promise.resolve(createError(ErrorType.Failed.toString(), error.message))
}
}
}

@ReactMethod
fun presentCustomerSheet(params: ReadableMap, promise: Promise) {
var timeout: Long? = null
if (params.hasKey("timeout")) {
timeout = params.getInt("timeout").toLong()
}
customerSheetFragment?.present(timeout, promise) ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
}
}

@ReactMethod
fun retrieveCustomerSheetPaymentOptionSelection(promise: Promise) {
customerSheetFragment?.retrievePaymentOptionSelection(promise) ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
}
}

@ReactMethod
fun customerAdapterFetchPaymentMethodsCallback(paymentMethodJsonObjects: ReadableArray, promise: Promise) {
customerSheetFragment?.let { fragment ->
val paymentMethods = mutableListOf<PaymentMethod>()
for (paymentMethodJson in paymentMethodJsonObjects.toArrayList()) {
PaymentMethod.fromJson(JSONObject((paymentMethodJson as HashMap<*, *>)))?.let {
paymentMethods.add(it)
} ?: run {
Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method")
}
}
fragment.customerAdapter?.fetchPaymentMethodsCallback?.complete(paymentMethods)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun customerAdapterAttachPaymentMethodCallback(paymentMethodJson: ReadableMap, promise: Promise) {
customerSheetFragment?.let {
val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap()))
if (paymentMethod == null) {
Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method")
return
}
it.customerAdapter?.attachPaymentMethodCallback?.complete(paymentMethod)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun customerAdapterDetachPaymentMethodCallback(paymentMethodJson: ReadableMap, promise: Promise) {
customerSheetFragment?.let {
val paymentMethod = PaymentMethod.fromJson(JSONObject(paymentMethodJson.toHashMap()))
if (paymentMethod == null) {
Log.e("StripeReactNative", "There was an error converting Payment Method JSON to a Stripe Payment Method")
return
}
it.customerAdapter?.detachPaymentMethodCallback?.complete(paymentMethod)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun customerAdapterSetSelectedPaymentOptionCallback(promise: Promise) {
customerSheetFragment?.let {
it.customerAdapter?.setSelectedPaymentOptionCallback?.complete(Unit)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun customerAdapterFetchSelectedPaymentOptionCallback(paymentOption: String?, promise: Promise) {
customerSheetFragment?.let {
it.customerAdapter?.fetchSelectedPaymentOptionCallback?.complete(paymentOption)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun customerAdapterSetupIntentClientSecretForCustomerAttachCallback(clientSecret: String, promise: Promise) {
customerSheetFragment?.let {
it.customerAdapter?.setupIntentClientSecretForCustomerAttachCallback?.complete(clientSecret)
} ?: run {
promise.resolve(CustomerSheetFragment.createMissingInitError())
return
}
}

@ReactMethod
fun addListener(eventName: String) {
eventListenerCount++
Expand Down
Loading

0 comments on commit 168771c

Please sign in to comment.