diff --git a/.changeset/stripe-checkout-payment-mode.md b/.changeset/stripe-checkout-payment-mode.md new file mode 100644 index 0000000000..1e85328efe --- /dev/null +++ b/.changeset/stripe-checkout-payment-mode.md @@ -0,0 +1,5 @@ +--- +"@better-auth/stripe": patch +--- + +Stop logging a "Stripe webhook failed" error when a one-time `payment` mode checkout session completes. diff --git a/packages/stripe/src/hooks.ts b/packages/stripe/src/hooks.ts index ea12c40288..8fddca33ed 100644 --- a/packages/stripe/src/hooks.ts +++ b/packages/stripe/src/hooks.ts @@ -46,7 +46,14 @@ export async function onCheckoutSessionCompleted( try { const client = options.stripeClient; const checkoutSession = event.data.object as Stripe.Checkout.Session; - if (checkoutSession.mode === "setup" || !options.subscription?.enabled) { + /** + * Only `subscription` mode is currently supported. + * @see https://docs.stripe.com/api/checkout/sessions/object#checkout_session_object-mode + */ + if ( + checkoutSession.mode !== "subscription" || + !options.subscription?.enabled + ) { return; } const subscription = await client.subscriptions.retrieve( diff --git a/packages/stripe/test/webhook.test.ts b/packages/stripe/test/webhook.test.ts index 96ae937671..f5f5b564ae 100644 --- a/packages/stripe/test/webhook.test.ts +++ b/packages/stripe/test/webhook.test.ts @@ -2714,4 +2714,70 @@ describe("stripe webhook", () => { expect(updatedSub!.endedAt!.getTime()).toBe(now * 1000); }); }); + + /** + * @see https://github.com/better-auth/better-auth/issues/4565 + */ + describe("webhook: non-subscription checkout session modes", () => { + test.for([ + "payment", + "setup", + ] as const)("mode: %s does not call subscriptions.retrieve or onSubscriptionComplete", async (mode, { + memory, + stripeOptions, + }) => { + const onSubscriptionComplete = vi.fn(); + const subscriptionsRetrieve = vi.fn(); + + const webhookEvent = { + type: "checkout.session.completed", + data: { + object: { + id: `cs_4565_${mode}`, + mode, + subscription: null, + customer: `cus_4565_${mode}`, + metadata: {}, + }, + }, + }; + + const stripeForTest = { + ...stripeOptions.stripeClient, + subscriptions: { + ...stripeOptions.stripeClient.subscriptions, + retrieve: subscriptionsRetrieve, + }, + webhooks: { + constructEventAsync: vi.fn().mockResolvedValue(webhookEvent), + }, + }; + + const testOptions = { + ...stripeOptions, + stripeClient: stripeForTest as unknown as Stripe, + subscription: { + ...stripeOptions.subscription, + onSubscriptionComplete, + }, + } satisfies StripeOptions; + + const { auth: webhookAuth } = await getTestInstance( + { database: memory, plugins: [stripe(testOptions)] }, + { disableTestUser: true }, + ); + + const response = await webhookAuth.handler( + new Request("http://localhost:3000/api/auth/stripe/webhook", { + method: "POST", + headers: { "stripe-signature": "test_signature" }, + body: JSON.stringify(webhookEvent), + }), + ); + + expect(response.status).toBe(200); + expect(subscriptionsRetrieve).not.toHaveBeenCalled(); + expect(onSubscriptionComplete).not.toHaveBeenCalled(); + }); + }); });