Skip to content
Back to skills

Media Images And Files

ASecurity

Shows, picks and stores images - expo-image, picker, camera, a media store. Use when adding photos, avatars, attachments, uploads or save-to-Photos, or images flicker. Not for icons (icons-app-icon-and-splash).

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 18, 2026
developmentgoreactnodetestingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

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

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill media-images-and-files --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Media Images And Files?

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

Security grade badge for Media Images And Files
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-media-images-and-files/badge)](https://www.skillsdirectory.com/skills/zakariaf-media-images-and-files)

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: media-images-and-files
description: Shows, picks and stores images - expo-image, picker, camera, a media store. Use when adding photos, avatars, attachments, uploads or save-to-Photos, or images flicker. Not for icons (icons-app-icon-and-splash).
---

# Media: images and files

Every content image is an expo-image view with a box, a cache policy for its kind of source, a recycling
key in recycled lists and a name (or a decorative mark); photos are picked or captured only from a
press, through ports, the camera only after the permission gate; every picked file is downscaled and
re-encoded once, copied into the app's media folder under a new name and its temporaries discarded;
records hold that name (a MediaRef), never a file URI; files are never overwritten, swept when orphaned,
and leave the device only through ports. A zero-dependency checker proves it on iOS and Android code alike.

## Rules that must hold

1. **Every content image renders with expo-image inside a box.** A `style` with width and height (or
   one plus `aspectRatio`, or flex in a sized parent); `contentFit`, never React Native's `resizeMode`,
   `defaultSource` or `fadeDuration`. Why: without a box the view is 0 x 0 or jumps when the image
   loads, and the compatibility props are deprecated. (`image-size`, `image-legacy-prop`)
2. **Every expo-image `<Image>` is named or marked decorative.** `accessible` with
   `accessibilityLabel={t(...)}` for content, `accessible={false}` for decoration. Why: expo-image is not
   an accessibility element by default, so VoiceOver and TalkBack skip a photo silently. (`image-a11y`)
3. **An image in a recycled list cell has a `recyclingKey`** (the row id). Why: FlashList reuses the
   cell and its image view shows the previous row's photo until the new one loads. (`list-recycling-key`)
4. **The cache policy matches the source.** Stored files `cachePolicy="memory"` (they are on disk
   already); public remote images the default disk cache; private images memory-only or caches cleared
   on sign-out. Why: a disk cache duplicates stored files and keeps private photos after sign-out.
5. **Pick and capture only from a press, through `MediaPickerPort`: images only, no base64, no EXIF.**
   Why: a picker opened from an effect surprises the person and reopens after a cancel; base64 puts
   megabytes on the JS heap; EXIF carries the GPS position. (`picker-on-press`, `picker-options`)
6. **The camera opens only after the permission gate; only `src/services/permissions/` asks.** The
   library picker needs no permission on iOS 14+ and Android; the picker adapter reads the camera
   state through the gate's access (injected), never through its own permission functions. Why: iOS
   rejects `launchCameraAsync` without the grant and Android shows the system dialog with no
   explanation; one gate handles denied, rationale and blocked on both. (`permission-outside-gate`)
7. **The app config declares only the capture and library permissions the code uses.**
   `cameraPermission: false` when nothing opens a camera, `microphonePermission: false` when nothing
   records; these block the permission for the whole app, so never while expo-camera or expo-audio
   needs it. A save-only app keeps `READ_MEDIA_IMAGES`/`VIDEO` out. Why: the defaults add
   `RECORD_AUDIO`, and Play restricts broad photo reads (its Photo and Video Permissions policy).
   (`unused-permission`)
8. **Every picked or captured image is downscaled and re-encoded once (2048 px long edge, JPEG 0.8),
   one at a time, before it is stored; image bytes never live in JS.** Why: a 12 MP photo decodes to
   49 MB; the re-encode drops EXIF and turns HEIC into JPEG; base64 data URIs cost memory twice and
   defeat every cache. (`data-uri`)
9. **Kept media lives in the media store (`Paths.document/media/`), never in the cache, and records
   hold a MediaRef (a plain file name), never a URI.** Resolve with `uriOf(ref)` at render time. Why:
   the system purges the cache, and the absolute path of the app's folders changes after an update or a
   restore, so a stored `file://` URI breaks. (`media-in-cache`, `stored-uri`)
10. **A changed image is a new file under a new name; nothing is overwritten.** Write the partial file,
    rename it into place, save the record, then remove the old file. Why: image caches are keyed by URI
    and keep showing old pixels, and a crash mid-write corrupts the only copy. (`overwrite-in-place`)
11. **Every stored file has one owner record, and an orphan sweep runs after startup and after a
    restore.** Save the record first, then delete the file; the sweep keeps every file this process
    imported and every file the restore's safety copy references (`protectedRefs`), and removes stale
    partial copies. Why: crashes and abandoned imports leave files no record references, and nothing
    else removes them; the undo of a restore needs its photos.
    (`sweep-wired`)
12. **Only current SDK 57 APIs.** expo-file-system's `File`/`Directory`/`Paths` and expo-media-library's
    object API; never their legacy main-entry functions or `/legacy` entries. Why: in SDK 57 the old
    functions compile (declared `@deprecated`) and throw at runtime; `documentDirectory` and
    `cacheDirectory` are no longer exported (a tsc error, or `undefined` in untyped code). (`legacy-api`)
13. **Images leave the device only through ports, from a press**: the share sheet (data-export-and-
    restore's `ShareSheetPort`), `PhotoSaverPort`, and `FileTransferPort` for uploads and downloads,
    with URLs from `src/api` and headers from the session. Why: one adapter per SDK, the app's network
    policy keeps its single entry per transport, and only re-encoded, metadata-free files leave.
    (`transfer-outside-port`)

## Workflow

1. Read [references/displaying-images.md](references/displaying-images.md) for any image on screen, and
   [references/picking-and-capturing.md](references/picking-and-capturing.md) plus
   [references/files-and-storage.md](references/files-and-storage.md) when the person adds photos.
2. Prerequisites: the Result module (error-handling-typed-results) and the composition root
   (architecture-and-boundaries). Install:
   `npx expo install expo-image-picker expo-image-manipulator expo-file-system expo-device expo-crypto`
   (SDK 57: `~57.0.20`, `~57.0.20`, `~57.0.7`, `~57.0.2`, `~57.0.3`; `expo-image ~57.0.5` ships with the template, expo-file-system often arrives
   with persistence-and-migrations; verified 2026-10-14; re-verify with `npx expo install --check`).
3. Stop and ask the developer to classify the new SDKs (an architecture decision): merge
   `examples/contracts/architecture.json` into `contracts/architecture.json` and add the packages to
   `VENDOR_SDK_PATHS` in `eslint.config.mjs` in one commit with a Gate-Change trailer.
4. Copy the templates: `templates/src/core/media/`, `templates/src/services/{media-picker,media-store,image-resizer}/`,
   `templates/src/data/media/`, `templates/src/ui/media/` (list `StoredImage` in the component catalogue,
   ui-component-library). Copy the resizer adapter from
   `examples/src/services/image-resizer/` once expo-image-manipulator is installed.
5. Set the picker plugin options in `app.config.ts` (picking-and-capturing.md, section 8): purpose texts
   from the catalogs (i18n-strings-and-catalogs), `false` for every permission no code uses (they block
   it for the whole app: check expo-camera and expo-audio first).
6. Compose once: the three adapters in `createDeviceServices` (fakes in `createTestServices`), the picker
   with `isCameraAllowed: async () => isPermissionAllowed(await permissions.access.camera.permission())`
   and `isCameraPresent: () => Platform.OS !== 'ios' || Device.isDevice` (expo-device; opening the camera
   on the iOS simulator crashes, picking-and-capturing.md section 5), and `createMediaImporter({ picker, resizer, store, nextMediaId })`; features read `useServices()`.
   In the startup work after the data layer opened (app-startup-and-bootstrap), `await
   importer.importPendingPick()` first, then `sweepMedia(...)` with the safety copy's refs as
   `protectedRefs` (files-and-storage.md section 9); sweep again after a restore. `nextMediaId` is
   `() => randomUUID().toLowerCase()` from expo-crypto (Hermes has no global `crypto.randomUUID`).
7. Store MediaRefs: a `*_ref TEXT` column through a migration (persistence-and-migrations) or a
   `MediaRef` field in a store; a repository method lists every ref for the sweep.
8. Build the feature from `examples/src/features/tasks/` (model hook, jobs, photo strip): "Take photo"
   calls permissions-and-system-prompts' `usePermissionGate` `requestAccess` and the capture runs from
   its `onAllowed`, the screen renders the gate's sheet; one message per failure kind, the busy state
   blocking a second press, a failed record save removing the new files, remove = save then delete.
9. Sharing, Photos, uploads: [references/sharing-and-transfer.md](references/sharing-and-transfer.md)
   and `examples/src/services/{photo-saver,file-transfer}/`; tell networking-and-server-state and
   privacy-and-store-declarations about a new transport or data type before it ships.
10. Run the tests ([references/testing-and-checker.md](references/testing-and-checker.md)):
    `npx jest src/core/media src/services/media-picker src/services/media-store src/services/image-resizer src/data/media src/ui/media --ci`
    and `npm run typecheck`.
11. After an adapter change, run the media drill on a named iOS simulator and Android emulator
    (testing-and-checker.md, section 3), or say in the report that it is owed.
12. Run `node ${CLAUDE_SKILL_DIR}/scripts/check-media.mjs .`; fix every `FAIL` line and rerun until it
    prints `RESULT: PASS`.

## Definition of done

- [ ] Every expo-image `<Image>` has a box, `contentFit` (no legacy props), a name or
      `accessible={false}`, a cache policy fit for its source, and a `recyclingKey` in recycled cells.
- [ ] Picks and captures run from presses through `MediaPickerPort`; images only, no base64, no EXIF;
      the camera behind the permission gate; no plugin declares an unused capture or library permission.
- [ ] Every picked image goes through the importer: downscaled to 2048 px, JPEG, one at a time, stored
      under a new MediaRef in `Paths.document/media/`, temporaries discarded, all or nothing per batch.
- [ ] Records and stores hold MediaRefs, resolved with `uriOf` at render; nothing overwrites a stored
      file; deletes save the record first; the sweep runs after startup and after a restore and keeps
      the safety copy's refs (`protectedRefs`).
- [ ] No legacy expo-file-system or expo-media-library API; uploads and downloads only in the
      file-transfer adapter; sharing through `ShareSheetPort`.
- [ ] The tests of workflow step 10 pass; after an adapter change the drill passed on iOS and Android,
      or the report says it is owed.
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-media.mjs .` prints `RESULT: PASS`. Expected only before the
      app config exists: `SKIP app.config.ts [unused-permission] not yet created
      (app-variants-and-environment creates it)`. An app without media code prints `NOT APPLICABLE: no
      source file under src/ imports expo-image, expo-image-picker, expo-camera, expo-image-manipulator,
      expo-media-library, expo-file-system, and there is no src/services/media-store/`.

## Anti-patterns

- **Saving `asset.uri` from the picker in a record.** It is a cache file the system purges, at an
  absolute path that changes; import it and store the MediaRef.
- **`base64: true` "to upload it easily", or a `data:image` URI.** Megabytes on the JS heap, decoded
  twice, no cache; upload the stored file with `FileTransferPort`.
- **Opening the picker in `useEffect` when a screen mounts.** It reopens after a cancel and on every
  focus; open it from the press.
- **Calling `requestCameraPermissionsAsync` in a screen.** No explanation, no blocked state; the gate.
- **`cameraPermission: false` on the picker in an app with an expo-camera scanner.** The option strips
  `CAMERA` from the merged manifest for every package; the scanner can never be granted.
- **`granularPermissions` left at the default in a save-only app.** It declares `READ_MEDIA_IMAGES` and
  `READ_MEDIA_VIDEO`, which Play reviews under its Photo and Video Permissions policy.
- **Overwriting `photo.jpg` after a crop.** Every view keeps the old image; a new ref per change.
- **Deleting the file, then saving the record.** A crash leaves a record pointing at nothing; record
  first, the sweep removes leftovers.
- **Shipping full-size camera photos.** 5 to 10 MB per file, 49 MB per decode, Android's 25 MB backup
  quota gone after a few photos; downscale once on import.
- **Sharing the picked original.** It carries the GPS position; share the stored, re-encoded file.
- **`allowDownscaling={false}` or `contentFit="none"` in a list.** Full-size decodes per cell.
- **`saveToLibraryAsync` or `readAsStringAsync` from the main entries.** They throw in SDK 57; use the
  object APIs.
- **A second expo-sharing adapter.** Reuse data-export-and-restore's `ShareSheetPort`.

## Files in this skill

| File | What it is | Read/run when |
|---|---|---|
| [references/displaying-images.md](references/displaying-images.md) | expo-image API, sizing, cache policies, recycling, memory arithmetic, names, remote images, placeholders, platform differences, pitfalls | Workflow step 1, any image on screen |
| [references/picking-and-capturing.md](references/picking-and-capturing.md) | Picker and camera API, permissions per platform (read in the native sources), the gate, Android's lost activity, returned files, app config, expo-camera | Workflow steps 1-3, 5, 8 |
| [references/files-and-storage.md](references/files-and-storage.md) | The folders, MediaRef, the import, no overwrite, owners and the sweep, disk space and backups, the SDK 57 file-system API and its throwing legacy names, backups and media | Workflow steps 1, 6-7 |
| [references/sharing-and-transfer.md](references/sharing-and-transfer.md) | Share sheet, saving to Photos (expo-media-library 57), the file-transfer port, privacy, receiving images | Workflow step 9 |
| [references/testing-and-checker.md](references/testing-and-checker.md) | The shipped tests, testing with fakes, the media drill on named devices, every check-media rule with its fix, configuration | Workflow steps 10-12 |
| `templates/src/core/media/` | `media-ref.ts` (MediaRef, extensions), `image-fit.ts` (fit, decoded bytes, `MEDIA_LIMITS`), `media-sweep.ts` (the sweep plan), each with its test | Workflow step 4 |
| `templates/src/services/` | `media-picker/`, `media-store/`, `image-resizer/`: ports, the picker and file-system adapters, fakes with tests | Workflow steps 4, 6 |
| `templates/src/data/media/` | `media-importer.ts` (pick, downscale, store, discard; all or nothing; protected from the sweep from the moment each file is stored until the process ends) and `media-sweeper.ts` (stale partials, orphans), with tests | Workflow steps 4, 6 |
| `templates/src/ui/media/` | `stored-image.tsx` (sized, memory-cached, recycled, named) and its test | Workflow step 4 |
| `examples/src/services/` | `image-resizer/` (expo-image-manipulator adapter), `photo-saver/` (expo-media-library port and adapter), `file-transfer/` (upload and download port and adapter), type-checked against their SDK 57 packages | Workflow steps 4, 9 |
| `examples/src/features/tasks/` | A task's photos: model hook (the camera through `usePermissionGate`) and its test (allowed, refused, rationale, blocked, failed save), press jobs (import, save, remove, share), the photo strip (a FlatList: the list is bounded) | Workflow step 8 |
| `examples/contracts/architecture.json` | The vendor SDK entries to merge | Workflow step 3 |
| `examples/task-photos-messages.en.json` | The example's catalog keys | Workflow step 8 |
| `scripts/check-media.mjs` | The checker (15 rules; `--json`) | Workflow step 12, and at the end |
| `scripts/lib/media-rules.mjs` | The checker's rules | Never by hand |
| `scripts/lib/source-scan.mjs` | Shared source scanner, synced from the library (do not edit here) | Never by hand |
| `scripts/check-lib.mjs` | Shared script helper, synced from the library (do not edit here) | Never by hand |
| `scripts/selftest.mjs` | Proves the checker passes the templates and examples and catches every planted bug | After changing a script, a template or an example |
| `assets/shared.json` | The shared files this skill copies in | When adding a shared file |
| `assets/template-placeholders.json` | Template placeholder values (none) | When adding a placeholder |
| `tests/` | The fixture app (`bases/app`) and the overlay cases of check-media (one planted bug per rule) | When adding a rule |

## Related skills

- `icons-app-icon-and-splash` - icons, the logo and illustrations drawn in code; bundled art.
- `permissions-and-system-prompts` - the camera and photo-library permission gate.
- `data-export-and-restore` - `ShareSheetPort`, backups (media refs vs files).
- `architecture-and-boundaries` - ports, adapters, SDK classification, the composition root.
- `persistence-and-migrations` - the MediaRef columns and their migrations.
- `networking-and-server-state` - the ApiClient beside the file-transfer transport.
- `privacy-and-store-declarations` - Photos or Videos in the store answers, the network audit.
- `performance-budgets` - the memory budget and how to measure it on devices.
- `accessibility-as-code` - image names, Reduce Motion for transitions.
- `react-components-and-hooks` - lists, effects, `useIsAppActive` for a camera view.
- `expo-prebuild-and-config-plugins`, `i18n-strings-and-catalogs` - plugin listing and purpose texts.
- `auth-and-secure-storage` - headers for private images, clearing caches on sign-out.
- `unit-and-component-tests`, `e2e-maestro` - test conventions and device flows.
- `ios-simulator-build`, `android-emulator-build` - the builds the drill installs.

Files in this skill

  • SKILL.md17.7 KB
  • assets/shared.json155 B
  • assets/template-placeholders.json3 B
  • examples/contracts/architecture.json645 B
  • examples/src/features/tasks/components/task-photo-strip.tsx1.7 KB
  • examples/src/features/tasks/task-photos-jobs.ts5.3 KB
  • examples/src/features/tasks/use-task-photos-model.test.tsx6.4 KB
  • examples/src/features/tasks/use-task-photos-model.ts5.7 KB
  • examples/src/services/file-transfer/expo-file-system-file-transfer-adapter.ts4.1 KB
  • examples/src/services/file-transfer/file-transfer-port.ts2.5 KB
  • examples/src/services/image-resizer/expo-image-manipulator-image-resizer-adapter.ts2.9 KB
  • examples/src/services/photo-saver/expo-media-library-photo-saver-adapter.ts2.3 KB
  • examples/src/services/photo-saver/photo-saver-port.ts832 B
  • examples/task-photos-messages.en.json728 B
  • references/displaying-images.md12.5 KB
  • references/files-and-storage.md14.4 KB
  • references/picking-and-capturing.md16.8 KB
  • references/sharing-and-transfer.md12.2 KB
  • references/testing-and-checker.md14.6 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.