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..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 { getPostHogUrl } from "@utils/urls"; +import { getBillingUrl, getPostHogUrl } 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,9 +67,9 @@ export function PlanUsageSettings() { const { fetchSeat, upgradeToPro, cancelSeat, reactivateSeat, clearError } = useSeatStore(); const cloudRegion = useAuthStateValue((state) => state.cloudRegion); - const billingUrl = getPostHogUrl("/organization/billing", cloudRegion); + const billingUrl = getBillingUrl(cloudRegion); const redirectFullUrl = redirectUrl - ? getPostHogUrl(redirectUrl, cloudRegion) + ? (getPostHogUrl(redirectUrl, cloudRegion) ?? billingUrl) : null; const [showUpgradeDialog, setShowUpgradeDialog] = useState(false); 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..d0d77cf8a --- /dev/null +++ b/apps/code/src/renderer/utils/urls.test.ts @@ -0,0 +1,54 @@ +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 and the input is a path", () => { + expect(getPostHogUrl("/foo")).toBeNull(); + }); + + 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.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(); + }); + + 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..81e47d90e 100644 --- a/apps/code/src/renderer/utils/urls.ts +++ b/apps/code/src/renderer/utils/urls.ts @@ -3,11 +3,18 @@ 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( + regionOverride?: CloudRegion | null, +): string | null { + return getPostHogUrl("/organization/billing/overview", regionOverride); }