authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-07 03:58:26-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 00:58:26-07:00
logb98a0f0d2292d180db0ac7c6ae8ccdbc9f6478f7
tree35bc0e3442303bfd2bc8bb73743a263855f56e70
parent95305c36fa418301183e4750f13b6a525a20cafe
signature Signed by PGP key B5690EEEBB952194

fix(mobile): invisible T3 Connect devices can now be seen and removed (#5563)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

2 files changed, 60 insertions(+), 37 deletions(-)

apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+47-24
......@@ -21,6 +21,7 @@ import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
2121import { useThemeColor } from "../../lib/useThemeColor";
2222import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
2323import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
24import { hasCloudPublicConfig } from "../cloud/publicConfig";
2425import { ConnectionStatusDot } from "./ConnectionStatusDot";
2526import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";
2627
......@@ -42,6 +43,11 @@ interface CloudEnvironmentRowsProps {
4243 * with connect switches, availability status, refresh, and loading/error
4344 * states. Shared between the Settings environments screen and the T3 Connect
4445 * onboarding sheet.
46 *
47 * Already-connected relay environments render even without cloud config or a
48 * signed-in account — they are registered on this device and must stay
49 * reachable and removable. Only discovery (the available list, refresh, and
50 * its errors) requires a signed-in session.
4551 */
4652export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
4753 // Showcase captures run without a Clerk publishable key, so `ClerkProvider`
......@@ -50,20 +56,33 @@ export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
5056 if (props.showcaseSignedIn !== undefined) {
5157 return props.showcaseSignedIn ? <CloudEnvironmentRowsContent {...props} /> : null;
5258 }
59 // No cloud config means no `ClerkProvider` either, so `useAuth` would throw.
60 if (!hasCloudPublicConfig()) {
61 return <ConnectedOnlyCloudEnvironmentRows {...props} />;
62 }
5363 return <SignedInCloudEnvironmentRows {...props} />;
5464}
5565
5666function SignedInCloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
5767 const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
58 if (!isSignedIn) return null;
68 if (!isSignedIn) return <ConnectedOnlyCloudEnvironmentRows {...props} />;
5969 return <CloudEnvironmentRowsContent {...props} />;
6070}
6171
62function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {
72function ConnectedOnlyCloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
73 if (props.connectedCloudEnvironments.length === 0) return null;
74 return <CloudEnvironmentRowsContent {...props} discoveryAvailable={false} />;
75}
76
77function CloudEnvironmentRowsContent(
78 props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean },
79) {
6380 const controller = useConnectionController();
6481 const iconColor = useThemeColor("--color-icon");
65 const availableCloudEnvironments =
66 props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments;
82 const discoveryAvailable = props.discoveryAvailable ?? true;
83 const availableCloudEnvironments = discoveryAvailable
84 ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments)
85 : [];
6786 const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
6887 const hasCloudRows =
6988 props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0;
......@@ -89,25 +108,27 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {
89108 {showHeader ? (
90109 <View className="flex-row items-center justify-between px-1">
91110 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Connect</Text>
92 <Pressable
93 accessibilityRole="button"
94 disabled={controller.relayDiscovery.isRefreshing}
95 onPress={() => {
96 void controller.refreshRelayEnvironments();
97 }}
98 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
99 >
100 {controller.relayDiscovery.isRefreshing ? (
101 <ActivityIndicator color={iconColor} size="small" />
102 ) : (
103 <SymbolView
104 name="arrow.clockwise"
105 size={14}
106 tintColor={iconColor}
107 type="monochrome"
108 />
109 )}
110 </Pressable>
111 {discoveryAvailable ? (
112 <Pressable
113 accessibilityRole="button"
114 disabled={controller.relayDiscovery.isRefreshing}
115 onPress={() => {
116 void controller.refreshRelayEnvironments();
117 }}
118 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
119 >
120 {controller.relayDiscovery.isRefreshing ? (
121 <ActivityIndicator color={iconColor} size="small" />
122 ) : (
123 <SymbolView
124 name="arrow.clockwise"
125 size={14}
126 tintColor={iconColor}
127 type="monochrome"
128 />
129 )}
130 </Pressable>
131 ) : null}
111132 </View>
112133 ) : null}
113134
......@@ -152,7 +173,9 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {
152173
153174 {/* Rendered alongside any connected rows — a failed discovery must not
154175 hide behind an otherwise-healthy list. */}
155 {controller.relayDiscovery.error && !controller.relayDiscovery.isRefreshing ? (
176 {discoveryAvailable &&
177 controller.relayDiscovery.error &&
178 !controller.relayDiscovery.isRefreshing ? (
156179 <View collapsable={false} className="gap-3 rounded-[24px] bg-card p-5">
157180 <Text className="text-base font-t3-bold text-foreground">
158181 Could not load T3 Connect environments
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+13-13
......@@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
88
99import { AppText as Text } from "../../components/AppText";
1010import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { hasCloudPublicConfig } from "../cloud/publicConfig";
1211import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
1312import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";
1413import { splitEnvironmentSections } from "../connection/environmentSections";
......@@ -161,18 +160,19 @@ export function SettingsEnvironmentsRouteScreen() {
161160 </View>
162161 )}
163162
164 {hasCloudPublicConfig() || SHOWCASE_ENABLED ? (
165 <CloudEnvironmentRows
166 connectedCloudEnvironments={connectedCloudEnvironments}
167 onReconnectEnvironment={onReconnectEnvironment}
168 {...(SHOWCASE_ENABLED
169 ? {
170 showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,
171 showcaseSignedIn: true,
172 }
173 : {})}
174 />
175 ) : null}
163 {/* Always mounted: already-connected relay environments must stay
164 visible (and removable) even when cloud config is missing or the
165 user is signed out — the component gates discovery itself. */}
166 <CloudEnvironmentRows
167 connectedCloudEnvironments={connectedCloudEnvironments}
168 onReconnectEnvironment={onReconnectEnvironment}
169 {...(SHOWCASE_ENABLED
170 ? {
171 showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,
172 showcaseSignedIn: true,
173 }
174 : {})}
175 />
176176 </ScrollView>
177177 </View>
178178 );