whisper-money/resources/js
Víctor Falcón 52aae3fd67
feat: prompt free users to subscribe when enabling AI categorization (#696)
## Problem

On **Settings › Manage Plan**, a free (non-subscribed) user who ticked
**"Allow AI categorization"** silently recorded AI consent (`POST
/ai/consent`). That flipped `hasActiveAiConsent()` to `true`, which
caused two things on the **next navigation / refresh**:

1. `EnsureUserIsSubscribed` hard-redirected them to `/subscribe`, on
every gated page.
2. The paywall's `canUseFreePlan` (`!hasBankConnections &&
!hasActiveAiConsent`) became `false`, so the **"Continue for free"**
escape hatch disappeared.

Net effect: enabling AI locked a free user out of the app behind the
paywall, with no explanation and no way back.

## Fix

For a **free user**, ticking the box no longer records consent. Instead
it opens a contextual dialog that explains AI is a paid feature and
offers **Monthly / Annual** plans that start Stripe checkout. Because no
consent is recorded:

- Nothing locks them out — dismissing the dialog ("Maybe later") leaves
the app fully usable.
- **Pro users** keep the existing direct-consent behavior.
- A free user who somehow already has consent can still **untick to
revoke** and escape the lock.

The now-inaccurate "you can give consent now…" note was removed (a free
user can no longer give consent from here).

## Tests

- New `resources/js/pages/settings/billing.test.tsx` covers three paths:
free user → modal opens & **no consent POSTed**; pro user → consent
recorded directly; free user with existing consent → untick **revokes**.

## Demo

**Before** — enabling AI silently locks the free user behind the paywall
(no "Continue for free"):

<!-- 📎 PLACEHOLDER: drag in ~/Downloads/free-user-ai-paywall.mp4 -->


https://github.com/user-attachments/assets/a72790a4-2b2d-4af1-9f8f-4a16f821eaef



**After** — enabling AI shows a contextual subscribe modal; dismissing
it keeps the app working (no lock):

<!-- 📎 PLACEHOLDER: drag in ~/Downloads/free-user-ai-new-behavior.mp4
-->


https://github.com/user-attachments/assets/4a1c76d6-6c4c-4e94-baea-99e7c302b3df



> Note: the "after" clip ends at the modal + "no lock" proof rather than
crossing into Stripe Checkout, because this local dev env has a
pre-existing Stripe misconfig (`No such tax rate 'txr_…'`) that 500s on
`/subscribe/checkout` — unrelated to this change (the dialog's Upgrade
button reuses the same `checkout.url()` as the existing on-page Upgrade
button).
2026-07-18 14:45:56 +02:00
..
components feat(transactions): add delete button to the transaction edit modal (#697) 2026-07-18 11:03:09 +00:00
contexts Move transaction filtering from client-side to server-side (#126) 2026-02-16 12:49:04 +01:00
hooks fix(dashboard): stop negative asset balances inflating net worth (#660) 2026-07-08 15:22:16 +00:00
layouts feat(mcp): read-only MCP server for Pro accounts (#689) 2026-07-17 16:54:15 +02:00
lib fix(accounts): show credit cards as positive and exclude them from net worth (#673) 2026-07-13 07:27:52 +00:00
pages feat: prompt free users to subscribe when enabling AI categorization (#696) 2026-07-18 14:45:56 +02:00
providers fix: move community link to user menu (#442) 2026-05-27 17:39:26 +02:00
services feat(transactions): allow editing all fields of manual transactions (#683) 2026-07-16 08:59:10 +02:00
types feat(mcp): read-only MCP server for Pro accounts (#689) 2026-07-17 16:54:15 +02:00
utils feat(currencies): add Swedish Krona (SEK) (#684) 2026-07-16 14:25:33 +00:00
app.tsx feat(onboarding): clarify the "categorize at least 5" goal in the categorizer (#616) 2026-07-01 08:34:51 +02:00
ssr.tsx Remove plaintext-transactions feature flag & E2E references (#116) 2026-02-13 11:10:21 +01:00