Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Apple & Google Pay rework #1164

Merged
merged 30 commits into from
Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0e1e92e
implement isNativePaySupported, dismissApplePay, and presentNativePay…
charliecruzan-stripe Sep 21, 2022
56cb783
rename
charliecruzan-stripe Sep 21, 2022
61d127e
Add apple pay callbacks, more apple pay configuration
charliecruzan-stripe Sep 23, 2022
a41536a
implement confirm payment and setup intent methods
charliecruzan-stripe Sep 27, 2022
c8c5afe
migrate examples
charliecruzan-stripe Sep 27, 2022
9ddc682
native pay button component
charliecruzan-stripe Oct 5, 2022
68e4f6b
fix disabled on ios
charliecruzan-stripe Oct 5, 2022
54b0a1f
hooks
charliecruzan-stripe Oct 17, 2022
5ea2969
forgot this method
charliecruzan-stripe Oct 17, 2022
9744fc2
type cleanup
charliecruzan-stripe Oct 17, 2022
a85bf3f
partial migration guide
charliecruzan-stripe Oct 18, 2022
b03fa8c
fix issue with incorrect payment request values never resolving
charliecruzan-stripe Oct 24, 2022
03d9b75
move callback to props instead of functions
charliecruzan-stripe Nov 21, 2022
b861752
nativepay -> platformpay
charliecruzan-stripe Nov 21, 2022
2c94942
move to all platform-agnostic naming
charliecruzan-stripe Nov 22, 2022
a324580
only fire event if there are listeners
charliecruzan-stripe Nov 22, 2022
ad62a0d
mock functions, deprecations
charliecruzan-stripe Nov 22, 2022
d22daa7
upgrade github actions macos ios tests
charliecruzan-stripe Nov 22, 2022
42d9a9c
upgrade to iOS 15.5 for tests
charliecruzan-stripe Nov 28, 2022
0c80f52
Change android emulator profile and API
charliecruzan-stripe Nov 28, 2022
77cf3e0
pixel_3a
charliecruzan-stripe Nov 28, 2022
331d126
ios 16 sim
charliecruzan-stripe Nov 28, 2022
c4977ee
more test fixes
charliecruzan-stripe Nov 28, 2022
cab679a
[skip actions] comments
charliecruzan-stripe Nov 28, 2022
e83a6d3
revert back to galaxy nexus
charliecruzan-stripe Nov 28, 2022
70029d7
iphone 14 16.0 in script
charliecruzan-stripe Nov 29, 2022
a065c32
e2e tests- back to macos-latest
charliecruzan-stripe Nov 29, 2022
cb9431b
use PKPaymentNetwork.init using raw string
charliecruzan-stripe Nov 30, 2022
a812728
Update wdio.ios.js
charliecruzan-stripe Dec 1, 2022
031df55
review changes, more docs in migration guide
charliecruzan-stripe Dec 1, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
test-ios:
name: unit-test-ios
runs-on: macos-latest
runs-on: macos-12
steps:
- name: checkout
uses: actions/checkout@v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ class GooglePayButtonManager : SimpleViewManager<GooglePayButtonView?>() {

@ReactProp(name = "buttonType")
fun buttonType(view: GooglePayButtonView, buttonType: String) {
view.setButtonType(buttonType)
}

@ReactProp(name = "type")
fun type(view: GooglePayButtonView, buttonType: Int) {
view.setType(buttonType)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,66 @@
package com.reactnativestripesdk

import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import com.facebook.react.uimanager.ThemedReactContext

class GooglePayButtonView(private val context: ThemedReactContext) : FrameLayout(context) {
private var button: View? = null
// Used in legacy GooglePayButton implementations
private var buttonType: String? = null

// Used in the new PlatformPayButton implementations
private var type: Int? = null

fun initialize() {
val type =
when (buttonType) {
"pay" -> R.layout.pay_with_googlepay_button
"standard" -> R.layout.googlepay_button
else -> R.layout.googlepay_button
val resAsset: Int =
if (type != null) {
when (type) {
0 -> R.layout.plain_googlepay_button
1 -> R.layout.buy_with_googlepay_button
6 -> R.layout.book_with_googlepay_button
5 -> R.layout.checkout_with_googlepay_button
4 -> R.layout.donate_with_googlepay_button
11 -> R.layout.order_with_googlepay_button
1000 -> R.layout.pay_with_googlepay_button
7 -> R.layout.subscribe_with_googlepay_button
1001 -> R.layout.googlepay_mark_button
else -> R.layout.plain_googlepay_button
}
} else {
when (buttonType) {
"pay" -> R.layout.pay_with_googlepay_button
"standard" -> R.layout.plain_googlepay_button
else -> R.layout.plain_googlepay_button
}
}

val button = LayoutInflater.from(context).inflate(
type, null
button = LayoutInflater.from(context).inflate(
resAsset, null
)

addView(button)
viewTreeObserver.addOnGlobalLayoutListener { requestLayout() }
}

override fun requestLayout() {
super.requestLayout()
post(mLayoutRunnable)
}

fun setType(type: String) {
private val mLayoutRunnable = Runnable {
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
button?.layout(left, top, right, bottom)
}

fun setButtonType(type: String) {
buttonType = type
}

fun setType(type: Int) {
this.type = type
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.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 com.reactnativestripesdk.utils.*
import com.reactnativestripesdk.utils.createError
import com.reactnativestripesdk.utils.createMissingActivityError
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayLauncher

class GooglePayLauncherFragment : Fragment() {
enum class Mode {
ForSetup, ForPayment
}

private lateinit var launcher: GooglePayLauncher
private lateinit var clientSecret: String
private lateinit var mode: Mode
private lateinit var configuration: GooglePayLauncher.Config
private lateinit var currencyCode: String
private lateinit var callback: (result: GooglePayLauncher.Result?, error: WritableMap?) -> Unit

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?) {
launcher = GooglePayLauncher(
fragment = this,
config = configuration,
readyCallback = ::onGooglePayReady,
resultCallback = ::onGooglePayResult
)
}

fun presentGooglePaySheet(clientSecret: String, mode: Mode, googlePayParams: ReadableMap, context: ReactApplicationContext, callback: (GooglePayLauncher.Result?, error: WritableMap?) -> Unit) {
this.clientSecret = clientSecret
this.mode = mode
this.callback = callback
this.currencyCode = googlePayParams.getString("currencyCode") ?: "USD"
this.configuration = GooglePayLauncher.Config(
environment = if (googlePayParams.getBoolean("testEnv")) GooglePayEnvironment.Test else GooglePayEnvironment.Production,
merchantCountryCode = googlePayParams.getString("merchantCountryCode").orEmpty(),
merchantName = googlePayParams.getString("merchantName").orEmpty(),
isEmailRequired = googlePayParams.getBooleanOr("isEmailRequired", false),
billingAddressConfig = buildBillingAddressParameters(googlePayParams.getMap("billingAddressConfig")),
existingPaymentMethodRequired = googlePayParams.getBooleanOr("existingPaymentMethodRequired", false),
allowCreditCards = googlePayParams.getBooleanOr("allowCreditCards", true),
)

(context.currentActivity as? AppCompatActivity)?.let {
attemptToCleanupPreviousFragment(it)
commitFragmentAndStartFlow(it)
} ?: run {
callback(null, createMissingActivityError())
return
}
}

private fun attemptToCleanupPreviousFragment(currentActivity: AppCompatActivity) {
currentActivity.supportFragmentManager.beginTransaction()
.remove(this)
.commitAllowingStateLoss()
}

private fun commitFragmentAndStartFlow(currentActivity: AppCompatActivity) {
try {
currentActivity.supportFragmentManager.beginTransaction()
.add(this, TAG)
.commit()
} catch (error: IllegalStateException) {
callback(
null,
createError(ErrorType.Failed.toString(), error.message)
)
}
}

private fun onGooglePayReady(isReady: Boolean) {
if (isReady) {
when (mode) {
Mode.ForSetup -> {
launcher.presentForSetupIntent(clientSecret, currencyCode)
}
Mode.ForPayment -> {
launcher.presentForPaymentIntent(clientSecret)
}
}
} else {
callback(
null,
createError(
GooglePayErrorType.Failed.toString(),
"Google Pay is not available on this device. You can use isPlatformPaySupported to preemptively check for Google Pay support."
)
)
}
}

private fun onGooglePayResult(result: GooglePayLauncher.Result) {
callback(result, null)
}

companion object {
const val TAG = "google_pay_launcher_fragment"

private fun buildBillingAddressParameters(params: ReadableMap?): GooglePayLauncher.BillingAddressConfig {
val isRequired = params?.getBooleanOr("isRequired", false)
val isPhoneNumberRequired = params?.getBooleanOr("isPhoneNumberRequired", false)
val format = when (params?.getString("format").orEmpty()) {
"FULL" -> GooglePayLauncher.BillingAddressConfig.Format.Full
"MIN" -> GooglePayLauncher.BillingAddressConfig.Format.Min
else -> GooglePayLauncher.BillingAddressConfig.Format.Min
}

return GooglePayLauncher.BillingAddressConfig(
isRequired = isRequired ?: false,
format = format,
isPhoneNumberRequired = isPhoneNumberRequired ?: false
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.reactnativestripesdk

import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableNativeMap
import com.google.android.gms.tasks.Task
import com.google.android.gms.wallet.*
import com.reactnativestripesdk.utils.*
import com.reactnativestripesdk.utils.createError
import com.reactnativestripesdk.utils.mapFromPaymentMethod
import com.reactnativestripesdk.utils.mapFromToken
import com.stripe.android.ApiResultCallback
import com.stripe.android.GooglePayJsonFactory
import com.stripe.android.Stripe
import com.stripe.android.model.GooglePayResult
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import org.json.JSONObject
import java.util.*

class GooglePayRequestHelper {
companion object {
internal const val LOAD_PAYMENT_DATA_REQUEST_CODE = 414243

internal fun createPaymentRequest(activity: AppCompatActivity, factory: GooglePayJsonFactory, googlePayParams: ReadableMap): Task<PaymentData> {
val transactionInfo = buildTransactionInfo(googlePayParams)
val merchantInfo = GooglePayJsonFactory.MerchantInfo(googlePayParams.getString("merchantName").orEmpty())
val billingAddressParameters = buildBillingAddressParameters(googlePayParams.getMap("billingAddressConfig"))
val shippingAddressParameters = buildShippingAddressParameters(googlePayParams.getMap("shippingAddressConfig"))

val request = factory.createPaymentDataRequest(
transactionInfo = transactionInfo,
merchantInfo = merchantInfo,
billingAddressParameters = billingAddressParameters,
shippingAddressParameters = shippingAddressParameters,
isEmailRequired = googlePayParams.getBooleanOr("isEmailRequired", false),
allowCreditCards = googlePayParams.getBooleanOr("allowCreditCards", true)
)

val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(if (googlePayParams.getBoolean("testEnv")) WalletConstants.ENVIRONMENT_TEST else WalletConstants.ENVIRONMENT_PRODUCTION)
.build()
return Wallet.getPaymentsClient(activity, walletOptions).loadPaymentData(PaymentDataRequest.fromJson(request.toString()))
}

@Suppress("UNCHECKED_CAST")
private fun buildShippingAddressParameters(params: ReadableMap?): GooglePayJsonFactory.ShippingAddressParameters {
val isPhoneNumberRequired = params?.getBooleanOr("isPhoneNumberRequired", false)
val isRequired = params?.getBooleanOr("isRequired", false)
val allowedCountryCodes = if (params?.hasKey("allowedCountryCodes") == true)
params.getArray("allowedCountryCodes") as Array<String> else Locale.getISOCountries()

return GooglePayJsonFactory.ShippingAddressParameters(
isRequired = isRequired ?: false,
allowedCountryCodes = allowedCountryCodes.toSet(),
phoneNumberRequired = isPhoneNumberRequired ?: false
)
}

private fun buildBillingAddressParameters(params: ReadableMap?): GooglePayJsonFactory.BillingAddressParameters {
val isRequired = params?.getBooleanOr("isRequired", false)
val isPhoneNumberRequired = params?.getBooleanOr("isPhoneNumberRequired", false)
val format = when (params?.getString("format").orEmpty()) {
"FULL" -> GooglePayJsonFactory.BillingAddressParameters.Format.Full
"MIN" -> GooglePayJsonFactory.BillingAddressParameters.Format.Min
else -> GooglePayJsonFactory.BillingAddressParameters.Format.Min
}

return GooglePayJsonFactory.BillingAddressParameters(
isRequired = isRequired ?: false,
format = format,
isPhoneNumberRequired = isPhoneNumberRequired ?: false
)
}

private fun buildTransactionInfo(params: ReadableMap): GooglePayJsonFactory.TransactionInfo {
val countryCode = params.getString("merchantCountryCode").orEmpty()
val currencyCode = params.getString("currencyCode") ?: "USD"
val amount = params.getInt("amount")

return GooglePayJsonFactory.TransactionInfo(
currencyCode = currencyCode,
totalPriceStatus = GooglePayJsonFactory.TransactionInfo.TotalPriceStatus.Estimated,
countryCode = countryCode,
totalPrice = amount,
checkoutOption = GooglePayJsonFactory.TransactionInfo.CheckoutOption.Default
)
}

internal fun createPaymentMethod(request: Task<PaymentData>, activity: AppCompatActivity) {
AutoResolveHelper.resolveTask(
request,
activity,
LOAD_PAYMENT_DATA_REQUEST_CODE
)
}

internal fun handleGooglePaymentMethodResult(resultCode: Int, data: Intent?, stripe: Stripe, promise: Promise) {
when (resultCode) {
Activity.RESULT_OK -> {
data?.let { intent ->
PaymentData.getFromIntent(intent)?.let {
resolveWithPaymentMethodAndToken(it, stripe, promise)
}
}
}
Activity.RESULT_CANCELED -> {
promise.resolve(createError(ErrorType.Canceled.toString(), "The payment has been canceled"))
}
AutoResolveHelper.RESULT_ERROR -> {
AutoResolveHelper.getStatusFromIntent(data)?.let {
promise.resolve(createError(ErrorType.Failed.toString(), it.statusMessage))
}
}
}
}

private fun resolveWithPaymentMethodAndToken(paymentData: PaymentData, stripe: Stripe, promise: Promise) {
val paymentInformation = JSONObject(paymentData.toJson())
val googlePayResult = GooglePayResult.fromJson(paymentInformation)
val promiseResult = WritableNativeMap()
googlePayResult.token?.let {
promiseResult.putMap("token", mapFromToken(it))
}
stripe.createPaymentMethod(
PaymentMethodCreateParams.createFromGooglePay(paymentInformation),
callback = object : ApiResultCallback<PaymentMethod> {
override fun onError(e: Exception) {
promise.resolve(createError("Failed", e))
}

override fun onSuccess(result: PaymentMethod) {
promiseResult.putMap("paymentMethod", mapFromPaymentMethod(result))
promise.resolve(promiseResult)
}
}
)
}
}
}

Loading