DEV Community

Cover image for One checkout button that renders Apple Pay, Google Pay, or a popup, depending on the browser
Doğukan Karakaş
Doğukan Karakaş

Posted on • Originally published at whop.com

One checkout button that renders Apple Pay, Google Pay, or a popup, depending on the browser

We just shipped a guide on Whop's express checkout button. It is one component that detects the visitor's browser and renders the fastest payment method available: Apple Pay on Safari, Google Pay on Chrome and Android, and the Whop Pay popup everywhere else. You never build a checkout page, and card data never touches your code.

Live demo | GitHub

There are two integration paths: a React component for Next.js apps, and a plain web component for everything else (Webflow, Framer, WordPress, static HTML). The guide walks both, plus the server side verification that makes the whole thing safe. Here is the shape of it.

Setup

npm install @whop/sdk @whop/checkout iron-session zod
Enter fullscreen mode Exit fullscreen mode

Four env vars: WHOP_COMPANY_API_KEY, WHOP_SANDBOX, SESSION_SECRET (32+ chars, openssl rand -base64 32), and APP_URL. The guide validates all of them with a Zod schema at first call so a missing value fails loudly instead of surfacing as a mystery 401 later.

Products and plans come from the Whop CLI:

whop products create --title "Pro pass" --description "..." --visibility visible
whop plans create --product_id prod_XXXXXXXXX --plan_type one_time --initial_price 19.99 --visibility visible
Enter fullscreen mode Exit fullscreen mode

The plan_... ID is what the button consumes.

The React path

import { WhopExpressCheckoutButton } from "@whop/checkout/react";
Enter fullscreen mode Exit fullscreen mode

The component wraps the button with a small verification loop. When the buyer pays, onComplete hands you a receipt ID, and the component polls a server route (up to 10 attempts, 2 second intervals) until the payment settles.

The server route is the part worth internalizing. It never trusts anything the browser sends. It takes the receipt ID, retrieves the payment from Whop directly, confirms the payment exists, confirms it bought the right product, and checks the status: pending or open returns a 202 (the client keeps polling), paid writes the confirmation into an encrypted iron-session cookie, anything else fails with a 403. The receipt ID that crosses from browser to server is worthless until Whop confirms what it paid for.

The web component path

For sites that are not React apps, the same button ships as a web component:

<script async defer src="https://js.whop.com/static/checkout/loader.js"></script>

<whop-express-checkout-button
  id="buy-button"
  plan-id="plan_XXXXXXXXX"
  return-url="https://yoursite.com/checkout/complete"
  environment="sandbox"
  skip-redirect="true"
  theme="light"
  theme-accent-color="orange"
></whop-express-checkout-button>
Enter fullscreen mode Exit fullscreen mode

The component emits three JavaScript events: express-method-resolved, complete, and payment-error. skip-redirect="true" mirrors the React onComplete behavior, so a Webflow or WordPress page can run the same verify-then-unlock logic with a small script.

The return URL flow

Some payment paths redirect the browser (bank approval steps, or a flow without onComplete). The button takes a returnUrl, and the return page receives three query parameters: status, payment_id, and state_id. The guide's return page verifies the payment server side (same route as the polling path) before showing any confirmation. And for buyers who close the browser mid redirect, registering the payment.succeeded webhook makes sure they still get what they paid for.

Gotchas the guide covers

  • Wallet buttons never render on sandbox. On sandbox you always get the Whop Pay button. Apple Pay and Google Pay appear only in production, so the real wallet test happens after the switch.
  • baseURL, capital URL. TypeScript silently accepts baseUrl as an unknown property, the SDK falls back to production, and every call 401s as if the key were bad.
  • Sandbox IDs do not carry to production. Recreate the product and plan with production keys and swap the IDs.
  • API key scopes. The key needs payment:basic:read, access_pass:basic:read, plan:basic:read, access_pass:create, and plan:create.
  • CSP. If your app sends Content-Security-Policy headers: https://*.whop.com in frame and connect sources, https://js.whop.com in script sources.

Going live

The production checklist: recreate products and plans on whop.com, create a production API key with the same scopes, remove WHOP_SANDBOX, fresh SESSION_SECRET, update APP_URL, register the payment.succeeded webhook, then test on Safari with Apple Pay and Chrome with Google Pay, since sandbox never showed you the wallets.

Links

If your product has a "buy" moment anywhere in it, this is the shortest path from that moment to a settled payment: one button, browser aware, verified server side.

Top comments (1)

Collapse
 
mihirkanzariya profile image
Mihir kanzariya

the browser-detection framing is the part i would sanity check before shipping this. apple pay availability is not really a property of safari, it is a property of the user having a card provisioned in wallet, which is why canMakePayments and canMakePaymentsWithActiveCard return different answers and plenty of safari users fail the second one. if the component keys off the user agent rather than the availability callback, those users get a wallet button that does nothing, and that converts worse than the popup they would otherwise have been shown.

related, and it tends to bite at launch rather than in dev: apple pay domain registration is per domain, so staging and production each need their own. your note about wallet buttons not rendering in sandbox reads like the same problem wearing a different hat.