package com.payments import android.app.Activity import android.content.Intent import android.util.Log import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.wallet.AutoResolveHelper import com.google.android.gms.wallet.IsReadyToPayRequest import com.google.android.gms.wallet.PaymentData import com.google.android.gms.wallet.PaymentDataRequest import com.google.android.gms.wallet.Wallet import com.google.android.gms.wallet.WalletConstants import org.json.JSONException import org.json.JSONObject // HINT: To see crashes use: `adb logcat "*:S" AndroidRuntime:E` // HINT: Check this setup: https://developers.google.com/pay/api/android/guides/setup Add it to the docs class PaymentsModule internal constructor(context: ReactApplicationContext) : PaymentsSpec(context) { // TODO: Promise ref, should resolve/reject when the payment is done/cancelled/error private var pendingPromise: Promise? = null // https://reactnative.dev/docs/native-modules-android#getting-activity-result-from-startactivityforresult private val activityEventListener = object : BaseActivityEventListener() { // https://developers.google.com/pay/api/android/guides/tutorial#java override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) { Log.d(NAME, "Received onActivityResult: $requestCode $resultCode") // value passed in AutoResolveHelper if (requestCode != LOAD_MASKED_WALLET_REQUEST_CODE) { return } when (resultCode) { Activity.RESULT_OK -> { val paymentData = PaymentData.getFromIntent(data!!) Log.d(NAME, "Received payment: $paymentData") handlePaymentSuccess(paymentData!!) } Activity.RESULT_CANCELED -> rejectPromise(E_CANCELLED_BY_USER, "User closed AndroidPay without completing the payment") else -> handleError(AutoResolveHelper.getStatusFromIntent(data)!!.statusCode) } } } init { context.addActivityEventListener(activityEventListener) } override fun getName(): String = NAME // Implementation See https://reactnative.dev/docs/native-modules-android // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentsClient#isReadyToPay(com.google.android.gms.wallet.IsReadyToPayRequest) // https://developers.google.com/pay/api/android/guides/tutorial#java @ReactMethod override fun canMakePayments(paymentMethodData: String, promise: Promise) { checkIsReadyToPay(paymentMethodData, false, promise) } // HINT: existingPaymentMethodRequired is an optimistic signal, not a guarantee a card is enrolled. // https://developers.google.com/android/reference/com/google/android/gms/wallet/IsReadyToPayRequest.Builder#setExistingPaymentMethodRequired(boolean) @ReactMethod override fun hasEnrolledInstrument(paymentMethodData: String, promise: Promise) { checkIsReadyToPay(paymentMethodData, true, promise) } private fun checkIsReadyToPay(paymentMethodData: String, existingPaymentMethodRequired: Boolean, promise: Promise) { val currentActivity = requireCurrentActivity() Log.d(NAME, "Checking if AndroidPay is available $currentActivity") // HINT: We store promise reference to resolve/reject it later pendingPromise = promise if (isGooglePlayServicesUnavailable(currentActivity)) { rejectPromise(E_UNSUPPORTED_ANDROID_PAY, "AndroidPay is not supported: Google Play services is unavailable") return } validatePaymentRequestJSON(paymentMethodData) // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentDataRequest#fromJson(java.lang.String) val request: IsReadyToPayRequest? = if (existingPaymentMethodRequired) { buildExistingPaymentMethodRequiredRequest(paymentMethodData) } else { IsReadyToPayRequest.fromJson(paymentMethodData) } if (request == null) { rejectPromise(E_UNSUPPORTED_ANDROID_PAY, "AndroidPay is not supported") return } // https://developers.google.com/android/reference/com/google/android/gms/wallet/Wallet.WalletOptions.Builder#setEnvironment(int) val options = Wallet.WalletOptions.Builder().setEnvironment(WalletConstants.ENVIRONMENT_TEST).build() // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentsClient val paymentsClient = Wallet.getPaymentsClient(currentActivity, options) paymentsClient.isReadyToPay(request).addOnCompleteListener { task -> if (task.isSuccessful) { promise.resolve(task.result) } else { promise.reject(E_UNSUPPORTED_ANDROID_PAY, "AndroidPay is not supported") } } } // Wallet.getPaymentsClient(...) itself never fails; it is the chained isReadyToPay()/loadPaymentData() // call that reaches into Play services and, on a device where it is missing or invalid, surfaces an // unrecoverable system dialog over the whole screen instead of failing the task. Checking availability // first keeps that dialog from ever appearing and reports AndroidPay as unsupported instead, exactly // like https://developers.google.com/android/guides/api-client recommends for any Play services caller. private fun isGooglePlayServicesUnavailable(activity: Activity): Boolean = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(activity) != ConnectionResult.SUCCESS private fun buildExistingPaymentMethodRequiredRequest(paymentMethodData: String): IsReadyToPayRequest? = try { val requestJson = JSONObject(paymentMethodData) requestJson.put("existingPaymentMethodRequired", true) IsReadyToPayRequest.fromJson(requestJson.toString()) } catch (e: JSONException) { null } @ReactMethod override fun show(requestId: String, paymentMethodData: String, details: ReadableMap, promise: Promise) { val currentActivity = requireCurrentActivity() Log.d(NAME, "Showing AndroidPay for request $requestId $currentActivity$details") // HINT: We store promise reference to resolve/reject it later pendingPromise = promise if (isGooglePlayServicesUnavailable(currentActivity)) { rejectPromise(E_FAILED_SHOWING_ANDROID_PAY, "Failed showing AndroidPay: Google Play services is unavailable") return } validatePaymentRequestJSON(paymentMethodData) try { // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentDataRequest#fromJson(java.lang.String) val request: PaymentDataRequest? = PaymentDataRequest.fromJson(paymentMethodData) if (request == null) { rejectPromise(E_FAILED_CREATING_PAYMENT_REQUEST, "Failed creating PaymentDataRequest") return } Log.d(NAME, "Created PaymentDataRequest $request") // https://developers.google.com/android/reference/com/google/android/gms/wallet/Wallet.WalletOptions.Builder#setEnvironment(int) val options = Wallet.WalletOptions.Builder().setEnvironment(getEnvironment(details)).build() // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentsClient val paymentsClient = Wallet.getPaymentsClient(currentActivity, options) // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentsClient#public-taskpaymentdata-loadpaymentdata-paymentdatarequest-request val loadPaymentDataTask = paymentsClient.loadPaymentData(request) // https://developers.google.com/android/reference/com/google/android/gms/wallet/AutoResolveHelper AutoResolveHelper.resolveTask(loadPaymentDataTask, currentActivity, LOAD_MASKED_WALLET_REQUEST_CODE) } catch (e: Exception) { rejectPromise(E_FAILED_SHOWING_ANDROID_PAY, "Failed showing AndroidPay$e") } } @ReactMethod override fun abort(promise: Promise) { Log.d(NAME, "Aborting AndroidPay for ${requireCurrentActivity()}") promise.resolve("AndroidPay abort is not supported") } @ReactMethod override fun complete(paymentComplete: String, promise: Promise) { Log.d(NAME, "Completing status $paymentComplete AndroidPay for ${requireCurrentActivity()}") promise.resolve("AndroidPay complete is not supported") } // https://www.w3.org/TR/payment-request/#dom-paymentresponse-retry @ReactMethod override fun retry(requestId: String, errorFields: ReadableMap, promise: Promise) { Log.d(NAME, "In-sheet retry is not supported by AndroidPay, ignoring $requestId") promise.resolve("AndroidPay retry is not supported") } @ReactMethod override fun setActiveEvents(requestId: String, eventNames: ReadableArray, promise: Promise) { Log.d(NAME, "Change events are not supported by AndroidPay, ignoring ${eventNames.size()} event(s) of $requestId") promise.resolve(null) } @ReactMethod override fun updatePaymentDetails( update: ReadableMap, displayItems: ReadableArray, shippingOptions: ReadableArray, promise: Promise ) { Log.d(NAME, "Payment details updates are not supported by AndroidPay") promise.resolve(null) } @ReactMethod override fun addListener(eventName: String) { Log.d(NAME, "Added a listener for $eventName, AndroidPay never emits change events") } @ReactMethod override fun removeListeners(count: Double) { Log.d(NAME, "Removed $count listener(s), AndroidPay never emits change events") } private fun requireCurrentActivity(): Activity = reactApplicationContext.currentActivity!! // https://developers.google.com/pay/api/android/guides/tutorial#checkoutactivity.java-java // https://developers.google.com/android/reference/com/google/android/gms/wallet/PaymentData // https://developers.google.com/pay/api/android/reference/request-objects#PaymentData private fun handlePaymentSuccess(paymentData: PaymentData) { val paymentInfo: String? = paymentData.toJson() if (paymentInfo == null) { rejectPromise(E_FAILED_PARSING_PAYMENT_RESPONSE, "Failed parsing payment response") return } Log.d(NAME, "Successfully received paymentData: ${requireCurrentActivity()} $paymentInfo") pendingPromise!!.resolve(paymentInfo) pendingPromise = null } // At this stage, the user has already seen a popup informing them an error occurred, so only logging is required. // https://developers.google.com/android/reference/com/google/android/gms/wallet/WalletConstants#constant-summary private fun handleError(statusCode: Int) { val errorMessage = when (statusCode) { WalletConstants.ERROR_CODE_BUYER_ACCOUNT_ERROR -> "Buyer account error occurred." WalletConstants.ERROR_CODE_MERCHANT_ACCOUNT_ERROR -> "Merchant account error occurred." WalletConstants.ERROR_CODE_DEVELOPER_ERROR -> "Developer error occurred. Please check your Google Pay API configuration." else -> "An unexpected error has occurred during payment." } rejectPromise(E_FAILED_PROCESSING, errorMessage) } private fun validatePaymentRequestJSON(paymentRequestJSON: String) { try { val jsonObject = JSONObject(paymentRequestJSON) Log.d(NAME, "Successfully validated paymentRequest JSON string $jsonObject") } catch (e: JSONException) { rejectPromise(E_FAILED_PARSING_PAYMENT_REQUEST, "Failed parsing PaymentRequest JSON string") } } private fun getEnvironment(details: ReadableMap): Int = if (details.hasKey("environment") && details.getString("environment")!! == "PRODUCTION") { WalletConstants.ENVIRONMENT_PRODUCTION } else { WalletConstants.ENVIRONMENT_TEST } private fun rejectPromise(code: String, message: String) { Log.e(NAME, message) pendingPromise?.reject(code, message) pendingPromise = null } companion object { const val NAME = "Payments" private const val E_FAILED_SHOWING_ANDROID_PAY = "E_FAILED_SHOWING_ANDROID_PAY" private const val E_CANCELLED_BY_USER = "E_CANCELLED_BY_USER" private const val E_UNSUPPORTED_ANDROID_PAY = "E_UNSUPPORTED_ANDROID_PAY" private const val E_FAILED_CREATING_PAYMENT_REQUEST = "E_FAILED_CREATING_PAYMENT_REQUEST" private const val E_FAILED_PROCESSING = "E_FAILED_PROCESSING" private const val E_FAILED_PARSING_PAYMENT_REQUEST = "E_FAILED_PARSING_PAYMENT_REQUEST" private const val E_FAILED_PARSING_PAYMENT_RESPONSE = "E_FAILED_PARSING_PAYMENT_RESPONSE" // Arbitrarily-picked constant integer you define to track a request for payment data activity. private const val LOAD_MASKED_WALLET_REQUEST_CODE = 88 } }