# Proposal: Freemium unlock via one-time in-app purchase ## Intent PluriWave (1.3.0+151, Internal Testing) has no monetization. Add one non-consumable purchase that permanently removes ads and unlocks the premium feature set, keeping the free tier usable. Purchasers get everything forever, restorable after reinstall, with no renewal or expiry concept. ## Scope ### In Scope - `EstadoEntitlement` ChangeNotifier (SharedPreferences, versioned key, prefs-lazy resolve for headless Android Auto), top-level provider in `app.dart`. - Activate `in_app_purchase`: buy flow, purchase stream, `restorePurchases()` from Settings. - Activate `google_mobile_ads`: persistent top banner anchored in `app.dart` (must not overlap or displace existing content), plus a full-screen interstitial before two specific actions — adding a station manually and adding an alarm. All ads absent when premium. - Gate 4 features: Android Auto reduced mode, alarm vacations, starting recordings, creating alarms past 5. - Paywall reachable from every gated entry point (Settings row + contextual upsell at each gate); distinct "limit reached" signal from `EstadoAlarmas.guardarAlarma` (not the existing `_error`). ### Out of Scope - Price point and Play Console product setup (console-side, undecided). - Server-side receipt validation — no backend exists; client + Play Billing trust accepted for v1. - Subscriptions, trials, promo codes, iOS store setup, CI/fastlane changes (none needed). - Deleting, hiding, or trimming content free users already created. - **The equalizer on the phone**: explicitly stays free for all users (user decision). Only its Android Auto surface is affected, as a consequence of Auto reduced mode. ## Business Rules | Rule | Decision | |------|----------| | Purchase | Non-consumable, permanent, per Play account | | Alarm cap | Free tier = 5 alarms total, enabled or not | | Alarm cap UX | 6th attempt shows an explanatory message with a secondary "unlock" action — never a bare paywall jump | | Grandfathering | Existing alarms/vacations/recordings survive; only new creation past the cap is blocked | | Entitlement failure | Fail-open: trust last persisted flag; never lock out a payer offline | | Equalizer (phone) | Free for everyone — not a gated feature | | Android Auto (free) | Reduced mode: current-station player only. No station browsing/switching, no local music. Every other car entry shows a "Premium feature" item | | Ads — banner | Persistent top banner, laid out so it never overlaps or covers existing UI | | Ads — interstitial | Full-screen ad before adding a station manually and before adding an alarm | | Ads lifecycle | Vanish immediately on purchase, no restart | | Purchase entry points | Settings row + contextual upsell at each gated feature | | Existing content | Viewing/managing stays free; only new gated actions are blocked | ## Capabilities ### New Capabilities - `premium-entitlement`: purchase, restore, persistence, offline policy. - `freemium-gating`: gated features, limits, and how a free user is informed. - `ad-display`: ad placement and lifecycle for free users only. ### Modified Capabilities - `android-auto-media`: browse tree becomes entitlement-aware — free tier collapses to a current-station-player-only tree. ## Approach Hybrid gating (exploration approach 3): UI entry points show the paywall; state-layer choke points (`guardarAlarma`, `EstadoGrabacion.iniciar`, Auto `getChildren`) hold the authoritative check. ## Affected Areas | Area | Impact | Description | |------|--------|-------------| | `lib/estado/estado_entitlement.dart` | New | Entitlement, purchase, restore | | `lib/app.dart` | Modified | Provider registration, top banner anchor | | `lib/estado/estado_alarmas.dart`, `estado_grabacion.dart` | Modified | Cap, vacation gate, recording gate | | `lib/servicios/servicio_audio.dart`, `navegacion_auto.dart` | Modified | Gate car tree | | `lib/pantallas/` (ajustes, vacaciones, alarmas, reproductor) | Modified | Paywall on gated CTAs | | `pubspec.yaml` | Modified | Uncomment both plugins | ## Risks | Risk | Likelihood | Mitigation | |------|------------|------------| | Client-only entitlement is tamperable | Med | Accepted for v1; no backend exists | | Cap feels like data loss | Med | Grandfather all data; explain at creation time | | Headless Auto has no Provider | Med | Prefs-lazy resolve, mirror `FuenteMusicaLocalAutoImpl` | | Missed gate on a call site | Low | State-layer choke points as backstop | | Interstitial before add-alarm/add-station reads as punitive, or trips AdMob's disruptive-ad policy | Med | Interstitial fires on the action's natural transition, never mid-task; enforce a frequency cap so repeated adds in one session don't chain ads; never stack it with the alarm-cap message in the same tap | | Auto reduced mode leaves a free driver with an empty-looking car UI | Med | Current-station player always present; every locked branch renders an explicit "Premium feature" item, never a blank list | ## Rollback Plan Additive and prefs-backed. Revert by re-commenting both plugins in `pubspec.yaml` and reverting the gate commits; no migration, no schema change. The persisted key is versioned (`compra_premium_v1`) so older builds ignore it. ## Dependencies - Play Console in-app product created and priced; AdMob ad unit IDs. ## Success Criteria - [x] Purchase unlocks every gated item with no restart and survives restart. Verified at the unit level: `EstadoEntitlement.comprar()`/`restaurar()` flip `esPremium` and `notifyListeners()` immediately on a `comprada`/`restaurada` event (no restart needed by construction — every gate reads `esPremium`/`esPremiumPersistido()` live), and the flag persists under `compra_premium_v1`. Full on-device Play Billing QA is still outstanding (deferred — no sandbox purchase available in this environment). - [x] `restorePurchases()` restores entitlement on a fresh install. Verified: `estado_entitlement_test.dart` covers found/not-found restore outcomes. - [x] Free tier blocks the 4 gated features and caps alarms at 5 without destroying data. Verified: `estado_alarmas_gating_test.dart` (cap + grandfathering), `estado_grabacion_gating_test.dart` (recording), `navegacion_auto_gating_test.dart`/`servicio_audio_gating_test.dart` (Android Auto). - [x] Equalizer remains fully usable on the phone for free users. Verified: zero `esPremium`/`EstadoEntitlement`/`esPremiumPersistido` references anywhere in `estado_ecualizador.dart`, `servicio_ecualizador.dart`, `pantalla_ajustes_ecualizador.dart`, `ecualizador_widget.dart`. - [x] Free-tier Android Auto still plays the current station and never shows a blank list. Verified: `respuestaBloqueadaPorEntitlement` never returns an empty list, `raiz(premium:)` keeps the root non-blank for every tier, and `debeBloquearCambioDeEmisora` only gates `playFromMediaId`/`playFromSearch`/`skipToNext`/`skipToPrevious` — `play`/`pause`/`stop` are untouched. - [x] Zero ads (banner and interstitial) for purchasers; offline cold start keeps a purchaser unlocked. Verified: `ServicioAnuncios.debeMostrarBanner`/`intentarInterstitial` gate on `esPremium` first; offline cold start is `esPremiumPersistido`'s fail-open persisted-flag read. - [x] Top banner never overlaps, covers, or displaces existing UI on any tab. Verified: `banner_anuncio_superior_test.dart` + `app.dart`'s `Column[banner, Expanded(body)]` (never a `Stack`). Real-device/Play Console/AdMob QA (purchase flow, restore on a fresh install, car head-unit browse, live ad rendering) remains outstanding per the Work Unit runtime-harness notes in `tasks.md` — none of it is exercisable from this environment. ## Open Questions 1. Price point (Play Console decision; 2.99–4.99 EUR was a benchmark, never confirmed).