From 4d706078893f71037c87f6672b0bac9321ffc22d Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Wed, 23 Sep 2026 11:29:02 +0200 Subject: [PATCH] deps: stripe (#2756) --- apps/dashboard/package.json | 1 - .../data-table/billing/data-table.tsx | 4 +- apps/dashboard/src/lib/stripe.ts | 16 ------- apps/status-page/package.json | 1 - apps/web/package.json | 1 - apps/web/src/lib/stripe/client.ts | 14 ------ packages/api/src/router/stripe/index.ts | 5 ++- packages/api/src/router/stripe/shared.ts | 16 ++++++- .../api/src/router/stripe/webhook.test.ts | 13 ++++-- packages/api/src/router/stripe/webhook.ts | 11 ++--- pnpm-lock.yaml | 43 +++++++------------ pnpm-workspace.yaml | 3 +- scripts/check-doc-refs.mts | 18 +++++--- 13 files changed, 61 insertions(+), 85 deletions(-) delete mode 100644 apps/dashboard/src/lib/stripe.ts delete mode 100644 apps/web/src/lib/stripe/client.ts diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 865d1f54..0b2fb9b7 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -69,7 +69,6 @@ "@radix-ui/react-slot": "catalog:", "@radix-ui/react-tooltip": "catalog:", "@sentry/nextjs": "catalog:", - "@stripe/stripe-js": "catalog:", "@tanstack/react-query": "catalog:", "@tanstack/react-table": "catalog:", "@trpc/client": "catalog:", diff --git a/apps/dashboard/src/components/data-table/billing/data-table.tsx b/apps/dashboard/src/components/data-table/billing/data-table.tsx index 5381f06c..b33042ca 100644 --- a/apps/dashboard/src/components/data-table/billing/data-table.tsx +++ b/apps/dashboard/src/components/data-table/billing/data-table.tsx @@ -31,7 +31,6 @@ import { Fragment, useState, useTransition } from "react"; import { toast } from "sonner"; import { config as featureGroups, plans } from "@/data/plans"; -import { getStripe } from "@/lib/stripe"; import { useTRPC } from "@/lib/trpc/client"; import { cn } from "@/lib/utils"; @@ -70,8 +69,7 @@ export function DataTable({ restrictTo }: { restrictTo?: WorkspacePlan[] }) { return; } - const stripe = await getStripe(); - stripe?.redirectToCheckout({ sessionId: data.session.id }); + if (data.session.url) window.location.assign(data.session.url); }, }), ); diff --git a/apps/dashboard/src/lib/stripe.ts b/apps/dashboard/src/lib/stripe.ts deleted file mode 100644 index c35ac44e..00000000 --- a/apps/dashboard/src/lib/stripe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Stripe as StripeProps } from "@stripe/stripe-js"; -import { loadStripe } from "@stripe/stripe-js"; - -let stripePromise: Promise; - -export const getStripe = () => { - if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) { - throw new Error("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set"); - } - - if (!stripePromise) { - stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); - } - - return stripePromise; -}; diff --git a/apps/status-page/package.json b/apps/status-page/package.json index fff752e1..4431ef33 100644 --- a/apps/status-page/package.json +++ b/apps/status-page/package.json @@ -46,7 +46,6 @@ "@radix-ui/react-dropdown-menu": "catalog:", "@radix-ui/react-hover-card": "catalog:", "@sentry/nextjs": "catalog:", - "@stripe/stripe-js": "catalog:", "@tanstack/react-query": "catalog:", "@tanstack/react-table": "catalog:", "@trpc/client": "catalog:", diff --git a/apps/web/package.json b/apps/web/package.json index da4aa3ea..5e81cb08 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,7 +44,6 @@ "@openstatus/upstash": "workspace:*", "@openstatus/utils": "workspace:*", "@sentry/nextjs": "catalog:", - "@stripe/stripe-js": "catalog:", "@t3-oss/env-nextjs": "catalog:", "@tailwindcss/container-queries": "catalog:", "@tailwindcss/typography": "catalog:", diff --git a/apps/web/src/lib/stripe/client.ts b/apps/web/src/lib/stripe/client.ts deleted file mode 100644 index 8e64d21e..00000000 --- a/apps/web/src/lib/stripe/client.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Stripe as StripeProps } from "@stripe/stripe-js"; -import { loadStripe } from "@stripe/stripe-js"; - -import { env } from "../../env"; - -let stripePromise: Promise; - -export const getStripe = () => { - if (!stripePromise) { - stripePromise = loadStripe(env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY); - } - - return stripePromise; -}; diff --git a/packages/api/src/router/stripe/index.ts b/packages/api/src/router/stripe/index.ts index dbc5b7ca..35c9e443 100644 --- a/packages/api/src/router/stripe/index.ts +++ b/packages/api/src/router/stripe/index.ts @@ -24,6 +24,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../../trpc"; import { buildFromSubscriptionOrThrow, + getCurrentPeriodEnd, getCurrentSubscription, stripe, } from "./shared"; @@ -246,8 +247,8 @@ export const stripeRouter = createTRPCRouter({ input: { plan: built.plan, subscriptionId: updated.id, - endsAt: new Date(updated.current_period_end * 1000), - paidUntil: new Date(updated.current_period_end * 1000), + endsAt: getCurrentPeriodEnd(updated), + paidUntil: getCurrentPeriodEnd(updated), limits: built.limits, reason: "plan_changed", }, diff --git a/packages/api/src/router/stripe/shared.ts b/packages/api/src/router/stripe/shared.ts index 87827819..49ef0183 100644 --- a/packages/api/src/router/stripe/shared.ts +++ b/packages/api/src/router/stripe/shared.ts @@ -5,7 +5,7 @@ import { env } from "../../env"; import { buildLimitsFromSubscription } from "./utils"; export const stripe = new Stripe(env.STRIPE_SECRET_KEY ?? "", { - apiVersion: "2023-08-16", + apiVersion: "2026-08-26.dahlia", appInfo: { name: "OpenStatus", version: "0.1.0", @@ -96,6 +96,20 @@ export async function getCurrentSubscription(customerId: string) { return { live, current }; } +/** + * When the subscription's current period ends. Since API version 2025-03-31 + * the period lives on each item rather than on the subscription. Items share + * one billing cycle here (add-ons are monthly-only and blocked on yearly + * plans), so the latest end is the date everything is paid through. + */ +export function getCurrentPeriodEnd(subscription: Stripe.Subscription) { + const ends = subscription.items.data.map((item) => item.current_period_end); + if (ends.length === 0) { + throw new Error(`Subscription ${subscription.id} has no items`); + } + return new Date(Math.max(...ends) * 1000); +} + /** * A customer carries exactly one subscription. `current` is the one to keep; * every subscription that predates it is a leftover that would otherwise keep diff --git a/packages/api/src/router/stripe/webhook.test.ts b/packages/api/src/router/stripe/webhook.test.ts index 1e449155..43ad0145 100644 --- a/packages/api/src/router/stripe/webhook.test.ts +++ b/packages/api/src/router/stripe/webhook.test.ts @@ -30,9 +30,13 @@ const now = () => Math.floor(Date.now() / 1000); // biome-ignore lint/suspicious/noExplicitAny: stubs over the Stripe and Resend clients type AnyStub = Stub; +// `current_period_end` lives on the items since API version 2025-03-31. function subscription( customer: string, - overrides: Partial = {}, + { + current_period_end = now() + 10 * DAY, + ...overrides + }: Partial & { current_period_end?: number } = {}, ) { return { id: "sub_test_1", @@ -40,9 +44,10 @@ function subscription( status: "active", created: now() - 30 * DAY, cancel_at_period_end: false, - current_period_end: now() + 10 * DAY, metadata: {}, - items: { data: [{ price: { id: TEAM_PRICE }, quantity: 1 }] }, + items: { + data: [{ price: { id: TEAM_PRICE }, quantity: 1, current_period_end }], + }, ...overrides, } as unknown as Stripe.Subscription; } @@ -278,7 +283,7 @@ describe("stripe webhook emails", () => { const [reminder, reminderOptions] = send.calls[1].args; expect(new Date(reminder.scheduledAt).getTime()).toBe( - (sub.current_period_end - 3 * DAY) * 1000, + (sub.items.data[0].current_period_end - 3 * DAY) * 1000, ); expect(reminderOptions).toEqual({ idempotencyKey: `stripe:${evt.event.id}:plan-ending-soon`, diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts index efae4d45..66b6bdad 100644 --- a/packages/api/src/router/stripe/webhook.ts +++ b/packages/api/src/router/stripe/webhook.ts @@ -28,6 +28,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; import { buildFromSubscriptionOrThrow, cancelSupersededSubscriptions, + getCurrentPeriodEnd, getCurrentSubscription, isNewerSubscription, listLiveSubscriptions, @@ -109,7 +110,7 @@ async function sendCancellationEmails(args: { eventId: string; }) { const { db, ws, customerId, current, plan, eventId } = args; - const endsAt = new Date(current.current_period_end * 1000); + const endsAt = getCurrentPeriodEnd(current); const to = await getBillingRecipients(db, ws.id, customerId); const preview = await previewWorkspaceDowngrade({ ctx: { @@ -224,8 +225,8 @@ export const webhookRouter = createTRPCRouter({ input: { plan: built.plan, subscriptionId: current.id, - endsAt: new Date(current.current_period_end * 1000), - paidUntil: new Date(current.current_period_end * 1000), + endsAt: getCurrentPeriodEnd(current), + paidUntil: getCurrentPeriodEnd(current), limits: built.limits, }, }); @@ -359,8 +360,8 @@ export const webhookRouter = createTRPCRouter({ input: { plan: built.plan, subscriptionId: subscription.id, - endsAt: new Date(subscription.current_period_end * 1000), - paidUntil: new Date(subscription.current_period_end * 1000), + endsAt: getCurrentPeriodEnd(subscription), + paidUntil: getCurrentPeriodEnd(subscription), limits: built.limits, reason: "checkout_session_completed", }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0c19df8..cfabae72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,9 +335,6 @@ catalogs: '@slack/web-api': specifier: 7.19.0 version: 7.19.0 - '@stripe/stripe-js': - specifier: 2.4.0 - version: 2.4.0 '@t3-oss/env-core': specifier: 0.13.11 version: 0.13.11 @@ -615,8 +612,8 @@ catalogs: specifier: 2.0.8 version: 2.0.8 stripe: - specifier: 13.11.0 - version: 13.11.0 + specifier: 22.6.2 + version: 22.6.2 sugar-high: specifier: 0.9.5 version: 0.9.5 @@ -859,9 +856,6 @@ importers: '@sentry/nextjs': specifier: 'catalog:' version: 10.75.1(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(next@16.3.5(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@8.1.1)(webpack@5.111.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(esbuild@0.28.2)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@26.6.2))) - '@stripe/stripe-js': - specifier: 'catalog:' - version: 2.4.0 '@tanstack/react-query': specifier: 'catalog:' version: 5.103.2(react@19.3.0) @@ -951,7 +945,7 @@ importers: version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) stripe: specifier: 'catalog:' - version: 13.11.0 + version: 22.6.2(@types/node@26.6.2) superjson: specifier: 'catalog:' version: 2.2.6 @@ -1348,9 +1342,6 @@ importers: '@sentry/nextjs': specifier: 'catalog:' version: 10.75.1(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(next@16.3.5(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@8.1.1)(webpack@5.111.1(@swc/core@1.16.2(@swc/helpers@0.5.23))(esbuild@0.28.2)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@26.6.2))) - '@stripe/stripe-js': - specifier: 'catalog:' - version: 2.4.0 '@tanstack/react-query': specifier: 'catalog:' version: 5.103.2(react@19.3.0) @@ -1589,9 +1580,6 @@ importers: '@sentry/nextjs': specifier: 'catalog:' version: 10.75.1(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(next@16.3.5(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(supports-color@8.1.1)(webpack@5.111.1(@swc/core@1.16.2)(esbuild@0.28.2)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@26.6.2))) - '@stripe/stripe-js': - specifier: 'catalog:' - version: 2.4.0 '@t3-oss/env-nextjs': specifier: 'catalog:' version: 0.13.11(typescript@7.0.2)(zod@4.6.5) @@ -1726,7 +1714,7 @@ importers: version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) stripe: specifier: 'catalog:' - version: 13.11.0 + version: 22.6.2(@types/node@26.6.2) sugar-high: specifier: 'catalog:' version: 0.9.5 @@ -2105,7 +2093,7 @@ importers: version: 19.3.0 stripe: specifier: 'catalog:' - version: 13.11.0 + version: 22.6.2(@types/node@26.6.2) superjson: specifier: 'catalog:' version: 2.2.6 @@ -7549,9 +7537,6 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@stripe/stripe-js@2.4.0': - resolution: {integrity: sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA==} - '@swc/core-darwin-arm64@1.16.2': resolution: {integrity: sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==} engines: {node: '>=10'} @@ -11486,9 +11471,14 @@ packages: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} - stripe@13.11.0: - resolution: {integrity: sha512-yPxVJxUzP1QHhHeFnYjJl48QwDS1+5befcL7ju7+t+i88D5r0rbsL+GkCCS6zgcU+TiV5bF9eMGcKyJfLf8BZQ==} - engines: {node: '>=12.*'} + stripe@22.6.2: + resolution: {integrity: sha512-PwRE2scocvXqDKPiskMa5vRJEAEkq46W69XyUYmAwrNSLr675lQnfqLwnsndAWizwgihOWw/irLmoFRo3ez7ew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true stubborn-fs@2.0.0: resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} @@ -15842,8 +15832,6 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@stripe/stripe-js@2.4.0': {} - '@swc/core-darwin-arm64@1.16.2': optional: true @@ -20310,10 +20298,9 @@ snapshots: strip-json-comments@5.0.3: {} - stripe@13.11.0: - dependencies: + stripe@22.6.2(@types/node@26.6.2): + optionalDependencies: '@types/node': 26.6.2 - qs: 6.16.0 stubborn-fs@2.0.0: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 433411f7..73db75f5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -112,7 +112,6 @@ catalog: "@sentry/deno": 10.75.1 "@sentry/nextjs": 10.75.1 "@slack/web-api": 7.19.0 - "@stripe/stripe-js": 2.4.0 "@t3-oss/env-core": 0.13.11 "@t3-oss/env-nextjs": 0.13.11 "@tailwindcss/cli": 4.3.3 @@ -205,7 +204,7 @@ catalog: shiki: 3.23.0 slugify: 1.6.9 sonner: 2.0.8 - stripe: 13.11.0 + stripe: 22.6.2 sugar-high: 0.9.5 superjson: 2.2.6 tailwind-merge: 3.7.0 diff --git a/scripts/check-doc-refs.mts b/scripts/check-doc-refs.mts index d0c4cc77..f5d1c6a7 100644 --- a/scripts/check-doc-refs.mts +++ b/scripts/check-doc-refs.mts @@ -55,13 +55,17 @@ const FILE_LIKE = /\.[a-z0-9]+$/i; type Violation = { file: string; line: number; path: string }; function trackedFiles(): string[] { - return execFileSync("git", ["ls-files", "-z"], { - cwd: REPO_ROOT, - encoding: "utf8", - maxBuffer: 64 * 1024 * 1024, - }) - .split("\0") - .filter((file) => file.length > 0); + return ( + execFileSync("git", ["ls-files", "-z"], { + cwd: REPO_ROOT, + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }) + .split("\0") + // Under jj the git index lags the working copy, so it still lists files + // deleted but not yet committed. + .filter((file) => file.length > 0 && existsSync(join(REPO_ROOT, file))) + ); } /** -- 2.51.2