Adds a permanent, non-consumable premium unlock (EstadoEntitlement + PuertoCompras/ServicioComprasPlayBilling) that removes ads and unlocks alarm vacations, alarms past a 5-alarm free cap, recording start, and full Android Auto browsing. The phone equalizer stays free for everyone. - Entitlement is prefs-backed (compra_premium_v1), fail-open, and resolvable headlessly via esPremiumPersistido() for the Android Auto audio handler, which registers before runApp. - Android Auto reduced mode keeps the real root folder labels for free users; browsing into any of them (and playFromMediaId/playFromSearch/ skipToNext/skipToPrevious) is blocked at the getChildren/servicio_audio choke points, with a locked "Función Premium" item as the backstop. Current-station play/pause/stop stays untouched. A free -> premium transition actively invalidates the head unit's cached browse tree. - Ads (top banner + capped interstitial before adding a station or an alarm) are gated behind entitlement via ServicioAnuncios, using official Google test ad unit IDs pending AdMob provisioning. - Alarm cap UX shows an explanatory message with a secondary unlock action rather than a bare paywall jump; existing data is grandfathered. - 4 new localization keys translated across all 13 supported locales. Co-located tests use strict TDD (RED test before implementation) for every new pure-logic unit; full existing suite passes unchanged.
103 lines
7.6 KiB
Markdown
103 lines
7.6 KiB
Markdown
# 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).
|