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 top bar: a glass Back button, the title "My patches" beside it (the app's convention for back-button pages, as on My Favorites), and a glass filter button in the right corner. A brass dot on the button is the only sign that a filter or a non-default sort is set.
- The header: "Your collection", the count big ("50 patches"), then "5 states · 15 collections · 6 rare or better". The header always counts your whole collection; a filter narrows only the grid.
- The grid: every collected patch, newest first, three across. Each tile is the patch art (full colour), its name, and a small pill with the date you got it ("Oct 8"). A Rare, Epic or Legendary patch also gets a tier-coloured gem in the pill; Common gets none. Tap a patch to open its patch page.
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):
- Sort: Newest, Oldest, A–Z, Rarest first, as rows with a check on the active one;
- Filter: Campaign, State and Rarity, each showing what is set ("2 on" or "Any") and opening its own list. A list's rows carry the campaign's own colour dot (or the tier colour for Rarity), a count and a check; "‹ Filter" goes back. Campaign puts "Other" (patches in no campaign) last; Rarity lists only the tiers you hold;
- Reset, which appears once anything is set.
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:
- 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. - 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. - The header:
summarize(selectors.ts:61), always over every row. "Collections" counts every collection you hold a patch of. - 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.
- The grid is a
FlashListof rows of three, each row aPatchGrid, 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
POST /api/feature-eventswithopened_my_patches, recorded on open. It is allowlisted inKNOWN_FEATURE_EVENTS(backend/src/achievements/feature-events.controller.ts:36). No achievement consumes it.GET /api/feature-flagsservesmy_patches_screen.
No new endpoint.
Key files
mobile/app/my-patches.tsx: the route.mobile/src/components/my-patches/MyPatchesScreen.tsx: the view model, the layout, the header, the empty, no-match and loading states.mobile/src/components/filter-menu/facets.ts: the facet counting (most first, ties A–Z, Other last), the themed-campaign rule and the pick test, shared with Near Me since 2026-10-12 —selectors.tscalls them.mobile/src/components/my-patches/MyPatchesFilterMenu.tsx: My Patches' sorts and facets bound to the shared pull-down (mobile/src/components/filter-menu/:PullDownFilterMenu, andFilterButton— the glass sliders and its dot).mobile/src/components/my-patches/selectors.ts: every derivation above.mobile/src/components/my-patches/myPatchesHref.ts: where "My Patches" goes, per the flag.mobile/src/components/patch-grid/PatchGrid.tsx: the shared patch grid, also used by the collection ring board (mobile/src/components/campaign/CampaignRingScreen.tsx:432).mobile/src/components/navigation/drawerSections.ts:239: the drawer row'srehref.mobile/app/collected-patches.tsx: the redirect.
Configuration and flags
my_patches_screen: default ON on both sides. Off: the entry points open the browser on collected.
Edge cases and known limits
- Nothing collected: the empty state, and no filter button (nothing to filter).
- The catalog failed to load with nothing cached: an error with Try again, never the empty state. (A failed refresh over a catalog already on the device changes nothing.)
- A filter that leaves nothing: "Nothing matches" with Clear filters. The header still counts the whole collection.
- A collected patch missing from the catalog (deleted or not yet synced): skipped.
- The count includes camera-roll imports: it is every patch you hold, the same set the catalog browser's "collected" shows. Badges, which count only in-person patches, are not on this screen.
- Dates are the device's local day of
collectedAt. - Tapping a patch opens its patch page, not its unlock celebration (the browser in collected mode replays the celebration; this screen does not).
What this feature does NOT do
- It does NOT share. There is no share button or share card.
- It does NOT edit your collection. There is no hiding, removing, reordering or pinning a patch.
- It does NOT group by collection. One grid, newest first. The Trophy Case's per-collection sections and progress bars are gone with it.
- It does NOT show badges or achievements, or the rarity ladder the Trophy Case had.
- It does NOT show patches you have not collected. That is the catalog browser.
- It does NOT search. There is no search field; Sort and the three filters are the filter.
- It does NOT remember a filter between visits. Every visit opens on Newest with nothing set.
- The Trophy Case screen is gone.
scout://collected-patchesredirects here.
Tests that cover it
mobile/screen-tests/my-patches.test.tsx(14 tests, real catalog rows: Boston Common, Copp's Hill Burying Ground, the New England Aquarium, the Statue of Liberty). It covers:- the header over the whole collection;
- newest first with dates, and a missing patch skipped;
- a tap opening the patch page;
- glass Back and filter buttons;
- the menu: clear Liquid Glass (falsified with a plain view), Sort and the three filters, no search field, no Reset until something is set;
- the filter: a campaign narrows the grid live while the header holds, the drill row reads "1 on", and the dot lights (falsified by counting the header from the filtered rows);
- facets: only what you hold, "Other" last, only the rarities you have;
- Oldest re-sorting the grid;
- Reset appearing once something is set, and putting everything back;
- a long list scrolling inside the glass;
- a failed catalog load showing the error and retry, never "No patches yet";
- Nothing matches and Clear;
- the empty state with no filter button;
- loading.
mobile/src/components/my-patches/__tests__/selectors.test.ts: rows, summary, facets, the four sorts, OR within and AND across facets, the dot rule,datePill(falsified by reversing newest-first), and A–Z on rows whose newest-first order is not alphabetical (falsified with a no-op A–Z).mobile/screen-tests/patch-grid.test.tsx: the shared grid: art, name, pill, the accent tone, the Rare+ gem, dim and check, the press,nameLines.mobile/screen-tests/campaign-new.test.tsxandcampaign-ring.test.tsx: the ring board onPatchGrid(its dimming falsified).mobile/src/components/navigation/__tests__/drawerSections.test.tsandprogressBlock.test.ts: the drawer row and the rarity row, flag on and off (falsified by ignoring the flag). Alsomobile/screen-tests/new-home.test.tsx(the Patches door, on and off),profile.test.tsx(the Collected shelf) andcollected-patches.test.tsx(the redirect, flag on and off).mobile/screen-tests/screen-mocks.test.tsx: the seven gallery states.
Open questions
- Should it share (a card of your collection)? Explored in the lab (two "showcase" looks) and set aside on 2026-10-09.
- The lab's photo header (A2) and colour wash (A3) are kept on the page for a later pass.