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";...@@ -21,6 +21,7 @@ import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
21import { useThemeColor } from "../../lib/useThemeColor";21import { useThemeColor } from "../../lib/useThemeColor";
22import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";22import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
23import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";23import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
24import { hasCloudPublicConfig } from "../cloud/publicConfig";
24import { ConnectionStatusDot } from "./ConnectionStatusDot";25import { ConnectionStatusDot } from "./ConnectionStatusDot";
25import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";26import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";
2627
...@@ -42,6 +43,11 @@ interface CloudEnvironmentRowsProps {...@@ -42,6 +43,11 @@ interface CloudEnvironmentRowsProps {
42 * with connect switches, availability status, refresh, and loading/error43 * with connect switches, availability status, refresh, and loading/error
43 * states. Shared between the Settings environments screen and the T3 Connect44 * states. Shared between the Settings environments screen and the T3 Connect
44 * onboarding sheet.45 * 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.
45 */51 */
46export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {52export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
47 // Showcase captures run without a Clerk publishable key, so `ClerkProvider`53 // Showcase captures run without a Clerk publishable key, so `ClerkProvider`
...@@ -50,20 +56,33 @@ export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {...@@ -50,20 +56,33 @@ export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
50 if (props.showcaseSignedIn !== undefined) {56 if (props.showcaseSignedIn !== undefined) {
51 return props.showcaseSignedIn ? <CloudEnvironmentRowsContent {...props} /> : null;57 return props.showcaseSignedIn ? <CloudEnvironmentRowsContent {...props} /> : null;
52 }58 }
59 // No cloud config means no `ClerkProvider` either, so `useAuth` would throw.
60 if (!hasCloudPublicConfig()) {
61 return <ConnectedOnlyCloudEnvironmentRows {...props} />;
62 }
53 return <SignedInCloudEnvironmentRows {...props} />;63 return <SignedInCloudEnvironmentRows {...props} />;
54}64}
5565
56function SignedInCloudEnvironmentRows(props: CloudEnvironmentRowsProps) {66function SignedInCloudEnvironmentRows(props: CloudEnvironmentRowsProps) {
57 const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false });67 const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
58 if (!isSignedIn) return null;68 if (!isSignedIn) return <ConnectedOnlyCloudEnvironmentRows {...props} />;
59 return <CloudEnvironmentRowsContent {...props} />;69 return <CloudEnvironmentRowsContent {...props} />;
60}70}
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) {
63 const controller = useConnectionController();80 const controller = useConnectionController();
64 const iconColor = useThemeColor("--color-icon");81 const iconColor = useThemeColor("--color-icon");
65 const availableCloudEnvironments =82 const discoveryAvailable = props.discoveryAvailable ?? true;
66 props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments;83 const availableCloudEnvironments = discoveryAvailable
84 ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments)
85 : [];
67 const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);86 const [expandedErrorId, setExpandedErrorId] = useState<string | null>(null);
68 const hasCloudRows =87 const hasCloudRows =
69 props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0;88 props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0;
...@@ -89,25 +108,27 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {...@@ -89,25 +108,27 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {
89 {showHeader ? (108 {showHeader ? (
90 <View className="flex-row items-center justify-between px-1">109 <View className="flex-row items-center justify-between px-1">
91 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Connect</Text>110 <Text className="text-sm font-t3-bold uppercase text-foreground-muted">T3 Connect</Text>
92 <Pressable111 {discoveryAvailable ? (
93 accessibilityRole="button"112 <Pressable
94 disabled={controller.relayDiscovery.isRefreshing}113 accessibilityRole="button"
95 onPress={() => {114 disabled={controller.relayDiscovery.isRefreshing}
96 void controller.refreshRelayEnvironments();115 onPress={() => {
97 }}116 void controller.refreshRelayEnvironments();
98 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"117 }}
99 >118 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
100 {controller.relayDiscovery.isRefreshing ? (119 >
101 <ActivityIndicator color={iconColor} size="small" />120 {controller.relayDiscovery.isRefreshing ? (
102 ) : (121 <ActivityIndicator color={iconColor} size="small" />
103 <SymbolView122 ) : (
104 name="arrow.clockwise"123 <SymbolView
105 size={14}124 name="arrow.clockwise"
106 tintColor={iconColor}125 size={14}
107 type="monochrome"126 tintColor={iconColor}
108 />127 type="monochrome"
109 )}128 />
110 </Pressable>129 )}
130 </Pressable>
131 ) : null}
111 </View>132 </View>
112 ) : null}133 ) : null}
113134
...@@ -152,7 +173,9 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {...@@ -152,7 +173,9 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) {
152173
153 {/* Rendered alongside any connected rows — a failed discovery must not174 {/* Rendered alongside any connected rows — a failed discovery must not
154 hide behind an otherwise-healthy list. */}175 hide behind an otherwise-healthy list. */}
155 {controller.relayDiscovery.error && !controller.relayDiscovery.isRefreshing ? (176 {discoveryAvailable &&
177 controller.relayDiscovery.error &&
178 !controller.relayDiscovery.isRefreshing ? (
156 <View collapsable={false} className="gap-3 rounded-[24px] bg-card p-5">179 <View collapsable={false} className="gap-3 rounded-[24px] bg-card p-5">
157 <Text className="text-base font-t3-bold text-foreground">180 <Text className="text-base font-t3-bold text-foreground">
158 Could not load T3 Connect environments181 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";...@@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
88
9import { AppText as Text } from "../../components/AppText";9import { AppText as Text } from "../../components/AppText";
10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { hasCloudPublicConfig } from "../cloud/publicConfig";
12import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";11import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
13import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";12import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";
14import { splitEnvironmentSections } from "../connection/environmentSections";13import { splitEnvironmentSections } from "../connection/environmentSections";
...@@ -161,18 +160,19 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -161,18 +160,19 @@ export function SettingsEnvironmentsRouteScreen() {
161 </View>160 </View>
162 )}161 )}
163162
164 {hasCloudPublicConfig() || SHOWCASE_ENABLED ? (163 {/* Always mounted: already-connected relay environments must stay
165 <CloudEnvironmentRows164 visible (and removable) even when cloud config is missing or the
166 connectedCloudEnvironments={connectedCloudEnvironments}165 user is signed out — the component gates discovery itself. */}
167 onReconnectEnvironment={onReconnectEnvironment}166 <CloudEnvironmentRows
168 {...(SHOWCASE_ENABLED167 connectedCloudEnvironments={connectedCloudEnvironments}
169 ? {168 onReconnectEnvironment={onReconnectEnvironment}
170 showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,169 {...(SHOWCASE_ENABLED
171 showcaseSignedIn: true,170 ? {
172 }171 showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,
173 : {})}172 showcaseSignedIn: true,
174 />173 }
175 ) : null}174 : {})}
175 />
176 </ScrollView>176 </ScrollView>
177 </View>177 </View>
178 );178 );