Grouped sessions list — current device badge, per-row sign-out on other sessions, revoke-all destructive footer
import * as React from "react";
import { View, ScrollView } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Icon, List, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Mount, Press, TintIcon, tintCard, SEPARATOR_INSET, World } from "../shared";
import type { IconName } from "@lumioui/ui";
export interface DeviceSession {
id: string;
/** Device or client name — "iPhone 17 Pro", "Safari on MacBook Pro". */
name: string;
location: string;
/** Human last-active label — "Active now", "2 hours ago". */
lastActive: string;
icon: "phone" | "globe";
hex: string;
current?: boolean;
}
export interface SessionDevicesScreenProps {
sessions?: DeviceSession[];
onRevoke?: (session: DeviceSession) => void;
onRevokeAll?: () => void;
onBack?: () => void;
}
const DEFAULT_SESSIONS: DeviceSession[] = [
{ id: "iphone", name: "iPhone 17 Pro", location: "San Francisco", lastActive: "Active now", icon: "phone", hex: "#4A7FD4", current: true },
{ id: "macbook", name: "Safari · MacBook Pro", location: "San Francisco", lastActive: "2 hours ago", icon: "globe", hex: "#129E8E" },
{ id: "ipad", name: "iPad Air", location: "Oakland", lastActive: "Yesterday", icon: "phone", hex: "#7C5CFC" },
{ id: "windows", name: "Chrome · Windows 11", location: "Austin", lastActive: "12 days ago", icon: "globe", hex: "#8B8FA3" },
];
/** Session devices — this-device hero card over a tinted-icon session
* ledger; each remote session carries its own revoke action. */
export function SessionDevicesScreen({
sessions = DEFAULT_SESSIONS,
onRevoke,
onRevokeAll,
onBack,
}: SessionDevicesScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const current = sessions.find((s) => s.current) ?? sessions[0];
const others = sessions.filter((s) => s.id !== current?.id);
const revoke = (session: DeviceSession) => {
haptics.warning();
onRevoke?.(session);
};
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View className="flex-row items-center gap-1 px-2 pb-1 pt-2">
<Press
onPress={() => {
haptics.light();
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>
<Mount>
<Text variant="h2" accessibilityRole="header" className="px-6 pb-4 pt-1">
Devices
</Text>
</Mount>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
contentContainerClassName="px-6"
>
{/* This device — wide hero card, not just another row. */}
{current ? (
<Mount delay={BLOCK_STAGGER}>
<View className="overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
<View className="flex-row items-center gap-4">
<View
className="h-14 w-14 items-center justify-center rounded-2xl"
style={{ backgroundColor: `${current.hex}26` }}
accessibilityElementsHidden
importantForAccessibility="no"
>
<Icon name={current.icon as IconName} size="lg" color={current.hex} />
</View>
<View className="flex-1">
<Text className="text-lg font-semibold tracking-tight">{current.name}</Text>
<Text variant="muted" className="text-sm">
{current.location} · {current.lastActive}
</Text>
</View>
</View>
<View className="mt-4 flex-row items-center gap-2">
<Badge variant="success" size="sm" icon={null}>This device</Badge>
<Text variant="muted" className="text-xs">
Signed in and active
</Text>
</View>
</View>
</Mount>
) : null}
{/* Other sessions — tinted icon rows with per-row Sign out. */}
<Mount delay={BLOCK_STAGGER * 2}>
<Text
variant="small"
className="px-1 pb-2 pt-7 font-medium uppercase tracking-widest text-muted-foreground"
>
Other sessions
</Text>
<List inset glass>
<List.Section>
{others.map((session, i) => (
<React.Fragment key={session.id}>
{i > 0 ? <List.Separator className={SEPARATOR_INSET} /> : null}
<List.Item
title={session.name}
subtitle={
session.location === "Austin"
? `${session.location} · ${session.lastActive} · New location`
: `${session.location} · ${session.lastActive}`
}
leading={<TintIcon name={session.icon as IconName} hex={session.hex} />}
trailing={
<Press
onPress={() => revoke(session)}
accessibilityRole="button"
accessibilityLabel={`Sign out ${session.name}`}
accessibilityHint="Ends that session on the other device"
hitSlop={8}
className="min-h-11 justify-center px-1"
>
<Text className="text-sm font-medium text-destructive">Sign out</Text>
</Press>
}
className="py-3"
accessibilityLabel={`${session.name}, last active ${session.lastActive}`}
/>
</React.Fragment>
))}
</List.Section>
</List>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => {
haptics.warning();
onRevokeAll?.();
}}
accessibilityRole="button"
accessibilityLabel="Sign out all other sessions"
accessibilityHint="Ends every session except this device"
className="mt-6 min-h-11 items-center justify-center"
>
<Text className="text-sm font-medium text-destructive">
Sign out all other sessions
</Text>
</Press>
<View className="flex-row items-center justify-center gap-1.5 pt-3">
<Icon name="mail" size="sm" color="mutedForeground" />
<Text variant="muted" className="text-center text-xs">
We email you when a new device signs in.
</Text>
</View>
</Mount>
</ScrollView>
</SafeAreaView>
</World>
);
}
SessionDevicesScreen.displayName = "SessionDevicesScreen";
Recreate the "Session Devices Screen" screen in React Native using LumioUI.
Goal: Grouped sessions list — current device badge, per-row sign-out on other sessions, revoke-all destructive footer 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-session-devices
```
Or pull just the components: `npx lumioui add badge icon list 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
### `badge` — Compact status pill with variants, icons and pulse option.
- `<Badge>` ×1
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×3
### `list` — Settings-style rows with separators and press feedback.
- `<List>` ×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/session-devices.tsx` exporting SessionDevicesScreen. 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 { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Badge, Icon, List, Text } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { BLOCK_STAGGER, Mount, Press, TintIcon, tintCard, SEPARATOR_INSET, World } from "../shared";
import type { IconName } from "@lumioui/ui";
export interface DeviceSession {
id: string;
/** Device or client name — "iPhone 17 Pro", "Safari on MacBook Pro". */
name: string;
location: string;
/** Human last-active label — "Active now", "2 hours ago". */
lastActive: string;
icon: "phone" | "globe";
hex: string;
current?: boolean;
}
export interface SessionDevicesScreenProps {
sessions?: DeviceSession[];
onRevoke?: (session: DeviceSession) => void;
onRevokeAll?: () => void;
onBack?: () => void;
}
const DEFAULT_SESSIONS: DeviceSession[] = [
{ id: "iphone", name: "iPhone 17 Pro", location: "San Francisco", lastActive: "Active now", icon: "phone", hex: "#4A7FD4", current: true },
{ id: "macbook", name: "Safari · MacBook Pro", location: "San Francisco", lastActive: "2 hours ago", icon: "globe", hex: "#129E8E" },
{ id: "ipad", name: "iPad Air", location: "Oakland", lastActive: "Yesterday", icon: "phone", hex: "#7C5CFC" },
{ id: "windows", name: "Chrome · Windows 11", location: "Austin", lastActive: "12 days ago", icon: "globe", hex: "#8B8FA3" },
];
/** Session devices — this-device hero card over a tinted-icon session
* ledger; each remote session carries its own revoke action. */
export function SessionDevicesScreen({
sessions = DEFAULT_SESSIONS,
onRevoke,
onRevokeAll,
onBack,
}: SessionDevicesScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const current = sessions.find((s) => s.current) ?? sessions[0];
const others = sessions.filter((s) => s.id !== current?.id);
const revoke = (session: DeviceSession) => {
haptics.warning();
onRevoke?.(session);
};
return (
<World primary="#65A30D">
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<View className="flex-row items-center gap-1 px-2 pb-1 pt-2">
<Press
onPress={() => {
haptics.light();
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>
<Mount>
<Text variant="h2" accessibilityRole="header" className="px-6 pb-4 pt-1">
Devices
</Text>
</Mount>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 32 }}
contentContainerClassName="px-6"
>
{/* This device — wide hero card, not just another row. */}
{current ? (
<Mount delay={BLOCK_STAGGER}>
<View className="overflow-hidden rounded-3xl bg-white p-5 dark:bg-[#1A1A20]">
<View className="flex-row items-center gap-4">
<View
className="h-14 w-14 items-center justify-center rounded-2xl"
style={{ backgroundColor: `${current.hex}26` }}
accessibilityElementsHidden
importantForAccessibility="no"
>
<Icon name={current.icon as IconName} size="lg" color={current.hex} />
</View>
<View className="flex-1">
<Text className="text-lg font-semibold tracking-tight">{current.name}</Text>
<Text variant="muted" className="text-sm">
{current.location} · {current.lastActive}
</Text>
</View>
</View>
<View className="mt-4 flex-row items-center gap-2">
<Badge variant="success" size="sm" icon={null}>This device</Badge>
<Text variant="muted" className="text-xs">
Signed in and active
</Text>
</View>
</View>
</Mount>
) : null}
{/* Other sessions — tinted icon rows with per-row Sign out. */}
<Mount delay={BLOCK_STAGGER * 2}>
<Text
variant="small"
className="px-1 pb-2 pt-7 font-medium uppercase tracking-widest text-muted-foreground"
>
Other sessions
</Text>
<List inset glass>
<List.Section>
{others.map((session, i) => (
<React.Fragment key={session.id}>
{i > 0 ? <List.Separator className={SEPARATOR_INSET} /> : null}
<List.Item
title={session.name}
subtitle={
session.location === "Austin"
? `${session.location} · ${session.lastActive} · New location`
: `${session.location} · ${session.lastActive}`
}
leading={<TintIcon name={session.icon as IconName} hex={session.hex} />}
trailing={
<Press
onPress={() => revoke(session)}
accessibilityRole="button"
accessibilityLabel={`Sign out ${session.name}`}
accessibilityHint="Ends that session on the other device"
hitSlop={8}
className="min-h-11 justify-center px-1"
>
<Text className="text-sm font-medium text-destructive">Sign out</Text>
</Press>
}
className="py-3"
accessibilityLabel={`${session.name}, last active ${session.lastActive}`}
/>
</React.Fragment>
))}
</List.Section>
</List>
</Mount>
<Mount delay={BLOCK_STAGGER * 3}>
<Press
haptic="light"
pressedScale={0.97}
onPress={() => {
haptics.warning();
onRevokeAll?.();
}}
accessibilityRole="button"
accessibilityLabel="Sign out all other sessions"
accessibilityHint="Ends every session except this device"
className="mt-6 min-h-11 items-center justify-center"
>
<Text className="text-sm font-medium text-destructive">
Sign out all other sessions
</Text>
</Press>
<View className="flex-row items-center justify-center gap-1.5 pt-3">
<Icon name="mail" size="sm" color="mutedForeground" />
<Text variant="muted" className="text-center text-xs">
We email you when a new device signs in.
</Text>
</View>
</Mount>
</ScrollView>
</SafeAreaView>
</World>
);
}
SessionDevicesScreen.displayName = "SessionDevicesScreen";
```
Screen: https://lumioui.dev/screens/auth-session-devices