The premium sheet had no close affordance or way to defer, and its copy only said "Función Premium" without stating what it unlocks. Adds a header close (X) button and a "not now" secondary action so dismissal is never harder than purchasing, and replaces the bare title with a concrete, honest breakdown of the 5 things premium unlocks (no ads, Android Auto, station recording, alarm vacation ranges, unlimited alarms) plus the one-time-purchase framing. The phone equalizer is never listed, since it stays free for everyone. New l10n keys added to all 13 locales.
220 lines
8.6 KiB
Dart
220 lines
8.6 KiB
Dart
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<void> mostrarHojaPremium(BuildContext context) {
|
|
return showModalBottomSheet<void>(
|
|
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<EstadoEntitlement>();
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|