import type { AvatarStyle } from "@rakazo/contracts"; import { useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { ActivityIndicator, Alert, Button, Platform, Pressable, ScrollView, StyleSheet, Switch, Text, TextInput, View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useAvatarStyle } from "../components/avatar-style"; import { BotAvatar } from "../components/bot-avatar"; import type { MobileBot, MobileMe } from "../lib/api"; import { currentApiBase, deleteAccount, loadSessionToken, rpc, selectedSpaceId, signOut, } from "../lib/api"; import { formatUpdateLabel, getAppVersionInfo } from "../lib/app-version"; import { getCachedAppearancePreference, mobileTokens, setAppearancePreference, } from "../lib/appearance"; import { explicitSignInRoute } from "../lib/auth-routing"; import { confirmDeleteBot } from "../lib/bot-lifecycle"; import { setUiLocale, useI18n } from "../lib/i18n"; import type { LiveNotificationSettings } from "../lib/live-notifications"; import { canPostPromotedNotifications, DEFAULT_LIVE_NOTIFICATION_SETTINGS, getLiveNotificationSettings, openLiveNotificationSettings, openPromotedNotificationSettings, setLiveNotificationSettings, } from "../lib/live-notifications"; import { presentMessageActionSheet } from "../lib/message-action-sheet"; import { native, useResolvedAppearance, useThemedStyles } from "../lib/native"; import { registerPushToken } from "../lib/push"; import type { AccountUiLocale } from "../lib/ui-locale"; import { ACCOUNT_UI_LOCALES, UI_LOCALE_LABELS } from "../lib/ui-locale"; /** Render account settings, including the entry point for voice configuration. */ export default function Account() { const { t, locale } = useI18n(); const colorScheme = useResolvedAppearance(); const router = useRouter(); const { focus } = useLocalSearchParams<{ focus?: string }>(); const [me, setMe] = useState(null); const [password, setPassword] = useState(""); const [localeSaving, setLocaleSaving] = useState(false); const [localeError, setLocaleError] = useState(null); const [pending, setPending] = useState(false); const [avatarPending, setAvatarPending] = useState(false); const [avatarError, setAvatarError] = useState(null); const [notifications, setNotifications] = useState( DEFAULT_LIVE_NOTIFICATION_SETTINGS, ); const [notificationsReady, setNotificationsReady] = useState(Platform.OS !== "android"); const [notificationPending, setNotificationPending] = useState(false); const [notificationError, setNotificationError] = useState(null); const [error, setError] = useState(null); const [archivedBots, setArchivedBots] = useState([]); const [usage, setUsage] = useState<{ runs: number; inputTokens: number; outputTokens: number; } | null>(null); const { avatarStyle, updateAvatarStyle } = useAvatarStyle(); const appearance = getCachedAppearancePreference(); const styles = useThemedStyles(createAccountStyles); const versionInfo = getAppVersionInfo(); const updateLabel = formatUpdateLabel(versionInfo.update, t); const versionAccessibility = [versionInfo.nativeLabel, updateLabel].filter(Boolean).join(". "); useEffect(() => { void rpc("me") .then(setMe) .catch(() => undefined); void rpc("bots/listArchived") .then(setArchivedBots) .catch(() => undefined); void rpc<{ runs: number; inputTokens: number; outputTokens: number }>("usage/summary") .then(setUsage) .catch(() => undefined); if (Platform.OS === "android") { void getLiveNotificationSettings() .then(setNotifications) .catch(() => undefined) .finally(() => setNotificationsReady(true)); } }, []); const usageBlock = ( {t("Usage")} {usage ? ( {t("{runs} runs · {tokens} tokens", { runs: usage.runs, tokens: usage.inputTokens + usage.outputTokens, })} ) : null} ); async function restoreBot(botId: string) { try { await rpc("bots/restore", { botId }); setArchivedBots((bots) => bots.filter((bot) => bot.id !== botId)); } catch (restoreError) { Alert.alert( t("Could not restore bot"), restoreError instanceof Error ? restoreError.message : t("Try again."), ); } } async function selectAvatarStyle(next: AvatarStyle) { if (next === avatarStyle) return; setAvatarPending(true); setAvatarError(null); try { await updateAvatarStyle(next); } catch { setAvatarError(t("Couldn't update avatars")); } finally { setAvatarPending(false); } } async function handleSignOut() { setPending(true); setError(null); try { await signOut(); router.dismissAll(); router.replace(explicitSignInRoute); } catch (err) { setError(err instanceof Error ? err.message : t("Could not sign out")); setPending(false); } } async function updateNotifications(next: LiveNotificationSettings) { const previous = notifications; setNotifications(next); setNotificationPending(true); setNotificationError(null); try { await setLiveNotificationSettings( next, currentApiBase(), await loadSessionToken(), selectedSpaceId() ?? "", ); if (next.liveConnection && !(await canPostPromotedNotifications())) { await openPromotedNotificationSettings(); } await registerPushToken(); } catch (cause) { setNotifications(previous); setNotificationError( cause instanceof Error ? cause.message : t("Could not update notifications"), ); } finally { setNotificationPending(false); } } function confirmDeletion() { setError(null); Alert.alert( t("Delete your account?"), t( "This permanently deletes your account, bots, conversations, memories, files, and saved connections. This cannot be undone.", ), [ { text: t("Cancel"), style: "cancel" }, { text: t("Delete account"), style: "destructive", onPress: () => void handleDeletion(), }, ], ); } function applyLocale(code: AccountUiLocale) { if (code === locale || localeSaving) return; setLocaleSaving(true); setLocaleError(null); void setUiLocale(code) .catch(() => { setLocaleError(t("Could not change language")); }) .finally(() => setLocaleSaving(false)); } function openLanguagePicker() { if (localeSaving) return; presentMessageActionSheet({ title: t("Language"), actions: ACCOUNT_UI_LOCALES.map((code) => ({ text: UI_LOCALE_LABELS[code], onPress: () => applyLocale(code), })), colorScheme, cancel: t("Cancel"), more: t("More"), }); } async function handleDeletion() { setPending(true); setError(null); try { await deleteAccount(password); router.dismissAll(); router.replace("/sign-in"); } catch (err) { setError(err instanceof Error ? err.message : t("Could not delete account")); } finally { setPending(false); } } return (