v0.1 public beta early access on GitHub
Home / Screens / core-terms-privacy
Screen template

Terms & privacy Screen

Legal document index with in-app reader view.

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection
$ npx lumioui add core-terms-privacy
import { TermsPrivacyScreen } from "@/screens/core/terms-privacy";
Registry dependencies4
Native packages installed6
@lumioui/ui@latestreact-native-safe-area-context@*@lumioui/core@latestreact-native-reanimated@*react-native-svg@*nativewind@*
core/terms-privacy.tsxsource · yours to edit
import * as React from "react";
import { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Glass, Icon, List, Text, type IconName } from "@lumioui/ui";
import { BLOCK_STAGGER, Mount, Press, RowIcon, SEPARATOR_INSET, type IconTone, } from "../shared";

export interface LegalDocument {
  id: string;
  title: string;
  updated: string;
  /** Rendered document body. */
  body: string;
  /** e.g. "4 min read" — shown after `updated` in the row subtitle. */
  readTime?: string;
  /** e.g. "Accepted Sep 2" — renders a green status chip on the row. */
  accepted?: string;
}

export interface TermsPrivacyScreenProps {
  documents?: LegalDocument[];
  /** Set to open directly into a document instead of the index. */
  initialDocument?: string;
  onOpenDocument?: (id: string) => void;
  onBack?: () => void;
}

const DEFAULT_DOCUMENTS: LegalDocument[] = [
  {
    id: "terms",
    title: "Terms of Service",
    updated: "Updated Sep 1, 2026",
    readTime: "4 min read",
    accepted: "Accepted Sep 2",
    body: `1. Acceptance of terms

By creating an account or using Lumio, you agree to these Terms of Service. If you do not agree, do not use the service. We may update these terms; material changes are announced 30 days in advance.

2. Your account

You are responsible for activity under your account and for keeping your credentials confidential. You must be at least 13 years old.

3. Acceptable use

You may not misuse the service — no scraping, no interfering with infrastructure, no unlawful content. We may suspend accounts that violate these rules.

4. Content you share

You retain ownership of content you post. You grant Lumio a limited license to host and display it so the service can function.

5. Termination

You can delete your account at any time from Settings. We may terminate access for violations with notice where required.`,
  },
  {
    id: "privacy",
    title: "Privacy Policy",
    updated: "Updated Aug 18, 2026",
    readTime: "3 min read",
    accepted: "Accepted Sep 2",
    body: `What we collect

Account details (name, email), content you create, and usage data such as feature interactions and crash reports. We do not sell personal data.

How we use it

To operate and improve the service, personalize your experience, and keep the platform secure.

Sharing

We share data with processors under contract (hosting, analytics) and when legally required — never with advertisers.

Your rights

Access, correct, export, or delete your data from Settings → Privacy. Contact privacy@lumio.dev for anything else.`,
  },
  {
    id: "cookies",
    title: "Cookie Policy",
    updated: "Updated Jul 30, 2026",
    readTime: "2 min read",
    accepted: "Accepted Sep 2",
    body: `What cookies we use

Strictly necessary cookies keep you signed in and remember your preferences. Analytics cookies help us understand which features are used. We do not use advertising cookies.

Managing cookies

You can disable non-essential cookies in Settings → Privacy → Cookies, or through your browser controls. Disabling some cookies may degrade sign-in and personalization.`,
  },
  {
    id: "licenses",
    title: "Open-source licenses",
    updated: "Updated Jun 12, 2026",
    readTime: "6 min read",
    accepted: "Accepted Sep 2",
    body: `Lumio is built on open source

React Native (MIT), Reanimated (MIT), Expo (MIT), NativeWind (MIT), React (MIT), and the broader ecosystem their maintainers keep alive.

Full texts

Complete license texts ship inside the app bundle and are listed per dependency. If a license seems misapplied, contact legal@lumio.dev.`,
  },
  {
    id: "dpa",
    title: "Data Processing Addendum",
    updated: "Updated Aug 18, 2026",
    readTime: "5 min read",
    body: `Scope

This addendum governs personal data Lumio processes on behalf of workspace administrators — member profiles, content, and operational logs.

Our commitments

Processing only on documented instructions, subprocessors under equivalent terms, 30-day breach notification, and deletion or return of data on termination.`,
  },
];

const DOC_ICONS: Record<string, { icon: IconName; tone: IconTone }> = {
  terms: { icon: "file-text", tone: "blue" },
  privacy: { icon: "shield", tone: "green" },
  cookies: { icon: "info", tone: "orange" },
  licenses: { icon: "book-open", tone: "purple" },
  dpa: { icon: "lock", tone: "teal" },
};

const LAST_UPDATE_BULLETS = [
  "Clarified how workspace content is licensed",
  "Added a plain-language data sharing section",
  "New cookie controls in Settings → Privacy",
];

/** Terms & privacy — document index + readable legal text view. */
export function TermsPrivacyScreen({
  documents = DEFAULT_DOCUMENTS,
  initialDocument,
  onOpenDocument,
  onBack,
}: TermsPrivacyScreenProps) {
  const insets = useSafeAreaInsets();
  const [openId, setOpenId] = React.useState<string | undefined>(initialDocument);
  const open = documents.find((d) => d.id === openId);

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back disc + centered title pill. */}
      <View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
        <View className="flex-row items-center gap-2">
          <Glass
            effect="regular"
            className="h-11 w-11 items-center justify-center rounded-full"
          >
            <Press
              onPress={() => (open ? setOpenId(undefined) : onBack?.())}
              accessibilityLabel="Go back"
              accessibilityRole="button"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <Glass
            effect="regular"
            className="h-11 flex-row items-center justify-center rounded-full px-5"
          >
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              {open ? open.title : "Terms & privacy"}
            </Text>
          </Glass>
        </View>
      </View>

      {open ? (
        <ScrollView
          showsVerticalScrollIndicator={false}
          contentContainerStyle={{
            paddingTop: insets.top + 60,
            paddingBottom: insets.bottom + 32,
            paddingHorizontal: 24,
          }}
        >
          <Text variant="small" className="pb-4 text-muted-foreground">
            {open.updated}
          </Text>
          <Text variant="small" className="leading-6 text-foreground">
            {open.body}
          </Text>
        </ScrollView>
      ) : (
        <ScrollView
          showsVerticalScrollIndicator={false}
          contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
        >
          {/* "What changed" hero — ink card with lime checks, diff highlights
              of the latest update so the index opens with substance. */}
          <Mount>
            <View className="mx-4 mb-4 overflow-hidden rounded-3xl bg-[#1B1B26]">
              <View className="gap-3 p-5">
                <View className="flex-row items-center gap-2">
                  <Icon name="zap" size="sm" color="#BEF04C" />
                  <Text variant="small" className="font-semibold uppercase tracking-widest text-white/55">
                    What changed in Sep
                  </Text>
                </View>
                <Text className="text-2xl font-bold tracking-tight text-white">
                  Terms, simplified
                </Text>
                {LAST_UPDATE_BULLETS.map((b) => (
                  <View key={b} className="flex-row items-center gap-2.5">
                    <View className="h-5 w-5 items-center justify-center rounded-full bg-[#BEF04C]">
                      <Icon name="check" size="sm" color="#1B1B26" />
                    </View>
                    <Text variant="small" className="flex-1 text-white/80">
                      {b}
                    </Text>
                  </View>
                ))}
                <Press
                  haptic="light"
                  pressedScale={0.97}
                  className="mt-1 flex-row items-center gap-1.5 self-start rounded-full bg-white/10 px-3.5 py-2"
                  onPress={() => setOpenId("terms")}
                  accessibilityRole="button"
                  accessibilityLabel="Read the updated terms"
                >
                  <Text variant="small" className="font-semibold text-[#BEF04C]">
                    Read the update
                  </Text>
                  <Icon name="chevron-right" size="sm" color="#BEF04C" />
                </Press>
              </View>
            </View>
          </Mount>

          <Mount delay={BLOCK_STAGGER}>
            <List inset>
              <List.Section>
                {documents.map((doc, ii) => (
                  <React.Fragment key={doc.id}>
                    {ii > 0 && <List.Separator className={SEPARATOR_INSET} />}
                    <List.Item
                      title={doc.title}
                      subtitle={[doc.updated, doc.readTime].filter(Boolean).join(" · ")}
                      leading={
                        <RowIcon
                          name={DOC_ICONS[doc.id]?.icon ?? "file-text"}
                          tone={DOC_ICONS[doc.id]?.tone ?? "gray"}
                        />
                      }
                      trailing={
                        doc.accepted ? (
                          <View className="flex-row items-center gap-1 rounded-full bg-success/15 px-2 py-1">
                            <Icon name="check" size="sm" color="success" />
                            <Text className="text-[11px] font-medium text-success">
                              {doc.accepted}
                            </Text>
                          </View>
                        ) : undefined
                      }
                      showChevron
                      haptic="light"
                      onPress={() => {
                        setOpenId(doc.id);
                        onOpenDocument?.(doc.id);
                      }}
                    />
                  </React.Fragment>
                ))}
              </List.Section>
            </List>
            <Text variant="small" className="px-8 pt-2 text-muted-foreground">
              Previous versions are available on request from legal@lumio.dev.
            </Text>
            {/* Doc footer — every real legal page ends with the feedback ask. */}
            <View className="mx-4 mt-3 flex-row items-center justify-between overflow-hidden rounded-2xl bg-white px-4 py-3 dark:bg-[#1A1A20]">
              <Text variant="small" className="text-muted-foreground">
                Was this document clear?
              </Text>
              <View className="flex-row gap-5">
                <Press haptic="light" accessibilityRole="button" accessibilityLabel="Yes, clear">
                  <Text variant="small" className="font-medium text-primary">Yes</Text>
                </Press>
                <Press haptic="light" accessibilityRole="button" accessibilityLabel="No, unclear">
                  <Text variant="small" className="font-medium text-muted-foreground">No</Text>
                </Press>
              </View>
            </View>
          </Mount>
        </ScrollView>
      )}
    </SafeAreaView>
  );
}
TermsPrivacyScreen.displayName = "TermsPrivacyScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Terms & privacy Screen" screen in React Native using LumioUI.

Goal: Legal document index with in-app reader view. 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 core-terms-privacy
```

Or pull just the components: `npx lumioui add button icon list text`

Native packages:
- @lumioui/ui@latest
- react-native-safe-area-context@*
- @lumioui/core@latest
- 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

### `button` — Tactile pill button with press spring, ink wash and async loading.
- imported but not rendered directly — likely used through a sibling or as a utility

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5

### `list` — Settings-style rows with separators and press feedback.
- `<List>` ×1

### `text` — Themed type scale from caption to display.
- `<Text>` ×12

## 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/core/terms-privacy.tsx` exporting TermsPrivacyScreen. 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 { ScrollView, View } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Glass, Icon, List, Text, type IconName } from "@lumioui/ui";
import { BLOCK_STAGGER, Mount, Press, RowIcon, SEPARATOR_INSET, type IconTone, } from "../shared";

export interface LegalDocument {
  id: string;
  title: string;
  updated: string;
  /** Rendered document body. */
  body: string;
  /** e.g. "4 min read" — shown after `updated` in the row subtitle. */
  readTime?: string;
  /** e.g. "Accepted Sep 2" — renders a green status chip on the row. */
  accepted?: string;
}

export interface TermsPrivacyScreenProps {
  documents?: LegalDocument[];
  /** Set to open directly into a document instead of the index. */
  initialDocument?: string;
  onOpenDocument?: (id: string) => void;
  onBack?: () => void;
}

const DEFAULT_DOCUMENTS: LegalDocument[] = [
  {
    id: "terms",
    title: "Terms of Service",
    updated: "Updated Sep 1, 2026",
    readTime: "4 min read",
    accepted: "Accepted Sep 2",
    body: `1. Acceptance of terms

By creating an account or using Lumio, you agree to these Terms of Service. If you do not agree, do not use the service. We may update these terms; material changes are announced 30 days in advance.

2. Your account

You are responsible for activity under your account and for keeping your credentials confidential. You must be at least 13 years old.

3. Acceptable use

You may not misuse the service — no scraping, no interfering with infrastructure, no unlawful content. We may suspend accounts that violate these rules.

4. Content you share

You retain ownership of content you post. You grant Lumio a limited license to host and display it so the service can function.

5. Termination

You can delete your account at any time from Settings. We may terminate access for violations with notice where required.`,
  },
  {
    id: "privacy",
    title: "Privacy Policy",
    updated: "Updated Aug 18, 2026",
    readTime: "3 min read",
    accepted: "Accepted Sep 2",
    body: `What we collect

Account details (name, email), content you create, and usage data such as feature interactions and crash reports. We do not sell personal data.

How we use it

To operate and improve the service, personalize your experience, and keep the platform secure.

Sharing

We share data with processors under contract (hosting, analytics) and when legally required — never with advertisers.

Your rights

Access, correct, export, or delete your data from Settings → Privacy. Contact privacy@lumio.dev for anything else.`,
  },
  {
    id: "cookies",
    title: "Cookie Policy",
    updated: "Updated Jul 30, 2026",
    readTime: "2 min read",
    accepted: "Accepted Sep 2",
    body: `What cookies we use

Strictly necessary cookies keep you signed in and remember your preferences. Analytics cookies help us understand which features are used. We do not use advertising cookies.

Managing cookies

You can disable non-essential cookies in Settings → Privacy → Cookies, or through your browser controls. Disabling some cookies may degrade sign-in and personalization.`,
  },
  {
    id: "licenses",
    title: "Open-source licenses",
    updated: "Updated Jun 12, 2026",
    readTime: "6 min read",
    accepted: "Accepted Sep 2",
    body: `Lumio is built on open source

React Native (MIT), Reanimated (MIT), Expo (MIT), NativeWind (MIT), React (MIT), and the broader ecosystem their maintainers keep alive.

Full texts

Complete license texts ship inside the app bundle and are listed per dependency. If a license seems misapplied, contact legal@lumio.dev.`,
  },
  {
    id: "dpa",
    title: "Data Processing Addendum",
    updated: "Updated Aug 18, 2026",
    readTime: "5 min read",
    body: `Scope

This addendum governs personal data Lumio processes on behalf of workspace administrators — member profiles, content, and operational logs.

Our commitments

Processing only on documented instructions, subprocessors under equivalent terms, 30-day breach notification, and deletion or return of data on termination.`,
  },
];

const DOC_ICONS: Record<string, { icon: IconName; tone: IconTone }> = {
  terms: { icon: "file-text", tone: "blue" },
  privacy: { icon: "shield", tone: "green" },
  cookies: { icon: "info", tone: "orange" },
  licenses: { icon: "book-open", tone: "purple" },
  dpa: { icon: "lock", tone: "teal" },
};

const LAST_UPDATE_BULLETS = [
  "Clarified how workspace content is licensed",
  "Added a plain-language data sharing section",
  "New cookie controls in Settings → Privacy",
];

/** Terms & privacy — document index + readable legal text view. */
export function TermsPrivacyScreen({
  documents = DEFAULT_DOCUMENTS,
  initialDocument,
  onOpenDocument,
  onBack,
}: TermsPrivacyScreenProps) {
  const insets = useSafeAreaInsets();
  const [openId, setOpenId] = React.useState<string | undefined>(initialDocument);
  const open = documents.find((d) => d.id === openId);

  return (
    <SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
      {/* Floating chrome — back disc + centered title pill. */}
      <View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
        <View className="flex-row items-center gap-2">
          <Glass
            effect="regular"
            className="h-11 w-11 items-center justify-center rounded-full"
          >
            <Press
              onPress={() => (open ? setOpenId(undefined) : onBack?.())}
              accessibilityLabel="Go back"
              accessibilityRole="button"
              className="h-9 w-9 items-center justify-center"
            >
              <Icon name="chevron-left" size="md" color="foreground" />
            </Press>
          </Glass>
        </View>
        <View pointerEvents="none" className="absolute inset-0 items-center justify-center">
          <Glass
            effect="regular"
            className="h-11 flex-row items-center justify-center rounded-full px-5"
          >
            <Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
              {open ? open.title : "Terms & privacy"}
            </Text>
          </Glass>
        </View>
      </View>

      {open ? (
        <ScrollView
          showsVerticalScrollIndicator={false}
          contentContainerStyle={{
            paddingTop: insets.top + 60,
            paddingBottom: insets.bottom + 32,
            paddingHorizontal: 24,
          }}
        >
          <Text variant="small" className="pb-4 text-muted-foreground">
            {open.updated}
          </Text>
          <Text variant="small" className="leading-6 text-foreground">
            {open.body}
          </Text>
        </ScrollView>
      ) : (
        <ScrollView
          showsVerticalScrollIndicator={false}
          contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 32 }}
        >
          {/* "What changed" hero — ink card with lime checks, diff highlights
              of the latest update so the index opens with substance. */}
          <Mount>
            <View className="mx-4 mb-4 overflow-hidden rounded-3xl bg-[#1B1B26]">
              <View className="gap-3 p-5">
                <View className="flex-row items-center gap-2">
                  <Icon name="zap" size="sm" color="#BEF04C" />
                  <Text variant="small" className="font-semibold uppercase tracking-widest text-white/55">
                    What changed in Sep
                  </Text>
                </View>
                <Text className="text-2xl font-bold tracking-tight text-white">
                  Terms, simplified
                </Text>
                {LAST_UPDATE_BULLETS.map((b) => (
                  <View key={b} className="flex-row items-center gap-2.5">
                    <View className="h-5 w-5 items-center justify-center rounded-full bg-[#BEF04C]">
                      <Icon name="check" size="sm" color="#1B1B26" />
                    </View>
                    <Text variant="small" className="flex-1 text-white/80">
                      {b}
                    </Text>
                  </View>
                ))}
                <Press
                  haptic="light"
                  pressedScale={0.97}
                  className="mt-1 flex-row items-center gap-1.5 self-start rounded-full bg-white/10 px-3.5 py-2"
                  onPress={() => setOpenId("terms")}
                  accessibilityRole="button"
                  accessibilityLabel="Read the updated terms"
                >
                  <Text variant="small" className="font-semibold text-[#BEF04C]">
                    Read the update
                  </Text>
                  <Icon name="chevron-right" size="sm" color="#BEF04C" />
                </Press>
              </View>
            </View>
          </Mount>

          <Mount delay={BLOCK_STAGGER}>
            <List inset>
              <List.Section>
                {documents.map((doc, ii) => (
                  <React.Fragment key={doc.id}>
                    {ii > 0 && <List.Separator className={SEPARATOR_INSET} />}
                    <List.Item
                      title={doc.title}
                      subtitle={[doc.updated, doc.readTime].filter(Boolean).join(" · ")}
                      leading={
                        <RowIcon
                          name={DOC_ICONS[doc.id]?.icon ?? "file-text"}
                          tone={DOC_ICONS[doc.id]?.tone ?? "gray"}
                        />
                      }
                      trailing={
                        doc.accepted ? (
                          <View className="flex-row items-center gap-1 rounded-full bg-success/15 px-2 py-1">
                            <Icon name="check" size="sm" color="success" />
                            <Text className="text-[11px] font-medium text-success">
                              {doc.accepted}
                            </Text>
                          </View>
                        ) : undefined
                      }
                      showChevron
                      haptic="light"
                      onPress={() => {
                        setOpenId(doc.id);
                        onOpenDocument?.(doc.id);
                      }}
                    />
                  </React.Fragment>
                ))}
              </List.Section>
            </List>
            <Text variant="small" className="px-8 pt-2 text-muted-foreground">
              Previous versions are available on request from legal@lumio.dev.
            </Text>
            {/* Doc footer — every real legal page ends with the feedback ask. */}
            <View className="mx-4 mt-3 flex-row items-center justify-between overflow-hidden rounded-2xl bg-white px-4 py-3 dark:bg-[#1A1A20]">
              <Text variant="small" className="text-muted-foreground">
                Was this document clear?
              </Text>
              <View className="flex-row gap-5">
                <Press haptic="light" accessibilityRole="button" accessibilityLabel="Yes, clear">
                  <Text variant="small" className="font-medium text-primary">Yes</Text>
                </Press>
                <Press haptic="light" accessibilityRole="button" accessibilityLabel="No, unclear">
                  <Text variant="small" className="font-medium text-muted-foreground">No</Text>
                </Press>
              </View>
            </View>
          </Mount>
        </ScrollView>
      )}
    </SafeAreaView>
  );
}
TermsPrivacyScreen.displayName = "TermsPrivacyScreen";
```

Screen: https://lumioui.dev/screens/core-terms-privacy
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗