1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
2import { useNavigation } from "@react-navigation/native";
3import { useAtomValue } from "@effect/atom-react";
4import { managedRelaySessionAtom } from "@t3tools/client-runtime/relay";
5import type { EnvironmentId } from "@t3tools/contracts";
6import { useCallback, useRef, useState } from "react";
7import { Platform, RefreshControl } from "react-native";
8import { useSafeAreaInsets } from "react-native-safe-area-context";
9
10import { SettingsScreen } from "./components/SettingsScreen";
11import { AndroidAnchoredMenu } from "../../components/AndroidAnchoredMenu";
12import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader";
13import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
14import { LocalEnvironmentList } from "../connection/LocalEnvironmentList";
15import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";
16import { SelfHostedServerSection } from "../connection/SelfHostedServerSection";
17import { splitEnvironmentSections } from "../connection/environmentSections";
18import { useUniwindTheme } from "../../lib/useUniwindTheme";
19import { useRemoteConnections } from "../../state/use-remote-environment-registry";
20import { relayEnvironmentDiscovery } from "../../state/relay";
21import { useAtomCommand } from "../../state/use-atom-command";
22import {
23 applyShowcaseLocalEnvironmentDisplayUrls,
24 resolveShowcaseEnvironmentUpdateDisplayUrl,
25 SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,
26 SHOWCASE_CONNECTED_CLOUD_ENVIRONMENTS,
27} from "../showcase/showcaseEnvironmentRows";
28
29const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1";
30
31export function SettingsEnvironmentsRouteScreen() {
32 const {
33 connectedEnvironments,
34 onReconnectEnvironment,
35 onRemoveEnvironmentPress,
36 onSetEnvironmentEnabled,
37 onUpdateEnvironment,
38 } = useRemoteConnections();
39 const navigation = useNavigation();
40 const insets = useSafeAreaInsets();
41 const environmentSections = splitEnvironmentSections({
42 connectedEnvironments,
43 cloudEnvironments: null,
44 });
45 const localEnvironments = SHOWCASE_ENABLED
46 ? applyShowcaseLocalEnvironmentDisplayUrls(environmentSections.localEnvironments)
47 : environmentSections.localEnvironments;
48 const connectedCloudEnvironments = SHOWCASE_ENABLED
49 ? SHOWCASE_CONNECTED_CLOUD_ENVIRONMENTS
50 : environmentSections.connectedCloudEnvironments;
51 const headerIconColor = useUniwindTheme()["--color-icon"];
52 const relaySession = useAtomValue(managedRelaySessionAtom);
53 const refreshRelayEnvironments = useAtomCommand(
54 relayEnvironmentDiscovery.refresh,
55 "relay environment refresh",
56 );
57 const [isRefreshingCloud, setIsRefreshingCloud] = useState(false);
58 const cloudRefreshPendingRef = useRef(false);
59 async function refreshCloudEnvironments() {
60 if (!relaySession || cloudRefreshPendingRef.current) return;
61 cloudRefreshPendingRef.current = true;
62 setIsRefreshingCloud(true);
63 try {
64 await refreshRelayEnvironments();
65 } finally {
66 cloudRefreshPendingRef.current = false;
67 setIsRefreshingCloud(false);
68 }
69 }
70
71 const openEnvironment = useCallback(
72 (environmentId: EnvironmentId) => {
73 navigation.navigate("SettingsSheet", {
74 screen: "SettingsContent",
75 params: { screen: "SettingsEnvironmentDetail", params: { environmentId } },
76 });
77 },
78 [navigation],
79 );
80 const handleUpdateEnvironment = useCallback(
81 (
82 environmentId: EnvironmentId,
83 updates: { readonly label: string; readonly displayUrl: string },
84 ) => {
85 if (!SHOWCASE_ENABLED) return onUpdateEnvironment(environmentId, updates);
86 const actualEnvironment = environmentSections.localEnvironments.find(
87 (environment) => environment.environmentId === environmentId,
88 );
89 const presentedEnvironment = localEnvironments.find(
90 (environment) => environment.environmentId === environmentId,
91 );
92 return onUpdateEnvironment(environmentId, {
93 ...updates,
94 displayUrl:
95 actualEnvironment && presentedEnvironment
96 ? resolveShowcaseEnvironmentUpdateDisplayUrl({
97 actualDisplayUrl: actualEnvironment.displayUrl,
98 presentedDisplayUrl: presentedEnvironment.displayUrl,
99 submittedDisplayUrl: updates.displayUrl,
100 })
101 : updates.displayUrl,
102 });
103 },
104 [environmentSections.localEnvironments, localEnvironments, onUpdateEnvironment],
105 );
106
107 return (
108 <SettingsScreen
109 title="Environments"
110 trailing={
111 Platform.OS === "android" && relaySession ? (
112 <AndroidAnchoredMenu
113 title="Environment options"
114 actions={[
115 {
116 id: "refresh",
117 title: "Refresh cloud environments",
118 attributes: { disabled: isRefreshingCloud },
119 },
120 ]}
121 onPressAction={({ nativeEvent }) => {
122 if (nativeEvent.event === "refresh") void refreshCloudEnvironments();
123 }}
124 >
125 {(open) => (
126 <AndroidHeaderIconButton
127 accessibilityLabel="Environment options"
128 icon="ellipsis"
129 onPress={open}
130 />
131 )}
132 </AndroidAnchoredMenu>
133 ) : undefined
134 }
135 actions={[
136 {
137 accessibilityLabel: "Add environment",
138 icon: "plus",
139 tintColor: headerIconColor,
140 onPress: () =>
141 navigation.navigate("SettingsSheet", {
142 screen: "SettingsContent",
143 params: { screen: "SettingsEnvironmentNew" },
144 }),
145 },
146 ]}
147 >
148 <ScrollView
149 alwaysBounceVertical
150 contentInsetAdjustmentBehavior="automatic"
151 showsVerticalScrollIndicator={false}
152 className="flex-1"
153 contentContainerClassName="px-5 pt-4"
154 contentContainerStyle={{
155 paddingBottom: Math.max(insets.bottom, 18) + 18,
156 }}
157 refreshControl={
158 relaySession ? (
159 <RefreshControl
160 refreshing={isRefreshingCloud}
161 onRefresh={() => void refreshCloudEnvironments()}
162 />
163 ) : undefined
164 }
165 >
166 <SelfHostedServerSection />
167 <LocalEnvironmentList
168 environments={localEnvironments}
169 expandedId={null}
170 onToggle={openEnvironment}
171 opensDetails
172 onReconnect={onReconnectEnvironment}
173 onRemove={onRemoveEnvironmentPress}
174 onSetEnabled={onSetEnvironmentEnabled}
175 onUpdate={handleUpdateEnvironment}
176 />
177
178 {/* Always mounted: already-connected relay environments must stay
179 visible (and removable) even when cloud config is missing or the
180 user is signed out — the component gates discovery itself. */}
181 <CloudEnvironmentRows
182 connectedCloudEnvironments={connectedCloudEnvironments}
183 onOpenEnvironment={openEnvironment}
184 onSetEnvironmentEnabled={onSetEnvironmentEnabled}
185 onRemoveEnvironment={onRemoveEnvironmentPress}
186 {...(SHOWCASE_ENABLED
187 ? {
188 showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS,
189 showcaseSignedIn: true,
190 }
191 : {})}
192 />
193 <GitHubRoutingSettings />
194 </ScrollView>
195 </SettingsScreen>
196 );
197}