Fixes 9 of 10 review findings (10th requires a manual Play Console step, no code change): 1. app.dart/banner_anuncio_superior.dart: move the top SafeArea inside BannerAnuncioSuperior so it only reserves status-bar height when an ad actually renders, restoring edge-to-edge layout for premium and free-unloaded users. 2. servicio_anuncios.dart: bound every interstitial await (load, presentation, and the injected implementation itself) with injectable timeouts so a callback that never fires can no longer hang a caller. 3. estado_entitlement.dart/hoja_premium.dart: expose a typed resultadoUsuario signal for purchase/restore failures and restore-found-nothing, with dedicated localized messages (compraError, restauracionSinCompras) across all 13 locales -- never the raw developer/exception string. 4. main.dart/servicio_consentimiento.dart: add a GDPR/UMP consent flow (ConsentInformation/ConsentForm) that gates Mobile Ads SDK init on canRequestAds(); premium users never see a consent form; failures degrade to no ads instead of crashing or blocking startup. 6. servicio_anuncios.dart: track real ad presentation (onAdShowedFullScreenContent) so a failed-to-show interstitial no longer consumes a session cap slot. 7. banner_anuncio_superior.dart: add an explicit load-attempted guard so repeated didChangeDependencies (e.g. entitlement notifyListeners during a purchase) can only ever trigger one banner load attempt. 8. servicio_anuncios.dart: make esPremium a required constructor parameter, matching the hardened contract already applied to EstadoAlarmas/EstadoGrabacion/EstadoRadio. 9. hoja_premium.dart: add a dedicated premiumActivo localized string instead of reusing the equalizer's equalizerActive translation, across all 13 locales. All fixes implemented RED-first (failing test before production code). Full suite: 1261 passed, 2 pre-existing skips, 0 failures. flutter analyze: 5 pre-existing issues only, 0 new. [version set]
152 lines
5.8 KiB
Dart
152 lines
5.8 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.funcionPremium,
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|