import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../estado/estado_entitlement.dart'; import '../l10n/gen/app_localizations.dart'; import '../tema/pluriwave_tokens.dart'; import 'pluri_glass_surface.dart'; import 'pluri_layout.dart'; /// Reusable paywall sheet (Design "File Changes" — `hoja_premium.dart`), /// opened from every gated entry point plus the Settings premium row /// (freemium-gating spec "Purchase Entry Points At Every Gate Plus /// Settings"). Mirrors `FormularioEmisoraPersonalizada`'s bottom-sheet /// shape (`ajustes_emisoras_personalizadas.dart`). Future mostrarHojaPremium(BuildContext context) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, builder: (_) => const HojaPremium(), ); } class HojaPremium extends StatelessWidget { const HojaPremium({super.key}); @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final entitlement = context.watch(); final bottom = MediaQuery.of(context).viewInsets.bottom; return Padding( padding: EdgeInsets.fromLTRB( PluriLayout.horizontal, PluriLayout.horizontal, PluriLayout.horizontal, PluriLayout.horizontal + bottom, ), child: PluriGlassSurface( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon( Icons.workspace_premium_rounded, color: PluriWaveTokens.brand, ), const SizedBox(width: 10), Expanded( child: Text( l10n.premiumHojaTitulo, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w900, ), ), ), // Explicit, obvious dismiss affordance (fix/import-alarmas-y- // paywall): a purchase sheet the user cannot easily escape is // a dark pattern and a Play policy risk. Reachable without // buying or restoring, same weight as any other icon button. IconButton( key: const ValueKey('hoja-premium-cerrar'), icon: const Icon(Icons.close_rounded), tooltip: l10n.closeAction, onPressed: () => Navigator.of(context).maybePop(), ), ], ), const SizedBox(height: 12), // Concrete, honest value list — accuracy is non-negotiable here: // these five are the ONLY things premium unlocks. The phone // equalizer stays free for everyone and must NEVER appear here; // only its Android Auto surface is affected, as a consequence of // Auto itself being gated. _BeneficioPremium(texto: l10n.premiumBeneficioSinAnuncios), _BeneficioPremium(texto: l10n.premiumBeneficioAndroidAuto), _BeneficioPremium(texto: l10n.premiumBeneficioGrabacion), _BeneficioPremium(texto: l10n.premiumBeneficioVacaciones), _BeneficioPremium(texto: l10n.premiumBeneficioAlarmasIlimitadas), const SizedBox(height: 12), Text( l10n.premiumPagoUnico, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 20), // FIX 3 (code review): user-facing feedback for a failed // purchase/restore, or a restore that found nothing — before // this, `resultadoUsuario` had ZERO UI, so the spinner just // stopped with no feedback at all. Never the raw // `EventoCompra.mensaje` developer string — always the mapped, // generic localized message. if (entitlement.resultadoUsuario != null) Padding( key: const ValueKey('hoja-premium-resultado'), padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( entitlement.resultadoUsuario == ResultadoEntitlementUsuario.error ? Icons.error_outline_rounded : Icons.info_outline_rounded, size: 18, color: entitlement.resultadoUsuario == ResultadoEntitlementUsuario.error ? Theme.of(context).colorScheme.error : Theme.of(context).textTheme.bodyMedium?.color, ), const SizedBox(width: 8), Expanded( child: Text( entitlement.resultadoUsuario == ResultadoEntitlementUsuario.error ? l10n.compraError : l10n.restauracionSinCompras, style: Theme.of(context).textTheme.bodyMedium, ), ), IconButton( key: const ValueKey('hoja-premium-resultado-descartar'), icon: const Icon(Icons.close_rounded, size: 18), onPressed: () => entitlement.consumirResultadoUsuario(), padding: EdgeInsets.zero, constraints: const BoxConstraints(), visualDensity: VisualDensity.compact, ), ], ), ), if (entitlement.esPremium) Padding( key: const ValueKey('hoja-premium-activo'), padding: const EdgeInsets.only(bottom: 12), child: Text( l10n.premiumActivo, style: Theme.of(context).textTheme.bodyMedium, ), ) else FilledButton.icon( key: const ValueKey('hoja-premium-comprar'), onPressed: entitlement.compraEnCurso ? null : () => entitlement.comprar(), icon: entitlement.compraEnCurso ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.lock_open_rounded), label: Text(l10n.desbloquearPremium), ), const SizedBox(height: 10), OutlinedButton( key: const ValueKey('hoja-premium-restaurar'), onPressed: entitlement.compraEnCurso ? null : () => entitlement.restaurar(), child: Text(l10n.restaurarCompras), ), if (!entitlement.esPremium) ...[ const SizedBox(height: 4), // Clearly-labelled, always-reachable decline — same weight as // any other secondary action, never made harder to find than // buying (hard constraint: no dark patterns, no guilt-shaming // decline copy). TextButton( key: const ValueKey('hoja-premium-ahora-no'), onPressed: () => Navigator.of(context).maybePop(), child: Text(l10n.premiumAhoraNo), ), ], ], ), ), ); } } /// One concrete, honest value-list row (fix/import-alarmas-y-paywall). class _BeneficioPremium extends StatelessWidget { const _BeneficioPremium({required this.texto}); final String texto; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.check_circle_rounded, size: 18, color: PluriWaveTokens.brand, ), const SizedBox(width: 8), Expanded( child: Text(texto, style: Theme.of(context).textTheme.bodyMedium), ), ], ), ); } }