Skip to content

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:

RequirementNotes
A browser Google Pay supportsChrome, Safari, Firefox, Edge, Opera or UC Browser, on desktop and mobile — see Browser support.
A card saved to Google PayThe 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 ​

typescript
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 ​

ParamTypeRequiredDescription
selectorstringYesCSS selector for the mount container
clientSecretstring | undefinedYes (pass undefined for the token-based flow)Client secret of the payment intent to confirm — see Confirming the payment
options.amountnumberYesTransaction amount, in the smallest currency unit
options.currencystringYesISO 4217 currency code (e.g. "VND", "USD")
options.countrystringYesISO 3166-1 alpha-2 country code (e.g. "VN", "US")
options.priceLabelstringNoLabel shown as the transaction total in the sheet
options.lineItemsArray<{ label, amount }>NoItemized breakdown shown in the sheet
options.merchantNamestringNoMerchant name displayed in the sheet
options.typeGooglePayButtonTypeNoButton label variant (default "pay")
options.colorGooglePayButtonColorNo"black" | "white" (default "black")
options.localestringNoButton locale (default "en")
options.size{ width?, height? }NoButton dimensions (default 100% × 48px)
options.allowedCardNetworksGooglePayCardNetwork[]NoAllowed card networks (default: amex, discover, interac, jcb, mastercard, visa)
options.allowedAuthMethodsGooglePayAuthMethod[]NoAllowed card authentication methods (default: PAN_ONLY, CRYPTOGRAM_3DS)
options.requestBillingAddressbooleanNoRequest a billing address
options.requestPayerDetails("email" | "phone")[]NoRequest payer contact details
options.requestShippingbooleanNoCollect a shipping address — see Collecting a shipping address
options.shippingOptionsGooglePayShippingOption[]NoInitial 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:

typescript
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):

typescript
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:

typescript
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.

typescript
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:

EventFires whenHandler 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:

BrowserSupport
Chrome, Edge, OperaSupported
Safari (macOS, iOS, iPadOS)Supported
FirefoxSupported
UC BrowserSupported

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.

Developers