Fix visibility and refresh#15
Conversation
- "Delete this one" used solid bg-destructive + text-white, but solid bg-destructive doesn't render under NativeWind v5-preview (only the /10 opacity variant does), so the white text was invisible. Use an explicit inline red background + white text. - Home nudge counts (review / duplicates / confirm-date) and the Subscriptions list didn't reflect actions taken on the pushed detail screen until relaunch. Expose refresh() from the context and re-pull on screen focus (useFocusEffect) so counts update at runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A same-name sub can be legitimate (paying for a partner's or child's subscription), so the detail banner now offers a real "Keep it" beside "Delete this one": - "Keep it" sets a persisted duplicate_acknowledged flag (migration v4), which excludes the sub from duplicate detection — the banner, card chip, and Home nudge all stop flagging it. - Copy advises renaming (e.g. "Netflix for Sally") so identical rows don't confuse. - Relabel the delete confirmation's cancel from "Keep it" to "Cancel" so it isn't mistaken for the new acknowledge action.
Confirming a renewal advanced confirmedThrough but not the stored renewalDate, so the detail's "Next renewal" stayed on the just-confirmed (today's) date. Also advance renewalDate to the following cycle.
Per the agreed journey: the add-time duplicate confirm now makes "Rename it" the primary action and "Add it anyway" secondary. Choosing "Add it anyway" creates the sub with duplicateAcknowledged = true, so a decision the user made isn't re-flagged as a possible duplicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- SubscriptionCard: soft, unique per-brand panel tint (colour-coding for scan/recall) plus a coloured left edge on cards needing attention (duplicate > confirm-date > renewed?), so warnings are findable in the list. - Detail hero now uses the same cardTint as the list card for consistency. - Home "Your subscriptions" card: overlapping brand-icon stack previews content and invites the tap; PressableScale for tactile feedback. - lib/brand: add tintColor() + cardTint() helpers. - Startup: hide the splash as soon as fonts load instead of waiting on Clerk auth (guest-first) — removes the visible menu delay on launch/reload.
- Preload tab-bar icon assets (expo-asset) and gate the splash-hide on them
alongside fonts, so the whole bottom bar paints at once. Fixes the "pop-in"
where only the active Home pill showed first and the other white glyphs
appeared a frame later.
- Rewrite the duplicate-warning copy (detail banner + add modal) to drop the
presumptuous "a partner's or child's / for Sally" phrasing. Now frames the
legitimate case as "a different plan or account" with a neutral rename
example ("Netflix – work") — relevant to every user, not just families.
Celebrate NOT spending — the emotional payoff competitors skip. When a subscription is cancelled (from the detail action or the renewal check-in's "I cancelled"), a mint confetti burst + success haptic reveals what the user just saved, counting up the yearly figure. - components/Confetti.tsx: in-house falling-confetti burst on Reanimated (plain Animated.Views, no new animation library — per the animation-stack decision). - components/CancelCelebration.tsx: overlay with the count-up saving and a self-contained, inline-styled share card captured via react-native-view-shot and shared through expo-sharing (RN Share text fallback). This is the plan's day-one viral loop — every cancellation becomes a shareable stat. - Detail screen: cancelAndCelebrate() snapshots the monthly saving before the status flips, wired into both cancel paths. - Adds expo-sharing + react-native-view-shot.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR adds persistent duplicate acknowledgement, cancellation celebrations with sharing and confetti, focus-triggered subscription refreshes, subscription list filtering, expanded onboarding brands, derived card styling, startup icon preloading, and removal of per-subscription currency fields. ChangesSubscription experience
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SubscriptionDetail
participant CancelCelebration
participant Confetti
participant Sharing
SubscriptionDetail->>CancelCelebration: pass cancellation savings
CancelCelebration->>Confetti: render animated confetti
CancelCelebration->>Sharing: capture and share celebration card
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@__tests__/migrations.test.ts`:
- Around line 53-56: Strengthen the migration test around MIGRATIONS version 4
to assert the duplicate_acknowledged column includes INTEGER, NOT NULL, and
DEFAULT 0, not just the column name. Add rowToSubscription assertions covering
duplicate_acknowledged values 0 → duplicateAcknowledged false and 1 → true,
using baseRow and an overridden copy.
In `@app/_layout.tsx`:
- Around line 98-122: Store notification deep-link routes in a pendingDeepLink
state from the addNotificationResponseReceivedListener instead of navigating
immediately. Add an effect that, once ready is true and a route is pending,
pushes it through router and clears the pending state; preserve listener cleanup
and subscriptionId validation.
In `@app/`(tabs)/index.tsx:
- Around line 384-394: Update the avatar wrapper View in the subscription
rendering block to replace the hardcoded borderColor value with the existing
semantic card-border styling mechanism, such as the NativeWind border-card
class, so the border follows dark mode and dynamic themes while preserving the
current width and radius.
In `@app/subscriptions/`[id].tsx:
- Around line 534-538: Update the CancelCelebration invocation to pass the
subscription’s currency for the monthlySaved amount instead of baseCurrency. Use
the currency field associated with subscription.price, while preserving the
existing celebration visibility, name, and savings value handling.
In `@components/Confetti.tsx`:
- Around line 49-54: Move the progress animation assignment in ConfettiPiece
into a useEffect with an appropriate dependency array so it starts only when the
component mounts. Preserve the existing piece.delay, duration, and easing
configuration, and keep the shared value declaration outside the effect.
In `@components/SubscriptionFormModal.tsx`:
- Around line 186-190: Update the subscription edit flow around
duplicateAcknowledged in SubscriptionFormModal so a subscription name change
clears the existing acknowledgement before duplicate evaluation; only preserve
or set duplicateAcknowledged when the name remains unchanged or the current
duplicate warning is explicitly acknowledged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: d5e82319-532d-4acb-80e0-c52eff6b87d3
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
__tests__/migrations.test.tsapp.jsonapp/(tabs)/index.tsxapp/(tabs)/subscriptions.tsxapp/_layout.tsxapp/subscriptions/[id].tsxcomponents/CancelCelebration.tsxcomponents/Confetti.tsxcomponents/SubscriptionCard.tsxcomponents/SubscriptionFormModal.tsxcontext/SubscriptionsContext.tsxdb/migrations.tsdb/subscriptionsRepo.tslib/brand.tslib/duplicates.tspackage.jsontype.d.ts
| <CancelCelebration | ||
| visible={celebration !== null} | ||
| name={celebration?.name ?? ""} | ||
| monthlySaved={celebration?.monthlySaved ?? 0} | ||
| currency={baseCurrency} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Use the subscription’s currency for the savings amount.
monthlySaved is derived from subscription.price, so labeling it with baseCurrency misstates savings when the stored subscription currency differs.
monthlySaved={celebration?.monthlySaved ?? 0}
- currency={baseCurrency}
+ currency={subscription.currency ?? baseCurrency}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <CancelCelebration | |
| visible={celebration !== null} | |
| name={celebration?.name ?? ""} | |
| monthlySaved={celebration?.monthlySaved ?? 0} | |
| currency={baseCurrency} | |
| <CancelCelebration | |
| visible={celebration !== null} | |
| name={celebration?.name ?? ""} | |
| monthlySaved={celebration?.monthlySaved ?? 0} | |
| currency={subscription.currency ?? baseCurrency} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/subscriptions/`[id].tsx around lines 534 - 538, Update the
CancelCelebration invocation to pass the subscription’s currency for the
monthlySaved amount instead of baseCurrency. Use the currency field associated
with subscription.price, while preserving the existing celebration visibility,
name, and savings value handling.
…ustness - _layout: buffer notification deep-links into pending state and flush once the navigator is mounted (ready), instead of pushing immediately — a cold start launched by a reminder tap could push before the Stack mounted. - SubscriptionFormModal: clear a stale duplicateAcknowledged when an edit renames the sub (the ack was granted for the old name); preserve it otherwise. - Confetti: start the fall animation in a mount effect, not during render. - Home avatar stack: use the border-card token instead of a hardcoded hex so the notch separator follows the card color. - migrations test: assert the full duplicate_acknowledged column shape (INTEGER NOT NULL DEFAULT 0) + rowToSubscription 0/1 mapping. Skipped: passing subscription.currency to the cancel celebration — the app formats all amounts with the single app-wide baseCurrency by design (no FX); per-sub currency is never used for display and equals baseCurrency anyway. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/Confetti.tsx`:
- Around line 53-57: Update the effect in Confetti’s animation setup to replace
direct progress.value assignment with the Reanimated shared-value set() method,
passing the existing withDelay animation unchanged. Preserve the current delay,
duration, and easing behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 65acf57a-aaec-4e2c-95f7-aee5189ba7e0
📒 Files selected for processing (5)
__tests__/migrations.test.tsapp/(tabs)/index.tsxapp/_layout.tsxcomponents/Confetti.tsxcomponents/SubscriptionFormModal.tsx
| useEffect(() => { | ||
| progress.value = withDelay( | ||
| piece.delay, | ||
| withTiming(1, { duration, easing: Easing.out(Easing.quad) }), | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use .set() instead of mutating .value for React Compiler compatibility.
Modifying the .value property of a shared value directly can be flagged by the React Compiler as an illegal mutation. Use the .set() method provided by Reanimated for React Compiler compatibility.
♻️ Proposed refactor
useEffect(() => {
- progress.value = withDelay(
- piece.delay,
- withTiming(1, { duration, easing: Easing.out(Easing.quad) }),
- );
+ progress.set(
+ withDelay(
+ piece.delay,
+ withTiming(1, { duration, easing: Easing.out(Easing.quad) }),
+ )
+ );
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| progress.value = withDelay( | |
| piece.delay, | |
| withTiming(1, { duration, easing: Easing.out(Easing.quad) }), | |
| ); | |
| useEffect(() => { | |
| progress.set( | |
| withDelay( | |
| piece.delay, | |
| withTiming(1, { duration, easing: Easing.out(Easing.quad) }), | |
| ) | |
| ); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []); |
🧰 Tools
🪛 React Doctor (0.7.6)
[warning] 54-54: This component misses React Compiler's automatic memoization & re-renders more than it should: This value cannot be modified. Rewrite the flagged code so the compiler can optimize it.
Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
If this is a Reanimated shared value, prefer its React Compiler-compatible .get() / .set() accessors over .value — https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue/#react-compiler-support
(immutability)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/Confetti.tsx` around lines 53 - 57, Update the effect in
Confetti’s animation setup to replace direct progress.value assignment with the
Reanimated shared-value set() method, passing the existing withDelay animation
unchanged. Preserve the current delay, duration, and easing behavior.
Source: Linters/SAST tools
We format every amount with the single app-wide base currency (no FX), so a per-sub currency was dead weight and a source of ambiguity. Remove it from the Subscription/Draft/UpcomingSubscription types and every read/write: - type.d.ts: drop `currency` from Subscription (cascades to SubscriptionDraft) and UpcomingSubscription. - subscriptionsRepo: drop from SubscriptionRow, rowToSubscription, the INSERT, and PATCH_COLUMNS. The DB column is kept dormant (NOT NULL DEFAULT 'USD') so no destructive migration runs on local-first user data; it can be revived if per-currency ever returns. - Remove the now-dead `currency: baseCurrency` from the add/edit form and onboarding bulk-add, and the unused UpcomingSubscription.currency in Home. - migrations test fixture updated to the new row shape.
The plan's "biggest daily-use win we currently lack" — the list only had search. Add lightweight inline controls on the Subscriptions tab: - Sort by Renewal (soonest first), Cost (monthly-equivalent so cycles compare fairly), or Name; tapping the active sort flips direction (↑/↓). Paused and cancelled subs have no active renewal, so they sink to the bottom of the renewal sort — matching the card, which only shows a countdown when active. - Multi-select category chips (derived from the data; shown only when there's more than one category) with a Clear affordance. Composes with search. Chips reuse the card/pill styling and the reliable bg-accent/10 active state.
The pick step showed the whole catalog as one flat 60-tile wall. Now: - Search box filters brands by name. - Brands render under ordered category headings (Entertainment, Gaming, Music, AI Tools, ... Bills & Utilities) so users scan to what they pay for. - Categorize every catalogued brand so the grouped view is tidy — no "Other" bucket (added categories for the Lobe AI tools + Trello/Asana/Netlify/etc. that previously inferred to Other). Expand the catalog with 37 real logos (only brands that still ship a simple-icons logo — Disney+/Hulu/Prime/Amazon/Canva/Adobe were dropped by simple-icons and are intentionally skipped rather than shown as monograms): - Health & Fitness: Peloton, Strava, Fitbit, Headspace - Food & Delivery: HelloFresh, DoorDash, Uber Eats, Deliveroo, Instacart, Just Eat, Zomato, Swiggy - Streaming: Apple TV, Plex, MUBI, Fubo - News & Reading: New York Times, The Guardian - Music: Pandora, iHeartRadio - Cloud & security: LastPass, Bitwarden, Backblaze, MEGA, Surfshark, Mullvad, Box, Proton - Gaming: EA, Ubisoft, NVIDIA (GeForce Now), Humble Bundle (+ moved Steam, PlayStation, Roblox, Epic Games into the new Gaming category) - Shopping: Target - Bills & Utilities: Verizon, Vodafone, O2, Spectrum Catalog: 77 -> 114 brands. Regenerated via generate-brand-icons.mjs.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/`(tabs)/subscriptions.tsx:
- Around line 154-156: Reconcile selectedCategories with the current categories
before applying the subscription filter, removing any values no longer present
while preserving valid selections. Update the state used by the filtering and
filter-row visibility logic so that when categories shrink to one or fewer,
stale selections cannot hide the list without an available Clear control; anchor
the change near the selectedCategories state/effect and the inCategory
calculation.
- Around line 78-84: The Chip Pressable currently lacks accessibility semantics
for its toggle state. Update the Pressable in the Chip component to expose a
button role and derive its selected state from the existing active value,
preserving the current press behavior and styling.
- Around line 260-263: Update the ListEmptyComponent in the subscriptions screen
to distinguish between an account with no subscriptions and an empty result
caused by active search or category filters. Show an unfiltered empty-state
message when no subscriptions exist and no filters are applied, while preserving
the existing filtered-results message when filters are active.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4b9d8654-ee50-4db9-8e88-9173b1b6b6e8
📒 Files selected for processing (5)
app/(tabs)/subscriptions.tsxapp/onboarding.tsxconstants/brandIcons.tsconstants/onboardingBrands.tsscripts/generate-brand-icons.mjs
| <Pressable | ||
| onPress={onPress} | ||
| className={clsx( | ||
| "rounded-full border px-3.5 py-1.5", | ||
| active ? "border-accent bg-accent/10" : "border-border bg-card", | ||
| )} | ||
| > |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Expose Chip toggle state to assistive tech.
The Pressable announces its label but not that it's a button or whether it's selected, so screen-reader users can't tell active sort/filter pills from inactive ones. Add a role and selected state.
♿ Proposed fix
<Pressable
onPress={onPress}
+ accessibilityRole="button"
+ accessibilityState={{ selected: active }}
className={clsx(
"rounded-full border px-3.5 py-1.5",
active ? "border-accent bg-accent/10" : "border-border bg-card",
)}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Pressable | |
| onPress={onPress} | |
| className={clsx( | |
| "rounded-full border px-3.5 py-1.5", | |
| active ? "border-accent bg-accent/10" : "border-border bg-card", | |
| )} | |
| > | |
| <Pressable | |
| onPress={onPress} | |
| accessibilityRole="button" | |
| accessibilityState={{ selected: active }} | |
| className={clsx( | |
| "rounded-full border px-3.5 py-1.5", | |
| active ? "border-accent bg-accent/10" : "border-border bg-card", | |
| )} | |
| > |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(tabs)/subscriptions.tsx around lines 78 - 84, The Chip Pressable
currently lacks accessibility semantics for its toggle state. Update the
Pressable in the Chip component to expose a button role and derive its selected
state from the existing active value, preserving the current press behavior and
styling.
| const inCategory = | ||
| selectedCategories.length === 0 || | ||
| selectedCategories.includes(categoryOf(subscription)); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Stale category selection can strand the list with no way to clear it.
selectedCategories is never reconciled against the currently available categories. If subscriptions change so that a previously selected category no longer exists (e.g. its last sub is deleted) and categories.length drops to <= 1, the filter row — including the "Clear" chip — is hidden at Line 218, yet the stale selection still filters at Lines 154-156. The result is an empty list the user can't recover from without other actions.
Prune selections against available categories, e.g.:
🔧 Proposed fix
const categories = useMemo(() => {
const set = new Set(subscriptions.map(categoryOf));
return Array.from(set).sort((a, b) => a.localeCompare(b));
}, [subscriptions]);
+
+ // Drop any selected categories that no longer exist so filters can't strand the list.
+ useEffect(() => {
+ setSelectedCategories((prev) => {
+ const next = prev.filter((c) => categories.includes(c));
+ return next.length === prev.length ? prev : next;
+ });
+ }, [categories]);Also applies to: 218-244
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(tabs)/subscriptions.tsx around lines 154 - 156, Reconcile
selectedCategories with the current categories before applying the subscription
filter, removing any values no longer present while preserving valid selections.
Update the state used by the filtering and filter-row visibility logic so that
when categories shrink to one or fewer, stale selections cannot hide the list
without an available Clear control; anchor the change near the
selectedCategories state/effect and the inCategory calculation.
| ListEmptyComponent={ | ||
| <Text className="home-empty-state"> | ||
| No subscriptions match your search. | ||
| No subscriptions match your filters. | ||
| </Text> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Empty-state text is misleading when there are no subscriptions or no active filters.
This message renders for every empty result, including a fresh/reset account with zero subscriptions and no search or category filter applied, where "match your filters" is inaccurate. Consider distinguishing the truly-empty case from the filtered-out case.
💬 Proposed fix
ListEmptyComponent={
<Text className="home-empty-state">
- No subscriptions match your filters.
+ {subscriptions.length === 0
+ ? "No subscriptions yet."
+ : "No subscriptions match your filters."}
</Text>
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ListEmptyComponent={ | |
| <Text className="home-empty-state"> | |
| No subscriptions match your search. | |
| No subscriptions match your filters. | |
| </Text> | |
| ListEmptyComponent={ | |
| <Text className="home-empty-state"> | |
| {subscriptions.length === 0 | |
| ? "No subscriptions yet." | |
| : "No subscriptions match your filters."} | |
| </Text> | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/`(tabs)/subscriptions.tsx around lines 260 - 263, Update the
ListEmptyComponent in the subscriptions screen to distinguish between an account
with no subscriptions and an empty result caused by active search or category
filters. Show an unfiltered empty-state message when no subscriptions exist and
no filters are applied, while preserving the existing filtered-results message
when filters are active.
Add the brands users asked for, using real logos where any available source ships one and lucide glyph tiles where none does. Real logos: - simple-icons: Google Cloud, Hostinger, Namecheap, GoDaddy, Vultr, Render, Railway, Supabase, Firebase, MongoDB, PlanetScale (Developer Tools). - Lobe: AWS, Azure (simple-icons dropped both over trademark; Lobe still ships them). Generic day-to-day category tiles (lucide glyphs, no brand identity) so household bills are one tap: Rent / Housing, Utilities, Mobile / Phone, Internet, Insurance, Gym, Transport, Streaming. Big brands neither source has a logo for get a themed glyph stand-in for this release (distinct tile colours keep them apart): Disney+, Hulu, Prime Video, Peacock, Canva, Adobe, Xbox, Nintendo Switch, Amazon Prime. lucide-static added as a build-time devDependency (same pattern as simple-icons/lobe). Catalog: 114 -> 144; still zero "Other" bucket. Keyword aliases kept specific to avoid greedy substring matches in the name resolver.
The "+" add-subscription sheet listed every brand in raw catalog order (looked random), while onboarding groups them by category with search. Make them match: - Extract groupOnboardingBrands() as the single source of truth for ordering/grouping; onboarding now uses it instead of inline logic. - New BrandPickerSheet renders the same category-grouped, searchable tile grid; the add form's "Browse brands" opens it (single-select) instead of the flat PickerSheet list. - Align the form's category chips + colors with the full onboarding taxonomy (Gaming, Health & Fitness, Food & Delivery, News & Reading, Shopping, Bills & Utilities) so categories are consistent everywhere.
Fixes a trial being tracked as if it renewed one cycle after the start date. A free trial's first charge lands when it converts (the trial-end date), and the user may or may not convert — so: - billing: firstChargeDate() anchors a trial's renewal to its end date; trialPendingConversion() flags a trial that has reached its end; the form now sets renewalDate from the trial end (not start + one cycle) and leaves confirmedThrough unset until conversion. - Detail: at/after trial end, a conversion check-in — "Did your trial convert to paid?" → Yes keeps it (starts the paid cycle from the trial end), No runs the cancel + savings celebration, Not yet snoozes. The renewal check-in is suppressed while on trial. - Card: "Free trial" badge + "Trial ends in N days" meta; a pulsing "Convert?" once ended. - Reminders: during a trial only the T-2/T-0 trial reminders fire (renewal reminders + generic check-in suppressed so they don't cluster on one day); T-0 copy nudges the keep-or-cancel decision. - Reconciler skips trials (no auto-assumed renewals before conversion). - Spend: free trials excluded from the monthly total (Home + Insights) until they convert; countsTowardSpend() + updated Insights disclosure. - Tests: firstChargeDate / trialPendingConversion / countsTowardSpend, and trials suppress renewal reminders. 60 pass.
No description provided.