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.
This commit is contained in:
+10
-1
@@ -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"
|
||||
}
|
||||
|
||||
+10
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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<EstadoNavegacionRaiz>().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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<EstadoNavegacionRaiz>().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)
|
||||
|
||||
|
||||
@@ -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<EstadoNavegacionRaiz>.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<Text>(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<NavigatorState>();
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<EstadoNavegacionRaiz>.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<void>(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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user