History

Refine T3 Connect authorization surfaces (#4159)

Julius Marminge committed 3 months ago

Commit 2b180a21 parent 7a820ab5 files · +89 −38Browse files

Unified · Split

5 files
  1. apps/server/src/cloud/cliAuthHtml.test.ts +21 −3
  2. apps/server/src/cloud/cliAuthHtml.ts +50 −35
  3. apps/server/vite.config.ts +3 −0
  4. apps/web/src/branding.logic.ts +4 −0
  5. apps/web/src/branding.test.ts +11 −0
apps/server/src/cloud/cliAuthHtml.test.ts+21−3
@@ -1,13 +1,31 @@
11 import { expect, it } from "@effect/vitest";
22
3−import { renderLoopbackAuthorizationCompleteHtml } from "./cliAuthHtml.ts";
3+import {
4+ renderLoopbackAuthorizationCompleteHtml,
5+ resolveLoopbackAuthorizationStage,
6+} from "./cliAuthHtml.ts";
47
58 it("renders the branded loopback authorization completion page", () => {
69 const html = renderLoopbackAuthorizationCompleteHtml();
710
8− expect(html).toContain("T3 Code");
11+ expect(resolveLoopbackAuthorizationStage()).toBe("dev");
12+ expect(html).toContain("T3 Code (Dev)");
13+ expect(html).toContain('class="stage stage-dev"');
914 expect(html).not.toContain("Secure terminal handoff");
1015 expect(html).toContain("You're connected");
11− expect(html).toContain("return to the terminal");
16+ expect(html).toContain("Return to your terminal");
17+ expect(html).not.toContain('class="next"');
1218 expect(html).toContain('name="viewport"');
19+ expect(html).not.toContain('class="status"');
20+});
21+
22+it("renders the matching header treatment for each release channel", () => {
23+ const nightly = renderLoopbackAuthorizationCompleteHtml("nightly");
24+ const latest = renderLoopbackAuthorizationCompleteHtml("latest");
25+
26+ expect(nightly).toContain("T3 Code (Nightly)");
27+ expect(nightly).toContain('class="stage stage-nightly"');
28+ expect(latest).toContain('<p class="brand">T3 Code</p>');
29+ expect(latest).not.toContain("(Latest)");
30+ expect(latest).toContain('class="stage stage-latest"');
1331 });
apps/server/src/cloud/cliAuthHtml.ts+50−35
@@ -1,4 +1,22 @@
1−export function renderLoopbackAuthorizationCompleteHtml(): string {
1+export type LoopbackAuthorizationStage = "dev" | "nightly" | "latest";
2+
3+declare const __T3CODE_BUILD_CHANNEL__: "nightly" | "latest" | undefined;
4+
5+export function resolveLoopbackAuthorizationStage(): LoopbackAuthorizationStage {
6+ return typeof __T3CODE_BUILD_CHANNEL__ === "undefined" ? "dev" : __T3CODE_BUILD_CHANNEL__;
7+}
8+
9+const stageBrands = {
10+ dev: "T3 Code (Dev)",
11+ nightly: "T3 Code (Nightly)",
12+ latest: "T3 Code",
13+} as const satisfies Record<LoopbackAuthorizationStage, string>;
14+
15+export function renderLoopbackAuthorizationCompleteHtml(
16+ stage: LoopbackAuthorizationStage = resolveLoopbackAuthorizationStage(),
17+): string {
18+ const stageBrand = stageBrands[stage];
19+
220 return `<!doctype html>
321 <html lang="en">
422 <head>
@@ -38,9 +56,16 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { · 33 unmodified lines
3856 overflow: hidden;
3957 padding: 22px 24px;
4058 color: white;
59+ }
60+ .stage-latest {
61+ background:
62+ radial-gradient(circle at 76% 18%, rgba(136, 204, 255, 0.52), transparent 38%),
63+ linear-gradient(135deg, #2468df, #172f82);
64+ }
65+ .stage-dev {
4166 background: linear-gradient(145deg, #5ab8fa 0%, #347ff8 46%, #1939bd 100%);
4267 }
43− .stage::before {
68+ .stage-dev::before {
4469 content: "";
4570 position: absolute;
4671 inset: 0;
@@ -52,6 +77,22 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { · 5 unmodified lines
5277 linear-gradient(90deg, rgba(234, 246, 255, 0.12) 1px, transparent 1px);
5378 background-size: 32px 32px, 32px 32px, 8px 8px, 8px 8px;
5479 }
80+ .stage-nightly {
81+ background:
82+ radial-gradient(22rem 8rem at 78% 18%, rgba(81, 101, 216, 0.42), transparent 58%),
83+ linear-gradient(145deg, #07152f 0%, #151443 52%, #32155b 100%);
84+ }
85+ .stage-nightly::before {
86+ content: "";
87+ position: absolute;
88+ inset: 0;
89+ opacity: 0.78;
90+ background-image:
91+ radial-gradient(circle at 12px 12px, rgba(228, 234, 255, 0.9) 0 1px, transparent 1.5px),
92+ radial-gradient(circle at 38px 28px, rgba(228, 234, 255, 0.58) 0 0.8px, transparent 1.3px),
93+ radial-gradient(circle at 58px 9px, rgba(200, 215, 255, 0.72) 0 0.9px, transparent 1.4px);
94+ background-size: 72px 48px, 96px 64px, 128px 56px;
95+ }
5596 .stage::after {
5697 content: "";
5798 position: absolute;
@@ -63,6 +104,10 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { · 5 unmodified lines
63104 .stage-content {
64105 position: relative;
65106 z-index: 1;
107+ height: 100%;
108+ display: flex;
109+ align-items: center;
110+ justify-content: space-between;
66111 }
67112 .brand {
68113 margin: 0;
@@ -73,21 +118,6 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { · 4 unmodified lines
73118 }
74119 .brand { color: rgba(255, 255, 255, 0.82); }
75120 .content { padding: 30px 32px 34px; }
76− .status {
77− width: 42px;
78− height: 42px;
79− display: grid;
80− place-items: center;
81− margin-top: -52px;
82− margin-bottom: 22px;
83− border: 4px solid white;
84− border-radius: 50%;
85− background: #2065df;
86− color: white;
87− font-size: 22px;
88− font-weight: 700;
89− box-shadow: 0 8px 22px rgba(25, 72, 177, 0.3);
90− }
91121 .eyebrow {
92122 margin: 0 0 8px;
93123 color: #2866cc;
@@ -98,41 +128,26 @@ export function renderLoopbackAuthorizationCompleteHtml(): string { · 4 unmodified lines
98128 }
99129 h1 { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.12; letter-spacing: -0.035em; }
100130 .description { margin: 12px 0 0; color: #646975; font-size: 15px; line-height: 1.6; }
101− .next {
102− margin-top: 24px;
103− padding: 14px 16px;
104− border: 1px solid rgba(23, 25, 31, 0.1);
105− border-radius: 12px;
106− background: #f7f8fa;
107− color: #454a55;
108− font-size: 13px;
109− }
110− .next strong { color: #17191f; }
111131 @media (prefers-color-scheme: dark) {
112132 :root { background: #101115; color: #f1f3f7; }
113133 body { background: radial-gradient(48rem 22rem at 50% -8rem, rgba(55, 102, 210, 0.2), transparent), #101115; }
114134 main { border-color: rgba(255, 255, 255, 0.1); background: rgba(25, 27, 33, 0.96); }
115− .status { border-color: #191b21; }
116135 .eyebrow { color: #77a8ff; }
117136 .description { color: #a8adb8; }
118− .next { border-color: rgba(255, 255, 255, 0.1); background: #20232a; color: #b9bec8; }
119− .next strong { color: #f1f3f7; }
120137 }
121138 </style>
122139 </head>
123140 <body>
124141 <main>
125− <header class="stage">
142+ <header class="stage stage-${stage}" data-stage="${stage}">
126143 <div class="stage-content">
127− <p class="brand">T3 Code</p>
144+ <p class="brand">${stageBrand}</p>
128145 </div>
129146 </header>
130147 <section class="content">
131− <div class="status" aria-hidden="true">✓</div>
132148 <p class="eyebrow">Browser authorization complete</p>
133149 <h1>You're connected</h1>
134− <p class="description">The authorization code was delivered securely to your waiting terminal.</p>
135− <div class="next"><strong>Next:</strong> return to the terminal to finish T3 Connect setup. You can close this window.</div>
150+ <p class="description">Return to your terminal to finish setting up T3 Connect. You can close this window.</p>
136151 </section>
137152 </main>
138153 </body>
apps/server/vite.config.ts+3−0
@@ -3,6 +3,7 @@ import { defineConfig, mergeConfig } from "vite-plus"; · 2 unmodified lines
33
44 import baseConfig from "../../vite.config.ts";
55 import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
6+import packageJson from "./package.json" with { type: "json" };
67
78 const bundledPackagePrefixes = [
89 "@pierre/diffs",
@@ -16,6 +17,7 @@ export function shouldBundleCliDependency(id: string): boolean { · 7 unmodified lines
1617 }
1718
1819 const repoEnv = loadRepoEnv();
20+const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest";
1921
2022 export default mergeConfig(
2123 baseConfig,
@@ -42,6 +44,7 @@ export default mergeConfig( · 20 unmodified lines
4244 js: "#!/usr/bin/env node\n",
4345 },
4446 define: {
47+ __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel),
4548 __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""),
4649 __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify(
4750 repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "",
apps/web/src/branding.logic.ts+4−0
@@ -4,6 +4,10 @@ export function formatAppDisplayName(input: { · 3 unmodified lines
44 readonly baseName: string;
55 readonly stageLabel: string;
66 }): string {
7+ if (input.stageLabel.trim().toLowerCase() === "latest") {
8+ return input.baseName;
9+ }
10+
711 return `${input.baseName} (${input.stageLabel})`;
812 }
913
apps/web/src/branding.test.ts+11−0
@@ -50,6 +50,17 @@ describe("branding", () => { · 49 unmodified lines
5050 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
5151 });
5252
53+ it("does not label the latest hosted app channel", async () => {
54+ vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "latest");
55+
56+ const branding = await import("./branding");
57+
58+ expect(branding.HOSTED_APP_CHANNEL).toBe("latest");
59+ expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Latest");
60+ expect(branding.APP_STAGE_LABEL).toBe("Latest");
61+ expect(branding.APP_DISPLAY_NAME).toBe("T3 Code");
62+ });
63+
5364 it("ignores unknown hosted app channels", async () => {
5465 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");
5566