diff --git a/apps/server/src/cloud/cliAuthHtml.test.ts b/apps/server/src/cloud/cliAuthHtml.test.ts index a49eaffeda7..1104927b980 100644 --- a/apps/server/src/cloud/cliAuthHtml.test.ts +++ b/apps/server/src/cloud/cliAuthHtml.test.ts @@ -1,13 +1,31 @@ import { expect, it } from "@effect/vitest"; -import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts"; +import { + renderLoopbackAuthorizationCompleteHtml, + resolveLoopbackAuthorizationStage, +} from "./cliAuthHtml.ts"; it("renders the branded loopback authorization completion page", () => { const html = renderLoopbackAuthorizationCompleteHtml(); - expect(html).toContain("T3 Code"); + expect(resolveLoopbackAuthorizationStage()).toBe("dev"); + expect(html).toContain("T3 Code (Dev)"); + expect(html).toContain('class="stage stage-dev"'); expect(html).not.toContain("Secure terminal handoff"); expect(html).toContain("You're connected"); - expect(html).toContain("return to the terminal"); + expect(html).toContain("Return to your terminal"); + expect(html).not.toContain('class="next"'); expect(html).toContain('name="viewport"'); + expect(html).not.toContain('class="status"'); +}); + +it("renders the matching header treatment for each release channel", () => { + const nightly = renderLoopbackAuthorizationCompleteHtml("nightly"); + const latest = renderLoopbackAuthorizationCompleteHtml("latest"); + + expect(nightly).toContain("T3 Code (Nightly)"); + expect(nightly).toContain('class="stage stage-nightly"'); + expect(latest).toContain('

T3 Code

'); + expect(latest).not.toContain("(Latest)"); + expect(latest).toContain('class="stage stage-latest"'); }); diff --git a/apps/server/src/cloud/cliAuthHtml.ts b/apps/server/src/cloud/cliAuthHtml.ts index 3c4141fba44..5a22a25993a 100644 --- a/apps/server/src/cloud/cliAuthHtml.ts +++ b/apps/server/src/cloud/cliAuthHtml.ts @@ -1,4 +1,22 @@ -export function renderLoopbackAuthorizationCompleteHtml(): string { +export type LoopbackAuthorizationStage = "dev" | "nightly" | "latest"; + +declare const __T3CODE_BUILD_CHANNEL__: "nightly" | "latest" | undefined; + +export function resolveLoopbackAuthorizationStage(): LoopbackAuthorizationStage { + return typeof __T3CODE_BUILD_CHANNEL__ === "undefined" ? "dev" : __T3CODE_BUILD_CHANNEL__; +} + +const stageBrands = { + dev: "T3 Code (Dev)", + nightly: "T3 Code (Nightly)", + latest: "T3 Code", +} as const satisfies Record; + +export function renderLoopbackAuthorizationCompleteHtml( + stage: LoopbackAuthorizationStage = resolveLoopbackAuthorizationStage(), +): string { + const stageBrand = stageBrands[stage]; + return ` @@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { overflow: hidden; padding: 22px 24px; color: white; + } + .stage-latest { + background: + radial-gradient(circle at 76% 18%, rgba(136, 204, 255, 0.52), transparent 38%), + linear-gradient(135deg, #2468df, #172f82); + } + .stage-dev { background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%); } - .stage::before { + .stage-dev::before { content: ""; position: absolute; inset: 0; @@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px); background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px; } + .stage-nightly { + background: + radial-gradient(22rem 8rem at 78% 18%, rgba(81, 101, 216, 0.42), transparent 58%), + linear-gradient(145deg, #07152f 0%, #151443 52%, #32155b 100%); + } + .stage-nightly::before { + content: ""; + position: absolute; + inset: 0; + opacity: 0.78; + background-image: + radial-gradient(circle at 12px 12px, rgba(228, 234, 255, 0.9) 0 1px, transparent 1.5px), + radial-gradient(circle at 38px 28px, rgba(228, 234, 255, 0.58) 0 0.8px, transparent 1.3px), + radial-gradient(circle at 58px 9px, rgba(200, 215, 255, 0.72) 0 0.9px, transparent 1.4px); + background-size: 72px 48px, 96px 64px, 128px 56px; + } .stage::after { content: ""; position: absolute; @@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { .stage-content { position: relative; z-index: 1; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; } .brand { margin: 0; @@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { } .brand { color: rgba(255, 255, 255, 0.82); } .content { padding: 30px 32px 34px; } - .status { - width: 42px; - height: 42px; - display: grid; - place-items: center; - margin-top: -52px; - margin-bottom: 22px; - border: 4px solid white; - border-radius: 50%; - background: #2065df; - color: white; - font-size: 22px; - font-weight: 700; - box-shadow: 0 8px 22px rgba(25, 72, 177, 0.3); - } .eyebrow { margin: 0 0 8px; color: #2866cc; @@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { } h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; } .description { margin: 12px 0 0; color: #646975; font-size: 15px; line-height: 1.6; } - .next { - margin-top: 24px; - padding: 14px 16px; - border: 1px solid rgba(23, 25, 31, 0.1); - border-radius: 12px; - background: #f7f8fa; - color: #454a55; - font-size: 13px; - } - .next strong { color: #17191f; } @media (prefers-color-scheme: dark) { :root { background: #101115; color: #f1f3f7; } body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; } main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); } - .status { border-color: #191b21; } .eyebrow { color: #77a8ff; } .description { color: #a8adb8; } - .next { border-color: rgba(255, 255, 255, 0.1); background: #20232a; color: #b9bec8; } - .next strong { color: #f1f3f7; } }
-
+
-

T3 Code

+

${stageBrand}

-

Browser authorization complete

You're connected

-

The authorization code was delivered securely to your waiting terminal.

- +

Return to your terminal to finish setting up T3 Connect. You can close this window.

diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 473df069ed7..521654f3279 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus"; import baseConfig from "../../vite.config.ts"; import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; +import packageJson from "./package.json" with { type: "json" }; const bundledPackagePrefixes = [ "@pierre/diffs", @@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean { } const repoEnv = loadRepoEnv(); +const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest"; export default mergeConfig( baseConfig, @@ -42,6 +44,7 @@ export default mergeConfig( js: "#!/usr/bin/env node\n", }, define: { + __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel), __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""), __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify( repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "", diff --git a/apps/web/src/branding.logic.ts b/apps/web/src/branding.logic.ts index b87276f1b9c..056fbb76e6a 100644 --- a/apps/web/src/branding.logic.ts +++ b/apps/web/src/branding.logic.ts @@ -4,6 +4,10 @@ export function formatAppDisplayName(input: { readonly baseName: string; readonly stageLabel: string; }): string { + if (input.stageLabel.trim().toLowerCase() === "latest") { + return input.baseName; + } + return `${input.baseName} (${input.stageLabel})`; } diff --git a/apps/web/src/branding.test.ts b/apps/web/src/branding.test.ts index 4aa969c0279..e1c87bcf059 100644 --- a/apps/web/src/branding.test.ts +++ b/apps/web/src/branding.test.ts @@ -50,6 +50,17 @@ describe("branding", () => { expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)"); }); + it("does not label the latest hosted app channel", async () => { + vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "latest"); + + const branding = await import("./branding"); + + expect(branding.HOSTED_APP_CHANNEL).toBe("latest"); + expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest"); + expect(branding.APP_STAGE_LABEL).toBe("Latest"); + expect(branding.APP_DISPLAY_NAME).toBe("T3 Code"); + }); + it("ignores unknown hosted app channels", async () => { vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");