fix(iap): address code review defects in freemium/IAP change
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]
This commit is contained in:
+47
-27
@@ -35,6 +35,30 @@ import 'servicios/navegacion_auto.dart';
|
||||
import 'servicios/servicio_alarmas_android.dart';
|
||||
import 'servicios/servicio_dispositivo_audio.dart';
|
||||
|
||||
/// Extracted out of `_PaginaPrincipalState.build` (FIX 1, code review) so
|
||||
/// the banner + status-bar-inset composition is unit-testable in isolation
|
||||
/// — `_PaginaPrincipal` itself is library-private and constructs real
|
||||
/// platform-backed services (see `app_test.dart`'s own comments), so it
|
||||
/// cannot be safely widget-tested directly. Mirrors this file's existing
|
||||
/// `@visibleForTesting` top-level extraction convention
|
||||
/// (`main.dart`'s `orientacionesPara`/`aplicarPoliticaOrientacion`).
|
||||
///
|
||||
/// `BannerAnuncioSuperior` owns its OWN top `SafeArea` internally now (see
|
||||
/// `banner_anuncio_superior.dart`) — this function deliberately does NOT
|
||||
/// wrap it in one, since `SafeArea` reserves `MediaQuery.padding.top` even
|
||||
/// around a zero-size collapsed child, which used to leave a permanent
|
||||
/// blank status-bar-height strip for premium users and for free users
|
||||
/// before the first ad finished loading.
|
||||
@visibleForTesting
|
||||
Widget construirCuerpoPrincipal({required Widget contenido}) {
|
||||
return Column(
|
||||
children: [
|
||||
const BannerAnuncioSuperior(),
|
||||
Expanded(child: SafeArea(top: false, child: contenido)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class PluriWaveApp extends StatelessWidget {
|
||||
const PluriWaveApp({super.key, this.prefs, this.fuenteAuto, this.compras});
|
||||
|
||||
@@ -256,36 +280,32 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
|
||||
// (design.md ADR-6): a `Column` sibling, NEVER a `Stack`/overlay — the
|
||||
// banner RESERVES its own space above the existing body instead of
|
||||
// covering any of it. `BannerAnuncioSuperior` itself collapses to
|
||||
// `SizedBox.shrink()` (zero layout impact) for premium/unloaded.
|
||||
body: Column(
|
||||
children: [
|
||||
const SafeArea(bottom: false, child: BannerAnuncioSuperior()),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: AnimatedSwitcher(
|
||||
duration: context.pluriMotion.normal,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder:
|
||||
(child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.035, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<int>(indice),
|
||||
child: _paginas[indice],
|
||||
// `SizedBox.shrink()` (zero layout impact) for premium/unloaded, and
|
||||
// (FIX 1, code review) owns its OWN top `SafeArea` internally — this
|
||||
// level no longer wraps it in an unconditional `SafeArea`, which used
|
||||
// to reserve `MediaQuery.padding.top` even for a zero-size collapsed
|
||||
// child, leaving a permanent blank status-bar-height strip.
|
||||
body: construirCuerpoPrincipal(
|
||||
contenido: AnimatedSwitcher(
|
||||
duration: context.pluriMotion.normal,
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder:
|
||||
(child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.035, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: KeyedSubtree(
|
||||
key: ValueKey<int>(indice),
|
||||
child: _paginas[indice],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
|
||||
Reference in New Issue
Block a user