Skip to content

Commit

Permalink
WIP: Google pay android implementation (#441)
Browse files Browse the repository at this point in the history
* chore: google pay new approach implementation

* chore: clean up

* chore: google pay customization

* chore: add documentation, fix linter

* fix: appium tests

* chore: increate JVM memory size

* chore: improvements, refactor

* chore: bump android-emulator api level to 30

* chore: bump api level

* Update e2e-tests.yml

* fix: tests

* chore: bump node version

* chore: bump node version

* chore: bump node version

* chore: bump chrome driver

* chore: appium conf

* chore: appium conf

* chore: appium conf

* chore: downgrade api-level

* chore: use google apis

* chore: change arch

* chore: update chrome driver version

* chore: e2e config attempt

* chore: e2e config attempt

* chore: e2e conf attempt

* chore: accept terms and conditions

* chore: accept terms and conditions

* chore: accept terms and conditions

* chore: accept terms and conditions

* fix: init on mount only

* fix: init issue

* chore: handle exception

* chore: add google pay button

* chore: handle dark mode

* chore: clean up

* chore: tests clean up

* chore: clean up

* chore: clean up

Co-authored-by: Arkadiusz Kubaczkowski <arek.kubaczkowski@callstak.com>
  • Loading branch information
arekkubaczkowski and Arkadiusz Kubaczkowski authored Jul 30, 2021
1 parent 9646d1f commit 961d6d1
Show file tree
Hide file tree
Showing 156 changed files with 4,621 additions and 2,324 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.5
with:
node-version: 14.4.0
node-version: 14.15.0

- name: Install React Native CLI
run: npm install react-native-cli
Expand All @@ -41,7 +41,9 @@ jobs:
- name: Run Android Emulator and app
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
api-level: 28
target: google_apis
arch: x86
script: |
yarn run-example-android
sleep 15
Expand Down Expand Up @@ -88,7 +90,7 @@ jobs:
- name: Setup Node.js environment
uses: actions/setup-node@v2.1.5
with:
node-version: 14.4.0
node-version: 14.15.0

- name: Install React Native CLI
run: |
Expand Down
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
chromedriver_version=74.0.3729.6
chromedriver_version=2.44
5 changes: 5 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ var ON_PAYMENT_OPTION_ACTION = "com.reactnativestripesdk.PAYMENT_OPTION_ACTION"
var ON_CONFIGURE_FLOW_CONTROLLER = "com.reactnativestripesdk.CONFIGURE_FLOW_CONTROLLER_ACTION"
var ON_INIT_PAYMENT_SHEET = "com.reactnativestripesdk.INIT_PAYMENT_SHEET"
var ON_FRAGMENT_CREATED = "com.reactnativestripesdk.FRAGMENT_CREATED_ACTION"

var ON_GOOGLE_PAY_FRAGMENT_CREATED = "com.reactnativestripesdk.ON_GOOGLE_PAY_FRAGMENT_CREATED"
var ON_INIT_GOOGLE_PAY = "com.reactnativestripesdk.ON_INIT_GOOGLE_PAY"
var ON_GOOGLE_PAY_RESULT = "com.reactnativestripesdk.ON_GOOGLE_PAY_RESULT"
var ON_GOOGLE_PAYMENT_METHOD_RESULT = "com.reactnativestripesdk.ON_GOOGLE_PAYMENT_METHOD_RESULT"
4 changes: 4 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ enum class PaymentSheetErrorType {
Failed, Canceled
}

enum class GooglePayErrorType {
Failed, Canceled, Unknown
}

internal fun mapError(code: String, message: String?, localizedMessage: String?, declineCode: String?, type: String?, stripeErrorCode: String?): WritableMap {
val map: WritableMap = WritableNativeMap()
val details: WritableMap = WritableNativeMap()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.reactnativestripesdk

import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp

class GooglePayButtonManager : SimpleViewManager<GooglePayButtonView?>() {
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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.reactnativestripesdk

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

class GooglePayButtonView(private val context: ThemedReactContext) : FrameLayout(context) {
var buttonType: String? = null

fun initialize() {
val type = when (buttonType) {
"pay" -> R.layout.pay_with_googlepay_button_no_shadow
"pay_shadow" -> R.layout.pay_with_googlepay_button
"standard_shadow" -> R.layout.googlepay_button
"standard" -> R.layout.googlepay_button_no_shadow
else -> R.layout.googlepay_button
}
val button = LayoutInflater.from(context).inflate(
type, null
)

addView(button)
}

fun setType(type: String) {
buttonType = type
}
}
173 changes: 173 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/GooglePayFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.reactnativestripesdk

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import com.facebook.react.bridge.WritableNativeMap
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayLauncher
import com.stripe.android.googlepaylauncher.GooglePayPaymentMethodLauncher
import com.stripe.android.model.StripeIntent

class GooglePayFragment : Fragment() {
private var googlePayLauncher: GooglePayLauncher? = null
private var googlePayMethodLauncher: GooglePayPaymentMethodLauncher? = null

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?) {
super.onViewCreated(view, savedInstanceState)
val testEnv = arguments?.getBoolean("testEnv")
val merchantName = arguments?.getString("merchantName").orEmpty()
val countryCode = arguments?.getString("countryCode").orEmpty()
val isEmailRequired = arguments?.getBoolean("isEmailRequired") ?: false
val existingPaymentMethodRequired = arguments?.getBoolean("existingPaymentMethodRequired") ?: false

val billingAddressConfigBundle = arguments?.getBundle("billingAddressConfig") ?: Bundle()
val isRequired = billingAddressConfigBundle.getBoolean("isRequired")
val formatString = billingAddressConfigBundle.getString("format").orEmpty()
val isPhoneNumberRequired = billingAddressConfigBundle.getBoolean("isPhoneNumberRequired")

val billingAddressConfig = mapToGooglePayPaymentMethodLauncherBillingAddressConfig(formatString, isRequired, isPhoneNumberRequired)

googlePayMethodLauncher = GooglePayPaymentMethodLauncher(
fragment = this,
config = GooglePayPaymentMethodLauncher.Config(
environment = if (testEnv == true) GooglePayEnvironment.Test else GooglePayEnvironment.Production,
merchantCountryCode = countryCode,
merchantName = merchantName,
billingAddressConfig = billingAddressConfig,
isEmailRequired = isEmailRequired,
existingPaymentMethodRequired = existingPaymentMethodRequired
),
readyCallback = ::onGooglePayReady,
resultCallback = ::onGooglePayResult
)

val paymentMethodBillingAddressConfig = mapToGooglePayLauncherBillingAddressConfig(formatString, isRequired, isPhoneNumberRequired)
googlePayLauncher = GooglePayLauncher(
fragment = this,
config = GooglePayLauncher.Config(
environment = if (testEnv == true) GooglePayEnvironment.Test else GooglePayEnvironment.Production,
merchantCountryCode = countryCode,
merchantName = merchantName,
billingAddressConfig = paymentMethodBillingAddressConfig,
isEmailRequired = isEmailRequired,
existingPaymentMethodRequired = existingPaymentMethodRequired
),
readyCallback = ::onGooglePayReady,
resultCallback = ::onGooglePayResult
)

val intent = Intent(ON_GOOGLE_PAY_FRAGMENT_CREATED)
activity?.sendBroadcast(intent)
}

fun presentForPaymentIntent(clientSecret: String) {
val launcher = googlePayLauncher ?: run {
val intent = Intent(ON_GOOGLE_PAY_RESULT)
intent.putExtra("error", "GooglePayLauncher is not initialized.")
activity?.sendBroadcast(intent)
return
}
runCatching {
launcher.presentForPaymentIntent(clientSecret)
}.onFailure {
val intent = Intent(ON_GOOGLE_PAY_RESULT)
intent.putExtra("error", it.localizedMessage)
activity?.sendBroadcast(intent)
}
}

fun presentForSetupIntent(clientSecret: String, currencyCode: String) {
val launcher = googlePayLauncher ?: run {
val intent = Intent(ON_GOOGLE_PAY_RESULT)
intent.putExtra("error", "GooglePayLauncher is not initialized.")
activity?.sendBroadcast(intent)
return
}
runCatching {
launcher.presentForSetupIntent(clientSecret, currencyCode)
}.onFailure {
val intent = Intent(ON_GOOGLE_PAY_RESULT)
intent.putExtra("error", it.localizedMessage)
activity?.sendBroadcast(intent)
}
}

fun createPaymentMethod(currencyCode: String, amount: Int) {
val launcher = googlePayMethodLauncher ?: run {
val intent = Intent(ON_GOOGLE_PAYMENT_METHOD_RESULT)
intent.putExtra("error", "GooglePayPaymentMethodLauncher is not initialized.")
activity?.sendBroadcast(intent)
return
}

runCatching {
launcher.present(
currencyCode = currencyCode,
amount = amount
)
}.onFailure {
val intent = Intent(ON_GOOGLE_PAYMENT_METHOD_RESULT)
intent.putExtra("error", it.localizedMessage)
activity?.sendBroadcast(intent)
}
}

private fun onGooglePayReady(isReady: Boolean) {
val intent = Intent(ON_INIT_GOOGLE_PAY)
intent.putExtra("isReady", isReady)
activity?.sendBroadcast(intent)
}

private fun onGooglePayResult(result: GooglePayLauncher.Result) {
val intent = Intent(ON_GOOGLE_PAY_RESULT)
intent.putExtra("paymentResult", result)
activity?.sendBroadcast(intent)
}

private fun onGooglePayResult(result: GooglePayPaymentMethodLauncher.Result) {
val intent = Intent(ON_GOOGLE_PAYMENT_METHOD_RESULT)
intent.putExtra("paymentResult", result)
activity?.sendBroadcast(intent)
}

private fun mapToGooglePayLauncherBillingAddressConfig(formatString: String, isRequired: Boolean, isPhoneNumberRequired: Boolean): GooglePayLauncher.BillingAddressConfig {
val format = when (formatString) {
"FULL" -> GooglePayLauncher.BillingAddressConfig.Format.Full
"MIN" -> GooglePayLauncher.BillingAddressConfig.Format.Min
else -> GooglePayLauncher.BillingAddressConfig.Format.Min
}
return GooglePayLauncher.BillingAddressConfig(
isRequired = isRequired,
format = format,
isPhoneNumberRequired = isPhoneNumberRequired
)
}

private fun mapToGooglePayPaymentMethodLauncherBillingAddressConfig(formatString: String, isRequired: Boolean, isPhoneNumberRequired: Boolean): GooglePayPaymentMethodLauncher.BillingAddressConfig {
val format = when (formatString) {
"FULL" -> GooglePayPaymentMethodLauncher.BillingAddressConfig.Format.Full
"MIN" -> GooglePayPaymentMethodLauncher.BillingAddressConfig.Format.Min
else -> GooglePayPaymentMethodLauncher.BillingAddressConfig.Format.Min
}
return GooglePayPaymentMethodLauncher.BillingAddressConfig(
isRequired = isRequired,
format = format,
isPhoneNumberRequired = isPhoneNumberRequired
)
}
}
27 changes: 27 additions & 0 deletions android/src/main/java/com/reactnativestripesdk/Mappers.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.reactnativestripesdk

import android.os.Bundle
import android.util.Log
import com.facebook.react.bridge.*
import com.stripe.android.PaymentAuthConfig
import com.stripe.android.model.*
Expand Down Expand Up @@ -702,3 +704,28 @@ fun mapToPaymentIntentFutureUsage(type: String?): ConfirmPaymentIntentParams.Set
else -> null
}
}

fun toBundleObject(readableMap: ReadableMap?): Bundle? {
val result = Bundle()
if (readableMap == null) {
return result
}
val iterator = readableMap.keySetIterator()
while (iterator.hasNextKey()) {
val key = iterator.nextKey()
when (readableMap.getType(key)) {
ReadableType.Null -> result.putString(key, null)
ReadableType.Boolean -> result.putBoolean(key, readableMap.getBoolean(key))
ReadableType.Number -> try {
result.putInt(key, readableMap.getInt(key))
} catch (e: Exception) {
result.putDouble(key, readableMap.getDouble(key))
}
ReadableType.String -> result.putString(key, readableMap.getString(key))
ReadableType.Map -> result.putBundle(key, toBundleObject(readableMap.getMap(key)))
ReadableType.Array -> Log.e("toBundleException", "Cannot put arrays of objects into bundles. Failed on: $key.")
else -> Log.e("toBundleException", "Could not convert object with key: $key.")
}
}
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class PaymentSheetFragment : Fragment() {
val merchantDisplayName = arguments?.getString("merchantDisplayName").orEmpty()
val customerId = arguments?.getString("customerId").orEmpty()
val customerEphemeralKeySecret = arguments?.getString("customerEphemeralKeySecret").orEmpty()
val countryCode = arguments?.getString("countryCode").orEmpty()
val countryCode = arguments?.getString("merchantCountryCode").orEmpty()
val googlePayEnabled = arguments?.getBoolean("googlePay")
val testEnv = arguments?.getBoolean("testEnv")
paymentIntentClientSecret = arguments?.getString("paymentIntentClientSecret").orEmpty()
Expand Down
Loading

0 comments on commit 961d6d1

Please sign in to comment.