feat(placements): add placements content configuration API#7416
feat(placements): add placements content configuration API#7416DanielSinclair wants to merge 5 commits intodaniel/flags-storefrom
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Native dep commit so the rebuild requirement is clearly attributed.
Fetch enabled Firestore placement documents, sort items by order, persist with 1h stale / 2d cache.
Named constants prevent string drift between Firestore documents and client code.
Derives which Discover placement types are enabled from remote config, experimental flags, and testnet state. Sets discover_placements_enabled remote config default to true.
4eb7984 to
3d587fb
Compare
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4eb79840e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| testnet: false, | ||
| setTestnet: testnet => set({ testnet }), |
There was a problem hiding this comment.
Initialize testnet state before deriving availability
This store starts with testnet: false and only updates it in useEffect, so on a real testnet account the first render computes availability as if the user were on mainnet. Any consumer that reads useDiscoverPlacementAvailability during that initial render can briefly enable placements (and trigger dependent fetch/render work) before the effect flips the value, which is a user-visible and behaviorally incorrect transient state.
Useful? React with 👍 / 👎.
| .map((d: { id: string; data: () => Record<string, unknown> }) => ({ id: d.id, ...d.data() }) as Placement) | ||
| .map((p: Placement) => ({ | ||
| ...p, | ||
| items: (p.items ?? []).sort((a: PlacementItem, b: PlacementItem) => a.order - b.order), |
There was a problem hiding this comment.
Validate placement items before calling sort
The code assumes p.items is always an array and directly calls .sort after only nullish fallback. Because Firestore documents are schemaless, a malformed items value (e.g. object/string from bad config) will throw TypeError here, causing the whole placements fetch to fail instead of safely skipping/normalizing that document.
Useful? React with 👍 / 👎.
|
🧪 Flashlight Performance Report (AWS Device Farm) 🔀 Commit: 98b6605
|
| items: (p.items ?? []).sort((a: PlacementItem, b: PlacementItem) => a.order - b.order), | ||
| })); | ||
| } catch (e) { | ||
| logger.error(new RainbowError('[placementsStore]: Failed to fetch placements', e as Error), { error: e }); |
There was a problem hiding this comment.
The second argument (e as Error) doesn't require type casting. Also we should not pass the error object along in the metadata param anymore, it's regularly dropped by Sentry due to its size and is more properly used as the cause (which is the second argument of RainbowError).
All that being said, the query stores handle wrapping these fetches in try catch and logging the errors, so don't think we need this here at all.
| getPlacement: (id: string) => get().placements.find(p => p.id === id), | ||
| }), | ||
| { | ||
| partialize: state => ({ placements: state.placements }), |
There was a problem hiding this comment.
Should only define this partialize when there is some portion of the state we're omitting. I believe currently this has the same behavior as not defining it.
| export const PLACEMENT_IDS = { | ||
| PERPS: 'discover_featured_perps_carousel', | ||
| PREDICTIONS: 'discover_featured_predictions_carousel', | ||
| } as const; |
There was a problem hiding this comment.
If we had some other placement of perps on another screen / location what would it's key in this object be? I would assume "perps" and "predictions" are too generic to handle additions cleanly, and the keys would be the aligned with the values in this case.
| const useDiscoverPlacementNetworkStore = createRainbowStore<DiscoverPlacementNetworkState>(set => ({ | ||
| testnet: false, | ||
| setTestnet: testnet => set({ testnet }), | ||
| })); |
There was a problem hiding this comment.
I understand that supporting the isTestnet logic is maintaining how the perps and predictions cards were previously rendered, but given the extra complexity it adds here I think it's worth interrogating whether it's required at all.
Taking a quick trace through I'm not sure there is any functional reason these need to not be displayed when isTestnet is true.
| const polymarketLocal = $( | ||
| useExperimentalConfigStore, | ||
| state => IS_INTERNAL && (state.config[POLYMARKET] ?? defaultConfig[POLYMARKET].value) |
There was a problem hiding this comment.
Would be nice if the store exported a helper like getExperimentalFlagValue that did this IS_INTERNAL and default fallback given it will always be required when accessing a value outside of the useExperimentalFlag hook. That hook could use the same helper so that the logic only has one source of truth.
| return testnet; | ||
| } | ||
|
|
||
| export function computeDiscoverPlacementAvailability({ |
There was a problem hiding this comment.
Unless this is used somewhere else in a later PR, shouldn't be exported. Also it's adding misdirection being a separate function vs. just inlined above.
| return snap.docs | ||
| .map((d: { id: string; data: () => Record<string, unknown> }) => ({ id: d.id, ...d.data() }) as Placement) | ||
| .map((p: Placement) => ({ | ||
| ...p, | ||
| items: (p.items ?? []).sort((a: PlacementItem, b: PlacementItem) => a.order - b.order), | ||
| })); |
There was a problem hiding this comment.
This could be a little cleaner with a helper like this:
function parsePlacementDoc(doc: QueryDocumentSnapshot): Placement {
const data = doc.data() as Omit<Placement, 'id'>;
return {
...data,
id: doc.id,
items: [...(data.items ?? [])].sort((a, b) => a.order - b.order),
};
}Which makes it more clear what it's doing to each doc and lets us only iterate through the docs once.
| const PLACEMENTS_STALE_TIME = time.hours(1); | ||
| const PLACEMENTS_CACHE_TIME = time.days(2); |
There was a problem hiding this comment.
nit: these would be fine inlined below
| }, | ||
| (_set, get) => ({ | ||
| placements: [], | ||
| getPlacement: (id: string) => get().placements.find(p => p.id === id), |
There was a problem hiding this comment.
Obviously not consequential given the number of these, but given we'll always be getting these by id, I wonder if it would be better for the main data structure returned by the fetch function to be placementsById rather than an array, so that lookup doesn't require looping the whole thing every time.
| id: string; | ||
| screen: string; |
There was a problem hiding this comment.
We could type these more concretely ie. (typeof PLACEMENT_IDS)[keyof typeof PLACEMENT_IDS];

APP-3530, APP-3660
What changed
Introduces a generic placements API for remotely configuring featured content slots across the app. Built generically so future screens beyond Discover can add their own placement types without schema changes — screen-specific derived stores live under
src/features/placements/stores/<screen>/.Schema — each document in the Firestore
placementscollection (rainbow-me → (default) DB):usePlacementsStore— fetches enabled placement documents from Firestore, items sorted byorderuseDiscoverPlacementAvailabilityStore— derived store computing which Discover placement types are active; produces{ perps: boolean, predictions: boolean, enabled: boolean }consumed by all downstream Discover storesdiscover_placements_enabledremote config gates the feature (defaultstrue)useExperimentalConfigStore) and testnet state also gate individual placement types@react-native-firebase/firestore(npm),Firebase/Firestorepod — runpod installinios/after pullingScreen recordings / screenshots
What to test
placementscollection populated:usePlacementsStore.getState().getPlacement('discover-perps')returns the document with items sorted by order