# Exploration: iap-freemium-unlock One-time non-consumable IAP that removes ads and unlocks 6 currently-free features. Free-tier users see ads (`google_mobile_ads`, commented out in pubspec.yaml, never activated). Purchasers get zero ads and full access forever from a single purchase (not a subscription). ## Current State **State/persistence architecture.** `lib/app.dart` (`PluriWaveApp.build`) wires a `MultiProvider` at the app root: `ChangeNotifierProvider`, three `ListenableProvider`s exposing `EstadoRadio`'s owned children (`EstadoEcualizador`, `EstadoGrabacion`, `EstadoBusqueda`), then independent siblings `ChangeNotifierProvider`, `ChangeNotifierProvider`, `ChangeNotifierProvider`. A single `SharedPreferences` instance is resolved once in `lib/main.dart` and injected as `prefs` into every top-level notifier. Idiomatic per-domain notifier shape (cleanest example: `lib/estado/estado_idioma.dart`): `ChangeNotifier` subclass, optional injected `SharedPreferences?`, a `_resolverPrefs()` fallback to `SharedPreferences.getInstance()` (works from headless callers with no DI), a versioned key constant, `notifyListeners()` after every mutation+persist. **No existing tier/limit/entitlement concept anywhere** — confirmed via grep across `lib/modelos/alarma_musical.dart`, `lib/estado/estado_alarmas.dart`, `lib/servicios/servicio_alarmas.dart`. **pubspec.yaml** (version `1.3.0+151`): `google_mobile_ads` and `in_app_purchase` both commented out, lines ~52-56. Neither is an active dependency. **Fastlane/CI**: `fastlane/Appfile` → `package_name` = `es.freetimelab.pluriwave`; `fastlane/Fastfile` has one lane (`upload_internal`) publishing to Play's `internal` track; `.gitea/workflows/build.yml` auto-bumps version and calls that lane. No in-app-product ID or billing config exists anywhere in CI/fastlane — that's Play Console-side config only, zero CI/fastlane code changes required for this change. ## Affected Areas (gating points per feature) 1. **Equalizer** — `lib/estado/estado_ecualizador.dart`, screen `lib/pantallas/ajustes/pantalla_ajustes_ecualizador.dart`. UI entry: `lib/pantallas/pantalla_ajustes.dart` ~L108-123 (`FilaAjuste.onTap` → push `PantallaAjustesEcualizador`). Second surface: Android Auto's always-present `idEcualizador` folder + on/off custom action in `servicio_audio.dart`/`navegacion_auto.dart` — closed automatically once Android Auto itself is gated. 2. **Android Auto** — `lib/servicios/navegacion_auto.dart`'s pure `ConstructorArbolAuto` feeds `lib/servicios/servicio_audio.dart:1667` `getChildren()` → `constructor.raiz(...)`, the single dispatch point for the whole car tree. `PluriWaveAudioHandler` is registered in `main.dart` before `runApp`, so any gate here must read entitlement via a prefs-lazy fallback, never `BuildContext`/`Provider`. 3. **Alarm vacations** — `lib/pantallas/pantalla_vacaciones.dart` (2 create CTAs: header button `'vacation-add-header'`, mid-page `_CtaAnadirRango`), `lib/estado/estado_alarmas.dart` (`crearRangoVacaciones`/`editarRangoVacaciones`/`eliminarRangoVacaciones`/`guardarVacaciones` + 4 pure queries), `lib/servicios/servicio_alarmas.dart`. Entry from Alarms root: `lib/pantallas/pantalla_alarmas.dart`'s `_PanelVacaciones` (L93). 4. **Station recording** — `lib/servicios/servicio_grabacion_radio.dart` (engine), `lib/estado/estado_grabacion.dart`'s `EstadoGrabacion.iniciar({Duration? duracion})` (L90) is the single choke point for ≥3 UI call sites (`pantalla_reproductor.dart`'s recording panel ~L489-560, duration-picker sheet ~L601-724, mini-player shortcut `'player-tool-record'` ~L1064). `pantalla_grabaciones.dart`/`pantalla_ajustes_grabaciones.dart` manage *existing* recordings and should probably stay accessible regardless of entitlement. 5. **Alarm count limit (new)** — `EstadoAlarmas.guardarAlarma` (L104) is the one save call for create+edit; UI create/edit distinction lives in `_EditorAlarmaSheet` (`pantalla_alarmas.dart`, `widget.alarma == null` checks, save call ~L1189). Today's only failure channel is a `String? _error` used for native scheduling failures — a limit rejection needs a distinct signal, not reuse of `_error`. 6. **Ads** — zero ad code exists anywhere yet. Best candidates: (a) one global anchor in `lib/app.dart`'s `_PaginaPrincipalState.build` bottom `Column` (alongside `MiniReproductor`), covering all 5 tabs with one wiring point; (b) a `SliverToBoxAdapter` row in `PantallaInicio`'s `CustomScrollView` (mirrors `_seccionTusEmisoras`). ## Recommended entitlement architecture New `lib/estado/estado_entitlement.dart` `ChangeNotifier`, shaped like `EstadoIdioma` (injected optional `SharedPreferences`, versioned key e.g. `compra_premium_v1`, `bool get esPremium`, prefs-lazy fallback for the Android Auto path), registered as an independent sibling `ChangeNotifierProvider` in `app.dart` (not owned by `EstadoRadio` — it's cross-cutting). ## Approaches considered 1. **UI-entry-point gating only** (6 call sites) — small, reviewable diffs, matches idiomatic pattern; risk of a missed call site on future refactors. Effort: Medium. 2. **State-method-layer gating only** — unbypassable, but silent no-op UX unless paired with UI copy anyway (not a real alternative to #1). Effort: Medium-High. 3. **Hybrid (recommended)** — UI entries show the paywall (good UX) + state-layer choke points (`guardarAlarma`, `EstadoGrabacion.iniciar`, Android Auto `getChildren`) carry the authoritative check. Effort: Medium. ## Risks - Grandfathering: devices with 6+ alarms already before ship — candidate: grandfather existing, block only future creates once count ≥ 5 (needs design sign-off). - Restore-purchases flow for reinstalls/new devices — no UI placement decided yet. - Offline/failed entitlement checks — candidate: fail-open (trust last-persisted local flag) over fail-closed. - No backend exists in this codebase — entitlement will be client/Play-Billing-trusted only, an accepted risk unless design decides otherwise. - Android Auto's headless cold-start path requires the same "resolve prefs lazily, no DI at construction" convention already used by `FuenteMusicaLocalAutoImpl`. - Alarm-count rule (all alarms vs. only active/enabled) is undecided and affects UX. ## Ready for Proposal Yes.