- create auth, main shell, home, profile, notifications and settings modules.

- added navigation, utils, design system and shared packages
- implemented go router in entiered app
- implemented flutter riverpod instead provider
This commit is contained in:
AlcalaJulian
2025-11-13 15:16:00 +01:00
parent 75beafd771
commit 5ca37d2822
332 changed files with 7759 additions and 3452 deletions

31
modules/auth/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/

10
modules/auth/.metadata Normal file
View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: package

View File

@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
modules/auth/LICENSE Normal file
View File

@@ -0,0 +1 @@
TODO: Add your license here.

39
modules/auth/README.md Normal file
View File

@@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View File

@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,6 @@
export 'src/device_sign_up/link_watch/create_profile_screen.dart';
export 'src/onboarding/onboarding_builder.dart';
export 'src/login/link_phone_builder.dart';
export 'src/login/phone_code_builder.dart';
export 'src/login/login_builder.dart';
export 'src/recover_password/recover_password_builder.dart';

View File

@@ -0,0 +1,57 @@
import 'package:auth/src/device_sign_up/link_watch/create_profile_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// import 'package:sf_app_platform/payments/view/screens/core/dashboard_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/link_watch/create_profile_screen.dart';
class AddKidScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Column(
spacing: 15,
children: [
Spacer(flex: 6),
Text("Añade a tu peque"),
Text(
"Controla su gasto a la vez que aprende hábitos financieros responsables",
),
Container(
margin: EdgeInsets.symmetric(vertical: 30, horizontal: 50),
child: Row(
children: [
Column(children: [Text("1"), Text("2"), Text("3")]),
Column(
children: [
Text("Crea su perfil"),
Text("Vincula su correa y su reloj"),
Text("Carga su hucha"),
],
),
],
),
),
Text("¡Y todo listo para que tenga su dinero!"),
Text("Recuerda que necesitas tener un Plan SaveFamily"),
Text(
"Si aún no lo tienes, puedes conseguirlo a través de nuestra web",
),
Spacer(flex: 8),
Container(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => CreateProfileScreen()),
),
child: Text("¡Empezar!"),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
class ContactScreen extends StatelessWidget {
const ContactScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 10,
children: [
Text(
"Contáctanos",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
Text(
"Trasládanos tus dudas e intentaremos responderte lo antes posible",
),
DropdownMenu(
initialSelection: "es",
label: Text("País"),
dropdownMenuEntries: [
DropdownMenuEntry(value: "es", label: "España"),
DropdownMenuEntry(value: "fr", label: "Francia"),
DropdownMenuEntry(value: "pt", label: "Portugal"),
],
),
DropdownMenu(
initialSelection: "online",
label: Text("Canal de compra"),
dropdownMenuEntries: [
DropdownMenuEntry(value: "online", label: "SF online shop"),
],
),
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: "Nombre",
hintText: "Nombre y apellidos",
border: OutlineInputBorder(),
),
),
),
Expanded(
child: TextField(
decoration: InputDecoration(
labelText: "Correo electrónico",
hintText: "Correo electrónico",
border: OutlineInputBorder(),
),
),
),
Expanded(
child: TextField(
minLines: 3,
maxLines: 3,
decoration: InputDecoration(
labelText: "Asunto del mensaje",
hintText: "Escribe tu mensaje",
border: OutlineInputBorder(),
),
),
),
Expanded(
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: Text("Enviar"),
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:auth/src/device_sign_up/add_kid_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class AccountCreatedKidScreen extends ConsumerWidget {
const AccountCreatedKidScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
final model = "SaveWatch Plus 2";
final id = "1106652524";
final fullName = "Carlos Pérez Cruz";
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 20,
children: [
Spacer(flex: 2),
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
size: 50,
),
Text(
"Cuenta creada",
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
Text.rich(
TextSpan(
text: "Has creado la cuenta para:\n",
children: [
TextSpan(
text: fullName,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text("Reloj: $model"),
Text("ID del reloj: $id"),
Text(
"Ya puedes darle su primera paga paa que empiece a disfrutarla en su reloj",
style: TextStyle(fontWeight: FontWeight.bold),
),
Spacer(flex: 6),
Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(20),
topLeft: Radius.circular(20),
),
),
child: Column(
children: [
Expanded(
child: FilledButton(
onPressed: () => {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => AddKidScreen()),
),
},
child: Text("Dale su primera paga"),
),
),
TextButton(
onPressed: () => {},
child: Text("Añadir otro peque"),
),
],
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,304 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
class CreateProfileScreen extends ConsumerWidget {
CreateProfileScreen({super.key});
int currentStep = 0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 10,
children: [
Stepper(
type: StepperType.horizontal,
currentStep: currentStep,
onStepCancel: () => currentStep == 0,
// ? null
// :
// setState(() {
// currentStep -= 1;
// }),
controlsBuilder:
(BuildContext context, ControlsDetails controls) {
return FilledButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonPrimary),
),
),
onPressed: controls.onStepContinue,
child: const Text('Continuar'),
);
},
steps: [
Step(
state: currentStep > 0
? StepState.complete
: StepState.indexed,
isActive: currentStep >= 0,
stepStyle: currentStep >= 0
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.transparent,
boxShadow: BoxShadow(spreadRadius: 5),
indexStyle: TextStyle(color: Colors.transparent),
),
title: Text(""),
content: Column(
spacing: 10,
children: [
Text(
"Crea su perfil",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Text(
"Necesitamos estos datos para crear su cuenta y gestionar sus pagas y gastos",
),
Text(
"Comienza con un peque; luego podrás agregar más",
style: TextStyle(fontWeight: FontWeight.bold),
),
TextField(
decoration: InputDecoration(
labelText: "Nombre",
hintText: "Nombre",
border: OutlineInputBorder(),
),
),
TextField(
decoration: InputDecoration(
labelText: "Apellidos",
hintText: "Apellidos",
border: OutlineInputBorder(),
),
),
Row(
spacing: 10,
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
label: Text("Fecha de nacimiento"),
hintText: "DD",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "MM",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
),
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "AAAA",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
),
],
),
TextField(
decoration: InputDecoration(
labelText: "Dirección completa",
hintText: "Nombre de la calle",
border: OutlineInputBorder(),
),
),
TextButton(
onPressed: () => {},
child: Text(
"Cambiar dirección",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
Step(
state: currentStep > 1
? StepState.complete
: StepState.indexed,
isActive: currentStep >= 1,
stepStyle: currentStep >= 1
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.transparent,
boxShadow: BoxShadow(spreadRadius: 5),
indexStyle: TextStyle(color: Colors.transparent),
),
title: Text(""),
content: Column(
spacing: 10,
children: [
Text(
"Vincula su correa y su reloj",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
SvgPicture.asset("assets/images/ui/formulario.svg"),
Row(
spacing: 10,
children: [
Text("1"),
Column(
children: [
Text("Escanea la correa"),
Text("El peque podrá realizar pagos"),
],
),
],
),
Row(
spacing: 10,
children: [
Text("2"),
Column(
children: [
Text("Escanea el reloj"),
Text("Visualizarás los gastos que se hagan"),
],
),
],
),
],
),
),
Step(
state: currentStep > 2
? StepState.complete
: StepState.indexed,
isActive: currentStep >= 2,
stepStyle: currentStep >= 2
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.transparent,
boxShadow: BoxShadow(spreadRadius: 5),
indexStyle: TextStyle(color: Colors.transparent),
),
title: Text(""),
content: Column(
spacing: 10,
children: [
Text(
"¡Dale su primera paga!",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
),
),
Text(
"Enséñales a gestionar su dinero recargando su reloj",
),
TextField(
decoration: InputDecoration(
labelText: "Cantidad de dinero de la paga",
hintText: "0€",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
Text("Cantidad mínima: 10€"),
Text(
"Por seguridad sólo se puede disponer de un máximo de 150€ por wallet",
),
Text("Método de ingreso"),
Row(
spacing: 20,
children: [
OutlinedButton(
onPressed: () => {},
child: SvgPicture.asset(
"assets/images/ui/visa.svg",
),
),
OutlinedButton(
onPressed: () => {},
child: SvgPicture.asset(
"assets/images/ui/paypal.svg",
),
),
OutlinedButton(
onPressed: () => {},
child: Row(
children: [
SvgPicture.asset(
"assets/images/ui/banco.svg",
),
Text("Transferencia"),
],
),
),
],
),
Row(
children: [
Icon(Icons.lock_outline),
Text(
"EL pago en esta app es seguro y cumple la normativa europea. Sólo se usará el dinero que decidas para las huchas de tus hijos.",
),
],
),
],
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/login/presentation/link_phone_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class LinkPhoneBuilder {
const LinkPhoneBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: LinkPhoneScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/login/presentation/login_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class LoginBuilder {
const LoginBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: LoginScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/login/presentation/phone_code_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class PhoneCodeBuilder {
const PhoneCodeBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: PhoneCodeScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LinkPhoneScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const LinkPhoneScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
TextEditingController phoneController = TextEditingController();
// String? phone;
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Expanded(
child: Center(
child: Column(
spacing: 10,
children: [
Text(
"¡Nos alegra mucho tenerte por aquí!",
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
Text(
"Para poder entrar de forma segura, te vamos a enviar un código al teléfono",
),
Row(
spacing: 10,
children: [
DropdownMenu(
initialSelection: "es",
dropdownMenuEntries: List<DropdownMenuEntry>.generate(3, (
int index,
) {
return DropdownMenuEntry(
labelWidget: Icon(Icons.outlined_flag),
label: "es",
value: "es",
);
}),
),
Expanded(
child: TextField(
onSubmitted: (String value) {
// phone = value;
},
controller: phoneController,
decoration: InputDecoration(
labelText: "Teléfono móvil",
hintText: "Teléfono",
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
),
],
),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => navigationContract.pushTo('/phone_code'),
child: Text("Siguiente"),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
// import 'package:flutter_svg/flutter_svg.dart';
class LoadingGoogleScreen extends StatelessWidget {
const LoadingGoogleScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(
child: Column(
spacing: 50,
children: [
Spacer(flex: 8),
Text(
"Continuar con Google",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
CircularProgressIndicator(),
Text("Redirigiendo a Google"),
Spacer(flex: 10),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class LoadingScreen extends StatelessWidget{
const LoadingScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(
child: Column(
spacing: 50,
children: [
Spacer(flex: 8),
SvgPicture.asset("assets/images/ui/logo_sf.svg"),
CircularProgressIndicator(),
Spacer(flex: 10)
],
),
)
),
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:auth/src/login/presentation/loading_google_screen.dart';
import 'package:auth/src/recover_password/presentation/restore_password_screen.dart';
import 'package:auth/src/sign_up/signup_screen.dart';
import 'package:dashboard_shell/dashboard_shell.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
// import 'package:sf_app_platform/payments/view/screens/core/dashboard_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/loading_google_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/restore_password/restore_password_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/signup/signup_screen.dart';
class LoginScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const LoginScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
bool passwordVisible = true;
return Scaffold(
body: Expanded(
child: Center(
child: Container(
margin: EdgeInsets.all(30),
child: Column(
spacing: 10,
children: [
Icon(Icons.check, color: Color(0xFF329e95), size: 50),
Text(
"¡Te damos la bienvenida!",
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
TextField(
decoration: InputDecoration(
hintText: "Nombre de usuario",
labelText: "Nombre de usuario",
border: OutlineInputBorder(),
),
),
TextField(
obscureText: passwordVisible,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Contraseña",
hintText: "********",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
passwordVisible
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
// setState(() {
// passwordVisible = !passwordVisible;
// });
},
),
),
),
TextButton(
onPressed: () =>
navigationContract.pushTo('/recover_password'),
child: Text("¿Has olvidado la contraseña?"),
),
FilledButton(
onPressed: () =>
navigationContract.pushTo('/dashboard_shell'),
child: Text("Iniciar sesión"),
),
Stack(children: [Divider(), Text("o continúa con")]),
Row(
spacing: 20,
children: [
OutlinedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => LoadingGoogleScreen(),
),
),
child: Text("Google", semanticsLabel: "Google"),
),
OutlinedButton(
onPressed: () => {},
child: Icon(Icons.apple, semanticLabel: "Apple"),
),
],
),
Text("¿No tienes cuenta?"),
TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => SignupScreen()),
),
child: Text("Crear una ahora"),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'package:auth/src/login/presentation/login_screen.dart';
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:sf_app_platform/payments/view/screens/core/dashboard_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/login_screen.dart';
class PhoneCodeScreen extends ConsumerWidget {
// final String phone;
final NavigationContract navigationContract;
PhoneCodeScreen({super.key, required this.navigationContract});
// const PhoneCodeScreen({super.key, required this.phone});
// class PhoneCodeScreenState extends State<PhoneCodeScreen> {
final focusNodes = List<FocusNode>.generate(6, (int i) {
return FocusNode();
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Expanded(
child: Center(
child: Column(
spacing: 15,
children: [
Spacer(flex: 8),
Text(
"Conéctate",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
Text.rich(
TextSpan(
text: "Hemos enviado el código al ",
children: [
TextSpan(
// text: widget.phone,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text("Introduce el código aquí"),
Row(
spacing: 20,
children: List<Widget>.generate(6, (int i) {
return Expanded(
child: TextField(
focusNode: focusNodes[i],
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "0",
counterText: "",
border: OutlineInputBorder(),
),
maxLength: 1,
onChanged: (String value) => {
value != ""
? focusNodes[i + 1].requestFocus()
: focusNodes[i - 1].requestFocus(),
},
),
);
}),
),
FilledButton(
onPressed: () => {navigationContract.pushTo('/login')},
child: Text("Entrar"),
),
Text("¿No lo has recibido?"),
TextButton(
onPressed: () => {},
child: Text(
"Volver a intentarlo",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Spacer(flex: 10),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/onboarding/presentation/welcome_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class OnboardingBuilder {
const OnboardingBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: WelcomeScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:navigation/navigation.dart';
// import 'package:sf_app_platform/payments/view/screens/link_phone_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/signup/signup_screen.dart';
// import '../../../../../apps/mobile_app/lib/payments/view/screens/core/dashboard_screen.dart';
class WelcomeScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const WelcomeScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: Column(
children: [
Spacer(),
Expanded(
child: CarouselView(
scrollDirection: Axis.horizontal,
itemExtent: double.infinity,
itemSnapping: true,
shrinkExtent: 400,
children: generateSteps(),
),
),
FilledButton(
onPressed: () => navigationContract.goTo('/link_phone'),
child: const Text('Continuar'),
),
Spacer(),
],
),
),
);
}
void jumpToNext(BuildContext context) {
// Navigator.pushReplacement(
// context,
// MaterialPageRoute(builder: (_) => LinkPhoneScreen()),
// );
return;
}
List<Widget> generateSteps() {
return [
Column(
spacing: 30,
children: [
SvgPicture.asset("assets/images/ui/bienvenida_paso1.svg"),
Text(
"Aprende a gestionar su dinero",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
Text("Tu peque crea hábitos y se divierte mientras lo hace"),
],
),
Column(
children: [
SvgPicture.asset("assets/images/ui/bienvenida_paso2.svg"),
Text("Tranquilidad en cada pago que hacen"),
Text("Supervisa gastos, fija límites y acompáñalos en cada paso"),
],
),
Column(
children: [
SvgPicture.asset("assets/images/ui/bienvenida_paso3.svg"),
Text("Pagos fáciles y seguros en sus manos"),
Text("Podrá pagar desde su reloj.\n Sin móvil ni efectivo"),
],
),
];
}
}

View File

@@ -0,0 +1,92 @@
import 'package:auth/src/recover_password/presentation/new_password_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// import 'package:sf_app_platform/payments/domain/ports/theme_port.dart';
// import 'package:sf_app_platform/payments/view/screens/restore_password/new_password_screen.dart';
class EmailSentScreen extends StatefulWidget {
final String email;
const EmailSentScreen({super.key, required this.email});
@override
State<StatefulWidget> createState() => EmailSentScreenState();
}
class EmailSentScreenState extends State<EmailSentScreen> {
@override
Widget build(BuildContext context) {
final theme = context.read<ThemePort>();
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 20,
children: [
Spacer(flex: 8),
Text(
"Recuperar contraseña",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
Spacer(flex: 1),
Row(
spacing: 10,
children: [
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
),
Text(
"Correo enviado correctamente",
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
Spacer(flex: 1),
Text(
"Revisa tu email y haz clic en el enlace para crear una nueva contraseña",
),
Text(
"Si no recibes el correo en unos minutos, revisa tu carpeta de spam o pulsa \"Reenviar correo\"",
),
Row(
spacing: 10,
children: [
Expanded(
child: OutlinedButton(
onPressed: () => {},
child: Text("Reenviar correo"),
),
),
Expanded(
child: FilledButton(
onPressed: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => NewPasswordScreen(),
),
),
},
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonSecondary),
),
),
child: Text("Continuar"),
),
),
],
),
Spacer(flex: 10),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,191 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// import '../../../domain/ports/theme_port.dart';
class NewPasswordScreen extends StatefulWidget {
const NewPasswordScreen({super.key});
@override
State<StatefulWidget> createState() => NewPasswordScreenState();
}
class NewPasswordScreenState extends State<NewPasswordScreen> {
bool passwordVisible = false;
bool equalPasswords = false;
String password = "";
var securityChecks = {
"min": false,
"capital": false,
"number": false,
"special": false,
};
@override
void initState() {
passwordVisible = false;
equalPasswords = false;
String password = "";
securityChecks = {
"min": false,
"capital": false,
"number": false,
"special": false,
};
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = context.read<ThemePort>();
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 10,
children: [
Spacer(flex: 4),
Text(
"Recuperar contraseña",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
TextField(
obscureText: passwordVisible,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Nueva contraseña",
hintText: "********",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
passwordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
onChanged: (value) => {
setState(() {
password = value;
securityChecks = checkSecurity(value);
}),
},
),
TextField(
obscureText: passwordVisible,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Repetir contraseña",
hintText: "********",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
passwordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
),
onChanged: (value) => {
setState(() {
equalPasswords = password == value;
}),
},
),
Row(
children: [
securityChecks["min"]!
? Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
)
: Icon(
Icons.cancel_outlined,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
Text("Al menos 8 caracteres"),
],
),
Row(
children: [
securityChecks["capital"]!
? Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
)
: Icon(
Icons.cancel_outlined,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
Text("Una mayúscula"),
],
),
Row(
children: [
securityChecks["number"]!
? Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
)
: Icon(
Icons.cancel_outlined,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
Text("Un número"),
],
),
Row(
children: [
securityChecks["special"]!
? Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.buttonPrimary),
)
: Icon(
Icons.cancel_outlined,
color: theme.getColorFor(ThemeCode.buttonSecondary),
),
Text("Un carácter especial"),
],
),
Spacer(flex: 1),
FilledButton(
onPressed: () => {},
child: Container(
width: double.infinity,
padding: EdgeInsets.all(20),
child: Text("Aceptar"),
),
),
Spacer(flex: 4),
],
),
),
),
);
}
//TODO: Extraer de la vista
Map<String, bool> checkSecurity(String value) {
Map<String, bool> checks = {};
checks["min"] = value.length >= 8;
checks["capital"] = RegExp(r'[A-Z]').hasMatch(value);
checks["number"] = RegExp(r'[0-9]').hasMatch(value);
checks["special"] = RegExp(r'[^A-Za-z0-9]').hasMatch(value);
return checks;
}
}

View File

@@ -0,0 +1,80 @@
import 'package:auth/src/recover_password/presentation/email_sent_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:navigation/navigation.dart';
// import 'package:provider/provider.dart';
import 'package:navigation/navigation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import 'package:sf_app_platform/payments/domain/ports/theme_port.dart';
// import 'package:sf_app_platform/payments/view/screens/restore_password/email_sent_screen.dart';
class RestorePasswordScreen extends ConsumerWidget {
final NavigationContract navigationContract;
const RestorePasswordScreen({super.key, required this.navigationContract});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ThemePort theme = context.read<ThemePort>();
final theme = ref.watch(themePortProvider);
return Scaffold(
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 30,
children: [
Spacer(flex: 8),
Text(
"Recuperar contaseña",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30),
),
Text(
"Introduce tu email para enviarte un enlace de recuperación",
),
TextField(
decoration: InputDecoration(
labelText: "Correo electrónico",
hintText: "Correo electrónico",
border: OutlineInputBorder(),
),
),
Row(
spacing: 20,
children: [
Expanded(
child: OutlinedButton(
onPressed: () => {Navigator.pop(context)},
child: Text("Volver"),
),
),
Expanded(
child: FilledButton(
onPressed: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => EmailSentScreen(email: ""),
),
),
},
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonSecondary),
),
),
child: Text("Enviar"),
),
),
],
),
Spacer(flex: 10),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:auth/src/recover_password/presentation/restore_password_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get_it/get_it.dart';
import 'package:navigation/navigation.dart';
class RecoverPasswordBuilder {
const RecoverPasswordBuilder();
Page<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: RestorePasswordScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:auth/src/device_sign_up/add_kid_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// import 'package:sf_app_platform/payments/view/screens/add_kid_screen.dart';
// import 'package:sf_app_platform/payments/view/screens/core/dashboard_screen.dart';
// import '../../../../../apps/mobile_app/lib/payments/domain/ports/theme_port.dart';
class AccountCreatedScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
ThemePort theme = context.read<ThemePort>();
final email = "usuario@example.com";
final fullName = "Carlos Pérez Cruz";
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(30),
child: Center(
child: Column(
spacing: 20,
children: [
Spacer(flex: 10),
Icon(
Icons.check,
color: theme.getColorFor(ThemeCode.backgroundPrimary),
size: 50,
),
Text(
"Cuenta creada",
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
Text.rich(
TextSpan(
text: "Has creado la cuenta para:\n",
children: [
TextSpan(
text: fullName,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text.rich(
TextSpan(
text: "Hemos enviado un email de verificación a:\n",
children: [
TextSpan(
text: email,
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Text(
"Crea la cuenta de tu peque e ingresa su \nprimera paga para utilizarla con su reloj",
),
FilledButton(
onPressed: () => {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => AddKidScreen()),
),
},
child: Text("Continuar"),
),
Spacer(flex: 8),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class SignupAddressScreen extends StatelessWidget{
@override
Widget build(BuildContext context) {
return Column(
spacing: 30,
children: [
Text("Domicilio"),
Text("Tu dirección", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30)),
Text("Tu dirección nos ayuda a verificar y mantener la seguridad de tu cuenta"),
TextField(decoration: InputDecoration(hintText: "Dirección completa", border: OutlineInputBorder())),
TextField(decoration: InputDecoration(hintText: "Ciudad", border: OutlineInputBorder())),
DropdownMenu(
dropdownMenuEntries: List<DropdownMenuEntry>.generate(3, (int index) {
return DropdownMenuEntry(value: "España", label: "España");
}),
hintText: "País",
width: double.infinity,
),
TextField(decoration: InputDecoration(hintText: "Nacionalidad", border: OutlineInputBorder()))
],
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SignupPersonalScreen extends StatelessWidget{
const SignupPersonalScreen({super.key});
@override
Widget build(BuildContext context) {
return Column(
spacing: 30,
children: [
Text("Datos personales"),
Text("Identifícate", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30)),
Text("Nos aseguraremos de que la cuenta está a nombre del adulto responsable"),
TextField(decoration: InputDecoration(labelText: "Nombre", hintText: "Nombre", border: OutlineInputBorder())),
TextField(decoration: InputDecoration(labelText: "Apellidos", hintText: "Apellidos", border: OutlineInputBorder())),
Row(
children: [
Expanded( child: TextField(
decoration: InputDecoration(label: Text("Fecha de nacimiento"), hintText: "DD", border: OutlineInputBorder()),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)),
Expanded( child: TextField(
decoration: InputDecoration(hintText: "MM", border: OutlineInputBorder()),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)),
Expanded( child: TextField(
decoration: InputDecoration(hintText: "AAAA", border: OutlineInputBorder()),
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
)),
],
),
DropdownMenu(
width: double.infinity,
label: Text("¿Qué familiar eres?"),
dropdownMenuEntries: [
DropdownMenuEntry(label: "Padre", value: "Padre"),
DropdownMenuEntry(label: "Madre", value: "Madre"),
DropdownMenuEntry(label: "Tutor", value: "Tutor"),
],
),
],
);
}
}

View File

@@ -0,0 +1,140 @@
import 'package:auth/src/sign_up/account_created_screen.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
// import 'package:sf_app_platform/payments/view/screens/account_created_screen.dart';
import 'package:auth/src/sign_up/signup_address_screen.dart';
import 'package:auth/src/sign_up/signup_personal_screen.dart';
import 'package:auth/src/sign_up/signup_user_screen.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// import '../../../../../apps/mobile_app/lib/payments/domain/ports/theme_port.dart';
class SignupScreen extends ConsumerWidget {
SignupScreen({super.key});
int currentStep = 0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: Container(
color: theme.getColorFor(ThemeCode.backgroundPrimary),
padding: const EdgeInsets.all(20),
child: SizedBox(
child: Stepper(
controlsBuilder:
(BuildContext context, ControlsDetails controls) {
return Row(
children: <Widget>[
Expanded(
child: OutlinedButton(
onPressed: controls.onStepCancel,
child: const Text('Atrás'),
),
),
Expanded(
child: FilledButton(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll<Color>(
theme.getColorFor(ThemeCode.buttonSecondary),
),
),
onPressed: controls.onStepContinue,
child: const Text('Siguiente'),
),
),
],
);
},
type: StepperType.horizontal,
currentStep: currentStep,
onStepCancel: () => currentStep == 0,
// ? null
// : setState(() {
// currentStep -= 1;
// }),
onStepContinue: () {
bool isLastStep = (currentStep == getSteps().length - 1);
if (isLastStep) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => AccountCreatedScreen()),
);
} else {
// setState(() {
// currentStep += 1;
// });
}
},
steps: getSteps(),
),
),
),
),
),
);
}
List<Step> getSteps() {
return <Step>[
Step(
state: currentStep > 0 ? StepState.complete : StepState.indexed,
isActive: currentStep >= 0,
stepStyle: currentStep >= 0
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.transparent,
boxShadow: BoxShadow(spreadRadius: 5),
indexStyle: TextStyle(color: Colors.transparent),
),
title: const Text(""),
content: SignupPersonalScreen(),
),
Step(
state: currentStep > 1 ? StepState.complete : StepState.indexed,
isActive: currentStep >= 1,
stepStyle: currentStep >= 1
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.white,
boxShadow: BoxShadow(spreadRadius: 1),
indexStyle: TextStyle(color: Colors.transparent),
),
title: const Text(""),
content: SignupAddressScreen(),
),
Step(
state: currentStep > 2 ? StepState.complete : StepState.indexed,
isActive: currentStep >= 2,
stepStyle: currentStep >= 2
? StepStyle(
connectorThickness: 0,
color: Color(0xFF329e95),
indexStyle: TextStyle(color: Colors.transparent),
)
: StepStyle(
connectorThickness: 0,
color: Colors.white,
boxShadow: BoxShadow(spreadRadius: 1),
indexStyle: TextStyle(color: Colors.transparent),
),
title: const Text(""),
content: SignupUserScreen(),
),
];
}
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
class SignupUserScreen extends StatefulWidget{
const SignupUserScreen({super.key});
@override
State<SignupUserScreen> createState() => SignupUserScreenState();
}
class SignupUserScreenState extends State<SignupUserScreen>{
bool passwordVisible=false;
@override
Widget build(BuildContext context) {
return Column(
spacing: 30,
children: [
Text("Usuario y contacto"),
Text("Crea tu usuario", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 30)),
Text("Con tu email y tu número podremos mantenerte siempre informado"),
TextField(decoration: InputDecoration(labelText: "Correo electrónico", hintText: "Correo electrónico", border: OutlineInputBorder())),
Row(children: [
DropdownMenu(
initialSelection: "es",
dropdownMenuEntries: List<DropdownMenuEntry>.generate(3, (int index){
return DropdownMenuEntry(labelWidget: Icon(Icons.outlined_flag), label: "es", value: "es");
})
),
Expanded(child: TextField(
decoration: InputDecoration(labelText: "Teléfono móvil", hintText: "Teléfono", border: OutlineInputBorder()),
keyboardType: TextInputType.number)
)
]),
TextField(
obscureText: passwordVisible,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Contraseña",
hintText: "********",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(passwordVisible
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
)
),
TextField(obscureText: passwordVisible,
enableSuggestions: false,
autocorrect: false,
decoration: InputDecoration(
labelText: "Repetir contraseña",
hintText: "*******",
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(passwordVisible
? Icons.visibility
: Icons.visibility_off),
onPressed: () {
setState(() {
passwordVisible = !passwordVisible;
});
},
),
)
),
],
);
}
}

69
modules/auth/pubspec.yaml Normal file
View File

@@ -0,0 +1,69 @@
name: auth
# resolution: workspace
description: "A new Flutter package project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1
homepage:
environment:
sdk: ^3.9.2
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
#modules dependencies go here
dashboard_shell:
path: ../../modules/dashboard_shell
#packages dependencies go here
design_system:
path: ../../packages/design_system
navigation:
path: ../../packages/navigation
#dependencies go here
flutter_svg: ^2.2.1
get_it: ^9.0.5
go_router: ^17.0.0
flutter_riverpod: ^3.0.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

View File

@@ -0,0 +1,16 @@
# melos_managed_dependency_overrides: dashboard_shell,design_system,home,notifications,profile,sf_shared,navigation
dependency_overrides:
dashboard_shell:
path: ../dashboard_shell
design_system:
path: ../../packages/design_system
home:
path: ../home
navigation:
path: ../../packages/navigation
notifications:
path: ../notifications
profile:
path: ../profile
sf_shared:
path: ../../packages/sf_shared