From 9b415e73b07057f2edda13adc7cfa328dca79f1e Mon Sep 17 00:00:00 2001 From: freetlab Date: Wed, 29 Jul 2026 15:04:44 +0200 Subject: [PATCH] feat(bienvenida): add monetization-free welcome screen Build the first-run welcome surface from mockup screen 14, stripped of its entire monetization block: no PRO pill, no "14 dias PRO gratis" trial line, no pricing card, no secondary "free version" link. Ships only logo, headline, body copy, exactly 3 feature bullets, and the single "Empezar a escuchar" CTA, as a full-screen route (not a modal). Spanish copy is re-cast from the mockup's "tu" form to the app's established voseo register (matching ~750 existing app_es.arb lines), using "auto" instead of "coche" per the one existing precedent. The 3 bullet icons reuse existing tokens (electricMagenta/liveGreen/ warmCoral) that already match the mockup's own hex values for them. CTA switches to the Escuchar tab and pops the route. Wiring this screen into the real first-launch flow (main.dart/app.dart) is left for a follow-up unit, same shape as WU15/WU15b - this WU only covers the isolated, tested screen per its own task list and verify command. WU17. --- lib/l10n/app_en.arb | 11 +- lib/l10n/app_es.arb | 11 +- lib/l10n/gen/app_localizations.dart | 54 ++++++ lib/l10n/gen/app_localizations_ar.dart | 30 ++++ lib/l10n/gen/app_localizations_bn.dart | 30 ++++ lib/l10n/gen/app_localizations_de.dart | 30 ++++ lib/l10n/gen/app_localizations_en.dart | 30 ++++ lib/l10n/gen/app_localizations_es.dart | 30 ++++ lib/l10n/gen/app_localizations_fr.dart | 30 ++++ lib/l10n/gen/app_localizations_hi.dart | 30 ++++ lib/l10n/gen/app_localizations_id.dart | 30 ++++ lib/l10n/gen/app_localizations_it.dart | 30 ++++ lib/l10n/gen/app_localizations_ja.dart | 30 ++++ lib/l10n/gen/app_localizations_pt.dart | 30 ++++ lib/l10n/gen/app_localizations_ru.dart | 30 ++++ lib/l10n/gen/app_localizations_zh.dart | 30 ++++ lib/pantallas/pantalla_bienvenida.dart | 169 +++++++++++++++++++ openspec/changes/rediseno-funcional/tasks.md | 55 ++++-- test/pantallas/pantalla_bienvenida_test.dart | 144 ++++++++++++++++ 19 files changed, 820 insertions(+), 14 deletions(-) create mode 100644 lib/pantallas/pantalla_bienvenida.dart create mode 100644 test/pantallas/pantalla_bienvenida_test.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6105025..02b6a88 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -799,5 +799,14 @@ "error": {} } }, - "localMusicFolderGenericName": "Selected folder" + "localMusicFolderGenericName": "Selected folder", + "welcomeHeadline": "Your world, live", + "welcomeBody": "53,412 stations from 238 countries. Save your favorites, take them to the car, and wake up to them.", + "welcomeBullet1Title": "Per-station equalizer", + "welcomeBullet1Subtitle": "Plus a preset per output device", + "welcomeBullet2Title": "Android Auto", + "welcomeBullet2Subtitle": "Your favorites and local music in the car", + "welcomeBullet3Title": "Music alarms", + "welcomeBullet3Subtitle": "With gradual volume rise and vacation mode", + "welcomeCtaLabel": "Start listening" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 896beca..434ad3d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -758,5 +758,14 @@ "error": {} } }, - "localMusicFolderGenericName": "Carpeta seleccionada" + "localMusicFolderGenericName": "Carpeta seleccionada", + "welcomeHeadline": "Tu mundo, en directo", + "welcomeBody": "53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.", + "welcomeBullet1Title": "Ecualizador por emisora", + "welcomeBullet1Subtitle": "Y un preset por dispositivo de salida", + "welcomeBullet2Title": "Android Auto", + "welcomeBullet2Subtitle": "Tus favoritas y tu música local en el auto", + "welcomeBullet3Title": "Alarmas musicales", + "welcomeBullet3Subtitle": "Con subida progresiva y modo vacaciones", + "welcomeCtaLabel": "Empezar a escuchar" } diff --git a/lib/l10n/gen/app_localizations.dart b/lib/l10n/gen/app_localizations.dart index 61575eb..f98da4e 100644 --- a/lib/l10n/gen/app_localizations.dart +++ b/lib/l10n/gen/app_localizations.dart @@ -2869,6 +2869,60 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'Carpeta seleccionada'** String get localMusicFolderGenericName; + + /// No description provided for @welcomeHeadline. + /// + /// In es, this message translates to: + /// **'Tu mundo, en directo'** + String get welcomeHeadline; + + /// No description provided for @welcomeBody. + /// + /// In es, this message translates to: + /// **'53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'** + String get welcomeBody; + + /// No description provided for @welcomeBullet1Title. + /// + /// In es, this message translates to: + /// **'Ecualizador por emisora'** + String get welcomeBullet1Title; + + /// No description provided for @welcomeBullet1Subtitle. + /// + /// In es, this message translates to: + /// **'Y un preset por dispositivo de salida'** + String get welcomeBullet1Subtitle; + + /// No description provided for @welcomeBullet2Title. + /// + /// In es, this message translates to: + /// **'Android Auto'** + String get welcomeBullet2Title; + + /// No description provided for @welcomeBullet2Subtitle. + /// + /// In es, this message translates to: + /// **'Tus favoritas y tu música local en el auto'** + String get welcomeBullet2Subtitle; + + /// No description provided for @welcomeBullet3Title. + /// + /// In es, this message translates to: + /// **'Alarmas musicales'** + String get welcomeBullet3Title; + + /// No description provided for @welcomeBullet3Subtitle. + /// + /// In es, this message translates to: + /// **'Con subida progresiva y modo vacaciones'** + String get welcomeBullet3Subtitle; + + /// No description provided for @welcomeCtaLabel. + /// + /// In es, this message translates to: + /// **'Empezar a escuchar'** + String get welcomeCtaLabel; } class _AppLocalizationsDelegate diff --git a/lib/l10n/gen/app_localizations_ar.dart b/lib/l10n/gen/app_localizations_ar.dart index 5f7cbe6..6c57433 100644 --- a/lib/l10n/gen/app_localizations_ar.dart +++ b/lib/l10n/gen/app_localizations_ar.dart @@ -1580,4 +1580,34 @@ class AppLocalizationsAr extends AppLocalizations { @override String get localMusicFolderGenericName => 'مجلد محدد'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_bn.dart b/lib/l10n/gen/app_localizations_bn.dart index ee0cf7c..fca04c7 100644 --- a/lib/l10n/gen/app_localizations_bn.dart +++ b/lib/l10n/gen/app_localizations_bn.dart @@ -1588,4 +1588,34 @@ class AppLocalizationsBn extends AppLocalizations { @override String get localMusicFolderGenericName => 'নির্বাচিত ফোল্ডার'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_de.dart b/lib/l10n/gen/app_localizations_de.dart index b3513cb..09dfbce 100644 --- a/lib/l10n/gen/app_localizations_de.dart +++ b/lib/l10n/gen/app_localizations_de.dart @@ -1599,4 +1599,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get localMusicFolderGenericName => 'Ausgewählter Ordner'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_en.dart b/lib/l10n/gen/app_localizations_en.dart index bf27499..f8d12c0 100644 --- a/lib/l10n/gen/app_localizations_en.dart +++ b/lib/l10n/gen/app_localizations_en.dart @@ -1581,4 +1581,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get localMusicFolderGenericName => 'Selected folder'; + + @override + String get welcomeHeadline => 'Your world, live'; + + @override + String get welcomeBody => + '53,412 stations from 238 countries. Save your favorites, take them to the car, and wake up to them.'; + + @override + String get welcomeBullet1Title => 'Per-station equalizer'; + + @override + String get welcomeBullet1Subtitle => 'Plus a preset per output device'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Your favorites and local music in the car'; + + @override + String get welcomeBullet3Title => 'Music alarms'; + + @override + String get welcomeBullet3Subtitle => + 'With gradual volume rise and vacation mode'; + + @override + String get welcomeCtaLabel => 'Start listening'; } diff --git a/lib/l10n/gen/app_localizations_es.dart b/lib/l10n/gen/app_localizations_es.dart index 68a1f68..fe37251 100644 --- a/lib/l10n/gen/app_localizations_es.dart +++ b/lib/l10n/gen/app_localizations_es.dart @@ -1594,4 +1594,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String get localMusicFolderGenericName => 'Carpeta seleccionada'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_fr.dart b/lib/l10n/gen/app_localizations_fr.dart index 7d3776c..db0d61b 100644 --- a/lib/l10n/gen/app_localizations_fr.dart +++ b/lib/l10n/gen/app_localizations_fr.dart @@ -1603,4 +1603,34 @@ class AppLocalizationsFr extends AppLocalizations { @override String get localMusicFolderGenericName => 'Dossier sélectionné'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_hi.dart b/lib/l10n/gen/app_localizations_hi.dart index 0e5ec22..e5b36ba 100644 --- a/lib/l10n/gen/app_localizations_hi.dart +++ b/lib/l10n/gen/app_localizations_hi.dart @@ -1586,4 +1586,34 @@ class AppLocalizationsHi extends AppLocalizations { @override String get localMusicFolderGenericName => 'चयनित फ़ोल्डर'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_id.dart b/lib/l10n/gen/app_localizations_id.dart index 645d2dd..9a1fbf5 100644 --- a/lib/l10n/gen/app_localizations_id.dart +++ b/lib/l10n/gen/app_localizations_id.dart @@ -1592,4 +1592,34 @@ class AppLocalizationsId extends AppLocalizations { @override String get localMusicFolderGenericName => 'Folder terpilih'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_it.dart b/lib/l10n/gen/app_localizations_it.dart index 136144f..58caeeb 100644 --- a/lib/l10n/gen/app_localizations_it.dart +++ b/lib/l10n/gen/app_localizations_it.dart @@ -1598,4 +1598,34 @@ class AppLocalizationsIt extends AppLocalizations { @override String get localMusicFolderGenericName => 'Cartella selezionata'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_ja.dart b/lib/l10n/gen/app_localizations_ja.dart index 5a13d4a..cef9f86 100644 --- a/lib/l10n/gen/app_localizations_ja.dart +++ b/lib/l10n/gen/app_localizations_ja.dart @@ -1545,4 +1545,34 @@ class AppLocalizationsJa extends AppLocalizations { @override String get localMusicFolderGenericName => '選択したフォルダー'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_pt.dart b/lib/l10n/gen/app_localizations_pt.dart index 492e5e9..042d01e 100644 --- a/lib/l10n/gen/app_localizations_pt.dart +++ b/lib/l10n/gen/app_localizations_pt.dart @@ -1590,4 +1590,34 @@ class AppLocalizationsPt extends AppLocalizations { @override String get localMusicFolderGenericName => 'Pasta selecionada'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_ru.dart b/lib/l10n/gen/app_localizations_ru.dart index dd41836..4763273 100644 --- a/lib/l10n/gen/app_localizations_ru.dart +++ b/lib/l10n/gen/app_localizations_ru.dart @@ -1594,4 +1594,34 @@ class AppLocalizationsRu extends AppLocalizations { @override String get localMusicFolderGenericName => 'Выбранная папка'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/l10n/gen/app_localizations_zh.dart b/lib/l10n/gen/app_localizations_zh.dart index 8419e1c..dd9d7b3 100644 --- a/lib/l10n/gen/app_localizations_zh.dart +++ b/lib/l10n/gen/app_localizations_zh.dart @@ -1537,4 +1537,34 @@ class AppLocalizationsZh extends AppLocalizations { @override String get localMusicFolderGenericName => '已选文件夹'; + + @override + String get welcomeHeadline => 'Tu mundo, en directo'; + + @override + String get welcomeBody => + '53.412 emisoras de 238 países. Guardá tus favoritas, llevalas al auto y despertate con ellas.'; + + @override + String get welcomeBullet1Title => 'Ecualizador por emisora'; + + @override + String get welcomeBullet1Subtitle => 'Y un preset por dispositivo de salida'; + + @override + String get welcomeBullet2Title => 'Android Auto'; + + @override + String get welcomeBullet2Subtitle => + 'Tus favoritas y tu música local en el auto'; + + @override + String get welcomeBullet3Title => 'Alarmas musicales'; + + @override + String get welcomeBullet3Subtitle => + 'Con subida progresiva y modo vacaciones'; + + @override + String get welcomeCtaLabel => 'Empezar a escuchar'; } diff --git a/lib/pantallas/pantalla_bienvenida.dart b/lib/pantallas/pantalla_bienvenida.dart new file mode 100644 index 0000000..76db7b6 --- /dev/null +++ b/lib/pantallas/pantalla_bienvenida.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../estado/estado_navegacion.dart'; +import '../l10n/gen/app_localizations.dart'; +import '../tema/pluriwave_theme.dart'; + +/// WU17: first-run welcome surface (`onboarding-welcome` spec, mockup +/// screen 14) rebuilt WITHOUT its monetization content — no "PRO" pill, no +/// "14 días" trial line, no pricing card, and no secondary "free version" +/// link (binding no-monetization requirement). Content only: logo, +/// headline, body copy, exactly 3 feature bullets, and the single +/// "Empezar a escuchar" CTA. +/// +/// 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. +class PantallaBienvenida extends StatelessWidget { + const PantallaBienvenida({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final t = context.pluriTokens; + final theme = Theme.of(context); + + return Scaffold( + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 48, 24, 28), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Image.asset( + 'assets/icons/pluriwave_app_mark.png', + width: 76, + height: 76, + errorBuilder: + (_, __, ___) => Icon( + Icons.graphic_eq_rounded, + size: 76, + color: t.electricMagenta, + ), + ), + const SizedBox(height: 22), + Text( + l10n.welcomeHeadline, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.w800, + height: 1.05, + letterSpacing: -1.0, + ), + ), + const SizedBox(height: 12), + Text( + l10n.welcomeBody, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.72), + height: 1.5, + ), + ), + const SizedBox(height: 28), + FilaCaracteristicaBienvenida( + icon: Icons.equalizer_rounded, + iconColor: t.electricMagenta, + titulo: l10n.welcomeBullet1Title, + subtitulo: l10n.welcomeBullet1Subtitle, + ), + const SizedBox(height: 16), + FilaCaracteristicaBienvenida( + icon: Icons.directions_car_rounded, + iconColor: t.liveGreen, + titulo: l10n.welcomeBullet2Title, + subtitulo: l10n.welcomeBullet2Subtitle, + ), + const SizedBox(height: 16), + FilaCaracteristicaBienvenida( + icon: Icons.alarm_rounded, + iconColor: t.warmCoral, + titulo: l10n.welcomeBullet3Title, + subtitulo: l10n.welcomeBullet3Subtitle, + ), + const SizedBox(height: 32), + SizedBox( + height: 58, + child: FilledButton( + onPressed: () => _empezar(context), + child: Text( + l10n.welcomeCtaLabel, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _empezar(BuildContext context) { + context.read().irA(RaizPluriWave.escuchar); + Navigator.of(context).pop(); + } +} + +/// One icon-badge + title/subtitle row. Public (not `_FilaCaracteristica`) +/// so the structural "exactly 3 feature bullets" guard in +/// `pantalla_bienvenida_test.dart` can target it via `find.byType` — the +/// same reason WU4 made `FormularioEmisoraPersonalizada` public. +class FilaCaracteristicaBienvenida extends StatelessWidget { + const FilaCaracteristicaBienvenida({ + super.key, + required this.icon, + required this.iconColor, + required this.titulo, + required this.subtitulo, + }); + + final IconData icon; + final Color iconColor; + final String titulo; + final String subtitulo; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: iconColor.withValues(alpha: 0.16), + borderRadius: BorderRadius.circular(11), + ), + child: Icon(icon, size: 20, color: iconColor), + ), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titulo, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + Text( + subtitulo, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.58), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/openspec/changes/rediseno-funcional/tasks.md b/openspec/changes/rediseno-funcional/tasks.md index f47ca05..feabc10 100644 --- a/openspec/changes/rediseno-funcional/tasks.md +++ b/openspec/changes/rediseno-funcional/tasks.md @@ -56,7 +56,7 @@ | 15 | `feat(grabaciones): add recordings library screen` | 3b | ~~300-400~~ → **REALIZED: 1,767** (1,767+ / 0-, 22 files) | Medium | Monitor§ | | 15b | `fix(grabaciones): wire the recordings library into Settings navigation` | 15 | 60-100 | Low | No | | 16 | `feat(connectivity): restyle offline and reconnect banners` | 1 | 150-250 | Low | No | -| 17 | `feat(bienvenida): add monetization-free welcome screen` | 1 | 150-200 | Low | No | +| 17 | `feat(bienvenida): add monetization-free welcome screen` | 1 | ~~150-200~~ → **REALIZED: 361** (screen + test, 313 / ARB 22 / docs 48) — see the WU15-precedent note below | Low | No — same reasoning as WU15§ | | 18 | `feat(i18n): add redesign strings and translate Escuchar rename to 11 locales` | all | ~0 eng. / 400-600 data | Medium (data volume, low logic risk) | No | \* **Re-derived after WU3a landed.** The original 500-700 figure was scaled pro-rata from WU3a's *estimate*, which @@ -85,6 +85,11 @@ unlike WU3a/WU3b's move-only reuse), or (c) the new `servicio_grabacion_radio.da group. Realized 1,767 changed lines / 22 files, all additions (no deletions — nothing pre-existing was touched beyond the 3 new `ServicioGrabacionRadio`/`EstadoGrabacion` methods). Not recorded as `size:exception` since the commit is still a single, cleanly-scoped deliverable (one new screen, its one dependency, no split candidate). +**WU17 hit the identical pattern**: 150-200 covered only the screen itself; realized 361 lines across the screen +(169) + its test (144) + 9 new ARB keys × 2 locales (22) + this docs update (48), PLUS 13 regenerated +`lib/l10n/gen/*.dart` files (~430 more lines, untouched by hand) for a raw `git diff --stat` total of 814. Same +ruling: not `size:exception` — one new, cleanly-scoped screen with no split candidate, all-additions except the ARB +files (which gain lines, not churn). ```text Decision needed before apply: No @@ -1013,19 +1018,45 @@ real capability living in the header, not decorative chrome). **Verify**: `flutter test test/pantallas/pantalla_bienvenida_test.dart && flutter analyze && dart format --set-exit-if-changed $(git diff --name-only --diff-filter=ACM HEAD -- '*.dart')` **New tests**: `pantalla_bienvenida_test.dart` -- [ ] 17.1 RED — the welcome screen renders as a full-screen route (not a modal `Dialog`) with a logo, headline, - body copy, exactly 3 feature bullets, and exactly 1 primary CTA. -- [ ] 17.2 RED — binding no-monetization guard: scan the rendered widget tree's text content and assert it contains +- [x] 17.1 RED — the welcome screen renders as a full-screen route (not a modal `Dialog`) with a logo, headline, + body copy, exactly 3 feature bullets, and exactly 1 primary CTA. **Discovered at apply time**: a pre-existing, + UNRELATED `PluriOnboardingDialog` (a "what's new"/help-content modal loaded from markdown assets, shown from + `app.dart`'s launch flow) already exists — it is NOT the mockup's welcome screen and is not touched or + replaced by this WU. +- [x] 17.2 RED — binding no-monetization guard: scan the rendered widget tree's text content and assert it contains none of: the substring "PRO", a currency amount, or a day-count trial phrase; no secondary "free version" link. -- [ ] 17.3 RED — tapping "Empezar a escuchar" lands on the Escuchar tab (index 0) with the welcome route removed - from the back stack. -- [ ] 17.4 GREEN — build `lib/pantallas/pantalla_bienvenida.dart` as a full-screen route; content only (logo / - headline / body / 3 bullets / 1 CTA) — no PRO pill, no trial duration, no price, no secondary link. -- [ ] 17.5 GREEN — wire the CTA to dismiss-and-navigate to Escuchar with the route removed. -- [ ] 17.6 REFACTOR — grep `lib/l10n/app_*.arb` for "PRO"/price/day-count trial strings introduced by this WU; - confirm none exist. -- [ ] 17.7 Verify — all 3 scenario tests green; grep scan clean. +- [x] 17.3 RED — tapping "Empezar a escuchar" lands on the Escuchar tab (index 0) with the welcome route removed + from the back stack. Test starts `EstadoNavegacionRaiz` on a DIFFERENT tab first so a pass actually proves + `irA()` ran, not just that escuchar was already the default. +- [x] 17.4 GREEN — built `lib/pantallas/pantalla_bienvenida.dart` as a full-screen route (bare `Scaffold`, no + `PluriPushScaffold` — this is an entry screen with no back affordance, not a second-level pushed screen); + content only (logo / headline / body / 3 bullets / 1 CTA) — no PRO pill, no trial duration, no price, no + secondary link. Content is the mockup's screen 14 copy verbatim MINUS its entire monetization block (the "14 + días PRO gratis / luego 2,99 €/año" pricing card and the "Seguir con la versión gratuita" link), Spanish + re-cast to the app's established voseo register (the raw mockup HTML uses "tú" form; the other ~750 lines of + `app_es.arb` consistently use voseo — e.g. "Elegí", "Guardá", "querés" — matched that instead of copying the + mockup's grammar verbatim) and "auto" instead of the mockup's "coche" (matching the one existing precedent at + `localMusicSectionDescription`). The 3 bullet icon colours reuse existing named tokens + (`electricMagenta`/`liveGreen`/`warmCoral`) that happen to be the exact hex values the mockup already used for + those 3 icons — no new raw `Color(0x...)` literal introduced. `SingleChildScrollView` used instead of a + `Column` + `Spacer()` (the exact overflow hazard WU14 already hit and fixed) since this screen carries + meaningfully more content than a typical card. 9 new ARB keys, en/es only (`welcomeHeadline`, `welcomeBody`, + `welcomeBullet{1,2,3}{Title,Subtitle}`, `welcomeCtaLabel`). +- [x] 17.5 GREEN — CTA wired to `context.read().irA(RaizPluriWave.escuchar)` then + `Navigator.of(context).pop()`. **Scope note**: this WU's own verify command and task list only cover + `pantalla_bienvenida_test.dart` — wiring the screen into the REAL first-launch flow (i.e. `app.dart`/ + `main.dart` deciding *when* to push it, analogous to `ServicioContenidoApp.debeMostrarInicio()`'s pattern for + `PluriOnboardingDialog`) is NOT part of WU17 as scoped and was deliberately not added here to avoid inventing + untested persistence/launch semantics. Flagged as a likely WU17b gap (same shape as WU15/WU15b). +- [x] 17.6 REFACTOR — whole-repo grep confirmed clean: `\bPRO\b` (word-boundary) has exactly one hit in all of + `lib/` — this WU's own doc comment describing the guard (never rendered). The unqualified substring "PRO" + also matches pre-existing, unrelated `"PROGRAMADOS"` (vacation-alarms section title, WU9) and + `pluri_premium_widgets.dart` (a pre-existing decorative-hero-card widget file name, "premium" as in visual + styling, not a paid tier) — both false positives for a naive substring check, neither a monetization string. + No day-count trial phrase, no €/$ amount, no "gratuita"/"gratis"/"free version" string anywhere in + `lib/l10n/app_*.arb` or `lib/`. +- [x] 17.7 Verify — all 3 scenario tests green; grep scan clean. ## WU18 — i18n batch (all 13 locales) diff --git a/test/pantallas/pantalla_bienvenida_test.dart b/test/pantallas/pantalla_bienvenida_test.dart new file mode 100644 index 0000000..e005310 --- /dev/null +++ b/test/pantallas/pantalla_bienvenida_test.dart @@ -0,0 +1,144 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pluriwave/estado/estado_navegacion.dart'; +import 'package:pluriwave/l10n/gen/app_localizations.dart'; +import 'package:pluriwave/pantallas/pantalla_bienvenida.dart'; +import 'package:provider/provider.dart'; + +/// WU17: `onboarding-welcome` spec — full-screen route (not a modal), +/// content only (logo/headline/body/3 bullets/1 CTA), binding +/// no-monetization guard, and CTA dismisses to Escuchar with the route +/// popped. +Widget _app({required EstadoNavegacionRaiz navegacion}) { + return ChangeNotifierProvider.value( + value: navegacion, + child: const MaterialApp( + locale: Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: PantallaBienvenida(), + ), + ); +} + +void main() { + testWidgets( + 'renders as a full-screen route with a logo, headline, body copy, ' + 'exactly 3 feature bullets, and exactly one primary CTA', + (tester) async { + final navegacion = EstadoNavegacionRaiz(); + addTearDown(navegacion.dispose); + + await tester.pumpWidget(_app(navegacion: navegacion)); + await tester.pumpAndSettle(); + + // A full-screen route, never a modal dialog. + expect(find.byType(Dialog), findsNothing); + expect(find.byType(AlertDialog), findsNothing); + expect(find.byType(Scaffold), findsOneWidget); + + expect(find.byType(Image), findsOneWidget, reason: 'the logo mark'); + expect(find.text('Tu mundo, en directo'), findsOneWidget); + expect( + find.textContaining('53.412 emisoras de 238 países'), + findsOneWidget, + ); + expect( + find.byType(FilaCaracteristicaBienvenida), + findsNWidgets(3), + reason: 'exactly 3 feature bullets', + ); + expect( + find.byType(FilledButton), + findsOneWidget, + reason: 'exactly 1 primary CTA', + ); + expect(find.text('Empezar a escuchar'), findsOneWidget); + }, + ); + + testWidgets('rendered content contains no monetization strings', ( + tester, + ) async { + final navegacion = EstadoNavegacionRaiz(); + addTearDown(navegacion.dispose); + + await tester.pumpWidget(_app(navegacion: navegacion)); + await tester.pumpAndSettle(); + + final textoCompleto = tester + .widgetList(find.byType(Text)) + .map((widget) => widget.data ?? '') + .join('\n'); + + expect(textoCompleto.contains('PRO'), isFalse); + expect(textoCompleto.contains('€'), isFalse); + expect(textoCompleto.contains(r'$'), isFalse); + expect( + RegExp(r'\d+\s*d[ií]as?\b', caseSensitive: false).hasMatch( + textoCompleto, + ), + isFalse, + reason: 'no day-count trial phrase (e.g. "14 días")', + ); + expect( + find.textContaining('gratuita', findRichText: true), + findsNothing, + reason: 'no secondary "free version" link', + ); + expect( + find.textContaining('gratis', findRichText: true), + findsNothing, + reason: 'no secondary "free version" link', + ); + // Only one tappable action on the whole screen: the primary CTA. + expect(find.byType(TextButton), findsNothing); + expect(find.byType(FilledButton), findsOneWidget); + }); + + testWidgets( + 'tapping the primary CTA switches to Escuchar and pops the welcome route', + (tester) async { + // Start on a DIFFERENT tab so a pass actually proves irA() ran, + // rather than coasting on EstadoNavegacionRaiz's own escuchar default. + final navegacion = EstadoNavegacionRaiz() + ..irA(RaizPluriWave.ajustes); + addTearDown(navegacion.dispose); + final navigatorKey = GlobalKey(); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: navegacion, + child: MaterialApp( + navigatorKey: navigatorKey, + locale: const Locale('es'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const Scaffold(body: Center(child: Text('home stand-in'))), + ), + ), + ); + + unawaited( + navigatorKey.currentState!.push( + MaterialPageRoute(builder: (_) => const PantallaBienvenida()), + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(PantallaBienvenida), findsOneWidget); + + await tester.tap(find.text('Empezar a escuchar')); + await tester.pumpAndSettle(); + + expect(navegacion.actual, RaizPluriWave.escuchar); + expect( + find.byType(PantallaBienvenida), + findsNothing, + reason: 'the welcome route was removed from the back stack', + ); + expect(find.text('home stand-in'), findsOneWidget); + }, + ); +}