Files
pluriwave/openspec/changes/iap-freemium-unlock/tasks.md
T
FreeTLab aa0b242374 feat(iap): add freemium unlock via one-time in-app purchase
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.
2026-08-10 20:37:07 +02:00

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_ads in pubspec.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, no BuildContext.
  • 1.2 GREEN estado_entitlement.dart: EstadoEntitlement ChangeNotifier (key compra_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 (sole in_app_purchase site); wire comprar/restaurar.

Phase 3: Alarm Gating

  • 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.
  • 3.2 GREEN estado_alarmas.dart: ResultadoGuardarAlarma enum, puedeCrearAlarma, gate guardarAlarma(: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: gate iniciar()(:90); upsell at 3 sites in pantalla_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: debeBloquearCambioDeEmisora free/premium; stale-id backstop wired into playFromMediaId/playFromSearch/skipToNext/skipToPrevious.
  • 5.3 RED: free->premium transition invokes the registered Auto-invalidation hook (registrarNotificacionDesbloqueoAuto/notificarDesbloqueoAuto), which pushes to PluriWaveAudioHandler.subscribeToChildren's per-id BehaviorSubjects (the current non-deprecated audio_service API — the plugin's OWN internal listener forwards each push to the platform's notifyChildrenChanged).
  • 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

  • 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)], never Stack.

Phase 7: Purchase UI Wiring

  • 7.1 GREEN hoja_premium.dart (paywall sheet) + app.dart: register EstadoEntitlement Provider.
  • 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) to app_es.arb; translate into 12 remaining locales.
  • 8.2 Run flutter gen-l10n; verify AppLocalizations getters 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.md Success Criteria checkboxes.