## 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).