fix(bienvenida): wire the welcome screen into the first-launch flow
This commit is contained in:
+21
-4
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,11 @@
|
||||
> **WU15b was added mid-apply, not planned upfront** — WU15 shipped `PantallaGrabaciones` (the recordings library)
|
||||
> fully tested but reachable from nowhere in the app. WU15b (below, after WU15's section) is the coordinator-ruled fix
|
||||
> that wires it into Settings navigation. It is small and does not change the 18-commit delivery model's shape.
|
||||
> **WU17b was added mid-apply, not planned upfront — same shape as WU15b.** WU17 shipped `PantallaBienvenida` (the
|
||||
> welcome screen) fully tested but reachable from nowhere in the app (`rg "PantallaBienvenida" lib/app.dart lib/main.dart`
|
||||
> found nothing). WU17b (below, after WU17's section) wires it into the genuine first-launch flow and establishes how
|
||||
> it coexists with the pre-existing, unrelated `PluriOnboardingDialog` ("what's new" modal). It is small and does not
|
||||
> change the 18-commit delivery model's shape.
|
||||
> Strict TDD is ON. Runner: `flutter test`. `flutter analyze` and a **scoped** `dart format` gate every commit.
|
||||
> **`flutter build` is never run.**
|
||||
>
|
||||
@@ -1058,6 +1063,61 @@ real capability living in the header, not decorative chrome).
|
||||
`lib/l10n/app_*.arb` or `lib/`.
|
||||
- [x] 17.7 Verify — all 3 scenario tests green; grep scan clean.
|
||||
|
||||
## WU17b — Wire the welcome screen into the first-launch flow
|
||||
|
||||
**Not in the original plan — added to close the WU17 gap noted above, same shape as WU15/WU15b.** WU17 built
|
||||
`PantallaBienvenida` fully tested and committed, but left it unreachable from the app: `rg "PantallaBienvenida"
|
||||
lib/app.dart lib/main.dart` found nothing. This work unit exists solely to fix that.
|
||||
|
||||
**Commit**: `fix(bienvenida): wire the welcome screen into the first-launch flow`
|
||||
**Depends on**: WU17
|
||||
**Spec refs**: `onboarding-welcome` — Full-Screen Welcome Route (reachability; the render/content/no-monetization/CTA
|
||||
scenarios stay WU17's own, unmodified)
|
||||
**Verify**: `flutter test test/servicios/servicio_bienvenida_test.dart test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart test/pantallas/pantalla_bienvenida_test.dart test/widget_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --cached --name-only --diff-filter=ACM HEAD -- '*.dart')`
|
||||
**New tests**: `test/servicios/servicio_bienvenida_test.dart`, `test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart`
|
||||
**Modified tests**: none (WU17's own `pantalla_bienvenida_test.dart` re-run unmodified as a regression check)
|
||||
|
||||
**Coexistence ruling applied.** `PluriOnboardingDialog` (an unrelated, pre-existing "what's new"/help-content modal
|
||||
loaded from markdown assets) is NOT deleted, merged, or replaced — first-launch welcome and a what's-new modal are
|
||||
different things, confirmed by reading `assets/content/onboarding/en.md`: it is a detailed feature-reference
|
||||
walkthrough plus per-version update notes, structurally different content from the welcome screen's 3-bullet
|
||||
marketing intro. Both now run from `app.dart`'s `_PaginaPrincipalState`, in this order on every cold start: the
|
||||
welcome screen's once-ever check resolves FIRST, then the pre-existing what's-new dialog's own independent
|
||||
per-version due-or-not check runs exactly as it did before this WU. Sequencing (not two independent fire-and-forget
|
||||
calls) is what prevents the two from ever racing onto the screen at the same time.
|
||||
|
||||
- [x] 17b.1 RED — `test/servicios/servicio_bienvenida_test.dart`: a new `ServicioBienvenida` (mirroring
|
||||
`ServicioContenidoApp`'s injectable-`SharedPreferences`, versioned-key convention, S3-R4) is due before it has
|
||||
ever been marked seen, is not due after `marcarBienvenidaVista()`, and respects a seen flag already persisted
|
||||
by a prior launch (`SharedPreferences.setMockInitialValues`).
|
||||
- [x] 17b.2 RED — `test/pantallas/pantalla_bienvenida_primer_lanzamiento_test.dart`: a new
|
||||
`PantallaBienvenida.mostrarSiProcede(context)` static method (mirroring `PluriOnboardingDialog.mostrarSiProcede`'s
|
||||
check-then-show-then-mark-seen shape) pushes the welcome screen on a first launch (no seen flag persisted), does
|
||||
NOT push it when the seen flag is already persisted, and — in one continuous session — persists the flag after
|
||||
being shown once so a later check in the same run skips it.
|
||||
- [x] 17b.3 GREEN — created `lib/servicios/servicio_bienvenida.dart` (`ServicioBienvenida`, key
|
||||
`pluri_bienvenida_vista_v1`, plain one-time boolean — no version comparison needed, unlike
|
||||
`ServicioContenidoApp`, since this welcome is a single first-impression surface, not a per-version one).
|
||||
- [x] 17b.4 GREEN — added the static `PantallaBienvenida.mostrarSiProcede(BuildContext)` method to the existing
|
||||
`pantalla_bienvenida.dart` file (no new wrapper class needed — unlike `PluriOnboardingDialog`, there is only one
|
||||
call shape here); updated the class doc comment to describe how it coexists with `PluriOnboardingDialog`.
|
||||
- [x] 17b.5 GREEN — wired `app.dart`: imported `pantalla_bienvenida.dart`; renamed the existing
|
||||
`_onboardingInicialSolicitado` guard flag to `_flujoPrimerLanzamientoSolicitado` (it now covers the combined
|
||||
sequence, not only the dialog); added `_mostrarFlujoPrimerLanzamiento()`, which awaits
|
||||
`PantallaBienvenida.mostrarSiProcede(context)` then calls the pre-existing, untouched
|
||||
`_mostrarOnboardingInicial()` — replacing the single `unawaited(_mostrarOnboardingInicial())` call site with
|
||||
`unawaited(_mostrarFlujoPrimerLanzamiento())`.
|
||||
- [x] 17b.6 REFACTOR — confirmed `_mostrarOnboardingInicial()`'s own body (900ms delay, `_alarmaSonandoActiva` guard,
|
||||
`PluriOnboardingDialog.mostrarSiProcede` call) is byte-for-byte unchanged — only its call site moved one level
|
||||
deeper into the new sequencing method. No scenario in `PantallaBienvenida`'s own WU17 test file needed to
|
||||
change (all 3 pass unmodified, confirming the CTA/content/no-monetization behavior is untouched).
|
||||
- [x] 17b.7 Verify — scoped suite green: 13/13 (3 `servicio_bienvenida_test.dart` + 3
|
||||
`pantalla_bienvenida_primer_lanzamiento_test.dart` + 3 `pantalla_bienvenida_test.dart` [byte-identical,
|
||||
unmodified] + 4 `widget_test.dart`). `flutter analyze`: 1 issue, identical to baseline. Scoped `dart format`:
|
||||
reformatted 1 of 5 touched files (whitespace-only string-literal wrapping), stable on re-run. Literal-encoding
|
||||
scan: one console-rendering false positive on the pre-existing "días" string (verified byte-correct UTF-8 via a
|
||||
direct file read with the encoding pinned), zero real corruption.
|
||||
|
||||
## WU18 — i18n batch (all 13 locales)
|
||||
|
||||
**Commit**: `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales`
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
||||
import 'package:pluriwave/pantallas/pantalla_bienvenida.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// WU17b: `PantallaBienvenida.mostrarSiProcede` wires the welcome screen
|
||||
/// into the genuine first-launch flow. WU17 built and tested the screen
|
||||
/// fully in isolation, but nothing in `lib/app.dart` or `lib/main.dart`
|
||||
/// ever referenced it — it was unreachable. These tests pin the actual
|
||||
/// gap: the screen must show on a genuine first launch and never again
|
||||
/// once `ServicioBienvenida` has recorded it as seen.
|
||||
Widget _appConDisparador(GlobalKey<NavigatorState> navigatorKey) {
|
||||
return MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
locale: const Locale('es'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder:
|
||||
(context) => Scaffold(
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => PantallaBienvenida.mostrarSiProcede(context),
|
||||
child: const Text('disparar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets(
|
||||
'first launch (no seen flag persisted) shows the welcome screen',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
await tester.pumpWidget(_appConDisparador(navigatorKey));
|
||||
await tester.tap(find.text('disparar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PantallaBienvenida), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'second launch (seen flag already persisted) does not show it again',
|
||||
(tester) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'pluri_bienvenida_vista_v1': true,
|
||||
});
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
await tester.pumpWidget(_appConDisparador(navigatorKey));
|
||||
await tester.tap(find.text('disparar'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(PantallaBienvenida), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('showing it once persists the flag so a later check in the same '
|
||||
'session skips it', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
await tester.pumpWidget(_appConDisparador(navigatorKey));
|
||||
await tester.tap(find.text('disparar'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(PantallaBienvenida), findsOneWidget);
|
||||
|
||||
// Dismiss it the same way the CTA does (a plain pop), simulating the
|
||||
// welcome screen being resolved before the next check ever happens.
|
||||
navigatorKey.currentState!.pop();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('disparar'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(PantallaBienvenida), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/servicio_bienvenida.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// WU17b: `ServicioBienvenida` decides whether the first-run welcome
|
||||
/// screen (`PantallaBienvenida`) is still due — same injectable-prefs,
|
||||
/// versioned-key convention as `ServicioContenidoApp` (S3-R4), but a plain
|
||||
/// boolean flag since this welcome is a one-time, not per-version, surface.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('ServicioBienvenida', () {
|
||||
test(
|
||||
'debeMostrarBienvenida is true before it has ever been marked seen',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final servicio = ServicioBienvenida();
|
||||
|
||||
expect(await servicio.debeMostrarBienvenida(), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'debeMostrarBienvenida is false after marcarBienvenidaVista',
|
||||
() async {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final servicio = ServicioBienvenida();
|
||||
|
||||
await servicio.marcarBienvenidaVista();
|
||||
|
||||
expect(await servicio.debeMostrarBienvenida(), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('respects a seen flag already persisted by a prior launch', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'pluri_bienvenida_vista_v1': true,
|
||||
});
|
||||
final servicio = ServicioBienvenida();
|
||||
|
||||
expect(await servicio.debeMostrarBienvenida(), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user