From 1751ea424d47f7cc33b6ff0ad17d9c7a967440a0 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Thu, 16 Oct 2025 19:38:37 -0700 Subject: [PATCH] complete payments setup warning (#960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## High-level PR Summary This PR adds a warning mechanism for incomplete Stripe payments setup by checking if `charges_enabled` is true on the connected Stripe account. The backend now retrieves and passes the `charges_enabled` status through the purchase flow, and the frontend checkout form displays an error message when payments are not fully enabled, preventing users from attempting purchases on misconfigured accounts. Additionally, minor cleanup was performed including removing unused test mode toggle state management and fixing a description typo. ⏱️ Estimated Review Time: 15-30 minutes
💡 Review Order Suggestion | Order | File Path | |-------|-----------| | 1 | `apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx` | | 2 | `apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts` | | 3 | `apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts` | | 4 | `apps/dashboard/src/components/payments/checkout.tsx` | | 5 | `apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx` | | 6 | `apps/e2e/tests/backend/endpoints/api/v1/payments/validate-code.test.ts` | | 7 | `apps/e2e/tests/backend/endpoints/api/v1/payments/before-offer-to-product-rename/outdated--validate-code.test.ts` | | 8 | `apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx` | | 9 | `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx` |
⚠️ Inconsistent Changes Detected | File Path | Warning | |-----------|---------| | `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx` | Removes error handling and loading state management for test mode toggle, which seems unrelated to the charges_enabled warning feature | | `apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx` | Changes description from 'Stripe price ID' to 'Stack auth price ID' which is a documentation change unrelated to the charges_enabled warning |
[![Need help? Join our Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U) [![Analyze latest changes](https://img.shields.io/badge/Analyze%20latest%20changes-238636?style=plastic)](https://squash-322339097191.europe-west3.run.app/interactive/4240d8f2d626c1224598076b9ec44684a8463b30262fd8dcc7d31c573f29148b/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=960) ## Summary by CodeRabbit * **New Features** * Checkout interface now displays a "Payments not enabled" message with guidance when charges are not enabled on the payment account. * **Documentation** * Clarified price ID field description in payment session documentation. * **Tests** * Updated payment validation endpoint test expectations to reflect new response fields. --- .../purchases/create-purchase-url/route.ts | 9 ++++++--- .../purchases/purchase-session/route.tsx | 2 +- .../payments/purchases/validate-code/route.ts | 2 ++ .../purchases/verification-code-handler.tsx | 3 ++- .../products/page-client-catalogs-view.tsx | 15 +++------------ .../app/(main)/purchase/[code]/page-client.tsx | 2 ++ .../src/components/payments/checkout.tsx | 17 ++++++++++++++++- .../outdated--validate-code.test.ts | 2 ++ .../api/v1/payments/validate-code.test.ts | 3 +++ 9 files changed, 37 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts b/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts index 22eff849a..b22480c2b 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts +++ b/apps/backend/src/app/api/latest/payments/purchases/create-purchase-url/route.ts @@ -1,6 +1,6 @@ import { ensureProductIdOrInlineProduct, getCustomerPurchaseContext } from "@/lib/payments"; import { validateRedirectUrl } from "@/lib/redirect-urls"; -import { getStripeForAccount } from "@/lib/stripe"; +import { getStackStripe, getStripeForAccount } from "@/lib/stripe"; import { getPrismaClientForTenancy, globalPrismaClient } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { CustomerType } from "@prisma/client"; @@ -106,7 +106,9 @@ export const POST = createSmartRouteHandler({ where: { id: tenancy.project.id }, select: { stripeAccountId: true }, }); - + const stripeAccountId = project?.stripeAccountId ?? throwErr("Stripe account not configured"); + const stackStripe = getStackStripe(); + const connectedAccount = await stackStripe.accounts.retrieve(stripeAccountId); const { code } = await purchaseUrlVerificationCodeHandler.createCode({ tenancy, expiresInMs: 1000 * 60 * 60 * 24, @@ -116,7 +118,8 @@ export const POST = createSmartRouteHandler({ productId: req.body.product_id, product: productConfig, stripeCustomerId: stripeCustomer.id, - stripeAccountId: project?.stripeAccountId ?? throwErr("Stripe account not configured"), + stripeAccountId, + chargesEnabled: connectedAccount.charges_enabled, }, method: {}, callbackUrl: undefined, diff --git a/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx b/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx index c977bccb1..c1e9dc265 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx +++ b/apps/backend/src/app/api/latest/payments/purchases/purchase-session/route.tsx @@ -25,7 +25,7 @@ export const POST = createSmartRouteHandler({ }), price_id: yupString().defined().meta({ openapiField: { - description: "The Stripe price ID to purchase", + description: "The Stack auth price ID to purchase", exampleValue: "price_1234567890abcdef" } }), diff --git a/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts b/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts index 7d6955135..8de5b837e 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts +++ b/apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts @@ -44,6 +44,7 @@ export const POST = createSmartRouteHandler({ display_name: yupString().defined(), }).defined()).defined(), test_mode: yupBoolean().defined(), + charges_enabled: yupBoolean().defined(), }).defined(), }), async handler({ body }) { @@ -98,6 +99,7 @@ export const POST = createSmartRouteHandler({ already_bought_non_stackable: alreadyBoughtNonStackable, conflicting_products: conflictingCatalogProducts, test_mode: tenancy.config.payments.testMode === true, + charges_enabled: verificationCode.data.chargesEnabled, }, }; }, diff --git a/apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx b/apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx index 1339821c1..2ddf13b74 100644 --- a/apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/payments/purchases/verification-code-handler.tsx @@ -1,6 +1,6 @@ import { createVerificationCodeHandler } from "@/route-handlers/verification-code-handler"; import { VerificationCodeType } from "@prisma/client"; -import { productSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { productSchema, yupBoolean, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; export const purchaseUrlVerificationCodeHandler = createVerificationCodeHandler({ type: VerificationCodeType.PURCHASE_URL, @@ -12,6 +12,7 @@ export const purchaseUrlVerificationCodeHandler = createVerificationCodeHandler( product: productSchema, stripeCustomerId: yupString().defined(), stripeAccountId: yupString().defined(), + chargesEnabled: yupBoolean().defined(), }), // @ts-ignore TODO: fix this async handler(_, __, data) { diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx index 62d514680..77d68c4cb 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/page-client-catalogs-view.tsx @@ -1438,7 +1438,6 @@ export default function PageClient({ onViewChange }: { onViewChange: (view: "lis const config = project.useConfig(); const switchId = useId(); const testModeSwitchId = useId(); - const [isUpdatingTestMode, setIsUpdatingTestMode] = useState(false); const paymentsConfig: CompleteConfig['payments'] = config.payments; @@ -1568,15 +1567,8 @@ export default function PageClient({ onViewChange }: { onViewChange: (view: "lis }; const handleToggleTestMode = async (enabled: boolean) => { - setIsUpdatingTestMode(true); - try { - await project.updateConfig({ "payments.testMode": enabled }); - toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" }); - } catch (_error) { - toast({ title: "Failed to update test mode", variant: "destructive" }); - } finally { - setIsUpdatingTestMode(false); - } + await project.updateConfig({ "payments.testMode": enabled }); + toast({ title: enabled ? "Test mode enabled" : "Test mode disabled" }); }; @@ -1597,8 +1589,7 @@ export default function PageClient({ onViewChange }: { onViewChange: (view: "lis void handleToggleTestMode(checked)} + onCheckedChange={(checked) => handleToggleTestMode(checked)} /> diff --git a/apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx b/apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx index 7d392a831..6c9cc7368 100644 --- a/apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/purchase/[code]/page-client.tsx @@ -19,6 +19,7 @@ type ProductData = { already_bought_non_stackable?: boolean, conflicting_products?: { product_id: string, display_name: string }[], test_mode: boolean, + charges_enabled: boolean, }; const apiUrl = getPublicEnvVar("NEXT_PUBLIC_STACK_API_URL") ?? throwErr("NEXT_PUBLIC_STACK_API_URL is not set"); @@ -282,6 +283,7 @@ export default function PageClient({ code }: { code: string }) { setupSubscription={setupSubscription} returnUrl={returnUrl ?? undefined} disabled={quantityNumber < 1 || isTooLarge || data.already_bought_non_stackable === true} + chargesEnabled={data.charges_enabled} onTestModeBypass={data.test_mode ? handleBypass : undefined} /> diff --git a/apps/dashboard/src/components/payments/checkout.tsx b/apps/dashboard/src/components/payments/checkout.tsx index 968eac0df..799b75b75 100644 --- a/apps/dashboard/src/components/payments/checkout.tsx +++ b/apps/dashboard/src/components/payments/checkout.tsx @@ -24,6 +24,7 @@ type Props = { returnUrl?: string, disabled?: boolean, onTestModeBypass?: () => Promise, + chargesEnabled: boolean, }; export function CheckoutForm({ @@ -33,6 +34,7 @@ export function CheckoutForm({ returnUrl, disabled, onTestModeBypass, + chargesEnabled, }: Props) { const stripe = useStripe(); const elements = useElements(); @@ -96,11 +98,24 @@ export function CheckoutForm({ ); } + if (!chargesEnabled) { + return ( +
+
+ Payments not enabled +

+ This project does not have payments enabled yet. Please contact the app developer to finish setting up payments. +

+
+
+ ); + } + return (