fix: harden cloud billing pricing table and webhook sync

This commit is contained in:
Matti Nannt
2026-03-02 20:09:42 +01:00
parent 680e46a4f1
commit e54ccaec3b
6 changed files with 28 additions and 8 deletions

View File

@@ -152,6 +152,8 @@ NOTION_OAUTH_CLIENT_SECRET=
# Stripe Billing Variables
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
CLOUD_STRIPE_PUBLISHABLE_KEY=
CLOUD_STRIPE_PRICING_TABLE_ID=
# Oauth credentials for Google sheet integration
GOOGLE_SHEETS_CLIENT_ID=
@@ -230,4 +232,4 @@ REDIS_URL=redis://localhost:6379
# Lingo.dev API key for translation generation
LINGODOTDEV_API_KEY=your_api_key_here
LINGODOTDEV_API_KEY=your_api_key_here

View File

@@ -85,6 +85,8 @@ export const env = createEnv({
SMTP_REJECT_UNAUTHORIZED_TLS: z.enum(["1", "0"]).optional(),
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
CLOUD_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
CLOUD_STRIPE_PRICING_TABLE_ID: z.string().optional(),
PUBLIC_URL: z
.string()
.url()
@@ -202,6 +204,8 @@ export const env = createEnv({
SMTP_AUTHENTICATED: process.env.SMTP_AUTHENTICATED,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
CLOUD_STRIPE_PUBLISHABLE_KEY: process.env.CLOUD_STRIPE_PUBLISHABLE_KEY,
CLOUD_STRIPE_PRICING_TABLE_ID: process.env.CLOUD_STRIPE_PRICING_TABLE_ID,
PUBLIC_URL: process.env.PUBLIC_URL,
TURNSTILE_SECRET_KEY: process.env.TURNSTILE_SECRET_KEY,
TURNSTILE_SITE_KEY: process.env.TURNSTILE_SITE_KEY,

View File

@@ -46,8 +46,12 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin
: null;
const customerId =
"customer" in eventObject && typeof eventObject.customer === "string" ? eventObject.customer : null;
const clientReferenceId =
"client_reference_id" in eventObject && typeof eventObject.client_reference_id === "string"
? eventObject.client_reference_id
: null;
let organizationId = metadataOrgId;
let organizationId = metadataOrgId ?? clientReferenceId;
if (!organizationId && customerId) {
organizationId = await findOrganizationIdByStripeCustomerId(customerId);
}
@@ -57,6 +61,9 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin
{ eventType: event.type, eventId: event.id },
"Skipping Stripe webhook: organization not resolved"
);
if (event.type === "checkout.session.completed") {
return { status: 500, message: "Checkout completed but organization could not be resolved." };
}
return { status: 200, message: { received: true } };
}
@@ -71,6 +78,7 @@ export const webhookHandler = async (requestBody: string, stripeSignature: strin
{ error, eventId: event.id, organizationId, eventType: event.type },
"Failed to sync billing snapshot from Stripe webhook"
);
return { status: 500, message: "Stripe webhook processing failed; please retry." };
}
return { status: 200, message: { received: true } };

View File

@@ -15,9 +15,6 @@ import {
} from "../actions";
import { BillingSlider } from "./billing-slider";
const STRIPE_MONTHLY_PRICING_TABLE_ID = "prctbl_1T6ZLKCng0KywbKlSUAiFqH5";
const STRIPE_MONTHLY_PRICING_PUBLISHABLE_KEY =
"pk_test_51Sqt6uCng0KywbKlmnLtd8p2B1FfEAcM8O9IDiYdo1F2B6X7VYdMALhrpOU1vDB8SB3ikJshBeHz8Kj9iv89K6j3009S9mmY0h";
const STRIPE_SUPPORTED_LOCALES = new Set([
"bg",
"cs",
@@ -83,6 +80,8 @@ interface PricingTableProps {
responseCount: number;
projectCount: number;
hasBillingRights: boolean;
cloudStripePublishableKey: string | null;
cloudStripePricingTableId: string | null;
}
const getCurrentCloudPlan = (
@@ -116,6 +115,8 @@ export const PricingTable = ({
responseCount,
projectCount,
hasBillingRights,
cloudStripePublishableKey,
cloudStripePricingTableId,
}: PricingTableProps) => {
const { t, i18n } = useTranslation();
const router = useRouter();
@@ -264,14 +265,14 @@ export const PricingTable = ({
</div>
</div>
{hasBillingRights && (
{hasBillingRights && cloudStripePublishableKey && cloudStripePricingTableId && (
<div className="mb-12 w-full">
<div className="w-full">
<div className="mx-auto w-full max-w-[1200px]">
<Script src="https://js.stripe.com/v3/pricing-table.js" strategy="afterInteractive" />
{createElement("stripe-pricing-table", {
"pricing-table-id": STRIPE_MONTHLY_PRICING_TABLE_ID,
"publishable-key": STRIPE_MONTHLY_PRICING_PUBLISHABLE_KEY,
"pricing-table-id": cloudStripePricingTableId,
"publishable-key": cloudStripePublishableKey,
...(stripeLocaleOverride
? {
"__locale-override": stripeLocaleOverride,

View File

@@ -1,6 +1,7 @@
import { notFound } from "next/navigation";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { env } from "@/lib/env";
import { getMonthlyOrganizationResponseCount } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -50,6 +51,8 @@ export const PricingPage = async (props) => {
responseCount={responseCount}
projectCount={projectCount}
hasBillingRights={hasBillingRights}
cloudStripePublishableKey={env.CLOUD_STRIPE_PUBLISHABLE_KEY ?? null}
cloudStripePricingTableId={env.CLOUD_STRIPE_PRICING_TABLE_ID ?? null}
/>
</PageContentWrapper>
);

View File

@@ -243,6 +243,8 @@
"CLOUD_STRIPE_PRODUCT_ID_PRO",
"CLOUD_STRIPE_PRODUCT_ID_SCALE",
"CLOUD_STRIPE_PRODUCT_ID_TRIAL",
"CLOUD_STRIPE_PUBLISHABLE_KEY",
"CLOUD_STRIPE_PRICING_TABLE_ID",
"SURVEYS_PACKAGE_MODE",
"SURVEYS_PACKAGE_BUILD",
"PUBLIC_URL",