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.
6.0 KiB
6.0 KiB
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
- 0.1 Uncomment
in_app_purchase/google_mobile_adsinpubspec.yaml;flutter pub get. - 0.2 Add AdMob test app ID to
AndroidManifest.xml.
Phase 1: Entitlement Core
- 1.1 RED
estado_entitlement_test.dart: default free; persisted true; fail-open on failure;esPremiumPersistido()headless, noBuildContext. - 1.2 GREEN
estado_entitlement.dart:EstadoEntitlementChangeNotifier(keycompra_premium_v1) +esPremiumPersistido(). - 1.3 REFACTOR: shared prefs-key constant; document fail-open contract.
Phase 2: Purchase I/O
- 2.1 RED
servicio_compras_test.dart:comprar()success/cancel/idempotent;restaurar()found/not-found, no error. - 2.2 GREEN
servicio_compras.dart:PuertoCompras+ServicioComprasPlayBilling(solein_app_purchasesite); wirecomprar/restaurar.
Phase 3: Alarm Gating
- 3.1 RED
estado_alarmas_gating_test.dart:puedeCrearAlarma4/5/6; 6th blocked pre-schedule; edit-at-cap ok; premium uncapped; 8 preexisting grandfathered, 9th blocked; vacations free-blocked/premium-ok. - 3.2 GREEN
estado_alarmas.dart:ResultadoGuardarAlarmaenum,puedeCrearAlarma, gateguardarAlarma(:104)+crearRangoVacaciones(:510). - 3.3 GREEN
pantalla_alarmas.dart/_EditorAlarmaSheet+pantalla_vacaciones.dart: cap message + "Desbloquear Premium" CTA; vacation upsell.
Phase 4: Recording Gating
- 4.1 RED
estado_grabacion_gating_test.dart:iniciar()blocked free/allowed premium; existing recordings stay free. - 4.2 GREEN
estado_grabacion.dart: gateiniciar()(:90); upsell at 3 sites inpantalla_reproductor.dart.
Phase 5: Android Auto Gating
- 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). - 5.2 RED
servicio_audio_gating_test.dart:debeBloquearCambioDeEmisorafree/premium; stale-id backstop wired intoplayFromMediaId/playFromSearch/skipToNext/skipToPrevious. - 5.3 RED: free->premium transition invokes the registered Auto-invalidation hook (
registrarNotificacionDesbloqueoAuto/notificarDesbloqueoAuto), which pushes toPluriWaveAudioHandler.subscribeToChildren's per-idBehaviorSubjects (the current non-deprecatedaudio_serviceAPI — the plugin's OWN internal listener forwards each push to the platform'snotifyChildrenChanged). - 5.4 GREEN:
raiz(premium:)+itemPremiumBloqueado()+respuestaBloqueadaPorEntitlement(navegacion_auto.dart); gategetChildren/playFromMediaId/playFromSearch/skipToNext/skipToPrevious+subscribeToChildren/notificarHijosCambiaronwiring (servicio_audio.dart).
Phase 6: Ads
- 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. - 6.2 GREEN
servicio_anuncios.dart: banner/interstitial port + AdMob adapter (test ad unit IDs) + frequency cap. - 6.3 RED+GREEN
banner_anuncio_superior.dart+app.dart: shrink when premium/unloaded, no overlap 5 tabs;Column[banner, Expanded(body)], neverStack.
Phase 7: Purchase UI Wiring
- 7.1 GREEN
hoja_premium.dart(paywall sheet) +app.dart: registerEstadoEntitlementProvider. - 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)
- 8.1 Add keys (
funcionPremium,limiteAlarmasAlcanzado,desbloquearPremium,restaurarCompras) toapp_es.arb; translate into 12 remaining locales. - 8.2 Run
flutter gen-l10n; verifyAppLocalizationsgetters generated. - 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
- 9.1 Run full suite; confirm every RED test above is GREEN.
- 9.2 Regression-check: phone equalizer has zero entitlement checks.
- 9.3 Update
proposal.mdSuccess Criteria checkboxes.