Google Pay™
Accept Google Pay™ on the web. bahn.mountGooglePay() renders the official Google Pay button and handles payment authorization — you mount it, listen for a few events, and confirm the payment.
Requirements
Google Pay has no domain-registration step. Cake configures Google Pay for your merchant; if it isn't configured, the button emits an error event with "Google Pay is not configured for this merchant". The rest is about the shopper's browser:
| Requirement | Notes |
|---|---|
| A browser Google Pay supports | Chrome, Safari, Firefox, Edge, Opera or UC Browser, on desktop and mobile — see Browser support. |
| A card saved to Google Pay | The shopper needs at least one card saved to their Google account to complete a payment. |
Google Pay policies
Merchants must adhere to the Google Pay and Wallet API's Acceptable Use Policy and accept the terms defined in the Google Pay API Terms of Service.
Quick start
const bahn = new Bahn({ pk: "pk_live_...", environment: "sandbox" });
const gp = bahn.mountGooglePay("#google-pay-container", clientSecret, {
amount: 100000, // smallest currency unit — VND is not divided by 100
currency: "VND",
country: "VN",
});
gp.on("ready", () => console.log("Google Pay button rendered"));
gp.on("success", (paymentIntent) => (window.location.href = "/success"));
gp.on("error", (err) => console.error((err as { message: string }).message));
gp.on("cancel", () => console.log("Shopper dismissed the sheet"));mountGooglePay() mounts the button at the CSS selector you give it, sized to fit the container, and manages everything else internally — checking whether Google Pay can be used, rendering the button, and running the full payment sheet. If Google Pay isn't available on the shopper's browser, no button renders and an "error" event fires instead — use that to fall back to another payment method.
mountGooglePay(selector, clientSecret, options) parameters
| Param | Type | Required | Description |
|---|---|---|---|
selector | string | Yes | CSS selector for the mount container |
clientSecret | string | undefined | Yes (pass undefined for the token-based flow) | Client secret of the payment intent to confirm — see Confirming the payment |
options.amount | number | Yes | Transaction amount, in the smallest currency unit |
options.currency | string | Yes | ISO 4217 currency code (e.g. "VND", "USD") |
options.country | string | Yes | ISO 3166-1 alpha-2 country code (e.g. "VN", "US") |
options.priceLabel | string | No | Label shown as the transaction total in the sheet |
options.lineItems | Array<{ label, amount }> | No | Itemized breakdown shown in the sheet |
options.merchantName | string | No | Merchant name displayed in the sheet |
options.type | GooglePayButtonType | No | Button label variant (default "pay") |
options.color | GooglePayButtonColor | No | "black" | "white" (default "black") |
options.locale | string | No | Button locale (default "en") |
options.size | { width?, height? } | No | Button dimensions (default 100% × 48px) |
options.allowedCardNetworks | GooglePayCardNetwork[] | No | Allowed card networks (default: amex, discover, interac, jcb, mastercard, visa) |
options.allowedAuthMethods | GooglePayAuthMethod[] | No | Allowed card authentication methods (default: PAN_ONLY, CRYPTOGRAM_3DS) |
options.requestBillingAddress | boolean | No | Request a billing address |
options.requestPayerDetails | ("email" | "phone")[] | No | Request payer contact details |
options.requestShipping | boolean | No | Collect a shipping address — see Collecting a shipping address |
options.shippingOptions | GooglePayShippingOption[] | No | Initial shipping options shown before an address is entered |
Returns: IGooglePayButtonComponent — .on(event, handler) (see Events), .unmount(). bahn.cleanup()/bahn.unmountGooglePay() also unmount it.
Confirming the payment
mountGooglePay() supports two ways to confirm the payment, chosen by whether you pass a clientSecret — same split as Apple Pay and card.
Direct confirmation
Pass the client_secret of a payment intent you created server-side. The SDK confirms the payment itself and reports the outcome:
const gp = bahn.mountGooglePay("#google-pay-container", clientSecret, {
amount: 100000,
currency: "VND",
country: "VN",
});
gp.on("success", (paymentIntent) => (window.location.href = "/success"));
gp.on("error", (err) => console.error((err as { message: string }).message));If the payment intent requires additional authentication, mountGooglePay() calls bahn.mount3DS() automatically to render the challenge, then reports "success"/"error" once it resolves.
Token-based flow
Omit clientSecret to confirm the payment through your own backend instead — call mountGooglePay(selector, undefined, options):
const gp = bahn.mountGooglePay("#google-pay-container", undefined, {
amount: 100000,
currency: "VND",
country: "VN",
merchantName: "Your Merchant Name",
});
gp.on("token", async ({ confirmation_token }) => {
// Send confirmation_token to your own backend, which confirms the
// PaymentIntent using confirmation_token and your secret key, and
// returns a client_secret.
const { client_secret } = await fetch("/api/confirm-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirmation_token, amount: 100000, currency: "vnd" }),
}).then((r) => r.json());
// Keep the Google Pay button mounted until this resolves — handleNextAction()
// needs it to check the payment intent's status.
await bahn.handleNextAction(client_secret, {
onSuccess: () => (window.location.href = "/success"),
onFailure: () => (window.location.href = "/failed"),
onTimeout: () => (window.location.href = "/processing"),
});
});
gp.on("error", (err) => console.error((err as { message: string }).message));
gp.on("cancel", () => console.log("Shopper dismissed the sheet"));In this flow, confirm the payment on your backend and surface the final result through your own redirect or UI, not through the Google Pay button; a "success" event is not emitted for this flow.
Collecting a shipping address
Set requestShipping: true and provide shipping options as the shopper enters an address:
const gp = bahn.mountGooglePay("#google-pay-container", clientSecret, {
amount: 100000,
currency: "VND",
country: "VN",
requestShipping: true,
// Shown before the shopper has entered an address. Omit if your rates
// always depend on the address — supply them from "shippingaddresschanged" instead.
shippingOptions: [{ id: "standard", label: "Standard", description: "3-5 days", amount: 0 }],
});
gp.on("shippingaddresschanged", (event) => {
const { address, resolve, reject } = event;
if (address.countryCode !== "VN") {
reject(); // shows an inline "can't ship here" error in the sheet
return;
}
resolve({
shippingOptions: [
{ id: "standard", label: "Standard", description: "3-5 days", amount: 0 },
{ id: "express", label: "Express", description: "1-2 days", amount: 20000 },
],
});
});
gp.on("shippingoptionchanged", (event) => {
const { shippingOption, resolve } = event;
// Recompute the total to include the selected shipping cost.
resolve({ amount: 100000 + shippingOption.amount });
});
gp.on("token", async ({ confirmation_token }) => { /* same as the token-based flow above */ });The sheet pauses after each selection until you call resolve() (with updated shipping options/total) or reject() (shows an inline error). The address passed to "shippingaddresschanged" is deliberately coarse — locality, administrative area, postal code, and country only, no street address — which is standard behavior for this kind of address collection; the full address becomes available once the payment is authorized. Every amount field here follows the same smallest-currency-unit convention as options.amount.
Checking availability before mounting
mountGooglePay() already checks availability internally and simply won't render the button if Google Pay can't be used. Call bahn.isGooglePayAvailable() yourself only if you want to control your page's layout before that — for example, hiding an entire "Or pay with Google Pay" section (button and any surrounding UI), rather than reacting to an "error" event after a container is already showing.
if (!(await bahn.isGooglePayAvailable())) {
// hide the entry point — see Browser support below for what this covers
}Events
.on(event, handler) on the object mountGooglePay() returns:
| Event | Fires when | Handler receives |
|---|---|---|
"ready" | The button has rendered and can be tapped | — |
"success" | Payment confirmed (and any 3D Secure challenge passed) | The payment_intent — direct confirmation only |
"token" | A confirmation token was created | { confirmation_token } — token-based flow only |
"error" | Authorization or confirmation failed | { message } |
"cancel" | The shopper dismissed the sheet without paying | — |
"shippingaddresschanged" | The shopper entered or changed a shipping address | { address, resolve, reject } — requires requestShipping: true |
"shippingoptionchanged" | The shopper chose a different shipping option | { shippingOption, resolve, reject } — requires requestShipping: true |
Browser support
Google decides which browsers can use Google Pay. The SDK doesn't check the browser itself. bahn.isGooglePayAvailable() loads Google's pay.js into your page, and mountGooglePay() loads it inside the Bahn iframe. Both ask Google's isReadyToPay().
Google lists these browsers as supported, on desktop and mobile:
| Browser | Support |
|---|---|
| Chrome, Edge, Opera | Supported |
| Safari (macOS, iOS, iPadOS) | Supported |
| Firefox | Supported |
| UC Browser | Supported |
On browsers other than Chrome, Google may open the payment sheet in a popup window where the shopper signs in to their Google account. A popup blocker, or a page that opens the button inside another popup, can stop this from working.
bahn.isGooglePayAvailable() resolves false when pay.js fails to load within 10 seconds, when isReadyToPay() returns false, or on any error.
Testing
Use a Google account with a card added to Google Pay, pointed at your sandbox environment. Test in Chrome, and in Safari or Firefox as well, because they show the payment sheet differently. Google Pay's test environment accepts the same test cards as your sandbox Bahn account.