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. */}
+
);