* Pick the voice from a carousel in the app language, local voice only Voice settings now list only the voices that speak the app language (German shows German voices, English shows English ones) as a horizontal carousel: one card per voice, a play button next to the name, tap to make it the active voice. Changing the app language moves the saved voice to that language. The hosted voice providers (ElevenLabs, OpenAI, Cartesia, Fish Audio) are removed; Kokoro on the server is the only voice. E2E runs against a stub Kokoro server so the carousel is covered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxoSHAyzMXvdtwcDJZcn57 * test(e2e): measure the workspace panel width after its transition settles Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxoSHAyzMXvdtwcDJZcn57 --------- Co-authored-by: Claude <noreply@anthropic.com>
706 lines
22 KiB
TypeScript
706 lines
22 KiB
TypeScript
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 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";
|
||
import { alignVoiceWithLocale } from "../lib/voice";
|
||
|
||
/** 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<MobileMe | null>(null);
|
||
const [password, setPassword] = useState("");
|
||
const [localeSaving, setLocaleSaving] = useState(false);
|
||
const [localeError, setLocaleError] = useState<string | null>(null);
|
||
const [pending, setPending] = useState(false);
|
||
const [notifications, setNotifications] = useState<LiveNotificationSettings>(
|
||
DEFAULT_LIVE_NOTIFICATION_SETTINGS,
|
||
);
|
||
const [notificationsReady, setNotificationsReady] = useState(Platform.OS !== "android");
|
||
const [notificationPending, setNotificationPending] = useState(false);
|
||
const [notificationError, setNotificationError] = useState<string | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [archivedBots, setArchivedBots] = useState<MobileBot[]>([]);
|
||
const [usage, setUsage] = useState<{
|
||
runs: number;
|
||
inputTokens: number;
|
||
outputTokens: number;
|
||
} | null>(null);
|
||
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<MobileMe>("me")
|
||
.then(setMe)
|
||
.catch(() => undefined);
|
||
void rpc<MobileBot[]>("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 = (
|
||
<View accessibilityLabel={t("Usage")} style={styles.profile}>
|
||
<Text style={styles.settingsTitle}>{t("Usage")}</Text>
|
||
{usage ? (
|
||
<Text style={styles.email}>
|
||
{t("{runs} runs · {tokens} tokens", {
|
||
runs: usage.runs,
|
||
tokens: usage.inputTokens + usage.outputTokens,
|
||
})}
|
||
</Text>
|
||
) : null}
|
||
</View>
|
||
);
|
||
|
||
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 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)
|
||
.then((activated) => alignVoiceWithLocale(activated).catch(() => undefined))
|
||
.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 (
|
||
<SafeAreaView edges={["bottom"]} style={styles.screen}>
|
||
<ScrollView contentContainerStyle={styles.content}>
|
||
<Button
|
||
color={mobileTokens().primary}
|
||
title="AI data sharing"
|
||
onPress={() => router.push("/ai-data-sharing")}
|
||
/>
|
||
{focus === "usage" ? usageBlock : null}
|
||
<View style={styles.profile}>
|
||
<Text style={styles.name}>{me?.name || t("Your account")}</Text>
|
||
{me?.email ? <Text style={styles.email}>{me.email}</Text> : null}
|
||
</View>
|
||
{focus !== "usage" ? usageBlock : null}
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
onPress={() => router.push("/change-password")}
|
||
style={({ pressed }) => [styles.settingsButton, pressed && styles.pressed]}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Change password")}</Text>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</Pressable>
|
||
|
||
<View accessibilityLabel={t("Appearance")} style={styles.appearanceSection}>
|
||
<Text style={styles.settingsTitle}>{t("Appearance")}</Text>
|
||
<View style={styles.appearanceOptions}>
|
||
{(
|
||
[
|
||
["system", "System"],
|
||
["light", "Light"],
|
||
["dark", "Dark"],
|
||
] as const
|
||
).map(([value, label]) => {
|
||
const selected = appearance === value;
|
||
const translated = t(label);
|
||
return (
|
||
<Pressable
|
||
key={value}
|
||
accessibilityLabel={translated}
|
||
accessibilityRole="button"
|
||
accessibilityState={{ selected }}
|
||
onPress={() => void setAppearancePreference(value)}
|
||
style={({ pressed }) => [
|
||
styles.appearanceOption,
|
||
selected && styles.appearanceOptionSelected,
|
||
pressed && styles.pressed,
|
||
]}
|
||
>
|
||
<Text style={styles.appearanceLabel}>{translated}</Text>
|
||
</Pressable>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
accessibilityLabel={t("Language")}
|
||
accessibilityValue={{ text: UI_LOCALE_LABELS[locale] }}
|
||
accessibilityState={{ disabled: localeSaving }}
|
||
disabled={localeSaving}
|
||
onPress={openLanguagePicker}
|
||
style={({ pressed }) => [
|
||
styles.settingsButton,
|
||
pressed && styles.pressed,
|
||
localeSaving && { opacity: 0.6 },
|
||
]}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Language")}</Text>
|
||
<View style={styles.settingsTrailing}>
|
||
<Text style={styles.settingsValue}>{UI_LOCALE_LABELS[locale]}</Text>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</View>
|
||
</Pressable>
|
||
{localeError ? <Text style={styles.error}>{localeError}</Text> : null}
|
||
|
||
{Platform.OS === "android" ? (
|
||
<View accessibilityLabel={t("Notifications")} style={styles.profile}>
|
||
<Text style={styles.settingsTitle}>{t("Notifications")}</Text>
|
||
<NotificationSwitch
|
||
label={t("Live working status")}
|
||
detail={t("While agents are working")}
|
||
value={notifications.liveConnection}
|
||
disabled={notificationPending || !notificationsReady}
|
||
onChange={(liveConnection) =>
|
||
void updateNotifications({ ...notifications, liveConnection })
|
||
}
|
||
/>
|
||
<NotificationSwitch
|
||
label={t("Agent messages")}
|
||
detail={t("Replies and completed work")}
|
||
value={notifications.messages}
|
||
disabled={notificationPending || !notificationsReady}
|
||
onChange={(messages) => void updateNotifications({ ...notifications, messages })}
|
||
/>
|
||
<NotificationSwitch
|
||
label={t("Scheduled tasks")}
|
||
detail={t("Alerts from routines")}
|
||
value={notifications.scheduledTasks}
|
||
disabled={notificationPending || !notificationsReady}
|
||
onChange={(scheduledTasks) =>
|
||
void updateNotifications({ ...notifications, scheduledTasks })
|
||
}
|
||
/>
|
||
<NotificationSwitch
|
||
label={t("Needs attention")}
|
||
detail={t("Questions, approvals, takeover")}
|
||
value={notifications.needsAttention}
|
||
disabled={notificationPending || !notificationsReady}
|
||
onChange={(needsAttention) =>
|
||
void updateNotifications({ ...notifications, needsAttention })
|
||
}
|
||
/>
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
onPress={() => void openPromotedNotificationSettings()}
|
||
style={{ minHeight: 44, justifyContent: "center" }}
|
||
>
|
||
<Text style={{ color: native.label, fontSize: 14 }}>{t("Live update settings")}</Text>
|
||
</Pressable>
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
onPress={() => void openLiveNotificationSettings()}
|
||
style={{ minHeight: 44, justifyContent: "center" }}
|
||
>
|
||
<Text style={{ color: native.label, fontSize: 14 }}>
|
||
{t("Notification settings")}
|
||
</Text>
|
||
</Pressable>
|
||
{notificationError ? <Text style={styles.error}>{notificationError}</Text> : null}
|
||
</View>
|
||
) : null}
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
disabled={pending}
|
||
onPress={() => router.push("/models")}
|
||
style={({ pressed }) => [styles.settingsButton, pressed && styles.pressed]}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Models")}</Text>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</Pressable>
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
disabled={pending}
|
||
onPress={() => router.push("/voice")}
|
||
style={({ pressed }) => [styles.settingsButton, pressed && styles.pressed]}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Voice")}</Text>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</Pressable>
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
disabled={pending}
|
||
onPress={() => router.push("/integrations")}
|
||
style={({ pressed }) => [styles.settingsButton, pressed && styles.pressed]}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Integrations")}</Text>
|
||
<Text style={styles.chevron}>›</Text>
|
||
</Pressable>
|
||
|
||
{me?.isDeploymentOwner ? (
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
onPress={() => router.push("/integration-setup")}
|
||
style={styles.settingsButton}
|
||
>
|
||
<Text style={styles.settingsTitle}>{t("Server integrations")}</Text>
|
||
</Pressable>
|
||
) : null}
|
||
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
disabled={pending}
|
||
onPress={() => void handleSignOut()}
|
||
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
|
||
>
|
||
<Text style={styles.buttonLabel}>{t("Sign out")}</Text>
|
||
</Pressable>
|
||
|
||
{archivedBots.length > 0 ? (
|
||
<View style={styles.archivedSection}>
|
||
<Text style={styles.sectionTitle}>{t("Archived bots")}</Text>
|
||
{archivedBots.map((bot) => (
|
||
<View key={bot.id} style={styles.archivedRow}>
|
||
<Text numberOfLines={1} style={styles.archivedName}>
|
||
{bot.name}
|
||
</Text>
|
||
<Pressable onPress={() => void restoreBot(bot.id)} hitSlop={8}>
|
||
<Text style={styles.restoreLabel}>{t("Restore")}</Text>
|
||
</Pressable>
|
||
<Pressable
|
||
onPress={() =>
|
||
confirmDeleteBot(bot, () =>
|
||
setArchivedBots((bots) => bots.filter((item) => item.id !== bot.id)),
|
||
)
|
||
}
|
||
hitSlop={8}
|
||
>
|
||
<Text style={styles.archivedDeleteLabel}>{t("Delete")}</Text>
|
||
</Pressable>
|
||
</View>
|
||
))}
|
||
</View>
|
||
) : null}
|
||
|
||
{versionInfo.nativeLabel || updateLabel ? (
|
||
<View
|
||
accessibilityLabel={versionAccessibility}
|
||
accessibilityRole="summary"
|
||
style={styles.versionFooter}
|
||
>
|
||
{versionInfo.nativeLabel ? (
|
||
<Text style={styles.versionLine}>{versionInfo.nativeLabel}</Text>
|
||
) : null}
|
||
{updateLabel ? <Text style={styles.versionLine}>{updateLabel}</Text> : null}
|
||
</View>
|
||
) : null}
|
||
|
||
<View style={styles.dangerZone}>
|
||
<Text style={styles.dangerTitle}>{t("Delete account")}</Text>
|
||
<TextInput
|
||
accessibilityLabel={t("Current password")}
|
||
autoCapitalize="none"
|
||
autoCorrect={false}
|
||
editable={!pending}
|
||
onChangeText={(value) => {
|
||
setPassword(value);
|
||
setError(null);
|
||
}}
|
||
placeholder={t("Current password")}
|
||
placeholderTextColor={native.tertiaryLabel}
|
||
secureTextEntry
|
||
style={styles.password}
|
||
textContentType="password"
|
||
value={password}
|
||
/>
|
||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
disabled={pending || !password}
|
||
onPress={confirmDeletion}
|
||
style={({ pressed }) => [
|
||
styles.deleteButton,
|
||
(pending || !password) && styles.disabled,
|
||
pressed && styles.pressed,
|
||
]}
|
||
>
|
||
{pending ? (
|
||
<ActivityIndicator color={mobileTokens().destructiveForeground} />
|
||
) : (
|
||
<Text style={styles.deleteLabel}>{t("Delete account")}</Text>
|
||
)}
|
||
</Pressable>
|
||
</View>
|
||
</ScrollView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
function NotificationSwitch({
|
||
label,
|
||
detail,
|
||
value,
|
||
disabled,
|
||
onChange,
|
||
}: {
|
||
label: string;
|
||
detail: string;
|
||
value: boolean;
|
||
disabled: boolean;
|
||
onChange: (value: boolean) => void;
|
||
}) {
|
||
return (
|
||
<View
|
||
style={{
|
||
minHeight: 54,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={{ color: native.label, fontSize: 15 }}>{label}</Text>
|
||
<Text style={{ color: native.secondaryLabel, fontSize: 12.5, marginTop: 2 }}>{detail}</Text>
|
||
</View>
|
||
<Switch
|
||
accessibilityLabel={label}
|
||
accessibilityHint={detail}
|
||
disabled={disabled}
|
||
value={value}
|
||
onValueChange={onChange}
|
||
/>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function createAccountStyles() {
|
||
const tokens = mobileTokens();
|
||
return StyleSheet.create({
|
||
screen: {
|
||
flex: 1,
|
||
backgroundColor: native.page,
|
||
},
|
||
content: {
|
||
flexGrow: 1,
|
||
padding: 20,
|
||
gap: 20,
|
||
},
|
||
profile: {
|
||
borderRadius: 16,
|
||
backgroundColor: native.fill,
|
||
padding: 18,
|
||
gap: 4,
|
||
},
|
||
name: {
|
||
color: native.label,
|
||
fontSize: 20,
|
||
fontWeight: "600",
|
||
},
|
||
email: {
|
||
color: native.secondaryLabel,
|
||
fontSize: 15,
|
||
},
|
||
button: {
|
||
minHeight: 50,
|
||
borderRadius: 14,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
backgroundColor: native.fill,
|
||
},
|
||
buttonLabel: {
|
||
color: native.label,
|
||
fontSize: 17,
|
||
fontWeight: "600",
|
||
},
|
||
archivedSection: {
|
||
borderRadius: 16,
|
||
backgroundColor: native.fill,
|
||
padding: 18,
|
||
gap: 14,
|
||
},
|
||
sectionTitle: {
|
||
color: native.secondaryLabel,
|
||
fontSize: 14,
|
||
fontWeight: "600",
|
||
},
|
||
archivedRow: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 14,
|
||
},
|
||
archivedName: {
|
||
flex: 1,
|
||
color: native.label,
|
||
fontSize: 16,
|
||
},
|
||
restoreLabel: {
|
||
color: native.label,
|
||
fontSize: 14,
|
||
fontWeight: "600",
|
||
},
|
||
archivedDeleteLabel: {
|
||
color: tokens.destructive,
|
||
fontSize: 14,
|
||
},
|
||
settingsButton: {
|
||
minHeight: 62,
|
||
borderRadius: 14,
|
||
backgroundColor: native.fill,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
},
|
||
appearanceSection: {
|
||
borderRadius: 16,
|
||
backgroundColor: native.fill,
|
||
padding: 18,
|
||
gap: 14,
|
||
},
|
||
appearanceOptions: {
|
||
flexDirection: "row",
|
||
gap: 8,
|
||
},
|
||
appearanceOption: {
|
||
flex: 1,
|
||
minHeight: 44,
|
||
borderRadius: 12,
|
||
borderWidth: StyleSheet.hairlineWidth,
|
||
borderColor: native.tertiaryLabel,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
},
|
||
appearanceOptionSelected: {
|
||
borderColor: native.label,
|
||
backgroundColor: native.fillPressed,
|
||
},
|
||
appearanceLabel: {
|
||
color: native.label,
|
||
fontSize: 14,
|
||
fontWeight: "600",
|
||
},
|
||
settingsTitle: {
|
||
color: native.label,
|
||
fontSize: 17,
|
||
fontWeight: "600",
|
||
},
|
||
settingsTrailing: {
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
minWidth: 0,
|
||
},
|
||
settingsValue: {
|
||
color: native.secondaryLabel,
|
||
fontSize: 15,
|
||
},
|
||
chevron: {
|
||
color: native.secondaryLabel,
|
||
fontSize: 28,
|
||
fontWeight: "300",
|
||
},
|
||
versionFooter: {
|
||
marginTop: 4,
|
||
alignItems: "center",
|
||
gap: 2,
|
||
},
|
||
versionLine: {
|
||
color: native.tertiaryLabel,
|
||
fontSize: 12,
|
||
textAlign: "center",
|
||
},
|
||
dangerZone: {
|
||
marginTop: 12,
|
||
borderRadius: 16,
|
||
borderWidth: StyleSheet.hairlineWidth,
|
||
borderColor: tokens.destructive,
|
||
padding: 18,
|
||
},
|
||
dangerTitle: {
|
||
color: tokens.destructive,
|
||
fontSize: 17,
|
||
fontWeight: "600",
|
||
},
|
||
password: {
|
||
height: 48,
|
||
borderRadius: 12,
|
||
backgroundColor: native.fill,
|
||
color: native.label,
|
||
paddingHorizontal: 14,
|
||
marginTop: 16,
|
||
fontSize: 16,
|
||
},
|
||
error: {
|
||
color: tokens.destructive,
|
||
fontSize: 14,
|
||
marginTop: 10,
|
||
},
|
||
deleteButton: {
|
||
minHeight: 50,
|
||
borderRadius: 12,
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
backgroundColor: tokens.destructive,
|
||
marginTop: 14,
|
||
},
|
||
deleteLabel: {
|
||
color: tokens.destructiveForeground,
|
||
fontSize: 16,
|
||
fontWeight: "700",
|
||
},
|
||
disabled: {
|
||
opacity: 0.45,
|
||
},
|
||
pressed: {
|
||
opacity: 0.7,
|
||
},
|
||
});
|
||
}
|