Scout — Full Product Context → feature documentation

My Patches

My Patches (scout://my-patches, behind my_patches_screen, default on): every patch you have collected on a screen of its own — the count and where they come from, then every patch in the app's shared patch grid, newest first, with the date you got it. A sort-and-filter menu narrows it by campaign, state and rarity.

Summary

My Patches is a screen of its own for the patches you have collected (scout://my-patches). Before 2026-10-09 the drawer's "My Patches" opened the catalog browser filtered to collected, and a separate Trophy Case screen also existed. Both are replaced by this one screen.

From the top:

The grid is the app's shared patch grid (PatchGrid). The collection ring screen's board ("All of Boston") draws the same tile, with a distance in the pill, patches still to find dimmed, and a check on collected ones when the ring draws checks. So a list of patches looks the same wherever it appears.

The filter is a pull-down: a Liquid Glass menu that drops from the filter button (no sheet, no search field):

Choices apply as you make them; the grid behind the menu updates live. Tapping outside closes it. It offers only what you actually hold, and a long list (a collector in many states) scrolls inside the glass. The menu appears with no fade: a Liquid Glass view under an animated opacity draws plain.

Design source: mobile/research/my-patches-lab.html, layout A (picked 2026-10-09) and the S3 pull-down filter (picked the same day over the S1 sheet; the final settings are recorded on the page).

Status

Built, behind my_patches_screen, default ON (mobile/src/config/feature-flags.ts:45, backend/src/admin/feature-flag-definitions.ts:40). Shipped in v1.1.104 (6cb54c55; live on the App Store in 1.1.107), and the flag reads ON in prod. The backend change (the opened_my_patches feature event, backend/src/achievements/feature-events.controller.ts:36) is deployed; an older backend would reject the event with a 400.

With the flag off, every My Patches entry point, and the old /collected-patches redirect, goes back to the catalog browser filtered to collected (/browse-patches?status=collected), as before. /my-patches itself still opens if visited directly.

User-facing surfaces

Surface Where testID
The screen mobile/app/my-patches.tsx → MyPatchesLayout (mobile/src/components/my-patches/MyPatchesScreen.tsx:102) my-patches
Top bar: Back, title, filter button and its dot MyPatchesScreen.tsx:102 (Scout.TopBar inline, glass IconButtons) my-patches-back, my-patches-title, my-patches-filter, my-patches-filter-dot
Header: the count and the stats Header, MyPatchesScreen.tsx:176 my-patches-count, my-patches-stats
The grid PatchGrid (mobile/src/components/patch-grid/PatchGrid.tsx:40), prefix my-patch my-patch-patch-<id>, my-patch-pill-<id>, my-patch-pill-dot-<id>
The filter menu MyPatchesFilterMenu (MyPatchesFilterMenu.tsx:20), a binding of the shared PullDownFilterMenu (mobile/src/components/filter-menu/PullDownFilterMenu.tsx:30, which Near Me wears too since 2026-10-12) on Scout.GlassSurface, in a transparent Modal my-patches-menu, my-patches-menu-scrim, my-patches-menu-scroll, my-patches-sort-<newest|oldest|az|rarest>, my-patches-drill-<campaign|state|rarity>, my-patches-menu-back, my-patches-campaign-<name>, my-patches-state-<name>, my-patches-rarity-<tier>, my-patches-reset
Nothing matches NoMatch, MyPatchesScreen.tsx:193 my-patches-nomatch, my-patches-clear
Nothing collected yet: "No patches yet", Find one near you (/near-me) and Scan your camera roll (/import-modal) Empty, MyPatchesScreen.tsx:217 my-patches-empty, my-patches-find, my-patches-scan
Loading: the same header and grid as shimmer MyPatchesSkeleton (MyPatchesScreen.tsx) my-patches-skeleton
The catalog failed to load with nothing cached: "Couldn't load your patches" and Try again — never "No patches yet" LoadError (MyPatchesScreen.tsx) my-patches-error, my-patches-retry
Entry points: the drawer's My Patches row, Home's Patches door, Profile's Collected shelf ("Browse all"), the drawer progress block's rarity row (that block is on hold) myPatchesHref (mobile/src/components/my-patches/myPatchesHref.ts:8) drawer-item-my-patches, nh-door-patches, collected-browse-all
The old Trophy Case route scout://collected-patches (in the deep-link launcher, achievement deep links and a message CTA) a redirect through the same rule, myPatchesHref: /my-patches, or the browser on collected with the flag off (mobile/app/collected-patches.tsx) —
Screen mock gallery scout://dev-screen-mock/my-patches — seven states: collected, filter open, filtered, nothing matches, load error, empty, loading (mobile/src/dev/mocks/my-patches.tsx) —

How it works

Everything is derived on the device, from synced content and your collected patches (useCollectedUserPatches, which carries each patch's collectedAt). The view model MyPatchesViewModelImpl (MyPatchesScreen.tsx:64) owns every hook; the derivations are pure functions in mobile/src/components/my-patches/selectors.ts:

  1. Rows: buildMyPatchRows (selectors.ts:39) turns each collected patch into a row: the patch, when you got it, its campaign and its state. Rows are newest first. A collected patch the catalog no longer carries is skipped; it never becomes a blank tile.
  2. A patch's campaign: the same rule Home colours a patch by, shared rather than copied: themedCollectionOf (mobile/src/components/home-new/selectors.ts:207). The patch's first themed collection wins over a City Challenge or US Cities one (those say where a place is, not what it is), so Copp's Hill Burying Ground is Ghost Adventures.
  3. The header: summarize (selectors.ts:61), always over every row. "Collections" counts every collection you hold a patch of.
  4. The filter:
    • facets (selectors.ts:79) lists what you hold, most first, with "Other" last.
    • applyFilter (selectors.ts:99) ORs within a facet, ANDs across facets, then sorts. There is no search.
    • isFiltered (:110) is anything that narrows the list. filterSet (:112) also counts a non-default sort, and lights the dot.
  5. The grid is a FlashList of rows of three, each row a PatchGrid, so a collection of hundreds scrolls smoothly. Rows are keyed by position, so a re-sort refills them in place. Building the rows asks each patch's themed collection once; the patch → collections index it reads is built once per synced membership array (membershipIndex, home-new/selectors.ts), not once per patch.

The top bar sits outside the page loader, as on My Favorites: Liquid Glass under the loader's animated opacity draws plain until something re-lays it out.

Data model

No new tables. It reads the synced catalog (patches, collections, campaigns, patchCollections) and the user's collected patches with their collectedAt.

API surface

No new endpoint.

Key files

Configuration and flags

Edge cases and known limits

What this feature does NOT do

Tests that cover it

Open questions