Native avatars now draw the shared Oo rig with react-native-svg instead of a visor with two equal eyes, so the Oo eyes look the same on every surface. The robot/organic avatar setting is gone from web and mobile because every bot is now an Oo; uploaded pictures show on native too. Claude-Session: https://claude.ai/code/session_01EKuME5CgL1ddGGN7LGvtBj Co-authored-by: Claude <noreply@anthropic.com>
704 lines
21 KiB
TypeScript
704 lines
21 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";
|
||
|
||
/** 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)
|
||
.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,
|
||
},
|
||
});
|
||
}
|