v0.1 public beta early access on GitHub
Home / Screens / auth-session-devices
Screen template

Session Devices Screen

Grouped sessions list — current device badge, per-row sign-out on other sessions, revoke-all destructive footer

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · warning
$ npx lumioui add auth-session-devices
import { SessionDevicesScreen } from "@/screens/auth/session-devices";
Registry dependencies4
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
auth/session-devices.tsxsource · yours to edit
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";
agent prompt — paste into Claude Code, Cursor, Devin…
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
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗