| 1 | import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView"; |
| 2 | import { NativeHeaderToolbar } from "../../native/StackHeader"; |
| 3 | import { useAuth } from "@clerk/expo"; |
| 4 | import { StackActions, useNavigation } from "@react-navigation/native"; |
| 5 | import { useCallback, useEffect, useState } from "react"; |
| 6 | import { Platform, Pressable, RefreshControl, View } from "react-native"; |
| 7 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 8 | |
| 9 | import { reportAtomCommandResult, settlePromise } from "@t3tools/client-runtime/state/runtime"; |
| 10 | import { AndroidSheetHeader } from "../../components/AndroidScreenHeader"; |
| 11 | import { AppText as Text } from "../../components/AppText"; |
| 12 | import { useRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 13 | import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; |
| 14 | import { splitEnvironmentSections } from "../connection/environmentSections"; |
| 15 | import { useConnectionController } from "../connection/useConnectionController"; |
| 16 | import { optOutOfConnectOnboarding } from "./connectOnboardingOptOut"; |
| 17 | import { useCloudAuthAvailable } from "./cloudAvailability"; |
| 18 | |
| 19 | /** |
| 20 | * Post-sign-in onboarding sheet for T3 Connect. Mobile never publishes |
| 21 | * environments itself — it consumes ones published elsewhere — so this simply |
| 22 | * surfaces the account's T3 Connect environments right after sign-in so every |
| 23 | * device can be connected in one go. It shows on every sign-in: sign-out |
| 24 | * clears the connected environments, so each new session starts from zero. |
| 25 | */ |
| 26 | export function ConnectOnboardingRouteScreen() { |
| 27 | const navigation = useNavigation(); |
| 28 | const cloudAvailable = useCloudAuthAvailable(); |
| 29 | |
| 30 | // The route is deep-linkable; without cloud config the sheet would present |
| 31 | // empty with no chrome to dismiss it, so bail back out instead. |
| 32 | useEffect(() => { |
| 33 | if (cloudAvailable) { |
| 34 | return; |
| 35 | } |
| 36 | if (navigation.canGoBack()) { |
| 37 | navigation.goBack(); |
| 38 | } else { |
| 39 | navigation.dispatch(StackActions.replace("Home")); |
| 40 | } |
| 41 | }, [cloudAvailable, navigation]); |
| 42 | |
| 43 | return cloudAvailable ? <ConfiguredConnectOnboardingRouteScreen /> : null; |
| 44 | } |
| 45 | |
| 46 | function ConfiguredConnectOnboardingRouteScreen() { |
| 47 | const navigation = useNavigation(); |
| 48 | const insets = useSafeAreaInsets(); |
| 49 | const { isSignedIn, userId } = useAuth({ treatPendingAsSignedOut: false }); |
| 50 | const { connectedEnvironments, onSetEnvironmentEnabled, onRemoveEnvironmentPress } = |
| 51 | useRemoteConnections(); |
| 52 | const { refreshRelayEnvironments } = useConnectionController(); |
| 53 | const { connectedCloudEnvironments } = splitEnvironmentSections({ |
| 54 | connectedEnvironments, |
| 55 | cloudEnvironments: null, |
| 56 | }); |
| 57 | |
| 58 | const [isRefreshing, setIsRefreshing] = useState(false); |
| 59 | const handleRefresh = useCallback(() => { |
| 60 | void (async () => { |
| 61 | setIsRefreshing(true); |
| 62 | try { |
| 63 | await refreshRelayEnvironments(); |
| 64 | } finally { |
| 65 | setIsRefreshing(false); |
| 66 | } |
| 67 | })(); |
| 68 | }, [refreshRelayEnvironments]); |
| 69 | |
| 70 | const handleClose = useCallback(() => { |
| 71 | navigation.goBack(); |
| 72 | }, [navigation]); |
| 73 | |
| 74 | // Persist before dismissing so a quick sign-out/sign-in cannot race ahead |
| 75 | // of the preference write; the write is a local secure-store update. |
| 76 | const handleDontShowAgain = useCallback(() => { |
| 77 | void (async () => { |
| 78 | if (userId) { |
| 79 | const result = await settlePromise(() => optOutOfConnectOnboarding(userId)); |
| 80 | reportAtomCommandResult(result, { label: "connect onboarding opt-out" }); |
| 81 | } |
| 82 | navigation.goBack(); |
| 83 | })(); |
| 84 | }, [navigation, userId]); |
| 85 | |
| 86 | return ( |
| 87 | <View collapsable={false} className="flex-1 bg-sheet"> |
| 88 | {Platform.OS === "android" ? ( |
| 89 | <AndroidSheetHeader |
| 90 | title="Set up T3 Connect" |
| 91 | actions={[ |
| 92 | { |
| 93 | accessibilityLabel: "Refresh environments", |
| 94 | disabled: isRefreshing, |
| 95 | icon: "arrow.clockwise", |
| 96 | loading: isRefreshing, |
| 97 | onPress: handleRefresh, |
| 98 | }, |
| 99 | { accessibilityLabel: "Close", icon: "xmark", onPress: handleClose }, |
| 100 | ]} |
| 101 | /> |
| 102 | ) : ( |
| 103 | <NativeHeaderToolbar placement="right"> |
| 104 | <NativeHeaderToolbar.Button icon="xmark" onPress={handleClose} separateBackground /> |
| 105 | </NativeHeaderToolbar> |
| 106 | )} |
| 107 | <ScrollView |
| 108 | nestedScrollEnabled |
| 109 | alwaysBounceVertical |
| 110 | contentInsetAdjustmentBehavior="automatic" |
| 111 | showsVerticalScrollIndicator={false} |
| 112 | className="flex-1" |
| 113 | contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }} |
| 114 | contentContainerStyle={{ |
| 115 | gap: 16, |
| 116 | paddingHorizontal: 20, |
| 117 | paddingTop: 16, |
| 118 | }} |
| 119 | refreshControl={ |
| 120 | Platform.OS === "ios" ? ( |
| 121 | <RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} /> |
| 122 | ) : undefined |
| 123 | } |
| 124 | > |
| 125 | {isSignedIn ? ( |
| 126 | <CloudEnvironmentRows |
| 127 | connectedCloudEnvironments={connectedCloudEnvironments} |
| 128 | onSetEnvironmentEnabled={onSetEnvironmentEnabled} |
| 129 | onRemoveEnvironment={onRemoveEnvironmentPress} |
| 130 | showHeader={false} |
| 131 | /> |
| 132 | ) : ( |
| 133 | <View collapsable={false} className="rounded-[24px] bg-grouped-card p-5"> |
| 134 | <Text className="text-sm leading-normal text-foreground-muted"> |
| 135 | Sign in to your T3 account to set up T3 Connect. |
| 136 | </Text> |
| 137 | </View> |
| 138 | )} |
| 139 | |
| 140 | {userId ? ( |
| 141 | <Pressable |
| 142 | accessibilityRole="button" |
| 143 | hitSlop={8} |
| 144 | onPress={handleDontShowAgain} |
| 145 | className="items-center py-1 active:opacity-70" |
| 146 | > |
| 147 | <Text className="text-xs text-foreground-muted">{"Don't show this again"}</Text> |
| 148 | </Pressable> |
| 149 | ) : null} |
| 150 | </ScrollView> |
| 151 | </View> |
| 152 | ); |
| 153 | } |