import { useAuth, useUser } from "@clerk/expo";
import Constants from "expo-constants";
import * as Notifications from "expo-notifications";
import * as Updates from "expo-updates";
import { useNavigation } from "@react-navigation/native";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { SymbolView } from "expo-symbols";
import * as Effect from "effect/Effect";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Alert, Linking, Platform, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
isAtomCommandInterrupted,
reportAtomCommandResult,
settleAsyncResult,
settlePromise,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { AppText as Text } from "../../components/AppText";
import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences";
import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions";
import { refreshAgentAwarenessRegistration } from "../agent-awareness/remoteRegistration";
import { refreshManagedRelayEnvironments } from "../cloud/managedRelayState";
import { useClerkSettingsSheetDetent } from "../cloud/ClerkSettingsSheetDetent";
import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/publicConfig";
import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
import { runtime } from "../../lib/runtime";
import { loadPreferences } from "../../lib/storage";
import { useThemeColor } from "../../lib/useThemeColor";
import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
import { SettingsRow } from "./components/SettingsRow";
import { SettingsSection } from "./components/SettingsSection";
import { SettingsSwitchRow } from "./components/SettingsSwitchRow";
type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported";
type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking";
export function SettingsRouteScreen() {
const navigation = useNavigation();
return (
<>
[
withNativeGlassHeaderItem({
accessibilityLabel: "Close settings",
icon: { name: "xmark", type: "sfSymbol" } as const,
identifier: "settings-close",
label: "",
onPress: () => navigation.goBack(),
type: "button",
}),
]
: undefined,
}}
/>
{hasCloudPublicConfig() ? : }
>
);
}
function LocalSettingsRouteScreen() {
const insets = useSafeAreaInsets();
const { savedConnectionsById } = useSavedRemoteConnections();
const environmentCount = Object.keys(savedConnectionsById).length;
return (
);
}
function ConfiguredSettingsRouteScreen() {
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const { expand: expandClerkSheet } = useClerkSettingsSheetDetent();
const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false });
const { user } = useUser();
const { savedConnectionsById } = useSavedRemoteConnections();
const [notificationStatus, setNotificationStatus] = useState("checking");
const [liveActivityStatus, setLiveActivityStatus] = useState("checking");
const connections = useMemo(() => Object.values(savedConnectionsById), [savedConnectionsById]);
const environmentCount = connections.length;
const accountLabel = useMemo(() => {
if (!isLoaded) return "Checking";
if (!isSignedIn) return "Request access";
return user?.primaryEmailAddress?.emailAddress ?? "Signed in";
}, [isLoaded, isSignedIn, user?.primaryEmailAddress?.emailAddress]);
const refreshNotifications = useCallback(async () => {
if (process.env.EXPO_OS !== "ios") {
setNotificationStatus("unsupported");
return;
}
const result = await settlePromise(() => Notifications.getPermissionsAsync());
if (result._tag === "Failure") {
reportAtomCommandResult(result, { label: "notification permission refresh" });
setNotificationStatus("disabled");
return;
}
setNotificationStatus(result.value.granted ? "enabled" : "disabled");
}, []);
useEffect(() => {
void refreshNotifications();
}, [refreshNotifications]);
useEffect(() => {
if (!isLoaded) {
setLiveActivityStatus("checking");
return;
}
if (!isSignedIn) {
setLiveActivityStatus("signed-out");
return;
}
void (async () => {
const result = await settlePromise(() => loadPreferences());
if (result._tag === "Failure") {
reportAtomCommandResult(result, { label: "live activity preference load" });
setLiveActivityStatus("enabled");
return;
}
setLiveActivityStatus(result.value.liveActivitiesEnabled === false ? "disabled" : "enabled");
})();
}, [isLoaded, isSignedIn]);
const requestNotifications = useCallback(async () => {
const result = await settleAsyncResult(() =>
runtime.runPromiseExit(
requestAgentNotificationPermission.pipe(
Effect.tap((permission) =>
permission.type === "granted" ? refreshAgentAwarenessRegistration() : Effect.void,
),
),
),
);
if (result._tag === "Failure") {
if (!isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
Alert.alert(
"Notifications unavailable",
error instanceof Error ? error.message : "Could not request notification permission.",
);
}
return;
}
if (result.value.type === "granted") {
setNotificationStatus("enabled");
Alert.alert(
"Notifications enabled",
"Live Activity notifications are enabled for this device.",
);
return;
}
if (result.value.type === "unsupported") {
setNotificationStatus("unsupported");
Alert.alert(
"Notifications unavailable",
"Live Activity notifications are only available on iOS.",
);
return;
}
setNotificationStatus("disabled");
if (result.value.canAskAgain) {
Alert.alert("Notifications disabled", "Notifications were not enabled.");
return;
}
Alert.alert(
"Notifications disabled",
"Notifications were denied for this app. Open Settings to enable them.",
[
{ text: "Cancel", style: "cancel" },
{ text: "Open Settings", onPress: () => void Linking.openSettings() },
],
);
}, []);
const promptSignIn = useCallback(() => {
Alert.alert(
"Request T3 Cloud access",
"Live Activity updates require approved T3 Cloud access so relay can deliver updates to this device.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Continue",
onPress: () => navigation.navigate("SettingsSheet", { screen: "SettingsWaitlist" }),
},
],
);
}, [navigation]);
const linkEnvironments = useCallback(async () => {
if (!isSignedIn) {
promptSignIn();
return;
}
setLiveActivityStatus("linking");
const tokenResult = await settlePromise(() => getToken(resolveRelayClerkTokenOptions()));
if (tokenResult._tag === "Failure") {
setLiveActivityStatus("disabled");
const error = squashAtomCommandFailure(tokenResult);
Alert.alert(
"Live Activities unavailable",
error instanceof Error ? error.message : "Could not enable Live Activity updates.",
);
return;
}
if (!tokenResult.value) {
promptSignIn();
setLiveActivityStatus("signed-out");
return;
}
const updateResult = await settleAsyncResult(() =>
runtime.runPromiseExit(
setLiveActivityUpdatesEnabled({
enabled: true,
clerkToken: tokenResult.value,
connections,
}),
),
);
if (updateResult._tag === "Failure") {
setLiveActivityStatus("disabled");
if (!isAtomCommandInterrupted(updateResult)) {
const error = squashAtomCommandFailure(updateResult);
Alert.alert(
"Live Activities unavailable",
error instanceof Error ? error.message : "Could not enable Live Activity updates.",
);
}
return;
}
refreshManagedRelayEnvironments();
setLiveActivityStatus("enabled");
Alert.alert(
"Live Activities enabled",
environmentCount > 0
? `${environmentCount} environment${environmentCount === 1 ? "" : "s"} linked for Live Activity updates.`
: "Live Activity updates are enabled. Add an environment to start receiving updates.",
);
}, [connections, environmentCount, getToken, isSignedIn, promptSignIn]);
const handleDeviceNotificationsChange = useCallback(
(enabled: boolean) => {
if (enabled) {
void requestNotifications();
return;
}
Alert.alert(
"Disable notifications",
"Notification permission is controlled by iOS. Open Settings to disable notifications for T3 Code.",
[
{ text: "Cancel", style: "cancel" },
{ text: "Open Settings", onPress: () => void Linking.openSettings() },
],
);
},
[requestNotifications],
);
const handleLiveActivitiesChange = useCallback(
(enabled: boolean) => {
if (!enabled) {
setLiveActivityStatus("disabled");
void (async () => {
let token: string | null = null;
if (isSignedIn) {
const tokenResult = await settlePromise(() =>
getToken(resolveRelayClerkTokenOptions()),
);
if (tokenResult._tag === "Failure") {
reportAtomCommandResult(tokenResult, {
label: "live activity disable token lookup",
});
return;
}
token = tokenResult.value;
}
const updateResult = await settleAsyncResult(() =>
runtime.runPromiseExit(
setLiveActivityUpdatesEnabled({
enabled: false,
clerkToken: token,
connections,
}),
),
);
if (updateResult._tag === "Failure") {
reportAtomCommandResult(updateResult, {
label: "live activity disable",
});
return;
}
refreshManagedRelayEnvironments();
})();
return;
}
if (!isSignedIn) {
promptSignIn();
return;
}
void linkEnvironments();
},
[connections, getToken, isSignedIn, linkEnvironments, promptSignIn],
);
const openAccount = useCallback(() => {
if (!isLoaded) return;
if (!isSignedIn) {
navigation.navigate("SettingsSheet", { screen: "SettingsWaitlist" });
return;
}
expandClerkSheet();
navigation.navigate("SettingsSheet", { screen: "SettingsAuth" });
}, [expandClerkSheet, isLoaded, isSignedIn, navigation]);
return (
T3 Code works locally without signing in. Cloud features are optional.
);
}
function AppSettingsSection() {
const icon = useThemeColor("--color-icon");
const version = Constants.expoConfig?.version ?? "0.0.0";
// Fall back to "production" to match resolveAppVariant in app.config.ts, so a
// missing variant never mislabels a production build as development.
const variant = (Constants.expoConfig?.extra?.appVariant as string | undefined) ?? "production";
const variantLabel = variant === "production" ? "" : capitalize(variant);
const versionLabel = variantLabel ? `${version} ยท ${variantLabel}` : version;
// Which JS is actually running: the bundle shipped in the binary, or an OTA
// update downloaded on top of it. Surfacing this makes "am I even on the
// right build?" answerable at a glance.
const bundleLabel = Updates.isEnabled
? Updates.isEmbeddedLaunch
? "Embedded"
: Updates.updateId
? `OTA ${Updates.updateId.slice(0, 7)}`
: null
: null;
return (
Version
{versionLabel}
{bundleLabel ? (
{bundleLabel}
) : null}
);
}
function capitalize(value: string): string {
return value.length > 0 ? value.charAt(0).toUpperCase() + value.slice(1) : value;
}
function ArchivedThreadsSettingsSection() {
return (
);
}