From 3117428870d3dcd2f1e55976e6a498e1e7c22810 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 6 Aug 2026 18:55:03 -0700 Subject: [PATCH] fix(mobile): keep connected T3 Connect environments visible and removable Registered relay environments disappeared from Settings -> Environments whenever cloud config was missing or the user was signed out, leaving devices connected (and retrying) with no way to see or remove them. Connected rows now always render; only discovery requires a session. Co-Authored-By: Claude Fable 5 --- .../connection/CloudEnvironmentRows.tsx | 71 ++++++++++++------- .../SettingsEnvironmentsRouteScreen.tsx | 26 +++---- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx index 173d093d849..b7cb2837681 100644 --- a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx +++ b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx @@ -21,6 +21,7 @@ import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { useThemeColor } from "../../lib/useThemeColor"; import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation"; +import { hasCloudPublicConfig } from "../cloud/publicConfig"; import { ConnectionStatusDot } from "./ConnectionStatusDot"; import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController"; @@ -42,6 +43,11 @@ interface CloudEnvironmentRowsProps { * with connect switches, availability status, refresh, and loading/error * states. Shared between the Settings environments screen and the T3 Connect * onboarding sheet. + * + * Already-connected relay environments render even without cloud config or a + * signed-in account — they are registered on this device and must stay + * reachable and removable. Only discovery (the available list, refresh, and + * its errors) requires a signed-in session. */ export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) { // Showcase captures run without a Clerk publishable key, so `ClerkProvider` @@ -50,20 +56,33 @@ export function CloudEnvironmentRows(props: CloudEnvironmentRowsProps) { if (props.showcaseSignedIn !== undefined) { return props.showcaseSignedIn ? : null; } + // No cloud config means no `ClerkProvider` either, so `useAuth` would throw. + if (!hasCloudPublicConfig()) { + return ; + } return ; } function SignedInCloudEnvironmentRows(props: CloudEnvironmentRowsProps) { const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); - if (!isSignedIn) return null; + if (!isSignedIn) return ; return ; } -function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) { +function ConnectedOnlyCloudEnvironmentRows(props: CloudEnvironmentRowsProps) { + if (props.connectedCloudEnvironments.length === 0) return null; + return ; +} + +function CloudEnvironmentRowsContent( + props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean }, +) { const controller = useConnectionController(); const iconColor = useThemeColor("--color-icon"); - const availableCloudEnvironments = - props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments; + const discoveryAvailable = props.discoveryAvailable ?? true; + const availableCloudEnvironments = discoveryAvailable + ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments) + : []; const [expandedErrorId, setExpandedErrorId] = useState(null); const hasCloudRows = props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0; @@ -89,25 +108,27 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) { {showHeader ? ( T3 Connect - { - void controller.refreshRelayEnvironments(); - }} - className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50" - > - {controller.relayDiscovery.isRefreshing ? ( - - ) : ( - - )} - + {discoveryAvailable ? ( + { + void controller.refreshRelayEnvironments(); + }} + className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50" + > + {controller.relayDiscovery.isRefreshing ? ( + + ) : ( + + )} + + ) : null} ) : null} @@ -152,7 +173,9 @@ function CloudEnvironmentRowsContent(props: CloudEnvironmentRowsProps) { {/* Rendered alongside any connected rows — a failed discovery must not hide behind an otherwise-healthy list. */} - {controller.relayDiscovery.error && !controller.relayDiscovery.isRefreshing ? ( + {discoveryAvailable && + controller.relayDiscovery.error && + !controller.relayDiscovery.isRefreshing ? ( Could not load T3 Connect environments diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index 93b806f6487..53bbe480646 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; -import { hasCloudPublicConfig } from "../cloud/publicConfig"; import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow"; import { splitEnvironmentSections } from "../connection/environmentSections"; @@ -161,18 +160,19 @@ export function SettingsEnvironmentsRouteScreen() { )} - {hasCloudPublicConfig() || SHOWCASE_ENABLED ? ( - - ) : null} + {/* Always mounted: already-connected relay environments must stay + visible (and removable) even when cloud config is missing or the + user is signed out — the component gates discovery itself. */} + );