fix(bienvenida): wire the welcome screen into the first-launch flow

This commit is contained in:
2026-07-29 15:49:44 +02:00
parent f3d744aeed
commit 2959941485
6 changed files with 265 additions and 8 deletions
+21 -4
View File
@@ -14,6 +14,7 @@ import 'l10n/gen/app_localizations.dart';
import 'modelos/alarma_musical.dart';
import 'pantallas/pantalla_alarmas.dart';
import 'pantallas/pantalla_alarma_sonando.dart';
import 'pantallas/pantalla_bienvenida.dart';
import 'pantallas/pantalla_inicio.dart';
import 'pantallas/pantalla_buscar.dart';
import 'pantallas/pantalla_favoritos.dart';
@@ -108,7 +109,10 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
EstadoRadio? _estadoSuscrito;
bool _alarmaInicialProcesada = false;
bool _alarmaSonandoActiva = false;
bool _onboardingInicialSolicitado = false;
// WU17b: renamed from `_onboardingInicialSolicitado` — this single guard
// now covers the whole first-launch sequence (welcome screen, then the
// pre-existing what's-new dialog), not only the dialog.
bool _flujoPrimerLanzamientoSolicitado = false;
String? _alarmaSonandoId;
Locale? _localeAlarmasConfigurado;
@@ -191,9 +195,9 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
_alarmaInicialProcesada = true;
unawaited(_procesarAlarmaInicial(alarmas));
}
if (!_onboardingInicialSolicitado) {
_onboardingInicialSolicitado = true;
unawaited(_mostrarOnboardingInicial());
if (!_flujoPrimerLanzamientoSolicitado) {
_flujoPrimerLanzamientoSolicitado = true;
unawaited(_mostrarFlujoPrimerLanzamiento());
}
}
@@ -278,6 +282,19 @@ class _PaginaPrincipalState extends State<_PaginaPrincipal>
}
}
// WU17b: runs the welcome screen's once-ever check BEFORE the recurring
// what's-new dialog, so the two never show at the same time. The welcome
// screen (PantallaBienvenida) is the genuine first-run surface; the
// pre-existing PluriOnboardingDialog is an unrelated "what's new"/help
// modal that keeps its own independent per-version due-or-not logic,
// completely unchanged by this sequencing.
Future<void> _mostrarFlujoPrimerLanzamiento() async {
if (mounted) {
await PantallaBienvenida.mostrarSiProcede(context);
}
await _mostrarOnboardingInicial();
}
Future<void> _mostrarOnboardingInicial() async {
await Future<void>.delayed(const Duration(milliseconds: 900));
if (!mounted || _alarmaSonandoActiva) return;
+23 -4
View File
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import '../estado/estado_navegacion.dart';
import '../l10n/gen/app_localizations.dart';
import '../servicios/servicio_bienvenida.dart';
import '../tema/pluriwave_theme.dart';
/// WU17: first-run welcome surface (`onboarding-welcome` spec, mockup
@@ -14,13 +15,31 @@ import '../tema/pluriwave_theme.dart';
///
/// A full-screen ROUTE, not a modal `Dialog` — deliberately distinct from
/// the pre-existing `PluriOnboardingDialog` (an unrelated "what's new"
/// help-content modal already shown from `app.dart`'s launch flow). This
/// work unit's own task list and verify command scope to this screen in
/// isolation; wiring it into the real first-launch flow (i.e. deciding
/// when `app.dart`/`main.dart` should push it) is not part of WU17.
/// help-content modal already shown from `app.dart`'s launch flow). Both
/// surfaces coexist: [mostrarSiProcede] (WU17b) is what actually wires this
/// screen into the genuine first-launch flow, called from `app.dart` BEFORE
/// `PluriOnboardingDialog.mostrarSiProcede` on every cold start, so the two
/// never race — the once-ever welcome resolves first, then the recurring
/// what's-new dialog runs its own unrelated per-version check exactly as
/// before.
class PantallaBienvenida extends StatelessWidget {
const PantallaBienvenida({super.key});
static final ServicioBienvenida _servicio = ServicioBienvenida();
/// WU17b: shows this screen once, on the genuine first launch, then never
/// again. Mirrors `PluriOnboardingDialog.mostrarSiProcede`'s shape
/// (check-then-show-then-mark-seen) so both first-launch surfaces share
/// the same call convention from `app.dart`.
static Future<void> mostrarSiProcede(BuildContext context) async {
if (!await _servicio.debeMostrarBienvenida()) return;
if (!context.mounted) return;
await Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (_) => const PantallaBienvenida()));
await _servicio.marcarBienvenidaVista();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
+32
View File
@@ -0,0 +1,32 @@
import 'package:shared_preferences/shared_preferences.dart';
/// WU17b: persists whether the first-run welcome screen (`PantallaBienvenida`,
/// `onboarding-welcome` spec) has already been shown, so it renders once
/// rather than on every launch.
///
/// Same injectable-`SharedPreferences`, versioned-key convention as
/// `ServicioContenidoApp` (S3-R4) — but a plain one-time boolean flag, since
/// this welcome screen is a single first-impression surface, not something
/// that re-triggers per app version the way the "what's new" onboarding
/// dialog does.
class ServicioBienvenida {
ServicioBienvenida({SharedPreferences? prefs}) : _prefs = prefs;
static const _keyBienvenidaVista = 'pluri_bienvenida_vista_v1';
final SharedPreferences? _prefs;
/// Injected startup instance (S3-R4); getInstance() is only a fallback.
Future<SharedPreferences> _resolverPrefs() async =>
_prefs ?? SharedPreferences.getInstance();
Future<bool> debeMostrarBienvenida() async {
final prefs = await _resolverPrefs();
return !(prefs.getBool(_keyBienvenidaVista) ?? false);
}
Future<void> marcarBienvenidaVista() async {
final prefs = await _resolverPrefs();
await prefs.setBool(_keyBienvenidaVista, true);
}
}