Skip to content
Back to skills

Settings And Preferences

ASecurity

Builds app settings - saved fields, defaults, rows, each setting's effect, resets that keep them. Use when adding a setting or toggle, or a setting is ignored or lost on restart. Not for theme tokens (design-system-structure).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentgobashsqlreactnodegitapi

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill settings-and-preferences --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Settings And Preferences?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Settings And Preferences
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-settings-and-preferences/badge)](https://www.skillsdirectory.com/skills/zakariaf-settings-and-preferences)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: settings-and-preferences
description: Builds app settings - saved fields, defaults, rows, each setting's effect, resets that keep them. Use when adding a setting or toggle, or a setting is ignored or lost on restart. Not for theme tokens (design-system-structure).
---

# Settings and preferences

Every setting is saved in one place, changed by one action, shown by one row, and applied by code
outside the settings screen, at once and without a Save button; data resets never touch it, and a
checker proves the whole chain against the app's own settings contract on iOS and Android alike.

## Rules that must hold

1. **One settings model, one owner.** Every saved preference is a field of one strict valibot object
   (`SETTINGS_SCHEMA` in `src/stores/settings/`) and is listed in `contracts/settings.json` with its
   validator, first-launch value, action and effect; nothing else keeps a copy (no `useState` mirror,
   no side cache, no zustand `persist`, no second key-value write). Why: one place to read, change
   and decode, so a restart can never show an older value. (`settings-schema`, `single-writer`)
2. **Saved through one synchronous adapter, read before the first paint.** Default: `expo-sqlite/kv-store`
   behind `SettingsStoragePort`, one JSON value under one key, read in the root layout's first-render
   initializer. Why: the first frame already has the saved theme and language, and one key is one
   atomic write. (`hydrate-sync`)
3. **Reduce, persist, then publish; a failed write publishes nothing.** The screen never shows a value
   that is not saved, the failure is shown (`settings.save-failed`), and storage that cannot be read
   runs the session in memory without overwriting it. No Save button, no delayed write.
   (`persist-then-publish`)
4. **One pure reducer action per change; ranges clamped in the reducer.** Imperative kebab-case
   (`set-theme`), a no-op returns the same object (nothing written), integer percent rounded and
   clamped with a property test. Why: every change is testable without React and replays the same.
   (`reducer-action`)
5. **Decode field by field; a stored field never changes shape in place.** A missing field takes its
   default quietly, an unreadable one falls back alone and is recorded, a new meaning gets a new field
   name, and a frozen sample of every shipped shape keeps decoding. Why: the key-value store has no
   migrations, and one bad value must never reset every setting.
6. **First-launch values follow the phone; noise is opt-in.** Settings that mirror a phone setting
   (language, theme, reduce motion, digits) start at `'system'`/`null`; sound and anything that
   interrupts start off; every other value is a product decision written in the contract.
   (`settings-default`)
7. **Every setting has an effect outside the settings code, applied at once**, through one of four
   mechanisms: a provider re-render (`PreferencesRoot` feeds `I18nProvider`, `AppThemeProvider
   preference`, the reduce-motion preference context the root stack's `RootMotion` hands to
   `MotionConfig`), a store subscription started at startup that returns its
   unsubscribe (sound), an adapter that reads the store at call time (the haptics gate: created
   before the services, connected once the store opens, silent until then), or a feature hook
   reading a selector. Why: a setting nothing reads is a bug people see. (`setting-effect`)
8. **Store canonical units; convert at the edge.** The volume is integer percent; `toSoundOutput`
   divides by 100 for the 0..1 an audio API takes; a 0..1 slider dispatches only when the whole
   percent changes. (`unit-scale`)
9. **Every row dispatches its action; rows that do not apply are hidden, never greyed.** Visibility
   is the pure `settingsGroupsFor(context)` over facts (haptics supported, Premium owned, a consent
   entry point needed), each rule unit-tested; testIDs and label keys come from one typed table.
   Why: a disabled control nobody can enable is noise for VoiceOver and TalkBack. (`row-dispatch`)
10. **A language change switches the text at once; only a layout-direction flip restarts, after
    the person confirms.** `planLanguageChange` compares the new language's direction with the
    layout's and the choice is saved either way. Why: React Native applies a direction at the next
    start, and a needless restart throws away state. (`restart-on-direction-only`)
11. **Resets never touch the settings.** "Delete all data" (`createDataResets`) deletes data through
    the data layer (persistence-and-migrations owns the SQL) and keeps settings and purchases (and
    first-run flags, unless the product decides otherwise); no code outside the storage adapter
    clears a whole key-value store (`Storage.clearSync()`, `AsyncStorage.clear()`); a failed delete
    is shown, never silent; "Reset settings" is its own action and confirmation; a save-document
    reset is one write inside `updateAndPublish`'s callback. (`reset-keeps-settings`, `reset-publish`)
12. **A switch plays its feedback once, after the dispatch** (the model input `onToggled`, which
    calls `playUiFeedback(services, 'toggle')`; never the press feedback); sliders and segments play
    none. Why: the feedback reads the settings at call time, so "sound off" is
    already silent. (`toggle-feedback`, when the contract sets it)
13. **Reduce motion is tri-state.** The switch shows the resolved value (`useReduceMotion()`) and
    writes the opposite explicitly (`'on'`/`'off'`); accessibility-as-code owns the resolution and the
    ban on Reanimated's `useReducedMotion`, and design-system-structure owns the theme provider.

## Workflow

1. Read [references/settings-model.md](references/settings-model.md): where each piece lives, the
   fields and first-launch values, persistence, decoding, the effects table, iOS and Android, resets,
   the contract keys and every checker rule.
2. **New settings model.** `npx expo install expo-sqlite expo-localization`, then
   `npm install -E zustand@5.0.15 valibot@1.5.0` (stack table, verified 2026-10-03; re-verify with
   `npm view zustand@5 version`). Copy `templates/src/stores/settings/` (schema, codec, reducer and
   its `settings-reducer-cases.ts` table, selectors, store), `templates/src/services/settings-storage/`,
   `templates/src/data/open-device-settings.ts` and `templates/contracts/` to `contracts/`. Replace the
   Tasks fields with the app's own, in the contract first, then the schema, defaults, codec
   `v.fallback` lines, actions, reducer cases and selectors.
3. **Wire the startup** (copy `templates/src/features/settings-effects/`): in the root layout's
   `decideStartup` create the gate (`createHapticsGate()`) before `createDeviceServices`, whose haptics
   adapter takes `gate.isEnabled`, and resolve the language from `readSavedLanguage(...)`
   ([examples/root-layout.tsx](examples/root-layout.tsx)); in AppRoot's `startApp`, after the data
   layer and the error sink, open the store (`openDeviceSettings(errorLog)`), then
   `gate.connect(settings)` and `connectSoundSettings` (audio-and-haptics-feedback owns both ports);
   render `SettingsStoreProvider` and `PreferencesRoot` in place of fixed provider values; the
   canonical `root-stack.tsx` stays as it is (its `RootMotion` reads the preference `PreferencesRoot`
   provides) ([examples/app-root.tsx](examples/app-root.tsx),
   [examples/app-providers.tsx](examples/app-providers.tsx)).
4. **The screen.** Read [references/rows-and-controls.md](references/rows-and-controls.md). Copy
   `templates/src/features/settings/` and `templates/src/config/external-links.ts`, merge
   `templates/src/i18n/catalogs/*.json` into the app's catalogs (translate for every app language),
   add the route as [examples/settings-route.tsx](examples/settings-route.tsx) and the view as
   [examples/settings-screen.tsx](examples/settings-screen.tsx) with the ui-component-library rows and
   the three confirm dialogs; gather the row facts (signed in, haptics, Premium, consent) and the
   `dataResets` wiring (`updateAndPublish` around the data layer's reset) as
   [examples/use-settings-input.ts](examples/use-settings-input.ts). Fill `PRIVACY_POLICY_PAGE`: its
   test fails while the placeholder is left in. The account rows hand off to auth-and-secure-storage.
5. **Adding or changing a setting:** follow "Adding or changing a setting" in the model reference:
   contract, schema and defaults, codec line and a frozen sample, action (test first), selector, row,
   effect. A new setting is a product decision: ask the developer first.
6. **Save document instead of the key-value store** (persistence-and-migrations' document mode): use
   [examples/settings-contract-document-mode.json](examples/settings-contract-document-mode.json) as
   the contract and read the model reference's appendix.
7. **Test**, then `npm run typecheck`:

   ```bash
   npx jest src/stores/settings src/services/settings-storage src/features/settings \
     src/features/settings-effects src/config --ci --maxWorkers=2
   ```

8. **On devices**, after a change to the storage or the startup: change the theme, kill and relaunch on
   a named iOS simulator and a named Android emulator (commands in the rows reference); the first frame
   must show the saved theme. Without devices, say in the report that this check is owed.
9. **Run the checker** from the repo root: `node ${CLAUDE_SKILL_DIR}/scripts/check-settings.mjs .`;
   fix every `FAIL` line (file, rule and fix are printed) and rerun until it prints `RESULT: PASS`.

## Definition of done

- [ ] `contracts/settings.json` lists every field (validator, first-launch value, action, effect) and
      every action; schema and defaults match it, and the codec has a `v.fallback` line per field.
- [ ] The store hydrates synchronously, persists before it publishes, publishes nothing on a failed
      write, and never writes when the saved settings could not be read; its tests pass.
- [ ] Every action has a reducer case and a test; every range is clamped with a property test; the
      codec test holds a frozen sample of every shipped shape.
- [ ] Every row dispatches its action; hidden rows follow `settingsGroupsFor` and its tests; row
      testIDs and labels come from the typed tables and every app catalog has the keys.
- [ ] Every field's effect is wired and tested outside the settings code; percent values are
      converted at the edge.
- [ ] A language of the other direction opens "Restart to apply"; the same direction switches at once.
- [ ] Data resets keep the settings and purchases (first-run flags as the product decided), and a
      failed delete shows `settings.delete-all-data.failed`; "Reset settings" is its own confirmed
      action.
- [ ] The Privacy policy row opens the app's real https page (`PRIVACY_POLICY_PAGE` filled in).
- [ ] The restart check passed on a named iOS simulator and a named Android emulator, or the report
      says it is owed.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-settings.mjs .` prints `RESULT: PASS` (expected only
      before the data layer exists: `SKIP src/data/reset-data.ts [reset-keeps-settings] not yet
      created (persistence-and-migrations creates it)`; an app with no settings prints `NOT APPLICABLE`).

## Anti-patterns

- **A local `useState` copy of a setting in the screen.** It drifts from the store and skips the save;
  read the store and dispatch.
- **A Save button or a debounced write.** Every change is saved at once; the store already persists
  before it publishes.
- **Loading settings with an async API after the first render.** The app flashes the default theme
  and language; read synchronously in the first-render initializer.
- **zustand's `persist` middleware or AsyncStorage for settings.** A second, unversioned copy that
  publishes before it saves; use the storage port.
- **One storage key per field.** A two-field change becomes two writes and can be half saved; one
  JSON value under one key.
- **Resetting every setting because one saved value is unreadable.** Decode field by field.
- **Reading `settings.reduceMotion` or `settings.theme` directly in a component.** `'system'` must
  resolve against the phone; the providers and `useReduceMotion()` do that.
- **Passing the stored percent to a 0..1 API or a 0..1 slider value straight into the store.**
  Convert at the edge; dispatch only whole-percent changes.
- **Greying out rows that do not apply.** Hide them, from the pure visibility function.
- **Restarting for every language change**, or opening the restart dialog after a failed save (the
  restart comes back in the old language).
- **Clearing the whole key-value store in "Delete all data"** (`Storage.clearSync()`,
  `AsyncStorage.clear()`): the settings go with it. Delete through the data layer.
- **Playing the toggle sound in the row component, or before the dispatch.** It sounds twice, and
  "sound off" still clicks.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/settings-model.md](references/settings-model.md) | Homes, fields, defaults, persistence, decoding, actions, effects, iOS and Android, resets, adding a setting, document mode, multi-package, contract keys, checker rules | Workflow steps 1, 5 and 6 |
| [references/rows-and-controls.md](references/rows-and-controls.md) | Route, model and view; each row kind with VoiceOver and TalkBack; slider pitfall; language list and restart; dialogs; copy keys; testIDs and device flows; tests; React Navigation appendix | Workflow steps 4 and 8 |
| `templates/src/stores/settings/` | Schema and defaults, codec, reducer and `settings-reducer-cases.ts` (the rules as data, also read by state-management-zustand's reducer checker), selectors, store, store context, each with its test (synced canonical copies: never edit here) | Workflow step 2 |
| `templates/src/services/settings-storage/` | `SettingsStoragePort` and its fake with a test (synced), and the `expo-sqlite/kv-store` adapter | Workflow step 2 |
| [templates/src/data/open-device-settings.ts](templates/src/data/open-device-settings.ts) | Creates the store over the key-value adapter (composition root) | Workflow steps 2 and 3 |
| `templates/src/features/settings-effects/` | `PreferencesRoot` (language, digits, theme, reduce motion), `connectSoundSettings`, `createHapticsGate`, `readSavedLanguage` (for rtl-and-direction's `DirectionGate`), with tests | Workflow step 3 |
| `templates/src/features/settings/` | Rows and visibility (with the account group), preference handlers, `data-resets.ts` ("Delete all data" after its confirmation), the model hook, the language-change plan, with tests | Workflow step 4 |
| `templates/src/config/` | `external-links.ts` (privacy policy page, store review page per platform, support mail) and its test | Workflow step 4 |
| `templates/src/i18n/catalogs/` | The settings copy keys in `en` and `ar`, merged into the app's catalogs | Workflow step 4 |
| `templates/contracts/` | `settings.json` (the Tasks contract) and `settings.schema.json` | Workflow steps 2 and 5 |
| [examples/root-layout.tsx](examples/root-layout.tsx) | The canonical root layout with the haptics gate and the saved language in `decideStartup` | Workflow step 3 |
| [examples/app-root.tsx](examples/app-root.tsx) | The canonical AppRoot opening the store, connecting the effects and rendering `PreferencesRoot` | Workflow step 3 |
| [examples/app-providers.tsx](examples/app-providers.tsx) | AppRoot's outer providers with `SettingsStoreProvider` after the data layer | Workflow step 3 |
| [examples/settings-route.tsx](examples/settings-route.tsx) | Thin Expo Router route and its feature hook shape (row hand-offs, the three dialogs) | Workflow step 4 |
| [examples/settings-screen.tsx](examples/settings-screen.tsx) | The pure screen view over the model: rows, failure notes heard on both platforms, confirm dialogs | Workflow step 4 |
| [examples/use-settings-input.ts](examples/use-settings-input.ts) | Where the row facts, locales, layout direction and the data-reset wiring come from | Workflow step 4 |
| [examples/settings-contract-document-mode.json](examples/settings-contract-document-mode.json) | The contract for settings kept in the save document | Workflow step 6 |
| `scripts/check-settings.mjs` | Checker for the contract, schema, defaults, actions, store, writers, rows, effects, units, resets, restarts and feedback | Workflow step 9, and at the end |
| `scripts/selftest.mjs` | Proves the checker passes the templates and catches every planted bug | After changing the checker |
| `scripts/check-lib.mjs` | Shared script helpers, synced from the library (do not edit here) | Never by hand |
| `scripts/lib/object-literal.mjs` | Shared object-literal reader, synced (do not edit here) | Never by hand |
| `assets/shared.json` | Declares the synced files (check-lib, the object-literal reader, the canonical store and port files) | When adding a shared file |
| `assets/template-placeholders.json` | Sample values for the template placeholders (`__PRIVACY_POLICY_URL__`) used when the templates are verified | When adding a placeholder |
| `tests/` | `bases/app/` (a minimal app) and `fixtures/` (overlay cases over the templates, including document mode and silent decoys) | When adding a rule to the checker |

## Related skills

- `state-management-zustand` - the store pattern, selectors and `useShallow`, `updateAndPublish`.
- `persistence-and-migrations` - the key-value boundary, the save document, data resets.
- `design-system-structure` - `AppThemeProvider` and its `preference` prop.
- `accessibility-as-code` - `MotionConfig`, `useReduceMotion()`, roles and targets.
- `i18n-strings-and-catalogs` - `I18nProvider`, language resolution, digits, catalogs.
- `rtl-and-direction` - the layout direction and the restart that applies it.
- `audio-and-haptics-feedback` - the audio and haptics ports the effects drive.
- `ui-component-library` - the row, slider, segmented and dialog components.
- `auth-and-secure-storage` - the Sign out and Delete account rows' flows.
- `navigation-and-routing` - the settings routes and sub-screens.
- `in-app-purchases`, `admob-ads-and-consent` - the Premium and privacy-choices rows.
- `local-notifications`, `permissions-and-system-prompts` - reminder settings and system settings links.
- `e2e-maestro` - the restart flow on named devices.

Files in this skill

  • SKILL.md18 KB
  • assets/shared.json747 B
  • assets/template-placeholders.json68 B
  • examples/app-providers.tsx1.3 KB
  • examples/app-root.tsx4.2 KB
  • examples/root-layout.tsx3.8 KB
  • examples/settings-contract-document-mode.json2.6 KB
  • examples/settings-route.tsx2.7 KB
  • examples/settings-screen.tsx6.2 KB
  • examples/use-settings-input.ts3.1 KB
  • references/rows-and-controls.md16.2 KB
  • references/settings-model.md26.3 KB
  • scripts/check-settings.mjs32 KB
  • scripts/lib/object-literal.mjs3.5 KB
  • scripts/selftest.mjs579 B
  • templates/contracts/settings.json3 KB
  • templates/contracts/settings.schema.json7.6 KB
  • templates/src/config/external-links.test.ts2 KB
  • templates/src/config/external-links.ts2.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.