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", () => {...@@ -48,7 +48,7 @@ describe("available cloud environment presentation", () => {
48 }),48 }),
49 ).toMatchObject({49 ).toMatchObject({
50 connectionState: "unsupported",50 connectionState: "unsupported",
51 statusText: "Client not supported",51 statusText: expect.stringContaining("Update your app"),
52 });52 });
53 });53 });
5454
apps/mobile/src/features/cloud/cloudEnvironmentPresentation.ts+1-1
...@@ -27,7 +27,7 @@ export function availableCloudEnvironmentPresentation(input: {...@@ -27,7 +27,7 @@ export function availableCloudEnvironmentPresentation(input: {
27 connectionError: compatibilityError.message,27 connectionError: compatibilityError.message,
28 connectionErrorTraceId: null,28 connectionErrorTraceId: null,
29 connectionState: "unsupported",29 connectionState: "unsupported",
30 statusText: "Client not supported",30 statusText: compatibilityError.message,
31 };31 };
32 }32 }
33 if (input.status?.status === "online") {33 if (input.status?.status === "online") {
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+3-5
...@@ -2,10 +2,7 @@ import { AppActivityIndicator } from "../../components/AppActivityIndicator";...@@ -2,10 +2,7 @@ import { AppActivityIndicator } from "../../components/AppActivityIndicator";
2import { ConnectionTraceId } from "./ConnectionTraceId";2import { ConnectionTraceId } from "./ConnectionTraceId";
3import { useAuth } from "@clerk/expo";3import { useAuth } from "@clerk/expo";
4import { SymbolView } from "../../components/AppSymbol";4import { SymbolView } from "../../components/AppSymbol";
5import {5import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection";
6 connectionStatusText,
7 type EnvironmentConnectionPhase,
8} from "@t3tools/client-runtime/connection";
9import {6import {
10 type EnvironmentId,7 type EnvironmentId,
11 type EnvironmentMachineKind,8 type EnvironmentMachineKind,
...@@ -26,6 +23,7 @@ import { serverEnvironment } from "../../state/server";...@@ -26,6 +23,7 @@ import { serverEnvironment } from "../../state/server";
26import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";23import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
27import { hasCloudPublicConfig } from "../cloud/publicConfig";24import { hasCloudPublicConfig } from "../cloud/publicConfig";
28import { ConnectionStatusDot } from "./ConnectionStatusDot";25import { ConnectionStatusDot } from "./ConnectionStatusDot";
26import { environmentStatusText } from "./environmentStatusText";
29import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";27import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";
3028
31interface CloudEnvironmentRowsProps {29interface CloudEnvironmentRowsProps {
...@@ -307,7 +305,7 @@ function CloudEnvironmentRowShell(props: {...@@ -307,7 +305,7 @@ function CloudEnvironmentRowShell(props: {
307 const shouldPulse = isRetrying;305 const shouldPulse = isRetrying;
308 const statusText =306 const statusText =
309 props.statusText ??307 props.statusText ??
310 connectionStatusText({308 environmentStatusText({
311 phase: props.connectionState,309 phase: props.connectionState,
312 error: props.connectionError,310 error: props.connectionError,
313 traceId: props.connectionErrorTraceId,311 traceId: props.connectionErrorTraceId,
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+2-2
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { ConnectionTraceId } from "./ConnectionTraceId";1import { ConnectionTraceId } from "./ConnectionTraceId";
2import { SymbolView } from "../../components/AppSymbol";2import { SymbolView } from "../../components/AppSymbol";
3import { connectionStatusText } from "@t3tools/client-runtime/connection";
4import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";3import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";
5import { type EnvironmentId, resolveEnvironmentMachineKind } from "@t3tools/contracts";4import { type EnvironmentId, resolveEnvironmentMachineKind } from "@t3tools/contracts";
6import { useAtomValue } from "@effect/atom-react";5import { useAtomValue } from "@effect/atom-react";
...@@ -20,12 +19,13 @@ import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-typ...@@ -20,12 +19,13 @@ import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-typ
20import { serverEnvironment } from "../../state/server";19import { serverEnvironment } from "../../state/server";
21import { ConnectionFormField } from "./ConnectionFormField";20import { ConnectionFormField } from "./ConnectionFormField";
22import { ConnectionStatusDot } from "./ConnectionStatusDot";21import { ConnectionStatusDot } from "./ConnectionStatusDot";
22import { environmentStatusText } from "./environmentStatusText";
2323
24function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null {24function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null {
25 if (!environment.isEnabled && environment.connectionState !== "unsupported") {25 if (!environment.isEnabled && environment.connectionState !== "unsupported") {
26 return "Off";26 return "Off";
27 }27 }
28 return connectionStatusText({28 return environmentStatusText({
29 phase: environment.connectionState,29 phase: environment.connectionState,
30 error: environment.connectionError,30 error: environment.connectionError,
31 traceId: environment.connectionErrorTraceId,31 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}