# Tasks: Freemium unlock via one-time in-app purchase ## Review Workload Forecast Estimated changed lines: 1200-2000+ (5 new, ~12 modified Dart, 13 `.arb` locales, pubspec.yaml, AndroidManifest.xml, plus tests). Suggested split: single PR now (`single-pr`); Work Units below double as chained-PR slices if `size:exception` is declined. Delivery strategy: single-pr. Decision needed before apply: Yes Chained PRs recommended: Yes Chain strategy: size-exception 400-line budget risk: High Deferred, non-blocking: price point (Play Console); AdMob ad unit IDs — use Google test IDs. Do not invent values. ### Suggested Work Units | Unit | Goal | Focused test command | Runtime harness | Rollback boundary | |---|---|---|---|---| | 1 | Entitlement + purchase I/O | `flutter test test/estado/estado_entitlement_test.dart test/servicios/servicio_compras_test.dart` | Manual: Settings > Restaurar compras | `estado_entitlement.dart`, `servicio_compras.dart` | | 2 | Alarm, recording, Auto gates + cache invalidation | `flutter test test/estado/estado_alarmas_test.dart test/estado/estado_grabacion_test.dart test/servicios/navegacion_auto_test.dart test/servicios/servicio_audio_test.dart` | Auto head-unit browse smoke | gate diffs in `estado_alarmas.dart`, `estado_grabacion.dart`, `navegacion_auto.dart`, `servicio_audio.dart` | | 3 | Ads (banner + interstitial) | `flutter test test/servicios/servicio_anuncios_test.dart test/widgets/banner_anuncio_superior_test.dart` | Manual: banner/no-overlap 5 tabs | `servicio_anuncios.dart`, `banner_anuncio_superior.dart`, `app.dart` Column diff | | 4 | Paywall UI + localization | `flutter test test/pantallas/pantalla_ajustes_test.dart && flutter gen-l10n` | Manual: tap each gate | `hoja_premium.dart`, screen CTA diffs, `app_*.arb` keys | ## Phase 0: Foundation - [x] 0.1 Uncomment `in_app_purchase`/`google_mobile_ads` in `pubspec.yaml`; `flutter pub get`. - [x] 0.2 Add AdMob test app ID to `AndroidManifest.xml`. ## Phase 1: Entitlement Core - [x] 1.1 RED `estado_entitlement_test.dart`: default free; persisted true; fail-open on failure; `esPremiumPersistido()` headless, no `BuildContext`. - [x] 1.2 GREEN `estado_entitlement.dart`: `EstadoEntitlement` `ChangeNotifier` (key `compra_premium_v1`) + `esPremiumPersistido()`. - [x] 1.3 REFACTOR: shared prefs-key constant; document fail-open contract. ## Phase 2: Purchase I/O - [x] 2.1 RED `servicio_compras_test.dart`: `comprar()` success/cancel/idempotent; `restaurar()` found/not-found, no error. - [x] 2.2 GREEN `servicio_compras.dart`: `PuertoCompras` + `ServicioComprasPlayBilling` (sole `in_app_purchase` site); wire `comprar/restaurar`. ## Phase 3: Alarm Gating - [x] 3.1 RED `estado_alarmas_gating_test.dart`: `puedeCrearAlarma` 4/5/6; 6th blocked pre-schedule; edit-at-cap ok; premium uncapped; 8 preexisting grandfathered, 9th blocked; vacations free-blocked/premium-ok. - [x] 3.2 GREEN `estado_alarmas.dart`: `ResultadoGuardarAlarma` enum, `puedeCrearAlarma`, gate `guardarAlarma`(:104)+`crearRangoVacaciones`(:510). - [x] 3.3 GREEN `pantalla_alarmas.dart`/`_EditorAlarmaSheet` + `pantalla_vacaciones.dart`: cap message + "Desbloquear Premium" CTA; vacation upsell. ## Phase 4: Recording Gating - [x] 4.1 RED `estado_grabacion_gating_test.dart`: `iniciar()` blocked free/allowed premium; existing recordings stay free. - [x] 4.2 GREEN `estado_grabacion.dart`: gate `iniciar()`(:90); upsell at 3 sites in `pantalla_reproductor.dart`. ## Phase 5: Android Auto Gating - [x] 5.1 RED `navegacion_auto_gating_test.dart`: `raiz(premium:false)` non-blank tree with the real folder labels (design ADR-4: root labels stay visible for every tier, lock enforced one level down); `respuestaBloqueadaPorEntitlement(non-root,free)->[itemPremiumBloqueado()]`; premium unchanged (regression). - [x] 5.2 RED `servicio_audio_gating_test.dart`: `debeBloquearCambioDeEmisora` free/premium; stale-id backstop wired into `playFromMediaId`/`playFromSearch`/`skipToNext`/`skipToPrevious`. - [x] 5.3 RED: free->premium transition invokes the registered Auto-invalidation hook (`registrarNotificacionDesbloqueoAuto`/`notificarDesbloqueoAuto`), which pushes to `PluriWaveAudioHandler.subscribeToChildren`'s per-id `BehaviorSubject`s (the current non-deprecated `audio_service` API — the plugin's OWN internal listener forwards each push to the platform's `notifyChildrenChanged`). - [x] 5.4 GREEN: `raiz(premium:)`+`itemPremiumBloqueado()`+`respuestaBloqueadaPorEntitlement` (`navegacion_auto.dart`); gate `getChildren`/`playFromMediaId`/`playFromSearch`/`skipToNext`/`skipToPrevious` + `subscribeToChildren`/`notificarHijosCambiaron` wiring (`servicio_audio.dart`). ## Phase 6: Ads - [x] 6.1 RED `servicio_anuncios_test.dart`: cap 2/session >=3min (fake clock); over-cap no-op; suppressed with alarm-cap message; none when premium. - [x] 6.2 GREEN `servicio_anuncios.dart`: banner/interstitial port + AdMob adapter (test ad unit IDs) + frequency cap. - [x] 6.3 RED+GREEN `banner_anuncio_superior.dart` + `app.dart`: shrink when premium/unloaded, no overlap 5 tabs; `Column[banner, Expanded(body)]`, never `Stack`. ## Phase 7: Purchase UI Wiring - [x] 7.1 GREEN `hoja_premium.dart` (paywall sheet) + `app.dart`: register `EstadoEntitlement` Provider. - [x] 7.2 GREEN `pantalla_ajustes.dart`: buy/restore/premium-active row; `pantalla_favoritos.dart` + `ajustes_emisoras_personalizadas.dart`: interstitial before manual station add. ## Phase 8: Localization (13 locales, `app_es.arb` template) - [x] 8.1 Add keys (`funcionPremium`, `limiteAlarmasAlcanzado`, `desbloquearPremium`, `restaurarCompras`) to `app_es.arb`; translate into 12 remaining locales. - [x] 8.2 Run `flutter gen-l10n`; verify `AppLocalizations` getters generated. - [x] 8.3 Run literal-encoding scan on `lib/l10n/app_*.arb` — zero mojibake (only pre-existing "REPETIÇÃO" false positive, unrelated to this change). ## Phase 9: Verification - [x] 9.1 Run full suite; confirm every RED test above is GREEN. - [x] 9.2 Regression-check: phone equalizer has zero entitlement checks. - [x] 9.3 Update `proposal.md` Success Criteria checkboxes.