Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ For Google Pay, you need to configure your payment gateway:

```typescript
const checkout = usePaymentCheckout({
merchantName: "Your Store", // shown on the Google Pay sheet
currencyCode: "USD",
countryCode: "US",
// Google Pay specific
Expand All @@ -333,6 +334,8 @@ const checkout = usePaymentCheckout({
});
```

Without `merchantName`, the Google Pay sheet shows your app's label. If any cart item is `pending`, the total is sent as an estimate (`ESTIMATED` on Google Pay, `.pending` on Apple Pay).

#### Supported Payment Gateways

- **Stripe** - `googlePayGateway: 'stripe'`
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface PaymentItem {
- **final** — Confirmed amount (e.g. product price, tax).
- **pending** — Estimate (e.g. shipping that may change).

If any item is `pending`, the total is an estimate too: Google Pay receives `totalPriceStatus: 'ESTIMATED'` and Apple Pay marks the total summary item as `.pending`. Every item is sent to Google Pay as a `LINE_ITEM` display item, with `status` set to `FINAL` or `PENDING`.

---

## PaymentRequest
Expand Down Expand Up @@ -48,6 +50,8 @@ interface PaymentRequest {

On iOS, `applePayMerchantIdentifier` is optional and only needed when you want to override the Merchant ID configured in the native app entitlements.

`merchantName` is the name shown on the Google Pay sheet and the label of the Apple Pay total (when the cart has more than one item). If it is missing or blank, Android uses the app's label from the manifest; iOS labels the total "Total".

---

## PaymentResult
Expand Down
1 change: 1 addition & 0 deletions docs/docs/guides/dynamic-cart.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ const handleClearCart = () => {
- **At least one item** is required before `startPayment()`. Otherwise the hook sets an error and returns `null`.
- **total** is derived from `items`; you don’t set it manually.
- Use **type: 'final'** for confirmed amounts (e.g. tax, total); use **'pending'** for estimates (e.g. shipping) if your flow supports it.
- **One pending item makes the total an estimate.** Google Pay shows the total as estimated (`totalPriceStatus: 'ESTIMATED'`) and Apple Pay marks the total as pending. An estimate does not let you charge a different amount: Google Pay requires the amount you authorize and charge to match the `totalPrice` shown on the sheet, and a mismatch can be declined. If the final amount differs from the estimate, start a new payment with the final amount, unless your gateway explicitly supports adjusting it (for example, incremental authorization).
3 changes: 3 additions & 0 deletions docs/docs/setup/android-google-pay.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Google Pay needs a **Google Pay merchant ID** for production, plus your **paymen

```ts
usePaymentCheckout({
merchantName: 'Your Store', // shown on the Google Pay sheet
currencyCode: 'USD',
countryCode: 'US',
googlePayMerchantId: 'your_google_pay_merchant_id',
Expand All @@ -40,6 +41,8 @@ usePaymentCheckout({
})
```

If you omit `merchantName`, the Google Pay sheet shows your app's label (`android:label` in the manifest).

Supported gateways include: `stripe`, `braintree`, `square`, `adyen`, `authorizenet`, `checkoutltd`. See [Google Pay Gateway Tokens](https://developers.google.com/pay/api/android/reference/request-objects#gateway) for the full list and exact identifiers.

## 4. Prebuild and run
Expand Down
4 changes: 4 additions & 0 deletions package/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,9 @@ dependencies {

// Google Pay dependencies
implementation 'com.google.android.gms:play-services-wallet:19.3.0'

testImplementation 'junit:junit:4.13.2'
// android.jar only ships org.json stubs; JVM tests need the real implementation.
testImplementation 'org.json:json:20250517'
}

Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ object PaymentConstants {

// Transaction
const val TOTAL_PRICE_STATUS_FINAL = "FINAL"
const val TOTAL_PRICE_STATUS_ESTIMATED = "ESTIMATED"
const val TOTAL_PRICE_LABEL_DEFAULT = "Total"
const val LINE_ITEM_TYPE = "LINE_ITEM"
const val PENDING_TYPE = "PENDING"

// Merchant
const val DEFAULT_MERCHANT_NAME = "Example Merchant"
const val DISPLAY_ITEM_STATUS_FINAL = "FINAL"
const val DISPLAY_ITEM_STATUS_PENDING = "PENDING"

// Supported networks (lowercase for comparison)
val SUPPORTED_NETWORKS = listOf("visa", "mastercard", "amex", "discover")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,21 @@ object GooglePayRequestBuilder {

/**
* Creates a complete payment data request
*
* @param fallbackMerchantName shown when the request has no merchantName,
* typically the app label. When both are blank, merchantName is omitted.
*/
fun createPaymentDataRequest(
request: PaymentRequest,
environment: Int
environment: Int,
fallbackMerchantName: String? = null
): JSONObject {
validatePaymentRequest(request, environment)

return JSONObject().apply {
put("apiVersion", PaymentConstants.API_VERSION)
put("apiVersionMinor", PaymentConstants.API_VERSION_MINOR)
put("merchantInfo", createMerchantInfo(request, environment))
put("merchantInfo", createMerchantInfo(request, environment, fallbackMerchantName))
put("allowedPaymentMethods", JSONArray().apply {
put(createCardPaymentMethod(request, environment))
})
Expand All @@ -52,10 +56,14 @@ object GooglePayRequestBuilder {
*/
private fun createMerchantInfo(
request: PaymentRequest,
environment: Int
environment: Int,
fallbackMerchantName: String?
): JSONObject {
val merchantName = request.merchantName?.trim().takeUnless { it.isNullOrEmpty() }
?: fallbackMerchantName?.trim().takeUnless { it.isNullOrEmpty() }

return JSONObject().apply {
put("merchantName", request.merchantName ?: PaymentConstants.DEFAULT_MERCHANT_NAME)
merchantName?.let { put("merchantName", it) }
// Add merchant ID only for PRODUCTION environment
if (environment == WalletConstants.ENVIRONMENT_PRODUCTION) {
put("merchantId", request.googlePayMerchantId)
Expand Down Expand Up @@ -137,9 +145,16 @@ object GooglePayRequestBuilder {
*/
private fun createTransactionInfo(request: PaymentRequest): JSONObject {
val totalAmount = request.paymentItems.sumOf { it.amount }
val hasPendingItems = request.paymentItems.any { it.type == PaymentItemType.PENDING }

return JSONObject().apply {
put("totalPriceStatus", PaymentConstants.TOTAL_PRICE_STATUS_FINAL)
put(
"totalPriceStatus",
if (hasPendingItems)
PaymentConstants.TOTAL_PRICE_STATUS_ESTIMATED
else
PaymentConstants.TOTAL_PRICE_STATUS_FINAL
)
put("totalPrice", formatPrice(totalAmount))
put("totalPriceLabel", PaymentConstants.TOTAL_PRICE_LABEL_DEFAULT)
put("currencyCode", request.currencyCode)
Expand All @@ -160,14 +175,15 @@ object GooglePayRequestBuilder {
items.forEach { item ->
put(JSONObject().apply {
put("label", item.label)
put("type", PaymentConstants.LINE_ITEM_TYPE)
put("price", formatPrice(item.amount))
put(
"type",
if (item.type == PaymentItemType.FINAL)
PaymentConstants.LINE_ITEM_TYPE
else
PaymentConstants.PENDING_TYPE
"status",
if (item.type == PaymentItemType.PENDING)
PaymentConstants.DISPLAY_ITEM_STATUS_PENDING
else
PaymentConstants.DISPLAY_ITEM_STATUS_FINAL
)
put("price", formatPrice(item.amount))
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class HybridPaymentHandler : HybridPaymentHandlerSpec(), ActivityEventListener {

val paymentDataRequest = GooglePayRequestBuilder.createPaymentDataRequest(
request,
environment
environment,
fallbackMerchantName = appLabel()
)

logPaymentRequest(paymentDataRequest, environment)
Expand Down Expand Up @@ -147,6 +148,15 @@ class HybridPaymentHandler : HybridPaymentHandlerSpec(), ActivityEventListener {
return Wallet.getPaymentsClient(reactContext, walletOptions)
}

private fun appLabel(): String? {
return try {
reactContext.applicationInfo.loadLabel(reactContext.packageManager).toString()
} catch (e: Exception) {
Log.w(PaymentConstants.TAG_PAYMENT_HANDLER, "Unable to read app label", e)
null
}
}

private fun determineEnvironment(envConfig: GooglePayEnvironment?): Int {
return when (envConfig) {
GooglePayEnvironment.PRODUCTION -> WalletConstants.ENVIRONMENT_PRODUCTION
Expand Down
Loading
Loading