From 88aa90ab87e0d7aa566dc80f4c4fd1d3f75cee75 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 5 May 2026 16:06:01 +0200 Subject: [PATCH 1/2] fix: correct billing URL construction and prevent double-redirecting Generated-By: PostHog Code Task-Id: 156e6cbd-e2c4-4d62-bd3d-969dc2b57010 --- .../components/sections/PlanUsageSettings.tsx | 10 ++-- apps/code/src/renderer/utils/urls.test.ts | 49 +++++++++++++++++++ apps/code/src/renderer/utils/urls.ts | 6 +++ 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 apps/code/src/renderer/utils/urls.test.ts diff --git a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx index f86802341..73e31935a 100644 --- a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx @@ -22,7 +22,7 @@ import { import { Tooltip } from "@renderer/components/ui/Tooltip"; import { PLAN_PRO_ALPHA } from "@shared/types/seat"; import { logger } from "@utils/logger"; -import { getPostHogUrl } from "@utils/urls"; +import { getBillingUrl } from "@utils/urls"; import { useEffect, useState } from "react"; const log = logger.scope("plan-usage"); @@ -38,7 +38,7 @@ async function openBillingPage(orgId: string | null): Promise { log.warn("Failed to switch org before opening billing", err); } } - const url = getPostHogUrl("/organization/billing"); + const url = getBillingUrl(); if (url) window.open(url, "_blank"); } @@ -67,10 +67,8 @@ export function PlanUsageSettings() { const { fetchSeat, upgradeToPro, cancelSeat, reactivateSeat, clearError } = useSeatStore(); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); - const billingUrl = getPostHogUrl("/organization/billing", cloudRegion); - const redirectFullUrl = redirectUrl - ? getPostHogUrl(redirectUrl, cloudRegion) - : null; + const billingUrl = getBillingUrl(cloudRegion); + const redirectFullUrl = redirectUrl ? billingUrl : null; const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const isAlpha = seat?.plan_key === PLAN_PRO_ALPHA; diff --git a/apps/code/src/renderer/utils/urls.test.ts b/apps/code/src/renderer/utils/urls.test.ts new file mode 100644 index 000000000..51a875a4e --- /dev/null +++ b/apps/code/src/renderer/utils/urls.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@features/auth/hooks/authQueries", () => ({ + getCachedAuthState: () => ({ cloudRegion: null }), +})); + +import { getBillingUrl, getPostHogUrl } from "./urls"; + +describe("getPostHogUrl", () => { + it("returns null when no region is available", () => { + expect(getPostHogUrl("/foo")).toBeNull(); + }); + + it("joins base and path for us region", () => { + expect(getPostHogUrl("/foo", "us")).toBe("https://us.posthog.com/foo"); + }); + + it("adds a leading slash if missing", () => { + expect(getPostHogUrl("foo", "us")).toBe("https://us.posthog.com/foo"); + }); + + it("uses the eu base for eu region", () => { + expect(getPostHogUrl("/foo", "eu")).toBe("https://eu.posthog.com/foo"); + }); +}); + +describe("getBillingUrl", () => { + it("points at /organization/billing/overview on us", () => { + expect(getBillingUrl("us")).toBe( + "https://us.posthog.com/organization/billing/overview", + ); + }); + + it("points at /organization/billing/overview on eu", () => { + expect(getBillingUrl("eu")).toBe( + "https://eu.posthog.com/organization/billing/overview", + ); + }); + + it("returns null when no region is available", () => { + expect(getBillingUrl()).toBeNull(); + }); + + it("does not produce the malformed double-scheme URL we used to ship", () => { + const url = getBillingUrl("us"); + expect(url).not.toMatch(/https?:\/\/[^/]+\/https?:/); + expect(url).not.toContain("/project/"); + }); +}); diff --git a/apps/code/src/renderer/utils/urls.ts b/apps/code/src/renderer/utils/urls.ts index deaea2581..972e1c580 100644 --- a/apps/code/src/renderer/utils/urls.ts +++ b/apps/code/src/renderer/utils/urls.ts @@ -11,3 +11,9 @@ export function getPostHogUrl( const base = getCloudUrlFromRegion(region); return `${base}${path.startsWith("/") ? path : `/${path}`}`; } + +export function getBillingUrl( + regionOverride?: CloudRegion | null, +): string | null { + return getPostHogUrl("/organization/billing/overview", regionOverride); +} From aec1736b35a15cb6d084d31ae200b7467e2e7829 Mon Sep 17 00:00:00 2001 From: JonathanLab Date: Tue, 5 May 2026 16:14:59 +0200 Subject: [PATCH 2/2] fix(billing): honor backend redirect_url and parameterize tests Address Greptile feedback on PR #2034: - getPostHogUrl now passes absolute URLs through unchanged, so a backend-supplied redirect_url is no longer flattened into the generic billing overview when it points at a specific checkout/payment page. - PlanUsageSettings runs redirectUrl through getPostHogUrl again and falls back to the static billing URL only when the value can't be resolved. - Tests for region variants are parameterized with it.each, plus new coverage for absolute-URL passthrough. Generated-By: PostHog Code Task-Id: 156e6cbd-e2c4-4d62-bd3d-969dc2b57010 --- .../components/sections/PlanUsageSettings.tsx | 6 ++- apps/code/src/renderer/utils/urls.test.ts | 49 ++++++++++--------- apps/code/src/renderer/utils/urls.ts | 5 +- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx index 73e31935a..ab6a5f27b 100644 --- a/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/PlanUsageSettings.tsx @@ -22,7 +22,7 @@ import { import { Tooltip } from "@renderer/components/ui/Tooltip"; import { PLAN_PRO_ALPHA } from "@shared/types/seat"; import { logger } from "@utils/logger"; -import { getBillingUrl } from "@utils/urls"; +import { getBillingUrl, getPostHogUrl } from "@utils/urls"; import { useEffect, useState } from "react"; const log = logger.scope("plan-usage"); @@ -68,7 +68,9 @@ export function PlanUsageSettings() { useSeatStore(); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); const billingUrl = getBillingUrl(cloudRegion); - const redirectFullUrl = redirectUrl ? billingUrl : null; + const redirectFullUrl = redirectUrl + ? (getPostHogUrl(redirectUrl, cloudRegion) ?? billingUrl) + : null; const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); const isAlpha = seat?.plan_key === PLAN_PRO_ALPHA; diff --git a/apps/code/src/renderer/utils/urls.test.ts b/apps/code/src/renderer/utils/urls.test.ts index 51a875a4e..d0d77cf8a 100644 --- a/apps/code/src/renderer/utils/urls.test.ts +++ b/apps/code/src/renderer/utils/urls.test.ts @@ -7,35 +7,40 @@ vi.mock("@features/auth/hooks/authQueries", () => ({ import { getBillingUrl, getPostHogUrl } from "./urls"; describe("getPostHogUrl", () => { - it("returns null when no region is available", () => { + it("returns null when no region is available and the input is a path", () => { expect(getPostHogUrl("/foo")).toBeNull(); }); - it("joins base and path for us region", () => { - expect(getPostHogUrl("/foo", "us")).toBe("https://us.posthog.com/foo"); - }); - - it("adds a leading slash if missing", () => { - expect(getPostHogUrl("foo", "us")).toBe("https://us.posthog.com/foo"); - }); - - it("uses the eu base for eu region", () => { - expect(getPostHogUrl("/foo", "eu")).toBe("https://eu.posthog.com/foo"); + it.each([ + ["us", "/foo", "https://us.posthog.com/foo"], + ["us", "foo", "https://us.posthog.com/foo"], + ["eu", "/foo", "https://eu.posthog.com/foo"], + ] as const)( + "joins base and path for %s region (path=%s)", + (region, path, expected) => { + expect(getPostHogUrl(path, region)).toBe(expected); + }, + ); + + it.each([ + "https://app.posthog.com/organization/billing", + "http://localhost:8000/checkout", + "HTTPS://us.posthog.com/foo", + ])("passes absolute URLs through unchanged: %s", (url) => { + expect(getPostHogUrl(url, "us")).toBe(url); }); }); describe("getBillingUrl", () => { - it("points at /organization/billing/overview on us", () => { - expect(getBillingUrl("us")).toBe( - "https://us.posthog.com/organization/billing/overview", - ); - }); - - it("points at /organization/billing/overview on eu", () => { - expect(getBillingUrl("eu")).toBe( - "https://eu.posthog.com/organization/billing/overview", - ); - }); + it.each([ + ["us", "https://us.posthog.com/organization/billing/overview"], + ["eu", "https://eu.posthog.com/organization/billing/overview"], + ] as const)( + "points at /organization/billing/overview on %s", + (region, expected) => { + expect(getBillingUrl(region)).toBe(expected); + }, + ); it("returns null when no region is available", () => { expect(getBillingUrl()).toBeNull(); diff --git a/apps/code/src/renderer/utils/urls.ts b/apps/code/src/renderer/utils/urls.ts index 972e1c580..81e47d90e 100644 --- a/apps/code/src/renderer/utils/urls.ts +++ b/apps/code/src/renderer/utils/urls.ts @@ -3,13 +3,14 @@ import type { CloudRegion } from "@shared/types/regions"; import { getCloudUrlFromRegion } from "@shared/utils/urls"; export function getPostHogUrl( - path: string, + pathOrUrl: string, regionOverride?: CloudRegion | null, ): string | null { + if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl; const region = regionOverride ?? getCachedAuthState().cloudRegion; if (!region) return null; const base = getCloudUrlFromRegion(region); - return `${base}${path.startsWith("/") ? path : `/${path}`}`; + return `${base}${pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`}`; } export function getBillingUrl(