Destructive deletion flow — permanent-loss alert, concrete data summary, data-export escape hatch, and type-DELETE confirmation gate
import * as React from "react";
import { View, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState } from "react";
import { Alert, Button, Glass, Icon, Input, Text, KeyboardAvoiding } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Mount, Press, BLOCK_STAGGER, World } from "../shared";
export interface AccountDataSummary {
/** What disappears — e.g. "248 saved items" or "Pro until Oct 12". */
label: string;
detail?: string;
}
export interface DeleteAccountScreenProps {
/** User-facing identity being deleted. */
email?: string;
/** Concrete losses shown as a list — the honest cost of the action. */
losses?: AccountDataSummary[];
/** Days before deletion is final (grace window). */
graceDays?: number;
onDelete?: () => void;
onDownloadData?: () => void;
onBack?: () => void;
}
const DEFAULT_LOSSES: AccountDataSummary[] = [
{ label: "Lumio Pro", detail: "Active until Oct 12, 2026" },
{ label: "248 saved items", detail: "Lists, favorites, collections" },
{ label: "17 orders", detail: "Receipts become unavailable" },
{ label: "3 signed-in devices", detail: "All sessions end immediately" },
];
/** Delete account — destructive flow: permanent-loss alert, concrete data
* summary, export escape hatch, and type-DELETE confirmation gate. */
export function DeleteAccountScreen({
email = "amara.chen@lumio.dev",
losses = DEFAULT_LOSSES,
graceDays = 30,
onDelete,
onDownloadData,
onBack,
}: DeleteAccountScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [confirmText, setConfirmText] = useState("DELETE");
const [loading, setLoading] = useState(false);
const confirmed = confirmText.trim().toUpperCase() === "DELETE";
const submit = () => {
if (!confirmed) return;
setLoading(true);
haptics.warning();
onDelete?.();
setTimeout(() => setLoading(false), 800);
};
return (
<World primary="#65A30D">
<View className="flex-1 bg-[#EBE2E2] dark:bg-[#1D1111]">
{/* Top bar — back affordance clears the status bar */}
<View className="px-2" style={{ paddingTop: insets.top + 4 }}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => onBack?.()}
accessibilityRole="button"
accessibilityLabel="Go back"
hitSlop={8}
className="h-11 flex-row items-center gap-1 self-start px-2"
>
<Icon name="chevron-left" size="sm" color="#65A30D" />
<Text className="text-base text-[#65A30D]">Back</Text>
</Press>
</View>
<KeyboardAvoiding>
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerClassName="flex-grow px-6"
contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 16 }}
>
<Mount delay={0}>
<View className="gap-2">
<Text variant="h1" className="tracking-tight">
Delete your account?
</Text>
<Text variant="muted" className="max-w-xs text-base leading-relaxed">
<Text className="font-semibold text-foreground">{email}</Text> and
everything attached to it will be gone for good.
</Text>
</View>
</Mount>
<Mount delay={BLOCK_STAGGER}>
<Alert variant="destructive" className="mt-5">
<Alert.Title>This is permanent</Alert.Title>
<Alert.Description>
Deletion starts after a {graceDays}-day grace window and can't
be undone once it completes.
</Alert.Description>
</Alert>
{/* Concrete deadline — the exact date beats an abstract window. */}
<View className="mt-2 flex-row items-center gap-2 px-1">
<Icon name="clock" size="sm" color="destructive" />
<Text variant="small" className="text-destructive">
If you don't sign back in, everything deletes on{" "}
{new Date(Date.now() + graceDays * 86400000).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}.
</Text>
</View>
</Mount>
{/* What you lose — real rows, real numbers; honesty over decoration */}
<Mount delay={BLOCK_STAGGER * 2}>
<View className="mt-5 gap-3">
<Text variant="small" className="font-medium uppercase tracking-widest text-muted-foreground">
What you'll lose
</Text>
<View className="gap-3 overflow-hidden rounded-2xl bg-white p-4 dark:bg-white/5">
{losses.map((loss) => (
<View key={loss.label} className="flex-row items-baseline gap-3">
<View
className="mt-1.5 h-1.5 w-1.5 rounded-full bg-destructive"
accessibilityElementsHidden
importantForAccessibility="no"
/>
<View className="flex-1">
<Text variant="small" className="font-medium">
{loss.label}
</Text>
{loss.detail ? (
<Text variant="muted" className="text-xs">
{loss.detail}
</Text>
) : null}
</View>
</View>
))}
</View>
</View>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => onDownloadData?.()}
accessibilityRole="button"
accessibilityLabel="Download a copy of your data first"
hitSlop={8}
className="mt-4 min-h-11 flex-row items-center gap-2.5 self-start"
>
<Icon name="download" size="sm" color="#65A30D" />
<Text className="text-[#65A30D] font-medium">Download a copy of your data first</Text>
</Press>
</Mount>
{/* Confirmation gate — the typed word is the second factor */}
<Mount delay={BLOCK_STAGGER * 4} className="mt-6">
<View className="gap-3 pt-3">
<Input
label='Type "DELETE" to confirm'
placeholder="DELETE"
value={confirmText}
onChangeText={setConfirmText}
autoCapitalize="characters"
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={submit}
accessibilityHint={`Deleting ${email}`}
glass
/>
<Button
variant="destructive"
size="lg"
loading={loading}
disabled={!confirmed}
onPress={submit}
haptic={false}
className="w-full"
accessibilityLabel={`Permanently delete account ${email}`}
>
Delete my account
</Button>
<Button
variant="glass"
size="lg"
onPress={() => {
haptics.light();
onBack?.();
}}
haptic={false}
className="w-full"
>
Keep my account
</Button>
</View>
</Mount>
</ScrollView>
</KeyboardAvoiding>
</View>
</World>
);
}
DeleteAccountScreen.displayName = "DeleteAccountScreen";
Recreate the "Delete Account Screen" screen in React Native using LumioUI.
Goal: Destructive deletion flow — permanent-loss alert, concrete data summary, data-export escape hatch, and type-DELETE confirmation gate Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.
## Setup
Fastest path — install the screen plus its component deps:
```bash
npx lumioui add auth-delete-account
```
Or pull just the components: `npx lumioui add alert button icon input keyboard-avoiding text`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*
Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.
## Components — how, where, when
### `alert` — Inline status message with icon, title and variant tones.
- `<Alert>` ×1
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×2
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×1
### `keyboard-avoiding` — View that lifts above the keyboard on both platforms.
- `<KeyboardAvoiding>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×9
## Conventions (non-negotiable)
- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.
## Deliverable
One file: `screens/auth/delete-account.tsx` exporting DeleteAccountScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.
## Reference implementation
The LumioUI source for this screen — reproduce it faithfully:
```tsx
import * as React from "react";
import { View, ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useState } from "react";
import { Alert, Button, Glass, Icon, Input, Text, KeyboardAvoiding } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Mount, Press, BLOCK_STAGGER, World } from "../shared";
export interface AccountDataSummary {
/** What disappears — e.g. "248 saved items" or "Pro until Oct 12". */
label: string;
detail?: string;
}
export interface DeleteAccountScreenProps {
/** User-facing identity being deleted. */
email?: string;
/** Concrete losses shown as a list — the honest cost of the action. */
losses?: AccountDataSummary[];
/** Days before deletion is final (grace window). */
graceDays?: number;
onDelete?: () => void;
onDownloadData?: () => void;
onBack?: () => void;
}
const DEFAULT_LOSSES: AccountDataSummary[] = [
{ label: "Lumio Pro", detail: "Active until Oct 12, 2026" },
{ label: "248 saved items", detail: "Lists, favorites, collections" },
{ label: "17 orders", detail: "Receipts become unavailable" },
{ label: "3 signed-in devices", detail: "All sessions end immediately" },
];
/** Delete account — destructive flow: permanent-loss alert, concrete data
* summary, export escape hatch, and type-DELETE confirmation gate. */
export function DeleteAccountScreen({
email = "amara.chen@lumio.dev",
losses = DEFAULT_LOSSES,
graceDays = 30,
onDelete,
onDownloadData,
onBack,
}: DeleteAccountScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [confirmText, setConfirmText] = useState("DELETE");
const [loading, setLoading] = useState(false);
const confirmed = confirmText.trim().toUpperCase() === "DELETE";
const submit = () => {
if (!confirmed) return;
setLoading(true);
haptics.warning();
onDelete?.();
setTimeout(() => setLoading(false), 800);
};
return (
<World primary="#65A30D">
<View className="flex-1 bg-[#EBE2E2] dark:bg-[#1D1111]">
{/* Top bar — back affordance clears the status bar */}
<View className="px-2" style={{ paddingTop: insets.top + 4 }}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => onBack?.()}
accessibilityRole="button"
accessibilityLabel="Go back"
hitSlop={8}
className="h-11 flex-row items-center gap-1 self-start px-2"
>
<Icon name="chevron-left" size="sm" color="#65A30D" />
<Text className="text-base text-[#65A30D]">Back</Text>
</Press>
</View>
<KeyboardAvoiding>
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
contentContainerClassName="flex-grow px-6"
contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 16 }}
>
<Mount delay={0}>
<View className="gap-2">
<Text variant="h1" className="tracking-tight">
Delete your account?
</Text>
<Text variant="muted" className="max-w-xs text-base leading-relaxed">
<Text className="font-semibold text-foreground">{email}</Text> and
everything attached to it will be gone for good.
</Text>
</View>
</Mount>
<Mount delay={BLOCK_STAGGER}>
<Alert variant="destructive" className="mt-5">
<Alert.Title>This is permanent</Alert.Title>
<Alert.Description>
Deletion starts after a {graceDays}-day grace window and can't
be undone once it completes.
</Alert.Description>
</Alert>
{/* Concrete deadline — the exact date beats an abstract window. */}
<View className="mt-2 flex-row items-center gap-2 px-1">
<Icon name="clock" size="sm" color="destructive" />
<Text variant="small" className="text-destructive">
If you don't sign back in, everything deletes on{" "}
{new Date(Date.now() + graceDays * 86400000).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}.
</Text>
</View>
</Mount>
{/* What you lose — real rows, real numbers; honesty over decoration */}
<Mount delay={BLOCK_STAGGER * 2}>
<View className="mt-5 gap-3">
<Text variant="small" className="font-medium uppercase tracking-widest text-muted-foreground">
What you'll lose
</Text>
<View className="gap-3 overflow-hidden rounded-2xl bg-white p-4 dark:bg-white/5">
{losses.map((loss) => (
<View key={loss.label} className="flex-row items-baseline gap-3">
<View
className="mt-1.5 h-1.5 w-1.5 rounded-full bg-destructive"
accessibilityElementsHidden
importantForAccessibility="no"
/>
<View className="flex-1">
<Text variant="small" className="font-medium">
{loss.label}
</Text>
{loss.detail ? (
<Text variant="muted" className="text-xs">
{loss.detail}
</Text>
) : null}
</View>
</View>
))}
</View>
</View>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => onDownloadData?.()}
accessibilityRole="button"
accessibilityLabel="Download a copy of your data first"
hitSlop={8}
className="mt-4 min-h-11 flex-row items-center gap-2.5 self-start"
>
<Icon name="download" size="sm" color="#65A30D" />
<Text className="text-[#65A30D] font-medium">Download a copy of your data first</Text>
</Press>
</Mount>
{/* Confirmation gate — the typed word is the second factor */}
<Mount delay={BLOCK_STAGGER * 4} className="mt-6">
<View className="gap-3 pt-3">
<Input
label='Type "DELETE" to confirm'
placeholder="DELETE"
value={confirmText}
onChangeText={setConfirmText}
autoCapitalize="characters"
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={submit}
accessibilityHint={`Deleting ${email}`}
glass
/>
<Button
variant="destructive"
size="lg"
loading={loading}
disabled={!confirmed}
onPress={submit}
haptic={false}
className="w-full"
accessibilityLabel={`Permanently delete account ${email}`}
>
Delete my account
</Button>
<Button
variant="glass"
size="lg"
onPress={() => {
haptics.light();
onBack?.();
}}
haptic={false}
className="w-full"
>
Keep my account
</Button>
</View>
</Mount>
</ScrollView>
</KeyboardAvoiding>
</View>
</World>
);
}
DeleteAccountScreen.displayName = "DeleteAccountScreen";
```
Screen: https://lumioui.dev/screens/auth-delete-account