Popup (Upsell) Feature — Self-Review Plan
Fastest review path — bottom-up. Each step makes the next one self-explanatory.
Total time budget: ~30 min for thorough review, ~12 min for the critical core only.
Sequence (follow top to bottom)
| # | Phase | Time | Files | Why this order |
|---|---|---|---|---|
| 1 | Data contract | 2 min | shellel.server/src/usage-tracker/usage-tracker.schema.ts |
Tiny. Tells you the only 5 fields that matter: key, anonId, ip, count, lastUsedAt. Everything below is just reads/writes against this. |
| 2 | Server source of truth | 5 min |
shellel.server/src/usage-tracker/usage-tracker.service.ts → usage-tracker.module.ts
|
reserve() is the entire backend logic. Understand the atomic $inc, the anon: vs ip: key precedence, and the fail-open. Module file is one glance. |
| 3 | Server enforcement | 5 min |
shellel.server/src/calorie-check/calorie-check.controller.ts (L43-58) → shellel.server/src/protein-plan/protein-plan.controller.ts (L96-111) |
Both controllers do the same 3 lines: reserve() → if !allowed throw 403. Read calorie first, diff protein in your head. While here, answer: is /protein-plan/calculate skipping the reserve on purpose?
|
| 4 | Client state mirror | 5 min | shellel-client/src/app/shared/usage/usage-tracker.service.ts |
This is the client twin of step 2. Focus on count + showUpsell signals, getAnonId/setAnonId, and isOverLimit. Note it diverges from server (localStorage vs Mongo) — keep that in mind for step 5. |
| 5 | Client integration — pick ONE feature, fully | 7 min |
shellel-client/src/app/calorie-checker/calorie-checker.ts (L40-41, 171-173, 282-308, 326-329) + calorie-checker.html (L61, 275) |
Trace the 4 trigger points in one feature: mount-time check, pre-call gate, 403 catch, post-success increment. Once this clicks, you understand the whole pattern. |
| 6 | Diff the second feature against the first | 5 min |
shellel-client/src/app/protein-planner/protein-planner.ts (L228-238, 249-251, 393-431, 454-457) + protein-planner.html (L183, 448) |
Don't re-read — only look at what's DIFFERENT from calorie. The only real diffs: setAnonId from saved profile (L228-229, 250), and an extra "set up profile first" guard. Asymmetry confirmed → flag it. |
| 7 | Popup UI | 3 min |
shellel-client/src/app/shared/upsell/upsell-modal.component.ts → .html → .css
|
Saved for last on purpose — it's pure presentation. Only logic is the iOS UA detection. After steps 1-6 you'll read this in 30 seconds. |
| 8 | Edge-case sweep | 5-8 min | (no new files) | Walk down the 9-item issue list with the code already in your head. Each takes <1 min because you already know where to look. |
Why this order saves time
- Data → server → client → UI is the natural dependency direction; you never read code that depends on something you haven't seen.
-
Step 5 → 6 deliberately exploits the parallelism. Calorie and Protein are ~95% identical (the graph report flagged this with
semantically_similar_toedges). Reading both fully is wasted work — read one, diff the other. - Popup UI last because it has no logic worth fighting over until you know what state it's reflecting.
Time budget summary
| Scope | Time | What you cover |
|---|---|---|
| Minimum viable review | ~12 min | Steps 2, 3, 5 — the spine of the feature |
| Standard review | ~20 min | Steps 1-6 — full understanding, both features |
| Thorough review | ~30 min | All 8 steps including edge-case sweep |
One concrete starting move
Open these 3 in split panes side-by-side:
shellel.server/src/usage-tracker/usage-tracker.service.tsshellel-client/src/app/shared/usage/usage-tracker.service.tsshellel-client/src/app/calorie-checker/calorie-checker.ts
That triple is the spine of the feature. Everything else is wiring or chrome.
Edge cases to verify during Step 8
-
POST /protein-plan/calculatedoes NOT callusageTracker.reserve()(only/checkdoes). Intentional (onboarding is free) or a gap? - Client count and server count drift independently. localStorage vs Mongo — after clearing localStorage user gets fresh client UI but server may already be over limit (caught via 403 path).
-
Server fail-open on Mongo error (
usage-tracker.service.ts:53-56) — Mongo outage = unlimited free checks. Document if intentional. -
Key precedence is
anonIdORip, never both. Clearing localStorage = fresh counter. Acceptable for soft paywall. -
x-forwarded-foris trusted raw. Fine behind a proxy; spoofable if directly exposed. -
Pre-check gate uses client-only
isOverLimit(). Server is source of truth — good defense in depth via 403 branch. -
Asymmetry: Calorie counter is per-device; Protein unifies across sessions via
setAnonIdfrom saved profile. By design? - No backdrop ESC-key close — only click outside + X button (a11y nit).
-
maxLength=36onanonIdDTOs but no UUID format validation server-side (only on/history/:anonId). Arbitrary 36-char strings can be posted as counter keys.
Top comments (0)