import { useAuth } from "@clerk/expo"; import { Stack, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import { connectionStatusText, type EnvironmentConnectionPhase, } from "@t3tools/client-runtime/connection"; import type { EnvironmentId } from "@t3tools/contracts"; import { useCallback, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, Switch, type NativeSyntheticEvent, type TextLayoutEventData, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; import { type RelayEnvironmentView, useConnectionController, } from "../../features/connection/useConnectionController"; import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; import { availableCloudEnvironmentPresentation } from "../../features/cloud/cloudEnvironmentPresentation"; import { ConnectionEnvironmentRow } from "../../features/connection/ConnectionEnvironmentRow"; import { ConnectionStatusDot } from "../../features/connection/ConnectionStatusDot"; import { splitEnvironmentSections } from "../../features/connection/environmentSections"; import { cn } from "../../lib/cn"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { useThemeColor } from "../../lib/useThemeColor"; import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; export default function SettingsEnvironmentsRouteScreen() { const { connectedEnvironments, onReconnectEnvironment, onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); const router = useRouter(); const insets = useSafeAreaInsets(); const { localEnvironments, connectedCloudEnvironments } = splitEnvironmentSections({ connectedEnvironments, cloudEnvironments: null, }); const hasLocalEnvironments = localEnvironments.length > 0; const [expandedId, setExpandedId] = useState(null); const accentColor = useThemeColor("--color-icon-muted"); const handleToggle = useCallback((environmentId: EnvironmentId) => { setExpandedId((prev) => (prev === environmentId ? null : environmentId)); }, []); return ( router.push("/settings/environment-new")} separateBackground /> {hasLocalEnvironments ? ( {localEnvironments.map((environment, index) => ( handleToggle(environment.environmentId)} onReconnect={onReconnectEnvironment} onRemove={onRemoveEnvironmentPress} onUpdate={onUpdateEnvironment} /> ))} ) : ( No environments connected yet.{"\n"}Tap{" "} + to add one. )} {hasCloudPublicConfig() ? ( ) : null} ); } function ConfiguredCloudEnvironmentRows(props: { readonly connectedCloudEnvironments: ReadonlyArray; readonly onReconnectEnvironment: (environmentId: EnvironmentId) => void; }) { const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const controller = useConnectionController(); const iconColor = useThemeColor("--color-icon"); const availableCloudEnvironments = controller.availableRelayEnvironments; const [expandedErrorId, setExpandedErrorId] = useState(null); const hasCloudRows = props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0; const handleConnectCloudEnvironment = useCallback( (entry: RelayEnvironmentView) => controller.connectRelayEnvironment(entry.environment), [controller], ); const handleDisconnectCloudEnvironment = useCallback( (environmentId: EnvironmentId) => controller.removeEnvironment(environmentId), [controller], ); const handleToggleCloudError = useCallback((environmentId: string) => { setExpandedErrorId((current) => (current === environmentId ? null : environmentId)); }, []); if (!isSignedIn) return null; return ( T3 Cloud { 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 ? ( ) : ( )} {hasCloudRows ? ( {props.connectedCloudEnvironments.map((environment, index) => ( props.onReconnectEnvironment(environment.environmentId)} onDisconnect={() => handleDisconnectCloudEnvironment(environment.environmentId)} errorExpanded={expandedErrorId === environment.environmentId} onToggleError={() => handleToggleCloudError(environment.environmentId)} /> ))} {availableCloudEnvironments.map((environment, index) => ( 0 || index !== 0} onConnect={() => handleConnectCloudEnvironment(environment)} errorExpanded={expandedErrorId === environment.environment.environmentId} onToggleError={() => handleToggleCloudError(environment.environment.environmentId)} /> ))} ) : controller.relayDiscovery.isRefreshing ? ( Loading linked cloud environments. ) : controller.relayDiscovery.error ? ( Could not load T3 Cloud environments {controller.relayDiscovery.error} {controller.relayDiscovery.errorTraceId ? ( ) : null} ) : ( No additional linked cloud environments. )} ); } function ConnectedCloudEnvironmentRow(props: { readonly environment: ConnectedEnvironmentSummary; readonly borderTop: boolean; readonly errorExpanded: boolean; readonly onConnect: () => void; readonly onDisconnect: () => void; readonly onToggleError: () => void; }) { return ( { if (enabled) { props.onConnect(); return; } props.onDisconnect(); }} onToggleError={props.onToggleError} value={props.environment.connectionState !== "available"} /> ); } function CloudEnvironmentRow(props: { readonly environment: RelayEnvironmentView; readonly borderTop: boolean; readonly errorExpanded: boolean; readonly onConnect: () => void; readonly onToggleError: () => void; }) { const presentation = availableCloudEnvironmentPresentation({ isStatusPending: props.environment.availability === "checking", status: props.environment.status, statusError: props.environment.error, statusErrorTraceId: props.environment.traceId, }); return ( { if (enabled) { props.onConnect(); } }} onToggleError={props.onToggleError} statusText={presentation.statusText} value={false} /> ); } function CloudEnvironmentRowShell(props: { readonly borderTop: boolean; readonly connectionError: string | null; readonly connectionErrorTraceId: string | null; readonly connectionState: EnvironmentConnectionPhase; readonly disabled?: boolean; readonly errorExpanded: boolean; readonly label: string; readonly onToggleError: () => void; readonly onValueChange: (enabled: boolean) => void; readonly statusText?: string; readonly value: boolean; }) { const activeTrack = String(useThemeColor("--color-switch-active")); const track = String(useThemeColor("--color-secondary-border")); const chevron = useThemeColor("--color-chevron"); const isRetrying = props.connectionState === "connecting" || props.connectionState === "reconnecting"; const shouldPulse = isRetrying; const statusText = props.statusText ?? connectionStatusText({ phase: props.connectionState, error: props.connectionError, traceId: props.connectionErrorTraceId, }); const statusClassName = props.connectionError ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted"; const [errorMeasurement, setErrorMeasurement] = useState<{ readonly text: string; readonly lineCount: number; } | null>(null); const errorTraceId = props.connectionErrorTraceId; const measuredErrorText = errorTraceId ? `${statusText} Trace ID: ${errorTraceId}` : statusText; const errorLineCount = errorMeasurement?.text === measuredErrorText ? errorMeasurement.lineCount : 0; const errorCanExpand = props.connectionError !== null && errorLineCount > 1; const isErrorExpanded = errorCanExpand && props.errorExpanded; const StatusContainer = errorCanExpand ? Pressable : View; const onMeasuredErrorTextLayout = useCallback( (event: NativeSyntheticEvent) => { if (!props.connectionError) { return; } const nextLineCount = event.nativeEvent.lines.length; setErrorMeasurement((currentMeasurement) => currentMeasurement?.text === measuredErrorText && currentMeasurement.lineCount === nextLineCount ? currentMeasurement : { text: measuredErrorText, lineCount: nextLineCount }, ); }, [measuredErrorText, props.connectionError], ); return ( {props.label} {props.connectionError ? ( {measuredErrorText} ) : null} {statusText} {errorTraceId ? ( <> {" Trace ID: "} { event.stopPropagation(); copyTextWithHaptic(errorTraceId, { target: "connection-trace-id" }); }} onPress={(event) => { event.stopPropagation(); }} style={{ textDecorationStyle: "dotted" }} > {errorTraceId} ) : null} {errorCanExpand ? ( ) : null} ); } function CopyTraceIdButton(props: { readonly traceId: string }) { const iconColor = useThemeColor("--color-icon"); return ( { copyTextWithHaptic(props.traceId, { target: "connection-trace-id" }); }} className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70" > Copy trace ID ); }