[PR #7391] [CLOSED] Add support for Stripe Elements (Embedded) #24179

Closed
opened 2026-04-15 22:13:05 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/7391
Author: @Jumaron
Created: 1/15/2026
Status: Closed

Base: canaryHead: feat/stripe-elements-support


📝 Commits (10+)

  • 1010192 chore: replace unsafe any in adapter factory types (#6819)
  • 542616e chore(db): remove deprecated types (#6793)
  • 94592d0 chore(sso): use default import for zod/v4 (#6833)
  • 3176eca docs: uses latest getRequestHeaders() to grab headers from request in Auth Middleware in Tanstack Start (#6824)
  • 18e642e docs: update logo for Christmas (#6806)
  • 2f68a18 docs(polar): update client import (#6830)
  • a79ecc9 docs(convex): clean up documentation (#6829)
  • 91cea0d chore(api-key): pass options to plugin config (#6843)
  • 6e67356 docs: change role type from string to enum values (#6844)
  • c01b867 fix: respect IP headers in dev/test environments (#6854)

📊 Changes

362 files changed (+26275 additions, -10477 deletions)

View changed files

📝 .cspell/company-names.txt (+2 -1)
📝 .cspell/names.txt (+15 -1)
📝 .cspell/tech-terms.txt (+2 -0)
.github/workflows/auto-cherry-pick-to-main.yml (+0 -337)
.github/workflows/cherry-pick-to-main.yml (+0 -325)
.github/workflows/claude.yml (+53 -0)
📝 .github/workflows/e2e.yml (+1 -1)
📝 .gitignore (+2 -0)
CLAUDE.md (+75 -0)
📝 biome.json (+3 -1)
📝 demo/nextjs/app/(auth)/sign-in/_components/sign-in.tsx (+69 -82)
📝 demo/nextjs/app/api/auth/[...all]/route.ts (+7 -1)
📝 demo/nextjs/app/dashboard/_components/subscription-card.tsx (+52 -24)
📝 demo/nextjs/app/globals.css (+18 -0)
📝 demo/nextjs/app/layout.tsx (+5 -2)
📝 demo/nextjs/components/account-switch.tsx (+12 -7)
demo/nextjs/components/background-ripple-effect.tsx (+134 -0)
📝 demo/nextjs/components/forms/sign-in-form.tsx (+1 -1)
📝 demo/nextjs/components/forms/sign-up-form.tsx (+45 -39)
📝 demo/nextjs/components/subscription-tier.tsx (+1 -1)

...and 80 more files

📄 Description

PR Description

Summary

This Pull Request adds support for Stripe Embedded Checkout (ui_mode: 'embedded') to the @better-auth/stripe plugin.

This feature allows developers to mount the Stripe payment form directly within their application using Stripe.js, eliminating the need to redirect users to an external Stripe-hosted page. This results in a seamless, branded payment experience and higher conversion rates.

Changes

  • New Endpoint: POST /subscription/create-embedded-checkout
    • Generates a Stripe Checkout Session with ui_mode: 'embedded'.
    • Returns the clientSecret required to mount the form on the client.
    • Handles subscription upgrades, seat adjustments, and metadata inheritance.
  • New Endpoint: GET /subscription/checkout-status
    • Retrieves the status of a session (open, complete, expired) to verify payments on the return page without webhooks.
  • Client SDK Update:
    • Updated stripeClient definition to include type inference for the new routes.
    • Added proper Zod schema validation for the new endpoints.
  • Logic Updates:
    • Added support for passing {CHECKOUT_SESSION_ID} in return URLs (required for embedded flows).
    • Logic to reuse incomplete subscriptions to prevent ghost subscriptions during checkout abandonment.

Motivation

Previously, the plugin only supported the hosted UI mode, which forces a redirect. Embedded Checkout is the modern standard for Stripe integrations, allowing for:

  1. Better UX: The user never leaves the application.
  2. Higher Conversion: Fewer redirects reduce drop-off.
  3. Modern Security: Handles 3D Secure and SCA authentication natively within the iframe.

Example Usage

1. Client Side (Creating the Session)

import { authClient } from "@/lib/auth-client";
import { loadStripe } from "@stripe/stripe-js";

// 1. Get the Client Secret from Better Auth
const { data, error } = await authClient.subscription.createEmbeddedCheckout({
    plan: "pro",
    annual: true,
    // Stripe replaces {CHECKOUT_SESSION_ID} automatically
    returnUrl: `${window.location.origin}/return?session_id={CHECKOUT_SESSION_ID}`,
    metadata: {
        customKey: "customValue"
    }
});

if (data) {
    // 2. Mount Stripe
    const stripe = await loadStripe("pk_test_...");
    const checkout = await stripe.initEmbeddedCheckout({
        clientSecret: data.clientSecret,
    });
    checkout.mount("#checkout");
}

2. Client Side (Return Page Verification)

const sessionId = searchParams.get("session_id");

const { data } = await authClient.subscription.getCheckoutStatus({
    query: { sessionId }
});

if (data?.status === 'complete') {
    console.log("Payment successful for:", data.customerEmail);
}

Checklist

  • Added new routes to stripeClient type definition.
  • Implemented Zod schemas for input validation.
  • Tested with user and organization customer types.
  • Verified trial logic and duplicate subscription prevention.
  • Verified returnUrl behavior.

Closes Nothing, this is personal.

Disclosure: Opus was cooking.


Summary by cubic

Adds Embedded Checkout support to the @better-auth/stripe plugin, enabling Stripe Elements to render in‑app. Introduces new endpoints and client types for session creation and payment verification.

  • New Features

    • POST /subscription/create-embedded-checkout: creates a session with ui_mode: "embedded", returns clientSecret; supports upgrades, seat changes, metadata; reuses incomplete subscriptions; supports {CHECKOUT_SESSION_ID} in return URLs.
    • GET /subscription/checkout-status: returns session status (open, complete, expired) to confirm payments without webhooks.
    • Client SDK: typed routes and Zod validation for the new endpoints.
  • Migration

    • Create a session: authClient.subscription.createEmbeddedCheckout({ plan, returnUrl: "/return?session_id={CHECKOUT_SESSION_ID}", ... }).
    • Mount in the client: const checkout = await stripe.initEmbeddedCheckout({ clientSecret }); checkout.mount("#checkout").
    • Verify on return: read session_id and call authClient.subscription.getCheckoutStatus({ query: { sessionId } }).

Written for commit 2d9c3e1495. Summary will update on new commits.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/7391 **Author:** [@Jumaron](https://github.com/Jumaron) **Created:** 1/15/2026 **Status:** ❌ Closed **Base:** `canary` ← **Head:** `feat/stripe-elements-support` --- ### 📝 Commits (10+) - [`1010192`](https://github.com/better-auth/better-auth/commit/1010192e4a7106bfccca02c5fda312adc5ac0593) chore: replace unsafe `any` in adapter factory types (#6819) - [`542616e`](https://github.com/better-auth/better-auth/commit/542616e77b57d5d9a0eac04a9bfaf4e1af1b3061) chore(db): remove deprecated types (#6793) - [`94592d0`](https://github.com/better-auth/better-auth/commit/94592d0a6789a6a66284bc0e73c56860e502a31d) chore(sso): use default import for zod/v4 (#6833) - [`3176eca`](https://github.com/better-auth/better-auth/commit/3176ecabeea0751751cbb52269a0bd5be4344b00) docs: uses latest `getRequestHeaders()` to grab headers from request in Auth Middleware in Tanstack Start (#6824) - [`18e642e`](https://github.com/better-auth/better-auth/commit/18e642ea1df339f12f90fbc8c61c426daed85426) docs: update logo for Christmas (#6806) - [`2f68a18`](https://github.com/better-auth/better-auth/commit/2f68a18cc787f8e1ea0a5a1db3af7238e717c177) docs(polar): update client import (#6830) - [`a79ecc9`](https://github.com/better-auth/better-auth/commit/a79ecc99b2be2ae15ed897919630c7452b87ac9b) docs(convex): clean up documentation (#6829) - [`91cea0d`](https://github.com/better-auth/better-auth/commit/91cea0d0a79e6009eccda62e43c39cb44ef59625) chore(api-key): pass options to plugin config (#6843) - [`6e67356`](https://github.com/better-auth/better-auth/commit/6e6735635519e6900e981b5d7857e8e717d46104) docs: change role type from string to enum values (#6844) - [`c01b867`](https://github.com/better-auth/better-auth/commit/c01b867bd4caa4b538c3d164a3acbe4a52cfd141) fix: respect IP headers in dev/test environments (#6854) ### 📊 Changes **362 files changed** (+26275 additions, -10477 deletions) <details> <summary>View changed files</summary> 📝 `.cspell/company-names.txt` (+2 -1) 📝 `.cspell/names.txt` (+15 -1) 📝 `.cspell/tech-terms.txt` (+2 -0) ➖ `.github/workflows/auto-cherry-pick-to-main.yml` (+0 -337) ➖ `.github/workflows/cherry-pick-to-main.yml` (+0 -325) ➕ `.github/workflows/claude.yml` (+53 -0) 📝 `.github/workflows/e2e.yml` (+1 -1) 📝 `.gitignore` (+2 -0) ➕ `CLAUDE.md` (+75 -0) 📝 `biome.json` (+3 -1) 📝 `demo/nextjs/app/(auth)/sign-in/_components/sign-in.tsx` (+69 -82) 📝 `demo/nextjs/app/api/auth/[...all]/route.ts` (+7 -1) 📝 `demo/nextjs/app/dashboard/_components/subscription-card.tsx` (+52 -24) 📝 `demo/nextjs/app/globals.css` (+18 -0) 📝 `demo/nextjs/app/layout.tsx` (+5 -2) 📝 `demo/nextjs/components/account-switch.tsx` (+12 -7) ➕ `demo/nextjs/components/background-ripple-effect.tsx` (+134 -0) 📝 `demo/nextjs/components/forms/sign-in-form.tsx` (+1 -1) 📝 `demo/nextjs/components/forms/sign-up-form.tsx` (+45 -39) 📝 `demo/nextjs/components/subscription-tier.tsx` (+1 -1) _...and 80 more files_ </details> ### 📄 Description ### PR Description ## Summary This Pull Request adds support for **Stripe Embedded Checkout** (`ui_mode: 'embedded'`) to the `@better-auth/stripe` plugin. This feature allows developers to mount the Stripe payment form directly within their application using Stripe.js, eliminating the need to redirect users to an external Stripe-hosted page. This results in a seamless, branded payment experience and higher conversion rates. ## Changes - **New Endpoint:** `POST /subscription/create-embedded-checkout` - Generates a Stripe Checkout Session with `ui_mode: 'embedded'`. - Returns the `clientSecret` required to mount the form on the client. - Handles subscription upgrades, seat adjustments, and metadata inheritance. - **New Endpoint:** `GET /subscription/checkout-status` - Retrieves the status of a session (open, complete, expired) to verify payments on the return page without webhooks. - **Client SDK Update:** - Updated `stripeClient` definition to include type inference for the new routes. - Added proper Zod schema validation for the new endpoints. - **Logic Updates:** - Added support for passing `{CHECKOUT_SESSION_ID}` in return URLs (required for embedded flows). - Logic to reuse `incomplete` subscriptions to prevent ghost subscriptions during checkout abandonment. ## Motivation Previously, the plugin only supported the `hosted` UI mode, which forces a redirect. Embedded Checkout is the modern standard for Stripe integrations, allowing for: 1. **Better UX:** The user never leaves the application. 2. **Higher Conversion:** Fewer redirects reduce drop-off. 3. **Modern Security:** Handles 3D Secure and SCA authentication natively within the iframe. ## Example Usage ### 1. Client Side (Creating the Session) ```typescript import { authClient } from "@/lib/auth-client"; import { loadStripe } from "@stripe/stripe-js"; // 1. Get the Client Secret from Better Auth const { data, error } = await authClient.subscription.createEmbeddedCheckout({ plan: "pro", annual: true, // Stripe replaces {CHECKOUT_SESSION_ID} automatically returnUrl: `${window.location.origin}/return?session_id={CHECKOUT_SESSION_ID}`, metadata: { customKey: "customValue" } }); if (data) { // 2. Mount Stripe const stripe = await loadStripe("pk_test_..."); const checkout = await stripe.initEmbeddedCheckout({ clientSecret: data.clientSecret, }); checkout.mount("#checkout"); } ``` ### 2. Client Side (Return Page Verification) ```typescript const sessionId = searchParams.get("session_id"); const { data } = await authClient.subscription.getCheckoutStatus({ query: { sessionId } }); if (data?.status === 'complete') { console.log("Payment successful for:", data.customerEmail); } ``` ## Checklist - [x] Added new routes to `stripeClient` type definition. - [x] Implemented Zod schemas for input validation. - [x] Tested with `user` and `organization` customer types. - [x] Verified trial logic and duplicate subscription prevention. - [x] Verified `returnUrl` behavior. --- Closes Nothing, this is personal. Disclosure: Opus was cooking. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds Embedded Checkout support to the @better-auth/stripe plugin, enabling Stripe Elements to render in‑app. Introduces new endpoints and client types for session creation and payment verification. - **New Features** - POST /subscription/create-embedded-checkout: creates a session with ui_mode: "embedded", returns clientSecret; supports upgrades, seat changes, metadata; reuses incomplete subscriptions; supports {CHECKOUT_SESSION_ID} in return URLs. - GET /subscription/checkout-status: returns session status (open, complete, expired) to confirm payments without webhooks. - Client SDK: typed routes and Zod validation for the new endpoints. - **Migration** - Create a session: authClient.subscription.createEmbeddedCheckout({ plan, returnUrl: "/return?session_id={CHECKOUT_SESSION_ID}", ... }). - Mount in the client: const checkout = await stripe.initEmbeddedCheckout({ clientSecret }); checkout.mount("#checkout"). - Verify on return: read session_id and call authClient.subscription.getCheckoutStatus({ query: { sessionId } }). <sup>Written for commit 2d9c3e1495ecd8c6b8c2bee9303da6672f05cb60. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-15 22:13:05 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#24179