Skip to content

Commit b7a9219

Browse files
t3dotggclaude
andcommitted
feat(web): point Nightly users to the beta mobile app
Nightly runs orchestrator V2, and the App Store and Google Play apps are still V1. Show a one-time toast on Nightly builds and add a Mobile app row to Settings > General > About with TestFlight and Google Play beta QR codes. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent db51460 commit b7a9219

4 files changed

Lines changed: 175 additions & 5 deletions

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { parseSemver } from "@t3tools/shared/semver";
2+
import { useNavigate } from "@tanstack/react-router";
3+
import * as Schema from "effect/Schema";
4+
import { SmartphoneIcon } from "lucide-react";
5+
import { useEffect } from "react";
6+
7+
import { APP_VERSION, HOSTED_APP_CHANNEL } from "../branding";
8+
import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
9+
import { getLocalStorageItem, setLocalStorageItem } from "../hooks/useLocalStorage";
10+
import { AndroidIcon, AppleIcon } from "./Icons";
11+
import { SettingsRow } from "./settings/settingsLayout";
12+
import { Button } from "./ui/button";
13+
import { Popover, PopoverPopup, PopoverTitle, PopoverTrigger } from "./ui/popover";
14+
import { QRCodeSvg } from "./ui/qr-code";
15+
import { toastManager } from "./ui/toast";
16+
17+
// Orchestrator V2 ships to Nightly first. The App Store and Google Play apps are
18+
// still V1 and cannot connect to a V2 server, so Nightly users need the V2 beta
19+
// app. Delete this file when the store apps move to V2. See #14871.
20+
21+
/** True on Nightly desktop, `npx t3@nightly`, and the hosted Nightly app. */
22+
export const IS_NIGHTLY_BUILD =
23+
parseSemver(APP_VERSION)?.prerelease[0] === "nightly" || HOSTED_APP_CHANNEL === "nightly";
24+
25+
const IOS_TESTFLIGHT_URL = "https://testflight.apple.com/join/XgaxaRtd";
26+
const ANDROID_BETA_GROUP_URL = "https://groups.google.com/g/t3-code-v2-beta";
27+
const ANDROID_PLAY_TESTING_URL = "https://play.google.com/apps/testing/com.t3tools.t3code";
28+
29+
const ROW_ID = "nightly-mobile-beta";
30+
const NOTICE_DISMISSED_STORAGE_KEY = "t3code:nightly-mobile-beta-notice-dismissed:v1";
31+
32+
// Guards against a second toast from a remount or a Strict Mode effect replay.
33+
let noticeShown = false;
34+
35+
function isNoticeDismissed(): boolean {
36+
try {
37+
return getLocalStorageItem(NOTICE_DISMISSED_STORAGE_KEY, Schema.Boolean) === true;
38+
} catch {
39+
return false;
40+
}
41+
}
42+
43+
function dismissNotice() {
44+
try {
45+
setLocalStorageItem(NOTICE_DISMISSED_STORAGE_KEY, true, Schema.Boolean);
46+
} catch {
47+
// Storage is unavailable. The notice shows again on the next launch.
48+
}
49+
}
50+
51+
/**
52+
* One-time Nightly toast that points to the beta mobile app. Any close (the
53+
* corner button, swipe, Dismiss, or the action) hides it for good on this client.
54+
*/
55+
export function NightlyMobileBetaNotice() {
56+
const navigate = useNavigate();
57+
58+
useEffect(() => {
59+
if (!IS_NIGHTLY_BUILD || noticeShown || isNoticeDismissed()) return;
60+
noticeShown = true;
61+
const toastId = toastManager.add({
62+
title: "Nightly needs the beta mobile app",
63+
description:
64+
"Nightly uses the new orchestrator. The App Store and Google Play versions of T3 Code cannot connect to it.",
65+
timeout: 0,
66+
onClose: dismissNotice,
67+
actionProps: {
68+
children: "Get the beta app",
69+
onClick: () => {
70+
toastManager.close(toastId);
71+
void navigate({ to: "/settings/general", hash: ROW_ID });
72+
},
73+
},
74+
data: {
75+
leadingIcon: <SmartphoneIcon className="size-4" />,
76+
actionLayout: "stacked-end",
77+
secondaryActionProps: {
78+
children: "Dismiss",
79+
onClick: () => toastManager.close(toastId),
80+
},
81+
secondaryActionVariant: "ghost",
82+
},
83+
});
84+
}, [navigate]);
85+
86+
return null;
87+
}
88+
89+
/** QR code plus a copy button for one beta link. */
90+
function BetaLinkQr({ url, label }: { url: string; label: string }) {
91+
const { copyToClipboard, isCopied } = useCopyToClipboard();
92+
return (
93+
<div className="flex flex-col items-center gap-2">
94+
<div className="rounded-lg bg-white p-1.5">
95+
<QRCodeSvg value={url} size={128} level="M" marginSize={1} title={label} />
96+
</div>
97+
<Button size="xs" variant="outline" onClick={() => copyToClipboard(url)}>
98+
{isCopied ? "Copied" : "Copy link"}
99+
</Button>
100+
</div>
101+
);
102+
}
103+
104+
/** Settings → General → About row with the beta app links. Render it only for Nightly. */
105+
export function NightlyMobileBetaRow() {
106+
return (
107+
<SettingsRow
108+
id={ROW_ID}
109+
title="Mobile app"
110+
description="Nightly needs the beta app. The App Store and Google Play versions cannot connect."
111+
control={
112+
<div className="flex items-center gap-2">
113+
<Popover>
114+
<PopoverTrigger render={<Button size="sm" variant="outline" />}>
115+
<AppleIcon />
116+
iPhone
117+
</PopoverTrigger>
118+
<PopoverPopup align="end">
119+
<div className="flex flex-col gap-3">
120+
<div className="space-y-1">
121+
<PopoverTitle>TestFlight beta</PopoverTitle>
122+
<p className="text-xs text-muted-foreground">Scan with your iPhone camera.</p>
123+
</div>
124+
<BetaLinkQr url={IOS_TESTFLIGHT_URL} label="TestFlight beta link" />
125+
</div>
126+
</PopoverPopup>
127+
</Popover>
128+
<Popover>
129+
<PopoverTrigger render={<Button size="sm" variant="outline" />}>
130+
<AndroidIcon />
131+
Android
132+
</PopoverTrigger>
133+
<PopoverPopup align="end">
134+
<div className="flex flex-col gap-3">
135+
<div className="space-y-1">
136+
<PopoverTitle>Google Play beta</PopoverTitle>
137+
<p className="max-w-72 text-xs text-muted-foreground">
138+
Use the same Google account for both steps. Step 2 can take up to an hour to
139+
work after you join the group.
140+
</p>
141+
</div>
142+
<div className="grid grid-cols-2 gap-4">
143+
<div className="space-y-2">
144+
<p className="text-center text-xs font-medium">1. Join the group</p>
145+
<BetaLinkQr url={ANDROID_BETA_GROUP_URL} label="Android beta group link" />
146+
</div>
147+
<div className="space-y-2">
148+
<p className="text-center text-xs font-medium">2. Become a tester</p>
149+
<BetaLinkQr url={ANDROID_PLAY_TESTING_URL} label="Google Play beta link" />
150+
</div>
151+
</div>
152+
</div>
153+
</PopoverPopup>
154+
</Popover>
155+
</div>
156+
}
157+
/>
158+
);
159+
}

‎apps/web/src/components/settings/SettingsPanels.tsx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import * as Duration from "effect/Duration";
5252
import * as Equal from "effect/Equal";
5353
import * as Schema from "effect/Schema";
5454
import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding";
55+
import { IS_NIGHTLY_BUILD, NightlyMobileBetaRow } from "../NightlyMobileBeta";
5556
import {
5657
canCheckForUpdate,
5758
getDesktopUpdateButtonTooltip,
@@ -282,6 +283,9 @@ function AboutVersionSection() {
282283
const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge);
283284
const selectedUpdateChannel = updateState?.channel ?? "latest";
284285
const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL;
286+
// Show the beta app links as soon as someone picks Nightly, before the update installs.
287+
const showNightlyMobileBeta =
288+
IS_NIGHTLY_BUILD || (hasDesktopBridge && selectedUpdateChannel === "nightly");
285289

286290
const handleUpdateChannelChange = useCallback(
287291
(channel: DesktopUpdateChannel) => {
@@ -502,6 +506,7 @@ function AboutVersionSection() {
502506
}
503507
/>
504508
) : null}
509+
{showNightlyMobileBeta ? <NightlyMobileBetaRow /> : null}
505510
</>
506511
);
507512
}
@@ -3329,10 +3334,13 @@ export function GeneralSettingsPanel() {
33293334
{isElectron || HOSTED_APP_CHANNEL ? (
33303335
<AboutVersionSection />
33313336
) : (
3332-
<SettingsRow
3333-
title={<AboutVersionTitle />}
3334-
description="Current version of the application."
3335-
/>
3337+
<>
3338+
<SettingsRow
3339+
title={<AboutVersionTitle />}
3340+
description="Current version of the application."
3341+
/>
3342+
{IS_NIGHTLY_BUILD ? <NightlyMobileBetaRow /> : null}
3343+
</>
33363344
)}
33373345
</SettingsSection>
33383346
<SettingsSection title="Diagnostics">

‎apps/web/src/routes/__root.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { SnapShotCoordinator } from "../components/desktop/SnapShotCoordinator";
2828
import { DesktopAppActivationCoordinator } from "../components/desktop/DesktopAppActivationCoordinator";
2929
import { RunningThreadKeepAlive } from "../components/desktop/RunningThreadKeepAlive";
3030
import { ProviderUpdateLaunchNotification } from "../components/ProviderUpdateLaunchNotification";
31+
import { NightlyMobileBetaNotice } from "../components/NightlyMobileBeta";
3132
import { LegacyThreadMigrationToast } from "../components/LegacyThreadMigrationToast";
3233
import { ThreadNotificationCoordinator } from "../components/ThreadNotificationCoordinator";
3334
import { ProjectCloneToastCoordinator } from "../components/ProjectCloneToastCoordinator";
@@ -242,6 +243,7 @@ function RootRouteView() {
242243
<EventRouter skipInitialBootstrapNavigation={returningFromWelcomeRef.current} />
243244
) : null}
244245
{primaryEnvironmentAuthenticated ? <ProviderUpdateLaunchNotification /> : null}
246+
{primaryEnvironmentAuthenticated ? <NightlyMobileBetaNotice /> : null}
245247
{appShell}
246248
{/* Above the router: a theme draft is judged by walking the app, so the
247249
editor has to survive navigation away from settings. */}

‎docs/user/install.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ command cannot reach the app, start or update the desktop app and try again.
9595
Install T3 Code from the
9696
[App Store](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824) or
9797
[Google Play](https://play.google.com/store/apps/details?id=com.t3tools.t3code).
98-
The phone connects to a server on another machine. Follow
98+
Nightly builds need the beta app instead. Get its links in **Settings → General → Mobile app** on
99+
a Nightly build. The phone connects to a server on another machine. Follow
99100
[remote access](./remote-access.md) to link it through T3 Connect or a pairing URL.
100101

101102
If the app crashes during launch, open Settings → Diagnostics on the next launch

0 commit comments

Comments
 (0)