From 1191501787d4580850f285a8f3bd19d7596f9fd3 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 31 Mar 2026 13:32:10 +0000 Subject: [PATCH 01/11] fix: wrap payments ErrorBoundary in html document shell --- apps/builder/app/routes/payments.$.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/builder/app/routes/payments.$.tsx b/apps/builder/app/routes/payments.$.tsx index a92e045117d5..0b2daff1faa8 100644 --- a/apps/builder/app/routes/payments.$.tsx +++ b/apps/builder/app/routes/payments.$.tsx @@ -120,9 +120,17 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { export const ErrorBoundary = () => { const error = useRouteError(); - if (isRouteErrorResponse(error)) { - return
{error.data}
; - } - - return
Unexpected error
; + const message = isRouteErrorResponse(error) ? error.data : "Unexpected error"; + + return ( + + + + Error + + +
{message}
+ + + ); }; From 074089d176dac40cae876a0d6288a27571d1ab0f Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Tue, 31 Mar 2026 14:47:31 +0000 Subject: [PATCH 02/11] lostpixel trigger From 571e8523e712b9f156f87c02730afc92cb7f734f Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 10:41:44 +0000 Subject: [PATCH 03/11] redepploy From 053496abf3a9113a977ba0fa51a69ddb3bb62999 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 14:55:38 +0000 Subject: [PATCH 04/11] fix: hardcode checkout/sessions worker path --- apps/builder/app/routes/payments.$.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/routes/payments.$.tsx b/apps/builder/app/routes/payments.$.tsx index 0b2daff1faa8..926c6be3433a 100644 --- a/apps/builder/app/routes/payments.$.tsx +++ b/apps/builder/app/routes/payments.$.tsx @@ -25,7 +25,7 @@ const zWorkerEnv = z.object({ PAYMENT_WORKER_TOKEN: z.string(), }); -export const loader = async ({ request, params }: LoaderFunctionArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { if (isDashboard(request) === false) { throw new Response("Not Found", { status: 404, @@ -56,7 +56,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const workerEnv = workerEnvParsed.data; const workerUrl = new URL(workerEnv.PAYMENT_WORKER_URL); - workerUrl.pathname = `${workerUrl.pathname}/${params["*"]}` + workerUrl.pathname = `${workerUrl.pathname}/checkout/sessions` .split("/") .filter(Boolean) .join("/"); From 437162ae55d59f6e74d89bd70356040a54e7f17b Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 15:18:03 +0000 Subject: [PATCH 05/11] fix: payments error boundary and worker path --- apps/builder/app/routes/payments.$.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/builder/app/routes/payments.$.tsx b/apps/builder/app/routes/payments.$.tsx index 926c6be3433a..a92e045117d5 100644 --- a/apps/builder/app/routes/payments.$.tsx +++ b/apps/builder/app/routes/payments.$.tsx @@ -25,7 +25,7 @@ const zWorkerEnv = z.object({ PAYMENT_WORKER_TOKEN: z.string(), }); -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const loader = async ({ request, params }: LoaderFunctionArgs) => { if (isDashboard(request) === false) { throw new Response("Not Found", { status: 404, @@ -56,7 +56,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const workerEnv = workerEnvParsed.data; const workerUrl = new URL(workerEnv.PAYMENT_WORKER_URL); - workerUrl.pathname = `${workerUrl.pathname}/checkout/sessions` + workerUrl.pathname = `${workerUrl.pathname}/${params["*"]}` .split("/") .filter(Boolean) .join("/"); @@ -120,17 +120,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { export const ErrorBoundary = () => { const error = useRouteError(); - const message = isRouteErrorResponse(error) ? error.data : "Unexpected error"; - - return ( - - - - Error - - -
{message}
- - - ); + if (isRouteErrorResponse(error)) { + return
{error.data}
; + } + + return
Unexpected error
; }; From 96c207b61eb445ed73c7e102ed2baa0a55d98428 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 16:23:33 +0000 Subject: [PATCH 06/11] fix: hardcode billing-portal/sessions worker path --- apps/builder/app/routes/payments.$.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/routes/payments.$.tsx b/apps/builder/app/routes/payments.$.tsx index a92e045117d5..317e6c990e68 100644 --- a/apps/builder/app/routes/payments.$.tsx +++ b/apps/builder/app/routes/payments.$.tsx @@ -25,7 +25,7 @@ const zWorkerEnv = z.object({ PAYMENT_WORKER_TOKEN: z.string(), }); -export const loader = async ({ request, params }: LoaderFunctionArgs) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { if (isDashboard(request) === false) { throw new Response("Not Found", { status: 404, @@ -56,7 +56,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const workerEnv = workerEnvParsed.data; const workerUrl = new URL(workerEnv.PAYMENT_WORKER_URL); - workerUrl.pathname = `${workerUrl.pathname}/${params["*"]}` + workerUrl.pathname = `${workerUrl.pathname}/billing-portal/sessions` .split("/") .filter(Boolean) .join("/"); From 0ba5fbbd7d1596579d58981244c8aed193a82ab4 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 18:28:11 +0000 Subject: [PATCH 07/11] fix: remove payments proxy route, point billing portal directly to worker --- apps/builder/app/dashboard/profile-menu.tsx | 8 +- apps/builder/app/env/env.server.ts | 3 - apps/builder/app/routes/payments.$.tsx | 128 ------------------ .../app/shared/router-utils/path-utils.ts | 2 +- 4 files changed, 6 insertions(+), 135 deletions(-) delete mode 100644 apps/builder/app/routes/payments.$.tsx diff --git a/apps/builder/app/dashboard/profile-menu.tsx b/apps/builder/app/dashboard/profile-menu.tsx index 2989e7dda162..32f29850cbf9 100644 --- a/apps/builder/app/dashboard/profile-menu.tsx +++ b/apps/builder/app/dashboard/profile-menu.tsx @@ -87,9 +87,11 @@ export const ProfileMenu = ({ purchase.subscriptionId ? ( - navigate(userPlanSubscriptionPath(purchase.subscriptionId)) - } + onSelect={() => { + window.location.href = userPlanSubscriptionPath( + purchase.subscriptionId + ); + }} > {purchase.planName} diff --git a/apps/builder/app/env/env.server.ts b/apps/builder/app/env/env.server.ts index 4ceaaa1777f8..1a88e0a5b622 100644 --- a/apps/builder/app/env/env.server.ts +++ b/apps/builder/app/env/env.server.ts @@ -54,9 +54,6 @@ const env = { N8N_WEBHOOK_URL: process.env.N8N_WEBHOOK_URL, N8N_WEBHOOK_TOKEN: process.env.N8N_WEBHOOK_TOKEN, - PAYMENT_WORKER_URL: process.env.PAYMENT_WORKER_URL, - PAYMENT_WORKER_TOKEN: process.env.PAYMENT_WORKER_TOKEN, - PUBLISHER_HOST: process.env.PUBLISHER_HOST || "wstd.work", STAGING_USERNAME: process.env.STAGING_USERNAME ?? "admin", diff --git a/apps/builder/app/routes/payments.$.tsx b/apps/builder/app/routes/payments.$.tsx deleted file mode 100644 index 317e6c990e68..000000000000 --- a/apps/builder/app/routes/payments.$.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; -import { z } from "zod"; -import { findAuthenticatedUser } from "~/services/auth.server"; -import { isDashboard, loginPath } from "~/shared/router-utils"; -import env from "~/env/env.server"; -import cookie from "cookie"; -import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie"; -import { redirect } from "~/services/no-store-redirect"; -import { allowedDestinations } from "~/services/destinations.server"; - -const zWorkerResponse = z.union([ - z.object({ - type: z.literal("error"), - error: z.string(), - }), - z.object({ - type: z.literal("redirect"), - to: z.string(), - }), -]); - -const zWorkerEnv = z.object({ - PAYMENT_WORKER_URL: z.string(), - PAYMENT_WORKER_TOKEN: z.string(), -}); - -export const loader = async ({ request }: LoaderFunctionArgs) => { - if (isDashboard(request) === false) { - throw new Response("Not Found", { - status: 404, - }); - } - - preventCrossOriginCookie(request); - allowedDestinations(request, ["document", "empty"]); - - const user = await findAuthenticatedUser(request); - - if (user === null) { - const url = new URL(request.url); - throw redirect( - loginPath({ - returnTo: `${url.pathname}?${url.searchParams.toString()}`, - }) - ); - } - - const workerEnvParsed = zWorkerEnv.safeParse(env); - if (workerEnvParsed.success === false) { - throw new Response(workerEnvParsed.error.message, { - status: 400, - }); - } - - const workerEnv = workerEnvParsed.data; - - const workerUrl = new URL(workerEnv.PAYMENT_WORKER_URL); - workerUrl.pathname = `${workerUrl.pathname}/billing-portal/sessions` - .split("/") - .filter(Boolean) - .join("/"); - workerUrl.search = new URL(request.url).search; - - const requestUrl = new URL(request.url); - - const response = await fetch(workerUrl.href, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${workerEnv.PAYMENT_WORKER_TOKEN}`, - }, - body: JSON.stringify({ - userId: user.id, - cookies: cookie.parse(request.headers.get("cookie") ?? ""), - requestUrl: requestUrl.href, - }), - }); - - if (response.ok === false) { - const text = await response.text(); - - throw new Response( - `Fetch error status="${response.status}"\nMessage:\n${text.slice( - 0, - 1000 - )}"`, - { - status: response.status, - } - ); - } - - const responseJson = await response.json(); - const workerResponseParsed = zWorkerResponse.safeParse(responseJson); - - if (workerResponseParsed.success === false) { - throw new Response(workerResponseParsed.error.message, { - status: 400, - }); - } - - const workerResponse = workerResponseParsed.data; - - if (workerResponse.type === "error") { - throw new Response(workerResponse.error, { - status: 400, - }); - } - - if (workerResponse.type === "redirect") { - throw redirect(workerResponse.to); - } - - workerResponse satisfies never; - - return json({}); -}; - -export const ErrorBoundary = () => { - const error = useRouteError(); - - if (isRouteErrorResponse(error)) { - return
{error.data}
; - } - - return
Unexpected error
; -}; diff --git a/apps/builder/app/shared/router-utils/path-utils.ts b/apps/builder/app/shared/router-utils/path-utils.ts index 50453ab1e325..0a1c9adb1643 100644 --- a/apps/builder/app/shared/router-utils/path-utils.ts +++ b/apps/builder/app/shared/router-utils/path-utils.ts @@ -117,7 +117,7 @@ export const userPlanSubscriptionPath = (subscriptionId?: string) => { } if (isFeatureEnabled("paymentWorker")) { - return `/payments/billing-portal/sessions?${urlSearchParams.toString()}`; + return `/builder-payments/billing-portal/sessions?${urlSearchParams.toString()}`; } return `/n8n/billing_portal/sessions?${urlSearchParams.toString()}`; From 7520bffe1e060f26f594ac41e9d389f9ca9d9e3a Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 18:31:14 +0000 Subject: [PATCH 08/11] fix: revert to navigate() for billing portal path --- apps/builder/app/dashboard/profile-menu.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/builder/app/dashboard/profile-menu.tsx b/apps/builder/app/dashboard/profile-menu.tsx index 32f29850cbf9..2989e7dda162 100644 --- a/apps/builder/app/dashboard/profile-menu.tsx +++ b/apps/builder/app/dashboard/profile-menu.tsx @@ -87,11 +87,9 @@ export const ProfileMenu = ({ purchase.subscriptionId ? ( { - window.location.href = userPlanSubscriptionPath( - purchase.subscriptionId - ); - }} + onSelect={() => + navigate(userPlanSubscriptionPath(purchase.subscriptionId)) + } > {purchase.planName} From 77bd287acb0cca0862e5f650190ade27dbbb94e7 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 18:40:27 +0000 Subject: [PATCH 09/11] fix: use window.location.href for billing portal to avoid React Router intercepting external redirect --- apps/builder/app/dashboard/profile-menu.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/builder/app/dashboard/profile-menu.tsx b/apps/builder/app/dashboard/profile-menu.tsx index 2989e7dda162..32f29850cbf9 100644 --- a/apps/builder/app/dashboard/profile-menu.tsx +++ b/apps/builder/app/dashboard/profile-menu.tsx @@ -87,9 +87,11 @@ export const ProfileMenu = ({ purchase.subscriptionId ? ( - navigate(userPlanSubscriptionPath(purchase.subscriptionId)) - } + onSelect={() => { + window.location.href = userPlanSubscriptionPath( + purchase.subscriptionId + ); + }} > {purchase.planName} From 4119f0a1ac49a207f5a9a19358f96bbd265be32b Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 18:52:52 +0000 Subject: [PATCH 10/11] fix: remove n8n route and env vars, billing portal always uses worker --- apps/builder/app/env/env.server.ts | 3 - apps/builder/app/routes/n8n.$.tsx | 137 ------------------ .../app/shared/router-utils/path-utils.ts | 7 +- 3 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 apps/builder/app/routes/n8n.$.tsx diff --git a/apps/builder/app/env/env.server.ts b/apps/builder/app/env/env.server.ts index 1a88e0a5b622..a362cc291716 100644 --- a/apps/builder/app/env/env.server.ts +++ b/apps/builder/app/env/env.server.ts @@ -51,9 +51,6 @@ const env = { projectId.trim() ) ?? [], - N8N_WEBHOOK_URL: process.env.N8N_WEBHOOK_URL, - N8N_WEBHOOK_TOKEN: process.env.N8N_WEBHOOK_TOKEN, - PUBLISHER_HOST: process.env.PUBLISHER_HOST || "wstd.work", STAGING_USERNAME: process.env.STAGING_USERNAME ?? "admin", diff --git a/apps/builder/app/routes/n8n.$.tsx b/apps/builder/app/routes/n8n.$.tsx deleted file mode 100644 index f6d81c803a63..000000000000 --- a/apps/builder/app/routes/n8n.$.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; -import { isRouteErrorResponse, useRouteError } from "@remix-run/react"; -import { z } from "zod"; -import { findAuthenticatedUser } from "~/services/auth.server"; -import { isDashboard, loginPath } from "~/shared/router-utils"; -import env from "~/env/env.server"; -import cookie from "cookie"; -import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie"; -import { redirect } from "~/services/no-store-redirect"; -import { allowedDestinations } from "~/services/destinations.server"; - -const zN8NResponse = z.union([ - z.object({ - type: z.literal("error"), - error: z.string(), - }), - - z.object({ - type: z.literal("redirect"), - to: z.string(), - }), -]); -const zWebhookEnv = z.object({ - N8N_WEBHOOK_URL: z.string(), - N8N_WEBHOOK_TOKEN: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - if (isDashboard(request) === false) { - throw new Response("Not Found", { - status: 404, - }); - } - - preventCrossOriginCookie(request); - allowedDestinations(request, ["document", "empty"]); - // CSRF token checks are not necessary for dashboard-only pages. - // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests - // or by allowedDestinations for iframe requests. - - const user = await findAuthenticatedUser(request); - - if (user === null) { - const url = new URL(request.url); - throw redirect( - loginPath({ - returnTo: `${url.pathname}?${url.searchParams.toString()}`, - }) - ); - } - - const webhookEnvParsed = zWebhookEnv.safeParse(env); - if (webhookEnvParsed.success === false) { - throw new Response(webhookEnvParsed.error.message, { - status: 400, - }); - } - - const webhookEnv = webhookEnvParsed.data; - - const n8nWebhookUrl = new URL(webhookEnv.N8N_WEBHOOK_URL); - n8nWebhookUrl.pathname = `${n8nWebhookUrl.pathname}/${ - env.DEPLOYMENT_ENVIRONMENT ?? "local" - }/${params["*"]}` - .split("/") - .filter(Boolean) - .join("/"); - n8nWebhookUrl.search = new URL(request.url).search; - - const requestUrl = new URL(request.url); - - const response = await fetch(n8nWebhookUrl.href, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${webhookEnv.N8N_WEBHOOK_TOKEN}`, - }, - body: JSON.stringify({ - userId: user.id, - // For anonymous tracking like posthog - cookies: cookie.parse(request.headers.get("cookie") ?? ""), - requestUrl: requestUrl.href, - }), - }); - - if (response.ok === false) { - const text = await response.text(); - - throw new Response( - `Fetch error status="${response.status}"\nMessage:\n${text.slice( - 0, - 1000 - )}"`, - { - status: response.status, - } - ); - } - const responseJson = await response.json(); - const n8nResponseParsed = zN8NResponse.safeParse(responseJson); - - if (n8nResponseParsed.success === false) { - throw new Response(n8nResponseParsed.error.message, { - status: 400, - }); - } - - const n8nResponse = n8nResponseParsed.data; - - if (n8nResponse.type === "error") { - throw new Response(n8nResponse.error, { - status: 400, - }); - } - - if (n8nResponse.type === "redirect") { - throw redirect(n8nResponse.to); - } - - n8nResponse satisfies never; - - return json({}); -}; - -export const ErrorBoundary = () => { - const error = useRouteError(); - - if (isRouteErrorResponse(error)) { - return
{error.data}
; - } - - if (error instanceof Error) { - return
{error.message}
; - } - - return
{String(error)}
; -}; diff --git a/apps/builder/app/shared/router-utils/path-utils.ts b/apps/builder/app/shared/router-utils/path-utils.ts index 0a1c9adb1643..01c0515891f4 100644 --- a/apps/builder/app/shared/router-utils/path-utils.ts +++ b/apps/builder/app/shared/router-utils/path-utils.ts @@ -2,7 +2,6 @@ import type { AUTH_PROVIDERS } from "~/shared/session"; import { publicStaticEnv } from "~/env/env.static"; import { getAuthorizationServerOrigin } from "./origins"; import type { BuilderMode } from "../nano-states/misc"; -import { isFeatureEnabled } from "@webstudio-is/feature-flags"; const searchParams = (params: Record) => { const searchParams = new URLSearchParams(); @@ -116,11 +115,7 @@ export const userPlanSubscriptionPath = (subscriptionId?: string) => { urlSearchParams.set("subscription", subscriptionId); } - if (isFeatureEnabled("paymentWorker")) { - return `/builder-payments/billing-portal/sessions?${urlSearchParams.toString()}`; - } - - return `/n8n/billing_portal/sessions?${urlSearchParams.toString()}`; + return `/builder-payments/billing-portal/sessions?${urlSearchParams.toString()}`; }; export const authCallbackPath = ({ From 64c918532c54029066ac3f5bc5ce0f31b4d22be2 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Wed, 1 Apr 2026 18:53:43 +0000 Subject: [PATCH 11/11] fix: remove paymentWorker feature flag --- packages/feature-flags/src/flags.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/feature-flags/src/flags.ts b/packages/feature-flags/src/flags.ts index 0169eb1541b6..0a952b87c8df 100644 --- a/packages/feature-flags/src/flags.ts +++ b/packages/feature-flags/src/flags.ts @@ -3,4 +3,3 @@ export const internalComponents = false; export const unsupportedBrowsers = false; export const resourceProp = false; export const tailwind = false; -export const paymentWorker = false;