Sign in →
Embed Widgets1 min read

AforoPaymentMethod

Display and manage the customer's stored payment methods. Lists methods, sets a default, removes a method, and surfaces a SetupIntent client secret as an event so the parent page wires its own Stripe / Razorpay / PayPal SDK to confirm new methods.

Updated 2026-06-15Suggest edits
Docs Embed Widgets PaymentMethod
Live preview — synthetic data
AforoPaymentMethod
Display + update the saved card via embedded provider iframes.
Loads the widget from https://embed-demo.aforo.ai with synthetic data.

What it renders#

A card (or list) of the customer's stored payment methods with brand, last-4 digits, expiry, holder name, and a Default badge. In expanded mode, each non-default row carries a "Set default" button; each row except the last carries a "Remove" button. A primary CTA ("Update" in compact, "+ Add" in expanded) kicks off the Add/Update flow.

  • List: fetched from GET /api/v1/portal/embed/payment-methods via the bridge-token session JWT (the customer id is derived from the JWT, never from the request).
  • Default badge: blue pill with text alternative (color is supplementary per FR-WIDGET-X-2). Screen-reader announces "Default payment method".
  • Set default: optimistic UI update + POST to {id}/set-default.
  • Remove: window.confirm gate (a destructive action requires confirmation) + DELETE call. The last remaining method cannot be removed — the customer must Add a replacement first.
  • Empty state: when the customer has no methods, a centered "No payment method on file" affordance + "+ Add method" CTA.
INFO
PaymentMethod ships with the event-based Add/Update flow only in v1.0.0. Clicking "Add" / "Update" calls POST /payment-methods/setup-intent and emits an aforo.payment-method.update_requested event with the SetupIntent clientSecret. The parent page wires its existing Stripe Elements / Razorpay Drop-in / PayPal Smart Buttons to confirm. Inline provider iframes are deferred to Phase 1.
From Operator Portal → Settings.
From Operator Portal → Embed Studio → Keys.
Minted by your backend per customer session — see Authentication.
Script tag
<div data-aforo-widget="payment-method"
     data-aforo-tenant-slug="your-tenant"
     data-aforo-embed-key="ek_live_replace_me"
     data-aforo-bridge-token="bridge.replace.me"
     data-aforo-mode="expanded"></div>

<script src="https://cdn.aforo.ai/embed/loader.js" async></script>
NPM / React
import { AforoPaymentMethod } from '@aforoai/storefront-widgets';

export function Pricing() {
  return (
    <AforoPaymentMethod
      tenantSlug="your-tenant"
      embedKey="ek_live_replace_me"
  bridgeToken="bridge.replace.me"
  mode="expanded"
    />
  );
}
Preview with my account
Fill in Tenant slug above to enable this preview.

Mount examples#

app/account/billing/page.tsx
import { AforoPaymentMethod } from '@aforoai/storefront-widgets';

export default async function BillingPage() {
  const session = await getSession();
  const bridgeToken = await mintBridgeToken({
    externalId: session.customerId,
    email: session.email,
  });

  return (
    <AforoPaymentMethod
      tenantSlug="acme"
      embedKey="embk_live_…"
      bridgeToken={bridgeToken}
      mode="compact"
      onUpdateRequested={(clientSecret, provider) => {
        // Wire your existing Stripe.js / Razorpay / PayPal SDK here.
        // Confirm the SetupIntent against the provider, then signal
        // back to the widget so it re-fetches the list.
        confirmSetupIntent(clientSecret, provider);
      }}
    />
  );
}

Props reference#

  • tenantSlug — required. Your Aforo tenant slug.
  • embedKey — required. Embed key minted in the Embed Studio.
  • bridgeToken — required. Short-lived customer-session bridge token your backend mints per page-view.
  • mode 'compact' (default) shows only the default method; 'expanded' shows the full list with per-row actions.
  • theme 'light' 'dark' 'auto' (default, honors prefers-color-scheme).
  • themeOverrides — partial token overrides. See Theming.
  • onUpdateRequested(clientSecret, provider) — callback fired alongside the postMessage event when the customer clicks "Add" or "Update".
  • onError(err) — optional error callback.

Compact vs expanded mode#

Compact shows a single card with the customer's default method and a single "Update" CTA. Sized for a sidebar or modal. Use this when payment management is one of many sub-tasks on the page.

Expanded shows the full list with per-row "Set default" + "Remove" actions and a header-level "+ Add" CTA. Use this on a dedicated "Payment methods" page where managing multiple cards is the primary task.

The Add/Update flow#

Because every provider has different iframe + 3DS semantics, v1.0.0 keeps the SDK provider-agnostic. The flow is:

  1. Customer clicks "Update" (compact) or "+ Add" (expanded).
  2. Widget posts POST /payment-methods/setup-intent with an idempotency key. The backend mints a provider-side SetupIntent.
  3. Widget emits aforo.payment-method.update_requested with { clientSecret, provider }.
  4. Parent page wires its existing Stripe Elements / Razorpay Drop-in / PayPal Smart Buttons to confirm the intent. The parent runs 3DS / SCA challenges as needed.
  5. On successful confirmation, parent page posts aforo:payment-method-attached back to the widget. The widget refetches the list and emits an advisory aforo.payment-method.updated event.
parent.ts
// Parent-side: confirm the SetupIntent then signal the widget.
window.addEventListener('message', async (ev) => {
  if (ev.data?.type !== 'aforo.payment-method.update_requested') return;
  const { clientSecret, provider } = ev.data.payload;

  if (provider === 'stripe') {
    const stripe = await loadStripe('pk_live_…');
    const { error } = await stripe.confirmSetup({
      clientSecret,
      confirmParams: { return_url: window.location.href },
    });
    if (!error) {
      // Tell the widget so it re-renders the new method as default.
      ev.source?.postMessage(
        { type: 'aforo:payment-method-attached', payload: {}, version: '1' },
        { targetOrigin: ev.origin },
      );
    }
  }
});
WARNING
Webhook is authoritative (FR-SEC-23). The aforo.payment-method.updated event is advisory and meant for UI affordance only. The truth that the method capture succeeded comes from your provider's webhook reaching Aforo. Do not trigger downstream business actions (e.g. unblocking subscriptions, retrying invoices) from this client event.

Events#

Outbound (widget → parent), all carry version: '1':

  • aforo.payment-method.ready — fires once on first successful list fetch.
  • aforo.payment-method.update_requested — customer clicked Add/Update, SetupIntent created.
  • aforo.payment-method.method_set_default — customer changed default.
  • aforo.payment-method.method_removed — customer removed a method.
  • aforo.payment-method.updated — advisory notification after a parent-confirmed Add (see warning above).
  • aforo.payment-method.error — any read or mutation failure with a typed code.

Inbound (parent → widget):

  • aforo:payment-method-attached — parent confirmed a provider-side capture. Widget refetches the list.

Limitations (v1.0.0)#

  • Provider iframes are not mounted inline. Add/Update emits an event; the parent page owns the provider SDK. Inline provider iframes land in Phase 1.
  • Last-method protection. The expanded-mode Remove button is disabled when only one method remains. Customers must Add a replacement first. This is deliberate to avoid leaving the customer with no way to pay.
  • No SCA / 3DS UI in the SDK. 3DS challenge UI is rendered by the parent's provider SDK on its own iframe / new window. The widget does not intercept the challenge.
  • One-shot ownership check. Set-default and Remove validate ownership by re-listing the customer's methods. For tenants with very many methods, a dedicated /owner endpoint would be more efficient. v1.0.0 keeps the simpler shape.