authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 02:02:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 02:02:50-07:00
logd10621de5f8f7e3af4831be04c0564a4ae65eade
treed9d4a0149fb3206c709c258441b4e0648a2e8599
parent5fc1c5cb4cfa562552fb0a5fc8278b176a765256
signature Signed by PGP key B5690EEEBB952194

fix(mobile): incompatible environments now say whether to update the app or the host (#19331)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

6 files changed, 61 insertions(+), 9 deletions(-)

apps/mobile/src/features/cloud/cloudEnvironmentPresentation.test.ts+1-1
......@@ -48,7 +48,7 @@ describe("available cloud environment presentation", () => {
4848 }),
4949 ).toMatchObject({
5050 connectionState: "unsupported",
51 statusText: "Client not supported",
51 statusText: expect.stringContaining("Update your app"),
5252 });
5353 });
5454
apps/mobile/src/features/cloud/cloudEnvironmentPresentation.ts+1-1
......@@ -27,7 +27,7 @@ export function availableCloudEnvironmentPresentation(input: {
2727 connectionError: compatibilityError.message,
2828 connectionErrorTraceId: null,
2929 connectionState: "unsupported",
30 statusText: "Client not supported",
30 statusText: compatibilityError.message,
3131 };
3232 }
3333 if (input.status?.status === "online") {
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+3-5
......@@ -2,10 +2,7 @@ import { AppActivityIndicator } from "../../components/AppActivityIndicator";
22import { ConnectionTraceId } from "./ConnectionTraceId";
33import { useAuth } from "@clerk/expo";
44import { SymbolView } from "../../components/AppSymbol";
5import {
6 connectionStatusText,
7 type EnvironmentConnectionPhase,
8} from "@t3tools/client-runtime/connection";
5import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
96import {
107 type EnvironmentId,
118 type EnvironmentMachineKind,
......@@ -26,6 +23,7 @@ import { serverEnvironment } from "../../state/server";
2623import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
2724import { hasCloudPublicConfig } from "../cloud/publicConfig";
2825import { ConnectionStatusDot } from "./ConnectionStatusDot";
26import { environmentStatusText } from "./environmentStatusText";
2927import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";
3028
3129interface CloudEnvironmentRowsProps {
......@@ -307,7 +305,7 @@ function CloudEnvironmentRowShell(props: {
307305 const shouldPulse = isRetrying;
308306 const statusText =
309307 props.statusText ??
310 connectionStatusText({
308 environmentStatusText({
311309 phase: props.connectionState,
312310 error: props.connectionError,
313311 traceId: props.connectionErrorTraceId,
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+2-2
......@@ -1,6 +1,5 @@
11import { ConnectionTraceId } from "./ConnectionTraceId";
22import { SymbolView } from "../../components/AppSymbol";
3import { connectionStatusText } from "@t3tools/client-runtime/connection";
43import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";
54import { type EnvironmentId, resolveEnvironmentMachineKind } from "@t3tools/contracts";
65import { useAtomValue } from "@effect/atom-react";
......@@ -20,12 +19,13 @@ import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-typ
2019import { serverEnvironment } from "../../state/server";
2120import { ConnectionFormField } from "./ConnectionFormField";
2221import { ConnectionStatusDot } from "./ConnectionStatusDot";
22import { environmentStatusText } from "./environmentStatusText";
2323
2424function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null {
2525 if (!environment.isEnabled && environment.connectionState !== "unsupported") {
2626 return "Off";
2727 }
28 return connectionStatusText({
28 return environmentStatusText({
2929 phase: environment.connectionState,
3030 error: environment.connectionError,
3131 traceId: environment.connectionErrorTraceId,
apps/mobile/src/features/connection/environmentStatusText.test.ts created+39
......@@ -0,0 +1,39 @@
1import { EnvironmentId, ORCHESTRATION_PROTOCOL_VERSION } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { orchestrationProtocolCompatibilityError } from "@t3tools/client-runtime/connection";
5
6import { environmentStatusText } from "./environmentStatusText";
7
8function compatibilityMessage(serverProtocolVersion: number): string {
9 const error = orchestrationProtocolCompatibilityError({
10 environmentId: EnvironmentId.make("environment-preview"),
11 label: "Studio Mac",
12 platform: { os: "darwin", arch: "arm64" },
13 serverVersion: "1.0.0",
14 orchestrationProtocolVersion: serverProtocolVersion,
15 capabilities: { repositoryIdentity: true },
16 });
17 if (error === null) throw new Error("expected an incompatible protocol");
18 return error.message;
19}
20
21describe("environment status text", () => {
22 it.each([
23 ["a newer server", ORCHESTRATION_PROTOCOL_VERSION + 1, "Update your app"],
24 ["an older server", ORCHESTRATION_PROTOCOL_VERSION - 1, "Update T3 Code on Studio Mac"],
25 ])("tells the user what to update for %s", (_, serverProtocolVersion, guidance) => {
26 const text = environmentStatusText({
27 phase: "unsupported",
28 error: compatibilityMessage(serverProtocolVersion),
29 traceId: null,
30 });
31 expect(text).toContain(guidance);
32 });
33
34 it("falls back to the generic label when there is no detail", () => {
35 expect(environmentStatusText({ phase: "unsupported", error: null, traceId: null })).toBe(
36 "Client not supported",
37 );
38 });
39});
apps/mobile/src/features/connection/environmentStatusText.ts created+15
......@@ -0,0 +1,15 @@
1import {
2 connectionStatusText,
3 type EnvironmentConnectionPresentation,
4} from "@t3tools/client-runtime/connection";
5
6/**
7 * Status line for mobile environment rows. Mobile has no tooltip like web, so
8 * an unsupported environment shows its compatibility detail, which says
9 * whether to update the app or the host.
10 */
11export function environmentStatusText(connection: EnvironmentConnectionPresentation): string {
12 return connection.phase === "unsupported" && connection.error !== null
13 ? connection.error
14 : connectionStatusText(connection);
15}