diff --git a/apps/mobile_app/android/app/src/main/AndroidManifest.xml b/apps/mobile_app/android/app/src/main/AndroidManifest.xml index 6f28b015..08efead2 100644 --- a/apps/mobile_app/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile_app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + UIStatusBarHidden + NSCameraUsageDescription + Necesitamos la cámara para escanear códigos QR UIViewControllerBasedStatusBarAppearance diff --git a/apps/mobile_app/lib/main.dart b/apps/mobile_app/lib/main.dart index 3530d545..7c6858a6 100644 --- a/apps/mobile_app/lib/main.dart +++ b/apps/mobile_app/lib/main.dart @@ -7,6 +7,8 @@ import 'package:design_system/design_system.dart'; import 'package:sf_app_platform/config/env/questia_env_config.dart'; import 'package:sf_app_platform/navigation/app_router.dart'; import 'package:navigation/navigation_module.dart'; +import 'package:sf_app_platform/providers/app_state_provider.dart'; +import 'package:sf_app_platform/providers/permissions/permissions_provider.dart'; import 'package:sf_infrastructure/sf_infrastructure.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -23,11 +25,39 @@ Future main() async { runApp(const ProviderScope(child: PlatformApp())); } -class PlatformApp extends ConsumerWidget { +class PlatformApp extends ConsumerStatefulWidget { const PlatformApp({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + PlatformAppState createState() => PlatformAppState(); +} + +class PlatformAppState extends ConsumerState + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + debugPrint('State: $state'); + ref.read(appLifecycleStateProvider.notifier).setState(state); + if (state == AppLifecycleState.resumed) { + ref.read(permissionsProvider.notifier).checkPermissions(); + } + super.didChangeAppLifecycleState(state); + } + + @override + Widget build(BuildContext context) { SizeUtils.init(context: context); return MaterialApp.router( diff --git a/apps/mobile_app/lib/navigation/app_router.dart b/apps/mobile_app/lib/navigation/app_router.dart index 406722d7..dd02c9cc 100644 --- a/apps/mobile_app/lib/navigation/app_router.dart +++ b/apps/mobile_app/lib/navigation/app_router.dart @@ -55,9 +55,9 @@ void configureAppRouter() { pageBuilder: RequestRecoveryBuilder().buildPage, ), GoRoute( - path: AppRoutes.deviceSignup, - name: 'device_signup', - pageBuilder: DeviceSignupBuilder().buildPage, + path: AppRoutes.deviceSetup, + name: 'device_setup', + pageBuilder: DeviceSetupBuilder().buildPage, ), StatefulShellRoute.indexedStack( builder: (context, state, navShell) { diff --git a/apps/mobile_app/lib/providers/app_state_provider.dart b/apps/mobile_app/lib/providers/app_state_provider.dart new file mode 100644 index 00000000..1adb00dd --- /dev/null +++ b/apps/mobile_app/lib/providers/app_state_provider.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/widgets.dart'; + +final appLifecycleStateProvider = + NotifierProvider( + AppLifecycleNotifier.new, + ); + +class AppLifecycleNotifier extends Notifier { + @override + AppLifecycleState build() { + return AppLifecycleState.resumed; + } + + void setState(AppLifecycleState newState) { + state = newState; + } +} diff --git a/apps/mobile_app/lib/providers/permissions/permissions_provider.dart b/apps/mobile_app/lib/providers/permissions/permissions_provider.dart new file mode 100644 index 00000000..bf59c6b3 --- /dev/null +++ b/apps/mobile_app/lib/providers/permissions/permissions_provider.dart @@ -0,0 +1,75 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; + +final permissionsProvider = + NotifierProvider( + PermissionsNotifier.new, + ); + +class PermissionsNotifier extends Notifier { + @override + PermissionsState build() { + final initialState = PermissionsState(); + checkPermissions(); + return initialState; + } + + Future checkPermissions() async { + final permissionsArray = await Future.wait([ + Permission.camera.status, + Permission.photos.status, + ]); + + state = state.copyWith( + camera: permissionsArray[0], + photoLibrary: permissionsArray[1], + ); + } + + void openSettinsScreen() { + openAppSettings(); + } + + void _checkPerssionState(PermissionStatus status) { + if (status == PermissionStatus.permanentlyDenied) { + openSettinsScreen(); + } + } + + Future requestCameraAccess() async { + final status = await Permission.camera.request(); + state = state.copyWith(camera: status); + _checkPerssionState(status); + return status == PermissionStatus.granted; + } + + Future requestPhotoLibraryAccess() async { + final status = await Permission.photos.request(); + state = state.copyWith(photoLibrary: status); + _checkPerssionState(status); + return status == PermissionStatus.granted; + } +} + +class PermissionsState { + final PermissionStatus camera; + final PermissionStatus photoLibrary; + + PermissionsState({ + this.camera = PermissionStatus.denied, + this.photoLibrary = PermissionStatus.denied, + }); + + bool get cameraGranted => camera == PermissionStatus.granted; + bool get photoLibraryGranted => photoLibrary == PermissionStatus.granted; + + PermissionsState copyWith({ + PermissionStatus? camera, + PermissionStatus? photoLibrary, + }) { + return PermissionsState( + camera: camera ?? this.camera, + photoLibrary: photoLibrary ?? this.photoLibrary, + ); + } +} diff --git a/apps/mobile_app/lib/providers/providers.dart b/apps/mobile_app/lib/providers/providers.dart new file mode 100644 index 00000000..291877bf --- /dev/null +++ b/apps/mobile_app/lib/providers/providers.dart @@ -0,0 +1,2 @@ +export 'app_state_provider.dart'; +export 'permissions/permissions_provider.dart'; diff --git a/apps/mobile_app/pubspec.lock b/apps/mobile_app/pubspec.lock index 53b85bf0..52ac93ca 100644 --- a/apps/mobile_app/pubspec.lock +++ b/apps/mobile_app/pubspec.lock @@ -200,6 +200,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cookie_jar: + dependency: transitive + description: + name: cookie_jar + sha256: a6ac027d3ed6ed756bfce8f3ff60cb479e266f3b0fdabd6242b804b6765e52de + url: "https://pub.dev" + source: hosted + version: "4.0.8" country_code_picker: dependency: "direct main" description: @@ -278,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.0" + dio_cookie_manager: + dependency: transitive + description: + name: dio_cookie_manager + sha256: d39c16abcc711c871b7b29bd51c6b5f3059ef39503916c6a9df7e22c4fc595e0 + url: "https://pub.dev" + source: hosted + version: "3.3.0" dio_web_adapter: dependency: transitive description: @@ -632,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: transitive + description: + name: mobile_scanner + sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 + url: "https://pub.dev" + source: hosted + version: "7.1.4" navigation: dependency: "direct main" description: @@ -686,6 +710,102 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -694,6 +814,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -1037,6 +1173,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -1055,4 +1199,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.32.0" + flutter: ">=3.35.0" diff --git a/apps/mobile_app/pubspec.yaml b/apps/mobile_app/pubspec.yaml index bab26b88..d4638029 100644 --- a/apps/mobile_app/pubspec.yaml +++ b/apps/mobile_app/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: flutter_dotenv: ^6.0.0 country_code_picker: ^3.4.1 flutter_native_splash: ^2.4.7 + permission_handler: ^12.0.1 dev_dependencies: flutter_test: sdk: flutter diff --git a/modules/auth/lib/auth.dart b/modules/auth/lib/auth.dart index 57994d61..1d497555 100644 --- a/modules/auth/lib/auth.dart +++ b/modules/auth/lib/auth.dart @@ -1,8 +1,7 @@ -export 'src/features/device_sign_up/link_watch/create_profile_screen.dart'; export 'src/features/onboarding/onboarding_builder.dart'; export 'src/features/link_phone/presentation/request_phone/request_link_phone_builder.dart'; export 'src/features/link_phone/presentation/verify_code/verify_link_phone_code_builder.dart'; export 'src/features/login/login_builder.dart'; export 'src/features/recover_password/presentation/request_recovery/request_recovery_builder.dart'; -export 'src/features/device_sign_up/device_signup_builder.dart'; +export 'src/features/device_setup/device_setup_builder.dart'; export 'src/features/sign_up/sign_up_builder.dart'; diff --git a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart index 136241fd..bc8413e0 100644 --- a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart +++ b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource.dart @@ -1,7 +1,11 @@ +import 'package:auth/src/core/data/models/get_me_response_model.dart'; +import 'package:auth/src/core/data/models/two_fa_secret_response_model.dart'; import 'package:auth/src/features/sign_up/domain/entities/sign_up_request_entity.dart'; import 'package:auth/src/features/sign_up/domain/entities/two_fa_secret_entity.dart'; abstract class AuthRemoteDatasource { + Future getMe(); + Future requestPhoneCode({required String phone}); Future verifyPhoneCode({required String phone, required String code}); @@ -11,12 +15,25 @@ abstract class AuthRemoteDatasource { Future twoFALogin({required String token, required String code}); Future signUp({required SignUpRequestEntity request}); - Future generateTwoFASignUp({required String token}); + Future generateTwoFASignUp({required String token}); Future verifyTwoFACodeSignUp({ required String token, required String code, }); - Future requestPasswordReset({String? phone, String? email}); + Future requestPasswordReset({required String email}); Future recoverPassword({required newPassword, required token}); + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }); } diff --git a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart index a09d339b..37f9deea 100644 --- a/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart +++ b/modules/auth/lib/src/core/data/datasource/auth_remote_datasource_impl.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:auth/src/core/data/models/get_me_response_model.dart'; import 'package:auth/src/core/data/models/sign_up_request_model.dart'; import 'package:auth/src/core/data/models/sign_up_response_model.dart'; import 'package:auth/src/core/data/models/two_fa_secret_response_model.dart'; @@ -16,6 +17,23 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { final QuestiaRepository _repository; + @override + Future getMe() async { + try { + final response = await _repository.get>('/auth/me'); + + final data = response.data; + if (data == null || data.isEmpty) { + throw Exception('Empty response from /auth/me'); + } + + final parsed = GetMeResponseModel.fromJson(data); + return parsed.item; + } on DioException catch (error) { + throw _mapDioError(error, defaultMessage: 'Error in /auth/me'); + } + } + @override Future requestPhoneCode({required String phone}) async { try { @@ -127,7 +145,9 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { } @override - Future generateTwoFASignUp({required String token}) async { + Future generateTwoFASignUp({ + required String token, + }) async { try { final response = await _repository.post>( '/auth/totp/secret', @@ -140,7 +160,7 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { } final model = TwoFASecretResponseModel.fromJson(data); - return model.toEntity(); + return model; } on DioException catch (error) { throw _mapDioError(error, defaultMessage: 'Error in twoFASignUp'); } @@ -169,32 +189,26 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { } @override - Future requestPasswordReset({ - String? phone, - String? email - }) async { + Future requestPasswordReset({required String email}) async { try { - if (phone == null && email == null) { - throw FormatException("No phone or email address given"); - } + late final Map body; + body = {'email': email}; - // late final Map body; - if (email != null) { - // body = {'email': email}; - return 'ec14b7e7-58dd-4a59-9f41-0da86eaabf14'; - } else { - // body = {'phone': phone!}; - return 'ec14b7e7-58dd-4a59-9f41-0da86eaabf14'; - // throw Exception("reset by phone is not currently implemented"); - } - /*final response = await _repository.put>( + final response = await _repository.put( '/auth/reset-password', body: body, ); - final token = response.data!['token']; - return token;*/ + final data = response.data; + if (data == null || data.isEmpty) { + throw Exception('Empty response from /auth/totp/code'); + } + + return data; } on DioException catch (error) { - throw _mapDioError(error, defaultMessage: 'Error to request password reset'); + throw _mapDioError( + error, + defaultMessage: 'Error to request password reset', + ); } } @@ -206,7 +220,52 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource { body: {'newPassword': newPassword, 'token': token}, ); } on DioException catch (error) { - throw _mapDioError(error, defaultMessage: 'Error to request password recovery'); + throw _mapDioError( + error, + defaultMessage: 'Error to request password recovery', + ); + } + } + + @override + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }) async { + try { + final response = await _repository.post>( + '/auth/child-profiles', + body: { + 'id': id, + 'parentId': parentId, + 'firstName': firstName, + 'lastName': lastName, + 'bornAt': bornAt, + 'gender': gender, + 'relationType': relationType, + 'address': address, + 'cardPublicKey': cardPublicKey, + 'deviceActivationCode': deviceActivationCode, + 'scaProof': scaProof, + }, + ); + final data = response.data; + if (data == null || data.isEmpty) { + throw Exception('Empty response from /auth/child-profiles'); + } else { + return data['id']; + } + } on DioException catch (error) { + throw _mapDioError(error, defaultMessage: 'Error in createChildProfile'); } } } diff --git a/modules/auth/lib/src/core/data/models/get_me_response_model.dart b/modules/auth/lib/src/core/data/models/get_me_response_model.dart new file mode 100644 index 00000000..8407dca7 --- /dev/null +++ b/modules/auth/lib/src/core/data/models/get_me_response_model.dart @@ -0,0 +1,36 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'get_me_response_model.freezed.dart'; +part 'get_me_response_model.g.dart'; + +@freezed +abstract class GetMeResponseModel with _$GetMeResponseModel { + const factory GetMeResponseModel({required MeUserModel item}) = + _GetMeResponseModel; + + factory GetMeResponseModel.fromJson(Map json) => + _$GetMeResponseModelFromJson(json); +} + +@freezed +abstract class MeUserModel with _$MeUserModel { + const factory MeUserModel({ + required String id, + required String delegationId, + required String email, + required int createdAt, + required int updatedAt, + required String status, + required String role, + required int lastLogin, + required int currentLogin, + required String language, + required String firstName, + required String lastName, + required bool hasApiKey, + required String phone, + }) = _MeUserModel; + + factory MeUserModel.fromJson(Map json) => + _$MeUserModelFromJson(json); +} diff --git a/modules/auth/lib/src/core/data/models/get_me_response_model.freezed.dart b/modules/auth/lib/src/core/data/models/get_me_response_model.freezed.dart new file mode 100644 index 00000000..54895d48 --- /dev/null +++ b/modules/auth/lib/src/core/data/models/get_me_response_model.freezed.dart @@ -0,0 +1,597 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'get_me_response_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$GetMeResponseModel { + + MeUserModel get item; +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$GetMeResponseModelCopyWith get copyWith => _$GetMeResponseModelCopyWithImpl(this as GetMeResponseModel, _$identity); + + /// Serializes this GetMeResponseModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is GetMeResponseModel&&(identical(other.item, item) || other.item == item)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,item); + +@override +String toString() { + return 'GetMeResponseModel(item: $item)'; +} + + +} + +/// @nodoc +abstract mixin class $GetMeResponseModelCopyWith<$Res> { + factory $GetMeResponseModelCopyWith(GetMeResponseModel value, $Res Function(GetMeResponseModel) _then) = _$GetMeResponseModelCopyWithImpl; +@useResult +$Res call({ + MeUserModel item +}); + + +$MeUserModelCopyWith<$Res> get item; + +} +/// @nodoc +class _$GetMeResponseModelCopyWithImpl<$Res> + implements $GetMeResponseModelCopyWith<$Res> { + _$GetMeResponseModelCopyWithImpl(this._self, this._then); + + final GetMeResponseModel _self; + final $Res Function(GetMeResponseModel) _then; + +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? item = null,}) { + return _then(_self.copyWith( +item: null == item ? _self.item : item // ignore: cast_nullable_to_non_nullable +as MeUserModel, + )); +} +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MeUserModelCopyWith<$Res> get item { + + return $MeUserModelCopyWith<$Res>(_self.item, (value) { + return _then(_self.copyWith(item: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [GetMeResponseModel]. +extension GetMeResponseModelPatterns on GetMeResponseModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _GetMeResponseModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _GetMeResponseModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _GetMeResponseModel value) $default,){ +final _that = this; +switch (_that) { +case _GetMeResponseModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _GetMeResponseModel value)? $default,){ +final _that = this; +switch (_that) { +case _GetMeResponseModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( MeUserModel item)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _GetMeResponseModel() when $default != null: +return $default(_that.item);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( MeUserModel item) $default,) {final _that = this; +switch (_that) { +case _GetMeResponseModel(): +return $default(_that.item);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( MeUserModel item)? $default,) {final _that = this; +switch (_that) { +case _GetMeResponseModel() when $default != null: +return $default(_that.item);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _GetMeResponseModel implements GetMeResponseModel { + const _GetMeResponseModel({required this.item}); + factory _GetMeResponseModel.fromJson(Map json) => _$GetMeResponseModelFromJson(json); + +@override final MeUserModel item; + +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$GetMeResponseModelCopyWith<_GetMeResponseModel> get copyWith => __$GetMeResponseModelCopyWithImpl<_GetMeResponseModel>(this, _$identity); + +@override +Map toJson() { + return _$GetMeResponseModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _GetMeResponseModel&&(identical(other.item, item) || other.item == item)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,item); + +@override +String toString() { + return 'GetMeResponseModel(item: $item)'; +} + + +} + +/// @nodoc +abstract mixin class _$GetMeResponseModelCopyWith<$Res> implements $GetMeResponseModelCopyWith<$Res> { + factory _$GetMeResponseModelCopyWith(_GetMeResponseModel value, $Res Function(_GetMeResponseModel) _then) = __$GetMeResponseModelCopyWithImpl; +@override @useResult +$Res call({ + MeUserModel item +}); + + +@override $MeUserModelCopyWith<$Res> get item; + +} +/// @nodoc +class __$GetMeResponseModelCopyWithImpl<$Res> + implements _$GetMeResponseModelCopyWith<$Res> { + __$GetMeResponseModelCopyWithImpl(this._self, this._then); + + final _GetMeResponseModel _self; + final $Res Function(_GetMeResponseModel) _then; + +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? item = null,}) { + return _then(_GetMeResponseModel( +item: null == item ? _self.item : item // ignore: cast_nullable_to_non_nullable +as MeUserModel, + )); +} + +/// Create a copy of GetMeResponseModel +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MeUserModelCopyWith<$Res> get item { + + return $MeUserModelCopyWith<$Res>(_self.item, (value) { + return _then(_self.copyWith(item: value)); + }); +} +} + + +/// @nodoc +mixin _$MeUserModel { + + String get id; String get delegationId; String get email; int get createdAt; int get updatedAt; String get status; String get role; int get lastLogin; int get currentLogin; String get language; String get firstName; String get lastName; bool get hasApiKey; String get phone; +/// Create a copy of MeUserModel +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MeUserModelCopyWith get copyWith => _$MeUserModelCopyWithImpl(this as MeUserModel, _$identity); + + /// Serializes this MeUserModel to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MeUserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.email, email) || other.email == email)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.status, status) || other.status == status)&&(identical(other.role, role) || other.role == role)&&(identical(other.lastLogin, lastLogin) || other.lastLogin == lastLogin)&&(identical(other.currentLogin, currentLogin) || other.currentLogin == currentLogin)&&(identical(other.language, language) || other.language == language)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.hasApiKey, hasApiKey) || other.hasApiKey == hasApiKey)&&(identical(other.phone, phone) || other.phone == phone)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,delegationId,email,createdAt,updatedAt,status,role,lastLogin,currentLogin,language,firstName,lastName,hasApiKey,phone); + +@override +String toString() { + return 'MeUserModel(id: $id, delegationId: $delegationId, email: $email, createdAt: $createdAt, updatedAt: $updatedAt, status: $status, role: $role, lastLogin: $lastLogin, currentLogin: $currentLogin, language: $language, firstName: $firstName, lastName: $lastName, hasApiKey: $hasApiKey, phone: $phone)'; +} + + +} + +/// @nodoc +abstract mixin class $MeUserModelCopyWith<$Res> { + factory $MeUserModelCopyWith(MeUserModel value, $Res Function(MeUserModel) _then) = _$MeUserModelCopyWithImpl; +@useResult +$Res call({ + String id, String delegationId, String email, int createdAt, int updatedAt, String status, String role, int lastLogin, int currentLogin, String language, String firstName, String lastName, bool hasApiKey, String phone +}); + + + + +} +/// @nodoc +class _$MeUserModelCopyWithImpl<$Res> + implements $MeUserModelCopyWith<$Res> { + _$MeUserModelCopyWithImpl(this._self, this._then); + + final MeUserModel _self; + final $Res Function(MeUserModel) _then; + +/// Create a copy of MeUserModel +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? delegationId = null,Object? email = null,Object? createdAt = null,Object? updatedAt = null,Object? status = null,Object? role = null,Object? lastLogin = null,Object? currentLogin = null,Object? language = null,Object? firstName = null,Object? lastName = null,Object? hasApiKey = null,Object? phone = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,delegationId: null == delegationId ? _self.delegationId : delegationId // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as int,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as String,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as String,lastLogin: null == lastLogin ? _self.lastLogin : lastLogin // ignore: cast_nullable_to_non_nullable +as int,currentLogin: null == currentLogin ? _self.currentLogin : currentLogin // ignore: cast_nullable_to_non_nullable +as int,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable +as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable +as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable +as String,hasApiKey: null == hasApiKey ? _self.hasApiKey : hasApiKey // ignore: cast_nullable_to_non_nullable +as bool,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [MeUserModel]. +extension MeUserModelPatterns on MeUserModel { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MeUserModel value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MeUserModel() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MeUserModel value) $default,){ +final _that = this; +switch (_that) { +case _MeUserModel(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MeUserModel value)? $default,){ +final _that = this; +switch (_that) { +case _MeUserModel() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String delegationId, String email, int createdAt, int updatedAt, String status, String role, int lastLogin, int currentLogin, String language, String firstName, String lastName, bool hasApiKey, String phone)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MeUserModel() when $default != null: +return $default(_that.id,_that.delegationId,_that.email,_that.createdAt,_that.updatedAt,_that.status,_that.role,_that.lastLogin,_that.currentLogin,_that.language,_that.firstName,_that.lastName,_that.hasApiKey,_that.phone);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String delegationId, String email, int createdAt, int updatedAt, String status, String role, int lastLogin, int currentLogin, String language, String firstName, String lastName, bool hasApiKey, String phone) $default,) {final _that = this; +switch (_that) { +case _MeUserModel(): +return $default(_that.id,_that.delegationId,_that.email,_that.createdAt,_that.updatedAt,_that.status,_that.role,_that.lastLogin,_that.currentLogin,_that.language,_that.firstName,_that.lastName,_that.hasApiKey,_that.phone);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String delegationId, String email, int createdAt, int updatedAt, String status, String role, int lastLogin, int currentLogin, String language, String firstName, String lastName, bool hasApiKey, String phone)? $default,) {final _that = this; +switch (_that) { +case _MeUserModel() when $default != null: +return $default(_that.id,_that.delegationId,_that.email,_that.createdAt,_that.updatedAt,_that.status,_that.role,_that.lastLogin,_that.currentLogin,_that.language,_that.firstName,_that.lastName,_that.hasApiKey,_that.phone);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _MeUserModel implements MeUserModel { + const _MeUserModel({required this.id, required this.delegationId, required this.email, required this.createdAt, required this.updatedAt, required this.status, required this.role, required this.lastLogin, required this.currentLogin, required this.language, required this.firstName, required this.lastName, required this.hasApiKey, required this.phone}); + factory _MeUserModel.fromJson(Map json) => _$MeUserModelFromJson(json); + +@override final String id; +@override final String delegationId; +@override final String email; +@override final int createdAt; +@override final int updatedAt; +@override final String status; +@override final String role; +@override final int lastLogin; +@override final int currentLogin; +@override final String language; +@override final String firstName; +@override final String lastName; +@override final bool hasApiKey; +@override final String phone; + +/// Create a copy of MeUserModel +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MeUserModelCopyWith<_MeUserModel> get copyWith => __$MeUserModelCopyWithImpl<_MeUserModel>(this, _$identity); + +@override +Map toJson() { + return _$MeUserModelToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MeUserModel&&(identical(other.id, id) || other.id == id)&&(identical(other.delegationId, delegationId) || other.delegationId == delegationId)&&(identical(other.email, email) || other.email == email)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.status, status) || other.status == status)&&(identical(other.role, role) || other.role == role)&&(identical(other.lastLogin, lastLogin) || other.lastLogin == lastLogin)&&(identical(other.currentLogin, currentLogin) || other.currentLogin == currentLogin)&&(identical(other.language, language) || other.language == language)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.hasApiKey, hasApiKey) || other.hasApiKey == hasApiKey)&&(identical(other.phone, phone) || other.phone == phone)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,delegationId,email,createdAt,updatedAt,status,role,lastLogin,currentLogin,language,firstName,lastName,hasApiKey,phone); + +@override +String toString() { + return 'MeUserModel(id: $id, delegationId: $delegationId, email: $email, createdAt: $createdAt, updatedAt: $updatedAt, status: $status, role: $role, lastLogin: $lastLogin, currentLogin: $currentLogin, language: $language, firstName: $firstName, lastName: $lastName, hasApiKey: $hasApiKey, phone: $phone)'; +} + + +} + +/// @nodoc +abstract mixin class _$MeUserModelCopyWith<$Res> implements $MeUserModelCopyWith<$Res> { + factory _$MeUserModelCopyWith(_MeUserModel value, $Res Function(_MeUserModel) _then) = __$MeUserModelCopyWithImpl; +@override @useResult +$Res call({ + String id, String delegationId, String email, int createdAt, int updatedAt, String status, String role, int lastLogin, int currentLogin, String language, String firstName, String lastName, bool hasApiKey, String phone +}); + + + + +} +/// @nodoc +class __$MeUserModelCopyWithImpl<$Res> + implements _$MeUserModelCopyWith<$Res> { + __$MeUserModelCopyWithImpl(this._self, this._then); + + final _MeUserModel _self; + final $Res Function(_MeUserModel) _then; + +/// Create a copy of MeUserModel +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? delegationId = null,Object? email = null,Object? createdAt = null,Object? updatedAt = null,Object? status = null,Object? role = null,Object? lastLogin = null,Object? currentLogin = null,Object? language = null,Object? firstName = null,Object? lastName = null,Object? hasApiKey = null,Object? phone = null,}) { + return _then(_MeUserModel( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,delegationId: null == delegationId ? _self.delegationId : delegationId // ignore: cast_nullable_to_non_nullable +as String,email: null == email ? _self.email : email // ignore: cast_nullable_to_non_nullable +as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as int,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as int,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable +as String,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as String,lastLogin: null == lastLogin ? _self.lastLogin : lastLogin // ignore: cast_nullable_to_non_nullable +as int,currentLogin: null == currentLogin ? _self.currentLogin : currentLogin // ignore: cast_nullable_to_non_nullable +as int,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable +as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable +as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable +as String,hasApiKey: null == hasApiKey ? _self.hasApiKey : hasApiKey // ignore: cast_nullable_to_non_nullable +as bool,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/modules/auth/lib/src/core/data/models/get_me_response_model.g.dart b/modules/auth/lib/src/core/data/models/get_me_response_model.g.dart new file mode 100644 index 00000000..d1383470 --- /dev/null +++ b/modules/auth/lib/src/core/data/models/get_me_response_model.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_me_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_GetMeResponseModel _$GetMeResponseModelFromJson(Map json) => + _GetMeResponseModel( + item: MeUserModel.fromJson(json['item'] as Map), + ); + +Map _$GetMeResponseModelToJson(_GetMeResponseModel instance) => + {'item': instance.item}; + +_MeUserModel _$MeUserModelFromJson(Map json) => _MeUserModel( + id: json['id'] as String, + delegationId: json['delegationId'] as String, + email: json['email'] as String, + createdAt: (json['createdAt'] as num).toInt(), + updatedAt: (json['updatedAt'] as num).toInt(), + status: json['status'] as String, + role: json['role'] as String, + lastLogin: (json['lastLogin'] as num).toInt(), + currentLogin: (json['currentLogin'] as num).toInt(), + language: json['language'] as String, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String, + hasApiKey: json['hasApiKey'] as bool, + phone: json['phone'] as String, +); + +Map _$MeUserModelToJson(_MeUserModel instance) => + { + 'id': instance.id, + 'delegationId': instance.delegationId, + 'email': instance.email, + 'createdAt': instance.createdAt, + 'updatedAt': instance.updatedAt, + 'status': instance.status, + 'role': instance.role, + 'lastLogin': instance.lastLogin, + 'currentLogin': instance.currentLogin, + 'language': instance.language, + 'firstName': instance.firstName, + 'lastName': instance.lastName, + 'hasApiKey': instance.hasApiKey, + 'phone': instance.phone, + }; diff --git a/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart b/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart index 1f2fb7a0..ba441654 100644 --- a/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart +++ b/modules/auth/lib/src/core/data/repositories/auth_repository_impl.dart @@ -1,4 +1,5 @@ import 'package:auth/src/core/data/datasource/auth_remote_datasource.dart'; +import 'package:auth/src/core/data/models/two_fa_secret_response_model.dart'; import 'package:auth/src/core/domain/repositories/auth_repository.dart'; import 'package:auth/src/features/sign_up/domain/entities/sign_up_request_entity.dart'; import 'package:auth/src/features/sign_up/domain/entities/two_fa_secret_entity.dart'; @@ -34,7 +35,9 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future generateTwoFASignUp({required String token}) { + Future generateTwoFASignUp({ + required String token, + }) { return _remote.generateTwoFASignUp(token: token); } @@ -47,8 +50,8 @@ class AuthRepositoryImpl implements AuthRepository { } @override - Future requestPasswordReset({String? phone, String? email}) { - return _remote.requestPasswordReset(phone: phone, email: email); + Future requestPasswordReset({required String email}) { + return _remote.requestPasswordReset(email: email); } @override @@ -58,4 +61,33 @@ class AuthRepositoryImpl implements AuthRepository { }) { return _remote.recoverPassword(newPassword: newPassword, token: token); } + + @override + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }) { + return _remote.createChildProfile( + id: id, + parentId: parentId, + firstName: firstName, + lastName: lastName, + bornAt: bornAt, + gender: gender, + relationType: relationType, + address: address, + cardPublicKey: cardPublicKey, + deviceActivationCode: deviceActivationCode, + scaProof: scaProof, + ); + } } diff --git a/modules/auth/lib/src/core/domain/repositories/auth_repository.dart b/modules/auth/lib/src/core/domain/repositories/auth_repository.dart index 5e78e95e..7abc8985 100644 --- a/modules/auth/lib/src/core/domain/repositories/auth_repository.dart +++ b/modules/auth/lib/src/core/domain/repositories/auth_repository.dart @@ -1,3 +1,4 @@ +import 'package:auth/src/core/data/models/two_fa_secret_response_model.dart'; import 'package:auth/src/features/sign_up/domain/entities/sign_up_request_entity.dart'; import 'package:auth/src/features/sign_up/domain/entities/two_fa_secret_entity.dart'; @@ -9,7 +10,7 @@ abstract class AuthRepository { Future login({required String email, required String password}); Future twoFactor({required String token, required String code}); - Future requestPasswordReset({String phone, String email}); + Future requestPasswordReset({required String email}); Future recoverPassword({ required String newPassword, @@ -18,9 +19,22 @@ abstract class AuthRepository { Future signUp({required SignUpRequestEntity request}); - Future generateTwoFASignUp({required String token}); + Future generateTwoFASignUp({required String token}); Future verifyTwoFACodeSignUp({ required String token, required String code, }); + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }); } diff --git a/modules/auth/lib/src/features/device_sign_up/device_signup_builder.dart b/modules/auth/lib/src/features/device_setup/device_setup_builder.dart similarity index 64% rename from modules/auth/lib/src/features/device_sign_up/device_signup_builder.dart rename to modules/auth/lib/src/features/device_setup/device_setup_builder.dart index d2f648ff..c1c24a4a 100644 --- a/modules/auth/lib/src/features/device_sign_up/device_signup_builder.dart +++ b/modules/auth/lib/src/features/device_setup/device_setup_builder.dart @@ -1,18 +1,18 @@ -import 'package:auth/src/features/device_sign_up/device_signup_screen.dart'; +import 'package:auth/src/features/device_setup/presentation/device_setup_screen.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:navigation/navigation.dart'; -class DeviceSignupBuilder { - const DeviceSignupBuilder(); +class DeviceSetupBuilder { + const DeviceSetupBuilder(); Page buildPage(BuildContext context, GoRouterState state) { final NavigationContract navigationContract = GetIt.I(); return MaterialPage( key: state.pageKey, - child: DeviceSignupScreen(navigationContract: navigationContract), + child: DeviceSetupScreen(navigationContract: navigationContract), ); } } diff --git a/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case.dart b/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case.dart new file mode 100644 index 00000000..816a855a --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case.dart @@ -0,0 +1,15 @@ +abstract class CreateChildProfileUseCase { + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }); +} diff --git a/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case_impl.dart b/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case_impl.dart new file mode 100644 index 00000000..b31ef0c9 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/domain/create_child_profile_use_case_impl.dart @@ -0,0 +1,37 @@ +import 'package:auth/src/core/domain/repositories/auth_repository.dart'; +import 'package:auth/src/features/device_setup/domain/create_child_profile_use_case.dart'; + +class CreateChildProfileUseCaseImpl implements CreateChildProfileUseCase { + CreateChildProfileUseCaseImpl(this._repository); + + final AuthRepository _repository; + + @override + Future createChildProfile({ + required String id, + required String parentId, + required String firstName, + required String lastName, + required int bornAt, + required String gender, + required String relationType, + required String address, + required String cardPublicKey, + required String deviceActivationCode, + required String scaProof, + }) { + return _repository.createChildProfile( + id: id, + parentId: parentId, + firstName: firstName, + lastName: lastName, + bornAt: bornAt, + gender: gender, + relationType: relationType, + address: address, + cardPublicKey: cardPublicKey, + deviceActivationCode: deviceActivationCode, + scaProof: scaProof, + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/add_kid_step_mapper.dart b/modules/auth/lib/src/features/device_setup/presentation/add_kid_step_mapper.dart new file mode 100644 index 00000000..71887b4c --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/add_kid_step_mapper.dart @@ -0,0 +1,19 @@ +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_main_step.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; + +extension AddKidStepMapper on AddKidStep { + AddKidMainStep get mainStep { + switch (this) { + case AddKidStep.linkInfo: + case AddKidStep.scanStrap: + case AddKidStep.scanWatch: + return AddKidMainStep.linkDevice; + case AddKidStep.profile: + return AddKidMainStep.profile; + case AddKidStep.success: + return AddKidMainStep.success; + case AddKidStep.intro: + return AddKidMainStep.linkDevice; + } + } +} diff --git a/modules/auth/lib/src/features/device_sign_up/contact_screen.dart b/modules/auth/lib/src/features/device_setup/presentation/contact_screen.dart similarity index 100% rename from modules/auth/lib/src/features/device_sign_up/contact_screen.dart rename to modules/auth/lib/src/features/device_setup/presentation/contact_screen.dart diff --git a/modules/auth/lib/src/features/device_setup/presentation/device_setup_screen.dart b/modules/auth/lib/src/features/device_setup/presentation/device_setup_screen.dart new file mode 100644 index 00000000..c021fbed --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/device_setup_screen.dart @@ -0,0 +1,71 @@ +import 'package:auth/src/features/device_setup/presentation/add_kid_step_mapper.dart'; +import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_model.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_main_step.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; +import 'package:auth/src/features/device_setup/presentation/step_body.dart'; +import 'package:auth/src/features/device_setup/presentation/widgets/flow_footer.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:navigation/navigation_contract.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class DeviceSetupScreen extends ConsumerWidget { + final NavigationContract navigationContract; + + const DeviceSetupScreen({super.key, required this.navigationContract}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(deviceSetupViewModelProvider); + final vm = ref.read(deviceSetupViewModelProvider.notifier); + final theme = ref.watch(themePortProvider); + final mainStep = state.step.mainStep; + + return Scaffold( + backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), + body: SafeArea( + child: Column( + children: [ + state.step == AddKidStep.intro || state.step == AddKidStep.success + ? const SizedBox(height: 24) + : StepIndicator( + total: AddKidMainStep.values.length, + current: mainStep.index + 1, + color: theme.getColorFor(ThemeCode.buttonPrimary), + ), + + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: StepBody(key: ValueKey(state.step), state: state), + ), + ), + + FlowFooter( + error: state.errorMessage, + primaryText: context.translate(primaryButtonText(state.step)), + secondaryText: state.step == AddKidStep.success + ? context.translate(I18n.deviceSetup_addAnotherKid) + : null, + onPrimary: vm.next, + onSecondary: state.step == AddKidStep.success ? () {} : null, + theme: theme, + ), + ], + ), + ), + ); + } + + String primaryButtonText(AddKidStep step) { + switch (step) { + case AddKidStep.intro: + return I18n.deviceSetup_start; + case AddKidStep.success: + return I18n.deviceSetup_giveFirstAllowance; + default: + return I18n.continueKey; + } + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_main_step.dart b/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_main_step.dart new file mode 100644 index 00000000..9dbec4d0 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_main_step.dart @@ -0,0 +1 @@ +enum AddKidMainStep { linkDevice, profile, success } diff --git a/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_step.dart b/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_step.dart new file mode 100644 index 00000000..e17ff405 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/enums/add_kid_step.dart @@ -0,0 +1 @@ +enum AddKidStep { intro, linkInfo, scanStrap, scanWatch, profile, success } diff --git a/modules/auth/lib/src/features/device_setup/presentation/enums/scan_link_step.dart b/modules/auth/lib/src/features/device_setup/presentation/enums/scan_link_step.dart new file mode 100644 index 00000000..ad300406 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/enums/scan_link_step.dart @@ -0,0 +1 @@ +enum ScanLinkStep { strap, watch } diff --git a/modules/auth/lib/src/features/device_setup/presentation/providers/create_child_profile_use_case_provider.dart b/modules/auth/lib/src/features/device_setup/presentation/providers/create_child_profile_use_case_provider.dart new file mode 100644 index 00000000..3058b2d9 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/providers/create_child_profile_use_case_provider.dart @@ -0,0 +1,10 @@ +import 'package:auth/src/core/providers/auth_repository_provider.dart'; +import 'package:auth/src/features/device_setup/domain/create_child_profile_use_case.dart'; +import 'package:auth/src/features/device_setup/domain/create_child_profile_use_case_impl.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final createChildProfileUseCaseProvider = + Provider.autoDispose((ref) { + final authRepository = ref.read(authRepositoryProvider); + return CreateChildProfileUseCaseImpl(authRepository); + }); diff --git a/modules/auth/lib/src/features/device_setup/presentation/qr_scanner_screen.dart b/modules/auth/lib/src/features/device_setup/presentation/qr_scanner_screen.dart new file mode 100644 index 00000000..075758db --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/qr_scanner_screen.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class QrScannerScreen extends StatefulWidget { + const QrScannerScreen({super.key}); + + @override + State createState() => _QrScannerScreenState(); +} + +class _QrScannerScreenState extends State { + late final MobileScannerController _controller; + + bool _alreadyReturned = false; + + @override + void initState() { + super.initState(); + _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.noDuplicates, + formats: const [BarcodeFormat.qrCode], + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _returnResult(String value) { + if (_alreadyReturned) return; + _alreadyReturned = true; + Navigator.of(context).pop(value); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text(context.translate(I18n.deviceSetup_scanQr)), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () => _controller.toggleTorch(), + ), + ], + ), + body: Stack( + children: [ + MobileScanner( + controller: _controller, + onDetect: (capture) { + if (capture.barcodes.isEmpty) return; + + final rawValue = capture.barcodes.first.rawValue; + if (rawValue == null || rawValue.isEmpty) return; + + _returnResult(rawValue); + }, + ), + + const Positioned.fill( + child: QrScannerOverlay( + cutOutSize: 260, + borderRadius: 18, + borderWidth: 3, + ), + ), + + Positioned( + left: 0, + right: 0, + bottom: 26, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(14), + ), + child: Text( + context.translate(I18n.deviceSetup_scanQr_hint), + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 15), + ), + ), + ), + ), + ], + ), + ); + } +} + +class QrScannerOverlay extends StatelessWidget { + const QrScannerOverlay({ + super.key, + required this.cutOutSize, + required this.borderRadius, + required this.borderWidth, + }); + + final double cutOutSize; + final double borderRadius; + final double borderWidth; + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: CustomPaint( + painter: _QrScannerOverlayPainter( + cutOutSize: cutOutSize, + borderRadius: borderRadius, + borderWidth: borderWidth, + ), + ), + ); + } +} + +class _QrScannerOverlayPainter extends CustomPainter { + _QrScannerOverlayPainter({ + required this.cutOutSize, + required this.borderRadius, + required this.borderWidth, + }); + + final double cutOutSize; + final double borderRadius; + final double borderWidth; + + @override + void paint(Canvas canvas, Size size) { + final screenRect = Rect.fromLTWH(0, 0, size.width, size.height); + + final cutOutRect = Rect.fromCenter( + center: screenRect.center, + width: cutOutSize, + height: cutOutSize, + ); + + final cutOutRRect = RRect.fromRectXY( + cutOutRect, + borderRadius, + borderRadius, + ); + + final backgroundPath = Path()..addRect(screenRect); + final cutOutPath = Path()..addRRect(cutOutRRect); + + final overlayPath = Path.combine( + PathOperation.difference, + backgroundPath, + cutOutPath, + ); + + final overlayPaint = Paint() + ..color = Colors.black.withValues(alpha: 0.55) + ..style = PaintingStyle.fill; + + canvas.drawPath(overlayPath, overlayPaint); + + final borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth + ..color = Colors.white; + + canvas.drawRRect(cutOutRRect, borderPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart new file mode 100644 index 00000000..6d713ee7 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart @@ -0,0 +1,213 @@ +import 'package:auth/src/features/device_setup/presentation/enums/scan_link_step.dart'; +import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_state.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final deviceSetupViewModelProvider = + NotifierProvider( + DeviceSetupViewModel.new, + ); + +class DeviceSetupViewModel extends Notifier { + late final TextEditingController bornAtController; + late final TextEditingController firstNameController; + late final TextEditingController lastNameController; + + @override + DeviceSetupViewState build() { + // _signUpUseCase = ref.read(signUpUseCaseProvider); + + final initial = DeviceSetupViewState(); + _initControllers(initial); + _addListeners(); + + ref.onDispose(disposeControllers); + + return initial; + } + + void _initControllers(DeviceSetupViewState s) { + firstNameController = TextEditingController(text: s.firstName); + lastNameController = TextEditingController(text: s.lastName); + bornAtController = TextEditingController( + text: s.bornAt == null ? '' : _formatDate(s.bornAt!), + ); + } + + void _addListeners() { + firstNameController.addListener(_onFirstNameChanged); + lastNameController.addListener(_onLastNameChanged); + bornAtController.addListener(_onBornAtTextChanged); + } + + void next() { + switch (state.step) { + case AddKidStep.intro: + state = state.copyWith(step: AddKidStep.linkInfo); + return; + case AddKidStep.linkInfo: + state = state.copyWith(step: AddKidStep.scanStrap); + return; + case AddKidStep.scanStrap: + if (state.strapQr.isEmpty) { + state = state.copyWith( + errorMessage: 'Escanea la correa para continuar', + ); + return; + } + state = state.copyWith(step: AddKidStep.scanWatch); + return; + case AddKidStep.scanWatch: + final hasWatch = state.watchQr.isNotEmpty || state.watchCode.isNotEmpty; + if (!hasWatch) { + state = state.copyWith( + errorMessage: 'Escanea el reloj o introduce el código', + ); + return; + } + state = state.copyWith(step: AddKidStep.profile); + return; + case AddKidStep.profile: + // final isValid = _validateProfile(); + // if (!isValid) return; + state = state.copyWith(step: AddKidStep.success); + return; + + case AddKidStep.success: + return; + } + } + + void back() { + switch (state.step) { + case AddKidStep.intro: + return; + + case AddKidStep.linkInfo: + state = state.copyWith(step: AddKidStep.intro, errorMessage: ''); + return; + case AddKidStep.scanStrap: + state = state.copyWith(step: AddKidStep.linkInfo); + return; + case AddKidStep.scanWatch: + state = state.copyWith(step: AddKidStep.scanStrap); + return; + case AddKidStep.profile: + state = state.copyWith(step: AddKidStep.scanWatch); + return; + case AddKidStep.success: + state = state.copyWith(step: AddKidStep.profile); + return; + } + } + + void onQrScanned({required ScanLinkStep step, required String qr}) { + switch (step) { + case ScanLinkStep.strap: + state = state.copyWith(strapQr: qr, step: AddKidStep.scanWatch); + break; + + case ScanLinkStep.watch: + state = state.copyWith(watchQr: qr, step: AddKidStep.profile); + break; + } + } + + Future pickBornAt(BuildContext context) async { + FocusManager.instance.primaryFocus?.unfocus(); + + final now = DateTime.now(); + final initial = state.bornAt ?? DateTime(now.year - 18, now.month, now.day); + + final safeInitial = initial.isAfter(now) ? now : initial; + + final picked = await showDatePicker( + context: context, + initialDate: safeInitial, + firstDate: DateTime(1900, 1, 1), + lastDate: now, + ); + + if (!ref.mounted) return; + if (picked == null) return; + + setBornAt(picked); + } + + void setBornAt(DateTime date) { + bornAtController.text = _formatDate(date); + state = state.copyWith(bornAt: date); + } + + bool _validateProfile() { + if (state.firstName.trim().isEmpty || + state.lastName.trim().isEmpty || + state.bornAt == null || + state.address.trim().isEmpty) { + state = state.copyWith(errorMessage: 'Completa todos los campos'); + return false; + } + return true; + } + + String _formatDate(DateTime date) { + final dd = date.day.toString().padLeft(2, '0'); + final mm = date.month.toString().padLeft(2, '0'); + final yyyy = date.year.toString(); + return '$dd/$mm/$yyyy'; + } + + void _onFirstNameChanged() { + final text = firstNameController.text; + if (text == state.firstName) return; + state = state.copyWith(firstName: text, errorMessage: ''); + } + + void _onLastNameChanged() { + final text = lastNameController.text; + if (text == state.lastName) return; + state = state.copyWith(lastName: text, errorMessage: ''); + } + + void _onBornAtTextChanged() { + final text = bornAtController.text; + final parsed = _tryParseDate(text); + + if (text.trim().isEmpty) { + if (state.bornAt != null) { + state = state.copyWith(bornAt: null, errorMessage: ''); + } + return; + } + + if (parsed != null && parsed != state.bornAt) { + state = state.copyWith(bornAt: parsed, errorMessage: ''); + } + } + + DateTime? _tryParseDate(String value) { + final v = value.trim(); + if (v.isEmpty) return null; + + final parts = v.split('/'); + if (parts.length != 3) return null; + + final d = int.tryParse(parts[0]); + final m = int.tryParse(parts[1]); + final y = int.tryParse(parts[2]); + + if (d == null || m == null || y == null) return null; + + final date = DateTime(y, m, d); + if (date.year != y || date.month != m || date.day != d) return null; + + return date; + } + + void disposeControllers() { + // firstNameController.dispose(); + // lastNameController.dispose(); + bornAtController.dispose(); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.dart b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.dart new file mode 100644 index 00000000..35cfdaff --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.dart @@ -0,0 +1,23 @@ +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'device_setup_view_state.freezed.dart'; + +@freezed +abstract class DeviceSetupViewState with _$DeviceSetupViewState { + const factory DeviceSetupViewState({ + @Default(AddKidStep.intro) AddKidStep step, + + @Default('') String firstName, + @Default('') String lastName, + DateTime? bornAt, + @Default('') String address, + + @Default('') String strapQr, + @Default('') String watchQr, + @Default('') String watchCode, + + @Default(false) bool loading, + @Default('') String errorMessage, + }) = _AddKidFlowState; +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.freezed.dart b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.freezed.dart new file mode 100644 index 00000000..9c9cc750 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/state/device_setup_view_state.freezed.dart @@ -0,0 +1,298 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'device_setup_view_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$DeviceSetupViewState { + + AddKidStep get step; String get firstName; String get lastName; DateTime? get bornAt; String get address; String get strapQr; String get watchQr; String get watchCode; bool get loading; String get errorMessage; +/// Create a copy of DeviceSetupViewState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DeviceSetupViewStateCopyWith get copyWith => _$DeviceSetupViewStateCopyWithImpl(this as DeviceSetupViewState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DeviceSetupViewState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,firstName,lastName,bornAt,address,strapQr,watchQr,watchCode,loading,errorMessage); + +@override +String toString() { + return 'DeviceSetupViewState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, strapQr: $strapQr, watchQr: $watchQr, watchCode: $watchCode, loading: $loading, errorMessage: $errorMessage)'; +} + + +} + +/// @nodoc +abstract mixin class $DeviceSetupViewStateCopyWith<$Res> { + factory $DeviceSetupViewStateCopyWith(DeviceSetupViewState value, $Res Function(DeviceSetupViewState) _then) = _$DeviceSetupViewStateCopyWithImpl; +@useResult +$Res call({ + AddKidStep step, String firstName, String lastName, DateTime? bornAt, String address, String strapQr, String watchQr, String watchCode, bool loading, String errorMessage +}); + + + + +} +/// @nodoc +class _$DeviceSetupViewStateCopyWithImpl<$Res> + implements $DeviceSetupViewStateCopyWith<$Res> { + _$DeviceSetupViewStateCopyWithImpl(this._self, this._then); + + final DeviceSetupViewState _self; + final $Res Function(DeviceSetupViewState) _then; + +/// Create a copy of DeviceSetupViewState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? step = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? strapQr = null,Object? watchQr = null,Object? watchCode = null,Object? loading = null,Object? errorMessage = null,}) { + return _then(_self.copyWith( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as AddKidStep,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable +as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable +as String,bornAt: freezed == bornAt ? _self.bornAt : bornAt // ignore: cast_nullable_to_non_nullable +as DateTime?,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable +as String,strapQr: null == strapQr ? _self.strapQr : strapQr // ignore: cast_nullable_to_non_nullable +as String,watchQr: null == watchQr ? _self.watchQr : watchQr // ignore: cast_nullable_to_non_nullable +as String,watchCode: null == watchCode ? _self.watchCode : watchCode // ignore: cast_nullable_to_non_nullable +as String,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String, + )); +} + +} + + +/// Adds pattern-matching-related methods to [DeviceSetupViewState]. +extension DeviceSetupViewStatePatterns on DeviceSetupViewState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AddKidFlowState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AddKidFlowState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AddKidFlowState value) $default,){ +final _that = this; +switch (_that) { +case _AddKidFlowState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AddKidFlowState value)? $default,){ +final _that = this; +switch (_that) { +case _AddKidFlowState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( AddKidStep step, String firstName, String lastName, DateTime? bornAt, String address, String strapQr, String watchQr, String watchCode, bool loading, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AddKidFlowState() when $default != null: +return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.strapQr,_that.watchQr,_that.watchCode,_that.loading,_that.errorMessage);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( AddKidStep step, String firstName, String lastName, DateTime? bornAt, String address, String strapQr, String watchQr, String watchCode, bool loading, String errorMessage) $default,) {final _that = this; +switch (_that) { +case _AddKidFlowState(): +return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.strapQr,_that.watchQr,_that.watchCode,_that.loading,_that.errorMessage);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( AddKidStep step, String firstName, String lastName, DateTime? bornAt, String address, String strapQr, String watchQr, String watchCode, bool loading, String errorMessage)? $default,) {final _that = this; +switch (_that) { +case _AddKidFlowState() when $default != null: +return $default(_that.step,_that.firstName,_that.lastName,_that.bornAt,_that.address,_that.strapQr,_that.watchQr,_that.watchCode,_that.loading,_that.errorMessage);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _AddKidFlowState implements DeviceSetupViewState { + const _AddKidFlowState({this.step = AddKidStep.intro, this.firstName = '', this.lastName = '', this.bornAt, this.address = '', this.strapQr = '', this.watchQr = '', this.watchCode = '', this.loading = false, this.errorMessage = ''}); + + +@override@JsonKey() final AddKidStep step; +@override@JsonKey() final String firstName; +@override@JsonKey() final String lastName; +@override final DateTime? bornAt; +@override@JsonKey() final String address; +@override@JsonKey() final String strapQr; +@override@JsonKey() final String watchQr; +@override@JsonKey() final String watchCode; +@override@JsonKey() final bool loading; +@override@JsonKey() final String errorMessage; + +/// Create a copy of DeviceSetupViewState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AddKidFlowStateCopyWith<_AddKidFlowState> get copyWith => __$AddKidFlowStateCopyWithImpl<_AddKidFlowState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AddKidFlowState&&(identical(other.step, step) || other.step == step)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bornAt, bornAt) || other.bornAt == bornAt)&&(identical(other.address, address) || other.address == address)&&(identical(other.strapQr, strapQr) || other.strapQr == strapQr)&&(identical(other.watchQr, watchQr) || other.watchQr == watchQr)&&(identical(other.watchCode, watchCode) || other.watchCode == watchCode)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); +} + + +@override +int get hashCode => Object.hash(runtimeType,step,firstName,lastName,bornAt,address,strapQr,watchQr,watchCode,loading,errorMessage); + +@override +String toString() { + return 'DeviceSetupViewState(step: $step, firstName: $firstName, lastName: $lastName, bornAt: $bornAt, address: $address, strapQr: $strapQr, watchQr: $watchQr, watchCode: $watchCode, loading: $loading, errorMessage: $errorMessage)'; +} + + +} + +/// @nodoc +abstract mixin class _$AddKidFlowStateCopyWith<$Res> implements $DeviceSetupViewStateCopyWith<$Res> { + factory _$AddKidFlowStateCopyWith(_AddKidFlowState value, $Res Function(_AddKidFlowState) _then) = __$AddKidFlowStateCopyWithImpl; +@override @useResult +$Res call({ + AddKidStep step, String firstName, String lastName, DateTime? bornAt, String address, String strapQr, String watchQr, String watchCode, bool loading, String errorMessage +}); + + + + +} +/// @nodoc +class __$AddKidFlowStateCopyWithImpl<$Res> + implements _$AddKidFlowStateCopyWith<$Res> { + __$AddKidFlowStateCopyWithImpl(this._self, this._then); + + final _AddKidFlowState _self; + final $Res Function(_AddKidFlowState) _then; + +/// Create a copy of DeviceSetupViewState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? step = null,Object? firstName = null,Object? lastName = null,Object? bornAt = freezed,Object? address = null,Object? strapQr = null,Object? watchQr = null,Object? watchCode = null,Object? loading = null,Object? errorMessage = null,}) { + return _then(_AddKidFlowState( +step: null == step ? _self.step : step // ignore: cast_nullable_to_non_nullable +as AddKidStep,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable +as String,lastName: null == lastName ? _self.lastName : lastName // ignore: cast_nullable_to_non_nullable +as String,bornAt: freezed == bornAt ? _self.bornAt : bornAt // ignore: cast_nullable_to_non_nullable +as DateTime?,address: null == address ? _self.address : address // ignore: cast_nullable_to_non_nullable +as String,strapQr: null == strapQr ? _self.strapQr : strapQr // ignore: cast_nullable_to_non_nullable +as String,watchQr: null == watchQr ? _self.watchQr : watchQr // ignore: cast_nullable_to_non_nullable +as String,watchCode: null == watchCode ? _self.watchCode : watchCode // ignore: cast_nullable_to_non_nullable +as String,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/modules/auth/lib/src/features/device_setup/presentation/step_body.dart b/modules/auth/lib/src/features/device_setup/presentation/step_body.dart new file mode 100644 index 00000000..48024328 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/step_body.dart @@ -0,0 +1,32 @@ +import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_state.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; +import 'package:auth/src/features/device_setup/presentation/enums/scan_link_step.dart'; +import 'package:auth/src/features/device_setup/presentation/steps/intro_step.dart'; +import 'package:auth/src/features/device_setup/presentation/steps/link_info_step.dart'; +import 'package:auth/src/features/device_setup/presentation/steps/profile_step.dart'; +import 'package:auth/src/features/device_setup/presentation/steps/scan_strap_and_watch_step.dart'; +import 'package:auth/src/features/device_setup/presentation/steps/success_step.dart'; +import 'package:flutter/material.dart'; + +class StepBody extends StatelessWidget { + const StepBody({super.key, required this.state}); + final DeviceSetupViewState state; + + @override + Widget build(BuildContext context) { + switch (state.step) { + case AddKidStep.intro: + return IntroStepScreen(); + case AddKidStep.linkInfo: + return LinkInfoStepScreen(); + case AddKidStep.scanStrap: + return ScanStrapAndWatchStepScreen(step: ScanLinkStep.strap); + case AddKidStep.scanWatch: + return ScanStrapAndWatchStepScreen(step: ScanLinkStep.watch); + case AddKidStep.profile: + return ProfileStepScreen(); + case AddKidStep.success: + return SuccessStepScreen(); + } + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/steps/intro_step.dart b/modules/auth/lib/src/features/device_setup/presentation/steps/intro_step.dart new file mode 100644 index 00000000..d0780751 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/steps/intro_step.dart @@ -0,0 +1,80 @@ +import 'package:auth/src/features/device_setup/presentation/widgets/numbered_steps.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class IntroStepScreen extends ConsumerWidget { + const IntroStepScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + context.translate(I18n.deviceSetup_intro_title), + style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 30), + Text( + context.translate(I18n.deviceSetup_intro_subtitle), + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + SizedBox(height: 40), + NumberedSteps( + steps: [ + context.translate(I18n.deviceSetup_intro_step_1), + context.translate(I18n.deviceSetup_intro_step_2), + context.translate(I18n.deviceSetup_intro_step_3), + ], + color: theme.getColorFor(ThemeCode.buttonPrimary), + ), + SizedBox(height: 40), + + Text( + context.translate(I18n.deviceSetup_intro_ready_title), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 40), + + Text( + context.translate(I18n.deviceSetup_intro_remember_prefix), + style: TextStyle(fontSize: 16), + ), + Text( + context.translate(I18n.deviceSetup_intro_plan_name), + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + SizedBox(height: 20), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 50.0), + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + text: context.translate(I18n.deviceSetup_intro_web_prefix), + style: TextStyle(fontSize: 16, color: Colors.black), + children: [ + TextSpan( + text: context.translate(I18n.deviceSetup_intro_web_link), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/steps/link_info_step.dart b/modules/auth/lib/src/features/device_setup/presentation/steps/link_info_step.dart new file mode 100644 index 00000000..15f8d103 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/steps/link_info_step.dart @@ -0,0 +1,71 @@ +import 'package:auth/src/features/device_setup/presentation/widgets/link_info_item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class LinkInfoStepScreen extends ConsumerWidget { + const LinkInfoStepScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 65), + child: Text( + context.translate(I18n.deviceSetup_linkInfo_title), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + height: 1.2, + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 25), + + SvgPicture.asset("assets/images/ui/formulario.svg"), + + const SizedBox(height: 40), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + LinkInfoItem( + number: 1, + boldWord: context.translate( + I18n.deviceSetup_linkInfo_item1_boldWord, + ), + titlePrefix: context.translate( + I18n.deviceSetup_linkInfo_item1_prefix, + ), + subtitle: context.translate( + I18n.deviceSetup_linkInfo_item1_subtitle, + ), + ), + SizedBox(height: 20), + LinkInfoItem( + number: 2, + boldWord: context.translate( + I18n.deviceSetup_linkInfo_item2_boldWord, + ), + titlePrefix: context.translate( + I18n.deviceSetup_linkInfo_item2_prefix, + ), + subtitle: context.translate( + I18n.deviceSetup_linkInfo_item2_subtitle, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/steps/profile_step.dart b/modules/auth/lib/src/features/device_setup/presentation/steps/profile_step.dart new file mode 100644 index 00000000..d94493f4 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/steps/profile_step.dart @@ -0,0 +1,74 @@ +import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_model.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class ProfileStepScreen extends ConsumerWidget { + const ProfileStepScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + + final vm = ref.read(deviceSetupViewModelProvider.notifier); + + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Text( + context.translate(I18n.deviceSetup_intro_step_1), + style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 30), + Text( + context.translate(I18n.deviceSetup_accountData_info), + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + SizedBox(height: 20), + + Text( + context.translate(I18n.deviceSetup_startWithOneKid_info), + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 24, right: 24), + child: Column( + children: [ + CustomTextField( + label: context.translate(I18n.firstNameLabel), + hint: context.translate(I18n.firstNameHint), + controller: vm.firstNameController, + ), + const SizedBox(height: 8), + CustomTextField( + label: context.translate(I18n.lastNameLabel), + hint: context.translate(I18n.lastNameHint), + controller: vm.lastNameController, + ), + const SizedBox(height: 8), + + GestureDetector( + onTap: () => vm.pickBornAt(context), + child: AbsorbPointer( + child: CustomTextField( + label: context.translate(I18n.birthDateLabel), + hint: context.translate(I18n.birthDateHint), + controller: vm.bornAtController, + readOnly: true, + keyboardType: TextInputType.none, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/steps/scan_strap_and_watch_step.dart b/modules/auth/lib/src/features/device_setup/presentation/steps/scan_strap_and_watch_step.dart new file mode 100644 index 00000000..25d4b3c4 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/steps/scan_strap_and_watch_step.dart @@ -0,0 +1,200 @@ +import 'package:auth/src/features/device_setup/presentation/enums/scan_link_step.dart'; +import 'package:auth/src/features/device_setup/presentation/qr_scanner_screen.dart'; +import 'package:auth/src/features/device_setup/presentation/state/device_setup_view_model.dart'; +import 'package:auth/src/features/device_setup/presentation/widgets/scan_link_steps_indicator.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class ScanStrapAndWatchStepScreen extends ConsumerWidget { + const ScanStrapAndWatchStepScreen({super.key, required this.step}); + + final ScanLinkStep step; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + final activeColor = theme.getColorFor(ThemeCode.buttonPrimary); + final inactiveCircleColor = theme.getColorFor( + ThemeCode.backgroundSecondary, + ); + final inactiveLineColor = Colors.grey.shade200; + final textPrimary = theme.getColorFor(ThemeCode.textPrimary); + + final vm = ref.read(deviceSetupViewModelProvider.notifier); + final state = ref.watch(deviceSetupViewModelProvider); + + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 30), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 65), + child: Text( + context.translate(I18n.deviceSetup_linkInfo_title), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + height: 1.2, + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 18), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 100), + child: ScanLinkStepsIndicator( + step: step, + activeColor: activeColor, + inactiveCircleColor: inactiveCircleColor, + inactiveLineColor: inactiveLineColor, + textPrimary: textPrimary, + ), + ), + + const SizedBox(height: 12), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 35), + child: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: context.translate( + I18n.deviceSetup_linkInfo_item1_prefix, + ), + ), + TextSpan( + text: context.translate( + I18n.deviceSetup_linkInfo_item1_boldWord, + ), + style: TextStyle(fontWeight: FontWeight.w800), + ), + ], + ), + style: const TextStyle(fontSize: 18), + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: context.translate( + I18n.deviceSetup_linkInfo_item2_prefix, + ), + ), + TextSpan( + text: context.translate( + I18n.deviceSetup_linkInfo_item2_boldWord, + ), + style: TextStyle(fontWeight: FontWeight.w800), + ), + ], + ), + style: const TextStyle(fontSize: 18), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 28), + + InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const QrScannerScreen()), + ); + if (result == null || result.isEmpty) return; + vm.onQrScanned(step: step, qr: result); + }, + child: Container( + width: 170, + height: 170, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade500, width: 1), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: SvgPicture.asset( + "assets/images/ui/qr.svg", + width: 90, + height: 90, + fit: BoxFit.contain, + ), + ), + ), + ), + + const SizedBox(height: 22), + + if (step == ScanLinkStep.watch) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.translate(I18n.deviceSetup_watchCode_orInsert), + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded(child: CustomTextField(hint: "XXXXXXXXXX")), + const SizedBox(width: 12), + Expanded( + child: PrimaryButton( + onPressed: () {}, + text: context.translate( + I18n.deviceSetup_watchCode_continueWithCode, + ), + size: 14, + color: theme.getColorFor(ThemeCode.buttonSecondary), + ), + ), + ], + ), + ], + ), + ), + ], + + const SizedBox(height: 10), + + Column( + children: [ + Text( + context.translate(I18n.deviceSetup_linkTroubleshoot_title), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + CustomTextButton( + onPressed: () {}, + text: context.translate(I18n.deviceSetup_contactUs), + weight: FontWeight.w800, + size: 18, + ), + ], + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/steps/success_step.dart b/modules/auth/lib/src/features/device_setup/presentation/steps/success_step.dart new file mode 100644 index 00000000..151b0b92 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/steps/success_step.dart @@ -0,0 +1,63 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_localizations/sf_localizations.dart'; + +class SuccessStepScreen extends ConsumerWidget { + const SuccessStepScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.check, + color: theme.getColorFor(ThemeCode.buttonPrimary), + size: 50, + ), + const SizedBox(height: 20), + Text( + context.translate(I18n.accountCreatedTitle), + style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 30), + Text( + context.translate(I18n.accountCreatedForLabel), + style: TextStyle(fontSize: 18), + textAlign: TextAlign.center, + ), + Text( + 'Julián Alcalá', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + textAlign: TextAlign.center, + ), + SizedBox(height: 40), + + Text( + 'Reloj: SaveWatch Plus 2', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + Text( + 'ID del reloj: 1106652524', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 40), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + context.translate(I18n.deviceSetup_firstAllowance_title), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/flow_footer.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/flow_footer.dart new file mode 100644 index 00000000..1596f340 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/flow_footer.dart @@ -0,0 +1,71 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +class FlowFooter extends StatelessWidget { + const FlowFooter({ + super.key, + required this.primaryText, + required this.onPrimary, + required this.theme, + this.secondaryText, + this.onSecondary, + this.error, + }); + + final String primaryText; + final VoidCallback onPrimary; + final ThemePort theme; + + final String? secondaryText; + final VoidCallback? onSecondary; + + final String? error; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: theme.getColorFor(ThemeCode.backgroundPrimary), + border: Border(top: BorderSide(color: Colors.grey.shade300, width: 1)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (error != null) ...[ + Text( + error!, + style: const TextStyle(color: Colors.red, fontSize: 13), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + ], + PrimaryButton( + text: primaryText, + onPressed: onPrimary, + color: theme.getColorFor(ThemeCode.buttonPrimary), + ), + if (secondaryText != null && onSecondary != null) ...[ + const SizedBox(height: 10), + Material( + child: InkWell( + onTap: onSecondary, + child: Text( + secondaryText ?? '', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/link_info_item.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/link_info_item.dart new file mode 100644 index 00000000..0467e97f --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/link_info_item.dart @@ -0,0 +1,60 @@ +import 'package:auth/src/features/device_setup/presentation/widgets/number_circle.dart'; +import 'package:flutter/material.dart'; + +class LinkInfoItem extends StatelessWidget { + const LinkInfoItem({ + super.key, + required this.number, + required this.titlePrefix, + required this.boldWord, + required this.subtitle, + }); + + final int number; + final String titlePrefix; + final String boldWord; + final String subtitle; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NumberCircle(number: number), + const SizedBox(width: 10), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: titlePrefix, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w500, + color: Color(0xFF4B4B4B), + ), + ), + TextSpan( + text: boldWord, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: Color(0xFF4B4B4B), + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Text(subtitle, style: const TextStyle(fontSize: 16)), + ], + ), + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/number_circle.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/number_circle.dart new file mode 100644 index 00000000..ff603d4b --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/number_circle.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class NumberCircle extends StatelessWidget { + const NumberCircle({super.key, required this.number}); + + final int number; + + @override + Widget build(BuildContext context) { + return Container( + width: 48, + height: 48, + alignment: Alignment.center, + decoration: const BoxDecoration( + color: Color(0xFFF2F2F2), + shape: BoxShape.circle, + ), + child: Text( + number.toString(), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: Color(0xFF5A5A5A), + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/numbered_steps.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/numbered_steps.dart new file mode 100644 index 00000000..d34fdb5b --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/numbered_steps.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +class NumberedSteps extends StatelessWidget { + const NumberedSteps({ + super.key, + required this.steps, + this.color, + this.textColor, + this.textStyle, + }); + + final List steps; + + final Color? color; + final Color? textColor; + + final TextStyle? textStyle; + + @override + @override + Widget build(BuildContext context) { + final Color resolvedColor = + color ?? Theme.of(context).colorScheme.secondaryContainer; + final Color resolvedTextColor = textColor ?? Colors.grey.shade800; + final TextStyle resolvedTextStyle = + textStyle ?? + TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: resolvedTextColor, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(steps.length, (index) { + final isLast = index == steps.length - 1; + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 32, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _StepCircle( + number: index + 1, + size: 32, + color: resolvedColor, + ), + if (!isLast) + Container( + width: 4, + height: 15, + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(99), + ), + ), + ], + ), + ), + const SizedBox(width: 10), + Padding( + padding: const EdgeInsets.only(top: 32 * 0.15), + child: Text(steps[index], style: resolvedTextStyle), + ), + ], + ); + }), + ); + } +} + +class _StepCircle extends StatelessWidget { + const _StepCircle({ + required this.number, + required this.size, + required this.color, + }); + + final int number; + final double size; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + child: Text( + '$number', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: size * 0.55, + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/scan_link_steps_indicator.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/scan_link_steps_indicator.dart new file mode 100644 index 00000000..bc9ca81b --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/scan_link_steps_indicator.dart @@ -0,0 +1,60 @@ +import 'package:auth/src/features/device_setup/presentation/enums/scan_link_step.dart'; +import 'package:auth/src/features/device_setup/presentation/widgets/step_circle.dart'; +import 'package:flutter/material.dart'; + +class ScanLinkStepsIndicator extends StatelessWidget { + const ScanLinkStepsIndicator({ + super.key, + required this.step, + required this.activeColor, + required this.inactiveCircleColor, + required this.inactiveLineColor, + required this.textPrimary, + }); + + final ScanLinkStep step; + final Color activeColor; + final Color inactiveCircleColor; + final Color inactiveLineColor; + final Color textPrimary; + + bool get isWatch => step == ScanLinkStep.watch; + + @override + Widget build(BuildContext context) { + const circleSize = 48.0; + const lineHeight = 4.0; + + return Row( + children: [ + StepCircle( + label: "1", + size: circleSize, + background: activeColor, + textColor: Colors.white, + ), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(99), + child: SizedBox( + height: lineHeight, + child: Row( + children: [ + Expanded(child: Container(color: activeColor)), + if (!isWatch) + Expanded(child: Container(color: inactiveLineColor)), + ], + ), + ), + ), + ), + StepCircle( + label: "2", + size: circleSize, + background: isWatch ? activeColor : inactiveCircleColor, + textColor: isWatch ? Colors.white : textPrimary, + ), + ], + ); + } +} diff --git a/modules/auth/lib/src/features/device_setup/presentation/widgets/step_circle.dart b/modules/auth/lib/src/features/device_setup/presentation/widgets/step_circle.dart new file mode 100644 index 00000000..dbf74856 --- /dev/null +++ b/modules/auth/lib/src/features/device_setup/presentation/widgets/step_circle.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class StepCircle extends StatelessWidget { + const StepCircle({ + super.key, + required this.label, + required this.size, + required this.background, + required this.textColor, + }); + + final String label; + final double size; + final Color background; + final Color textColor; + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration(color: background, shape: BoxShape.circle), + child: Text( + label, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + color: textColor, + ), + ), + ); + } +} diff --git a/modules/auth/lib/src/features/device_sign_up/add_kid_screen.dart b/modules/auth/lib/src/features/device_sign_up/add_kid_screen.dart deleted file mode 100644 index 5c9dc8c7..00000000 --- a/modules/auth/lib/src/features/device_sign_up/add_kid_screen.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class AddKidScreen extends ConsumerWidget { - final nextStep; - - const AddKidScreen({super.key, required this.nextStep}); - - @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: Column( - spacing: 15, - children: [ - Spacer(flex: 6), - Text("Añade a tu peque", style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)), - Text( - "Controla su gasto a la vez que aprende hábitos financieros responsables", - textAlign: TextAlign.center, - ), - Container( - margin: EdgeInsets.symmetric(vertical: 30, horizontal: 50), - child: Row( - spacing: 10, - children: [ - Stack( - children: [ - Column( - spacing: 16, - children: List.generate(3, (int index) => - Container( - decoration: ShapeDecoration( - shape: CircleBorder(), - color: theme.getColorFor(ThemeCode.buttonPrimary) - ), - width: 32, - height: 32, - child: Center(child: Text( - (index + 1).toString(), - style: TextStyle( - color: theme.getColorFor(ThemeCode.backgroundPrimary) - ) - )) - ) - ) - ), - Divider(color: Colors.red, thickness: 4,), - ], - ), - Column( - spacing: 16, - 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!", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 0 - ) - ), - Text( - "Recuerda que necesitas tener un Plan SaveFamily", - textAlign: TextAlign.center - ), - Text( - "Si aún no lo tienes, puedes conseguirlo a través de nuestra web", - textAlign: TextAlign.center, - ), - Spacer(flex: 8), - PrimaryButton( - onPressed: nextStep, - text: "¡Empezar!", - color: theme.getColorFor(ThemeCode.buttonPrimary) - ), - ], - ), - ), - ); - } -} diff --git a/modules/auth/lib/src/features/device_sign_up/device_signup_screen.dart b/modules/auth/lib/src/features/device_sign_up/device_signup_screen.dart deleted file mode 100644 index bc12c18e..00000000 --- a/modules/auth/lib/src/features/device_sign_up/device_signup_screen.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:auth/auth.dart'; -import 'package:auth/src/features/device_sign_up/add_kid_screen.dart'; -import 'package:auth/src/features/device_sign_up/link_watch/link_watch_screen.dart'; -import 'package:auth/src/features/device_sign_up/link_watch/link_watch_previous_screen.dart'; -import 'package:auth/src/features/sign_up/presentation/screens/account_created_screen.dart'; -import 'package:auth/src/widgets/layouts/form_step_layout.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:navigation/navigation.dart'; - -class DeviceSignupScreen extends ConsumerStatefulWidget { - final NavigationContract navigationContract; - - const DeviceSignupScreen({super.key, required this.navigationContract}); - - @override - ConsumerState createState() => - DeviceSignupScreenState(navigationContract); -} - -class DeviceSignupScreenState extends ConsumerState { - late int currentStep; - final NavigationContract navigationContract; - - DeviceSignupScreenState(this.navigationContract); - - @override - void initState() { - currentStep = 0; - } - - @override - Widget build(BuildContext context) { - return getSteps()[currentStep]; - } - - List getSteps() { - final theme = ref.watch(themePortProvider); - - final continueBtn = Container( - color: theme.getColorFor(ThemeCode.backgroundPrimary), - child: PrimaryButton( - onPressed: () => { - setState(() { - currentStep++; - }), - }, - text: "Continuar", - color: theme.getColorFor(ThemeCode.buttonPrimary), - ), - ); - - return [ - AddKidScreen( - nextStep: () => { - setState(() { - currentStep++; - }), - }, - ), - FormStepLayout( - title: "Crea su perfil", - subtitle: - "Necesitamos estos datos para crear su cuenta y gestionar sus pagos y gastos", - currentStep: 1, - numSteps: 3, - body: [CreateProfileScreen()], - footer: [ - Container( - padding: EdgeInsets.all(24), - color: theme.getColorFor(ThemeCode.backgroundPrimary), - child: continueBtn, - ), - ], - nextStep: () => {}, - previousStep: () => {}, - ), - FormStepLayout( - title: "Vincula su correa y su reloj", - currentStep: 2, - numSteps: 3, - body: [LinkWatchPreviousScreen()], - footer: [continueBtn], - nextStep: () => {}, - previousStep: () => {}, - ), - FormStepLayout( - title: "Vincula su correa\ny su reloj", - currentStep: 2, - numSteps: 3, - body: [LinkWatchScreen(step: 1)], - footer: [continueBtn], - nextStep: () => {}, - previousStep: () => {}, - ), - FormStepLayout( - title: "Vincula su correa\ny su reloj", - currentStep: 2, - numSteps: 3, - body: [LinkWatchScreen(step: 2)], - footer: [continueBtn], - nextStep: () => {}, - previousStep: () => {}, - ), - AccountCreatedScreen(navigationContract: navigationContract), - ]; - } -} diff --git a/modules/auth/lib/src/features/device_sign_up/link_watch/create_profile_screen.dart b/modules/auth/lib/src/features/device_sign_up/link_watch/create_profile_screen.dart deleted file mode 100644 index 7c397094..00000000 --- a/modules/auth/lib/src/features/device_sign_up/link_watch/create_profile_screen.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class CreateProfileScreen extends ConsumerWidget { - const CreateProfileScreen({super.key}); - - final bool firstTime = false; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = ref.watch(themePortProvider); - - return Column( - spacing: 24, - children: [ - Text( - "Comienza con un peque; luego podrás agregar más", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - letterSpacing: 0, - ), - ), - CustomTextField(label: "Nombre", hint: "Nombre"), - CustomTextField(label: "Apellidos", hint: "Apellidos"), - Column( - spacing: 8, - children: [ - Align( - alignment: Alignment.bottomLeft, - child: Text( - "Fecha de nacimiento", - style: TextStyle(fontSize: 14, letterSpacing: 0), - ), - ), - Row( - spacing: 10, - children: [ - Expanded( - child: CustomTextField( - keyboardType: TextInputType.number, - hint: "DD", - length: 2, - ), - ), - Expanded( - child: CustomTextField( - keyboardType: TextInputType.number, - hint: "MM", - length: 2, - ), - ), - Expanded( - child: CustomTextField( - keyboardType: TextInputType.number, - hint: "AAAA", - length: 4, - ), - ), - ], - ), - ], - ), - CustomTextField( - label: "Dirección completa", - hint: "Nombre de la calle", - ), - Align( - alignment: Alignment.topLeft, - child: CustomTextButton( - onPressed: () => {}, - text: "Cambiar dirección", - size: 18, - weight: FontWeight.w500, - ), - ), - ], - ); - } -} diff --git a/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_previous_screen.dart b/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_previous_screen.dart deleted file mode 100644 index 815d12f6..00000000 --- a/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_previous_screen.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; - -class LinkWatchPreviousScreen extends ConsumerWidget{ - @override - Widget build(BuildContext context, WidgetRef ref) { - final theme = ref.watch(themePortProvider); - - return Column( - spacing: 10, - children: [ - SvgPicture.asset("assets/images/ui/formulario.svg"), - Row( - spacing: 16, - children: [ - Container( - decoration: ShapeDecoration( - shape: CircleBorder(side: BorderSide(color: theme.getColorFor(ThemeCode.backgroundSecondary))), - color: theme.getColorFor(ThemeCode.backgroundSecondary) - ), - width: 48, - height: 48, - child: Center(child: Text("1", style: TextStyle(fontSize: 24))), - ), - Column( - children: [ - Text("Escanea la correa", textAlign: TextAlign.left, style: TextStyle(fontSize: 24)), - Text("El peque podrá realizar pagos"), - ], - ), - ], - ), - Row( - spacing: 16, - children: [ - Container( - decoration: ShapeDecoration( - shape: CircleBorder(side: BorderSide(color: theme.getColorFor(ThemeCode.backgroundSecondary))), - color: theme.getColorFor(ThemeCode.backgroundSecondary) - ), - width: 48, - height: 48, - child: Center(child: Text("2", style: TextStyle(fontSize: 24))), - ), - Column( - children: [ - Text("Escanea el reloj", textAlign: TextAlign.left, style: TextStyle(fontSize: 24)), - Text("Visualizarás los gastos que se hagan"), - ] - ) - ], - ), - ], - ); - } -} \ No newline at end of file diff --git a/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_screen.dart b/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_screen.dart deleted file mode 100644 index 85b9c60b..00000000 --- a/modules/auth/lib/src/features/device_sign_up/link_watch/link_watch_screen.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class LinkWatchScreen extends ConsumerStatefulWidget{ - final int step; - - const LinkWatchScreen({super.key, required this.step}); - - @override - ConsumerState createState() => LinkWatchScreenState(); -} - -class LinkWatchScreenState extends ConsumerState{ - - @override - Widget build(BuildContext context) { - final theme = ref.watch(themePortProvider); - - return Column( - spacing: 32, - children: [ - Stack( - children: [ - Divider( - color: theme.getColorFor(ThemeCode.buttonPrimary), - thickness: 4, - indent: 93, - endIndent: 93, - height: 48, - ), - if (widget.step==1)Divider( - color: theme.getColorFor(ThemeCode.backgroundSecondary), - thickness: 4, - indent: 186, - endIndent: 93, - height: 48, - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 69), - child: Row( - children: [ - Container( - decoration: ShapeDecoration( - shape: CircleBorder(), - color: theme.getColorFor(ThemeCode.buttonPrimary) - ), - width: 48, - height: 48, - child: Center(child: Text( - "1", - style: TextStyle( - color: theme.getColorFor(ThemeCode.backgroundPrimary), - fontSize: 24 - ) - )) - ), - Spacer(), - Container( - decoration: ShapeDecoration( - shape: CircleBorder(), - color: theme.getColorFor(widget.step==1 ? ThemeCode.backgroundSecondary : ThemeCode.buttonPrimary) - ), - width: 48, - height: 48, - child: Center(child: Text( - "2", - style: TextStyle( - color: theme.getColorFor(widget.step==1 ? ThemeCode.textPrimary : ThemeCode.backgroundSecondary), - fontSize: 24 - ) - )) - ), - ], - ), - ) - ], - ), - Row(children: [ - Spacer(), - Text("Escanea la correa"), - Spacer(), - Text("Escanea el reloj"), - Spacer(), - ]), - Container( - padding: EdgeInsets.all(40), - decoration: BoxDecoration( - border: Border.all(color: theme.getColorFor(ThemeCode.textPrimary)), - borderRadius: BorderRadius.all(Radius.circular(16)) - ), - child: SvgPicture.asset("assets/images/ui/qr.svg") - ), - if (widget.step == 2)Column( - spacing: 16, - children: [ - Align( - alignment: Alignment.bottomLeft, - child: Text("O inserta el código del reloj"), - ), - Row( - spacing: 16, - children: [ - Expanded(child: CustomTextField( - hint: "XXXXXXXXXX", - )), - Expanded(child: PrimaryButton( - onPressed: ()=>{}, - text: "Continuar con código", - size: 16, - color: theme.getColorFor(ThemeCode.buttonSecondary) - )) - ], - ), - ], - ), - Column( - spacing: 8, - children: [ - Text("Si no consigues vincular su correa o reloj"), - CustomTextButton(onPressed: ()=>{}, text: "Contáctanos", weight: FontWeight.w500, size: 18) - ], - ) - ], - ); - } -} \ No newline at end of file diff --git a/modules/auth/lib/src/features/login/domain/get_me_user_use_case.dart b/modules/auth/lib/src/features/login/domain/get_me_user_use_case.dart new file mode 100644 index 00000000..33f492bc --- /dev/null +++ b/modules/auth/lib/src/features/login/domain/get_me_user_use_case.dart @@ -0,0 +1,5 @@ +import 'package:auth/src/core/data/models/get_me_response_model.dart'; + +abstract class GetMeUserUseCase { + Future getMe(); +} diff --git a/modules/auth/lib/src/features/login/domain/get_me_user_use_case_impl.dart b/modules/auth/lib/src/features/login/domain/get_me_user_use_case_impl.dart new file mode 100644 index 00000000..a5246deb --- /dev/null +++ b/modules/auth/lib/src/features/login/domain/get_me_user_use_case_impl.dart @@ -0,0 +1,20 @@ +import 'package:auth/src/core/data/models/get_me_response_model.dart'; +import 'package:auth/src/core/domain/repositories/auth_repository.dart'; +import 'package:auth/src/features/login/domain/get_me_user_use_case.dart'; + +class GetMeUserUseCaseImpl implements GetMeUserUseCase { + GetMeUserUseCaseImpl(this._repository); + + final AuthRepository _repository; + + @override + Future login({required String email, required String password}) { + return _repository.login(email: email, password: password); + } + + @override + Future getMe() { + // TODO: implement getMe + throw UnimplementedError(); + } +} diff --git a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart index 76228be2..54f355a3 100644 --- a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart +++ b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case.dart @@ -1,7 +1,8 @@ abstract class RecoverPasswordUseCase { Future requestEmail({required String email}); - Future requestSms({required String phone}); - - Future recoverPassword({required String newPassword, required String token}); -} \ No newline at end of file + Future recoverPassword({ + required String newPassword, + required String token, + }); +} diff --git a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart index 6699e8f8..c97b1c0d 100644 --- a/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart +++ b/modules/auth/lib/src/features/recover_password/domain/use_cases/recover_password_use_case_impl.dart @@ -12,12 +12,10 @@ class RecoverPasswordUseCaseImpl implements RecoverPasswordUseCase { } @override - Future requestSms({required String phone}) async { - return await _repository.requestPasswordReset(phone: phone); - } - - @override - Future recoverPassword({required String newPassword, required String token}) async { + Future recoverPassword({ + required String newPassword, + required String token, + }) async { await _repository.recoverPassword(newPassword: newPassword, token: token); } } diff --git a/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart index ddec9f64..c8c7777a 100644 --- a/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart +++ b/modules/auth/lib/src/features/recover_password/presentation/sent/sent_screen.dart @@ -37,7 +37,9 @@ class SentScreen extends ConsumerWidget { letterSpacing: 0, ), ), - SizedBox(height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40)), + SizedBox( + height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40), + ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -45,22 +47,36 @@ class SentScreen extends ConsumerWidget { Icons.check, color: theme.getColorFor(ThemeCode.buttonPrimary), ), - SizedBox(width: SizeUtils.getByScreen(small: 10, big: 10, xl: 6)), + SizedBox( + width: SizeUtils.getByScreen(small: 10, big: 10, xl: 6), + ), Text( viewState.recoveryFormat == "email" ? context.translate(I18n.emailSent) : context.translate(I18n.smsSent), - style: TextStyle(fontSize: SizeUtils.getByScreen(small: 18, big: 18, xl: 15), fontWeight: FontWeight.bold), + style: TextStyle( + fontSize: SizeUtils.getByScreen( + small: 18, + big: 18, + xl: 15, + ), + fontWeight: FontWeight.bold, + ), ), ], ), - SizedBox(height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40)), + SizedBox( + height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40), + ), Text( viewState.recoveryFormat == "email" ? context.translate(I18n.checkEmail1) : context.translate(I18n.checkSms1), textAlign: TextAlign.center, - style: TextStyle(fontSize: SizeUtils.getByScreen(small: 17, big: 17, xl: 15), letterSpacing: 0), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 17, big: 17, xl: 15), + letterSpacing: 0, + ), ), SizedBox(height: 16), Text( @@ -68,18 +84,21 @@ class SentScreen extends ConsumerWidget { ? context.translate(I18n.checkEmail2) : context.translate(I18n.checkSms2), textAlign: TextAlign.center, - style: TextStyle(fontSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12), letterSpacing: 0), + style: TextStyle( + fontSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12), + letterSpacing: 0, + ), + ), + SizedBox( + height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40), ), - SizedBox(height: SizeUtils.getByScreen(small: 48, big: 48, xl: 40)), Row( children: [ Expanded( child: SecondaryButton( onPressed: () { - if ( viewState.recoveryFormat == "email") { + if (viewState.recoveryFormat == "email") { viewModel.requestEmail(); - } else { - viewModel.requestSms(); } }, text: viewState.recoveryFormat == "email" @@ -93,7 +112,11 @@ class SentScreen extends ConsumerWidget { child: PrimaryButton( onPressed: () => Navigator.push( context, - MaterialPageRoute(builder: (_) => NewPasswordScreen(navigationContract: navigationContract)), + MaterialPageRoute( + builder: (_) => NewPasswordScreen( + navigationContract: navigationContract, + ), + ), ), text: context.translate(I18n.continueKey), color: theme.getColorFor(ThemeCode.buttonSecondary), diff --git a/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart index 2fd912c7..efe31eca 100644 --- a/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart +++ b/modules/auth/lib/src/features/recover_password/presentation/state/recover_password_view_model.dart @@ -6,9 +6,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/recover_password_provider.dart'; final recoverPasswordViewModelProvider = -NotifierProvider.autoDispose( - RecoverPasswordViewModel.new, -); + NotifierProvider.autoDispose< + RecoverPasswordViewModel, + RecoverPasswordViewState + >(RecoverPasswordViewModel.new); class RecoverPasswordViewModel extends Notifier { late final RecoverPasswordUseCase _recoverPasswordUseCase; @@ -104,27 +105,18 @@ class RecoverPasswordViewModel extends Notifier { } void updateDialCode(String dialCode) { - state = state.copyWith( - dialCode: dialCode, - errorMessage: '', - ); + state = state.copyWith(dialCode: dialCode, errorMessage: ''); } void updateNewDialCode(String dialCode) { - state = state.copyWith( - newDialCode: dialCode, - errorMessage: '', - ); + state = state.copyWith(newDialCode: dialCode, errorMessage: ''); } - void togglePasswordVisible(){ - state = state.copyWith( - passwordVisible: !state.passwordVisible, - ); + void togglePasswordVisible() { + state = state.copyWith(passwordVisible: !state.passwordVisible); } Future requestRecovery() async { - final trimmedNumber = state.phoneNumber.trim(); final email = state.email.trim(); state = state.copyWith( @@ -135,8 +127,6 @@ class RecoverPasswordViewModel extends Notifier { if (email.isNotEmpty) { await requestEmail(); - } else if (trimmedNumber.isNotEmpty) { - await requestSms(); } else { state = state.copyWith( isLoading: false, @@ -150,7 +140,9 @@ class RecoverPasswordViewModel extends Notifier { final email = state.email.trim(); try { - final String token = await _recoverPasswordUseCase.requestEmail(email: email); + final String token = await _recoverPasswordUseCase.requestEmail( + email: email, + ); if (!ref.mounted) return; state = state.copyWith( @@ -172,34 +164,6 @@ class RecoverPasswordViewModel extends Notifier { } } - Future requestSms() async { - final trimmedNumber = state.phoneNumber.trim(); - - final fullPhone = '${state.dialCode}$trimmedNumber'; - - try { - final String token = await _recoverPasswordUseCase.requestSms(phone: fullPhone); - if (!ref.mounted) return; - - state = state.copyWith( - isLoading: false, - errorMessage: '', - recoveryRequested: true, - token: token, - recoveryFormat: 'sms' - ); - } catch (e) { - if (!ref.mounted) return; - - state = state.copyWith( - isLoading: false, - errorMessage: e.toString(), - recoveryRequested: false, - passwordChanged: false, - ); - } - } - Future recoverPassword() async { //final String fullPhone = state.newDialCode + state.newPhoneNumber; final String password = state.password; @@ -244,17 +208,13 @@ class RecoverPasswordViewModel extends Notifier { return; } - state = state.copyWith( - isLoading: true, - passwordChanged: false, - ); + state = state.copyWith(isLoading: true, passwordChanged: false); try { await _recoverPasswordUseCase.recoverPassword( - newPassword: password, token: state.token); - state = state.copyWith( - isLoading: false, - passwordChanged: true, + newPassword: password, + token: state.token, ); + state = state.copyWith(isLoading: false, passwordChanged: true); } catch (error) { state = state.copyWith( errorMessage: error.toString(), @@ -276,4 +236,4 @@ class RecoverPasswordViewModel extends Notifier { newPhoneNumberController.removeListener(_onNewPhoneNumberChanged); newPhoneNumberController.dispose(); } -} \ No newline at end of file +} diff --git a/modules/auth/lib/src/features/sign_up/domain/generate_two_fa_sign_up_use_case_impl.dart b/modules/auth/lib/src/features/sign_up/domain/generate_two_fa_sign_up_use_case_impl.dart index c0a10acf..4125f2bb 100644 --- a/modules/auth/lib/src/features/sign_up/domain/generate_two_fa_sign_up_use_case_impl.dart +++ b/modules/auth/lib/src/features/sign_up/domain/generate_two_fa_sign_up_use_case_impl.dart @@ -1,3 +1,4 @@ +import 'package:auth/src/core/data/models/two_fa_secret_response_model.dart'; import 'package:auth/src/core/domain/repositories/auth_repository.dart'; import 'package:auth/src/features/sign_up/domain/entities/two_fa_secret_entity.dart'; import 'package:auth/src/features/sign_up/domain/generate_two_fa_sign_up_use_case.dart'; @@ -8,6 +9,8 @@ class GenerateTwoFASignUpUseCaseImpl implements GenerateTwoFASignUpUseCase { final AuthRepository _repository; @override Future generateTwoFASignUp({required String token}) { - return _repository.generateTwoFASignUp(token: token); + return _repository + .generateTwoFASignUp(token: token) + .then((model) => model.toEntity()); } } diff --git a/modules/auth/pubspec.yaml b/modules/auth/pubspec.yaml index 02c81046..07194a58 100644 --- a/modules/auth/pubspec.yaml +++ b/modules/auth/pubspec.yaml @@ -38,6 +38,10 @@ dependencies: json_annotation: ^4.9.0 json_serializable: ^6.11.2 uuid: ^4.5.2 + mobile_scanner: ^7.1.4 + dio_cookie_manager: ^3.3.0 + cookie_jar: ^4.0.8 + path_provider: ^2.1.5 dev_dependencies: flutter_test: diff --git a/modules/home/lib/src/presentation/home_screen.dart b/modules/home/lib/src/presentation/home_screen.dart index 9197b9d1..7897353f 100644 --- a/modules/home/lib/src/presentation/home_screen.dart +++ b/modules/home/lib/src/presentation/home_screen.dart @@ -53,7 +53,8 @@ class HomeScreen extends ConsumerWidget { Align( alignment: Alignment.topLeft, child: TextButton( - onPressed: () => navigationContract.pushTo(AppRoutes.deviceSignup), + onPressed: () => + navigationContract.pushTo(AppRoutes.deviceSetup), child: Text( "+ Añadir otro peque", style: TextStyle( @@ -63,7 +64,11 @@ class HomeScreen extends ConsumerWidget { ), ), ), - WalletBalanceBlock(max: total, value: total - available, savings: savings), + WalletBalanceBlock( + max: total, + value: total - available, + savings: savings, + ), DepositBlock(max: 150 - total), ], ), @@ -73,7 +78,6 @@ class HomeScreen extends ConsumerWidget { } Widget walletsList(BuildContext context, List kids, WidgetRef ref) { - return Column( spacing: 20, children: List.generate(kids.length, (int index) { diff --git a/modules/splash/lib/src/presentation/splash_screen.dart b/modules/splash/lib/src/presentation/splash_screen.dart index a1e165c9..d91647f7 100644 --- a/modules/splash/lib/src/presentation/splash_screen.dart +++ b/modules/splash/lib/src/presentation/splash_screen.dart @@ -30,7 +30,7 @@ class _SplashScreenState extends State { Widget build(BuildContext context) { return AnimatedSplashScreen( splash: Image.asset('assets/images/logos/splash.gif'), - splashIconSize: 2000.0, + splashIconSize: 900.0, nextScreen: const SizedBox.shrink(), disableNavigation: true, backgroundColor: Colors.white, diff --git a/packages/design_system/lib/src/steps/step_indicator.dart b/packages/design_system/lib/src/steps/step_indicator.dart index 3dad7fd9..0a0df0ec 100644 --- a/packages/design_system/lib/src/steps/step_indicator.dart +++ b/packages/design_system/lib/src/steps/step_indicator.dart @@ -1,18 +1,23 @@ import 'package:flutter/material.dart'; -class StepIndicator extends StatelessWidget{ +class StepIndicator extends StatelessWidget { final int total; final int current; final Color color; - const StepIndicator({super.key, required this.total, required this.current, required this.color}); + const StepIndicator({ + super.key, + required this.total, + required this.current, + required this.color, + }); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - ...List.generate(total, (index){ + ...List.generate(total, (index) { final bool isActive = index < current; return AnimatedContainer( @@ -24,11 +29,11 @@ class StepIndicator extends StatelessWidget{ decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? color : Colors.white, - border: Border.all(color: color), + border: Border.all(color: isActive ? color : Colors.black), ), ); }), - ] + ], ); } -} \ No newline at end of file +} diff --git a/packages/navigation/lib/app_routes.dart b/packages/navigation/lib/app_routes.dart index 9d368821..0d6b2864 100644 --- a/packages/navigation/lib/app_routes.dart +++ b/packages/navigation/lib/app_routes.dart @@ -5,7 +5,7 @@ class AppRoutes { static const onboarding = '/onboarding'; static const linkPhone = '/request_link_phone'; static const phoneCode = '/verify_link_phone_code'; - static const deviceSignup = '/device_signup'; + static const deviceSetup = '/device_setup'; static const recoverPassword = '/recover_password'; static const dashboard = '/dashboard'; diff --git a/packages/sf_infrastructure/lib/src/network/dio_client.dart b/packages/sf_infrastructure/lib/src/network/dio_client.dart index 125ba431..14ddf93a 100644 --- a/packages/sf_infrastructure/lib/src/network/dio_client.dart +++ b/packages/sf_infrastructure/lib/src/network/dio_client.dart @@ -1,10 +1,12 @@ +import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; Dio buildDioClient({ required String baseUrl, required String origin, - // required String apiKey, bool log = false, + CookieJar? cookieJar, }) { final dio = Dio( BaseOptions( @@ -13,7 +15,6 @@ Dio buildDioClient({ receiveTimeout: const Duration(seconds: 20), sendTimeout: const Duration(seconds: 20), headers: { - // if (apiKey.isNotEmpty) 'x-api-key': apiKey, 'accept': 'application/json', 'content-type': 'application/json', 'origin': origin, @@ -21,13 +22,16 @@ Dio buildDioClient({ ), ); + final jar = cookieJar ?? CookieJar(); + dio.interceptors.add(CookieManager(jar)); + if (log) { dio.interceptors.add( LogInterceptor( request: true, requestHeader: false, requestBody: true, - responseHeader: false, + responseHeader: true, responseBody: true, error: true, ), diff --git a/packages/sf_infrastructure/pubspec.yaml b/packages/sf_infrastructure/pubspec.yaml index 58b9290d..423e59a1 100644 --- a/packages/sf_infrastructure/pubspec.yaml +++ b/packages/sf_infrastructure/pubspec.yaml @@ -8,10 +8,13 @@ environment: flutter: ">=1.17.0" dependencies: - dio: ^5.9.0 + dio_cookie_manager: ^3.3.0 + cookie_jar: ^4.0.8 + path_provider: ^2.1.5 flutter: sdk: flutter get_it: ^9.0.5 + dio: ^5.9.0 dev_dependencies: flutter_test: diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 1fca4204..8a07ee50 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -130,5 +130,34 @@ "secretCodeKeyCopied": "Schlüssel kopiert", "secretCodeStep3Title": "Generierten Code kopieren", "secretCodeStep3Body": "Nachdem du den QR-Code gescannt oder den Schlüssel in der Authenticator-App eingegeben hast, kopiere den generierten 6-stelligen Code und gib ihn im nächsten Bildschirm ein.", - "secretCodeConfigure": "Einrichten" + "secretCodeConfigure": "Einrichten", + "deviceSetup_intro_title": "Füge dein Kind hinzu", + "deviceSetup_intro_subtitle": "Behalte die Ausgaben im Blick, während es verantwortungsvolle Finanzgewohnheiten lernt", + "deviceSetup_intro_step_1": "Erstelle sein Profil", + "deviceSetup_intro_step_2": "Verbinde seine Uhr und das Armband", + "deviceSetup_intro_step_3": "Lade sein Sparschwein auf", + "deviceSetup_intro_ready_title": "Und fertig - alles ist bereit, damit es sein Geld hat!", + "deviceSetup_intro_remember_prefix": "Denk daran, dass du einen", + "deviceSetup_intro_plan_name": "SaveFamily-Plan", + "deviceSetup_intro_web_prefix": "Wenn du ihn noch nicht hast, kannst du ihn über ", + "deviceSetup_intro_web_link": "unsere Website bekommen", + "deviceSetup_linkInfo_title": "Verbinde Armband und Uhr", + "deviceSetup_linkInfo_item1_prefix": "Scanne das ", + "deviceSetup_linkInfo_item1_boldWord": "Armband", + "deviceSetup_linkInfo_item1_subtitle": "Dein Kind kann damit Zahlungen durchführen", + "deviceSetup_linkInfo_item2_prefix": "Scanne die ", + "deviceSetup_linkInfo_item2_boldWord": "Uhr", + "deviceSetup_linkInfo_item2_subtitle": "Du kannst die getätigten Ausgaben sehen", + "deviceSetup_watchCode_orInsert": "Oder gib den Code der Uhr ein", + "deviceSetup_watchCode_continueWithCode": "Mit Code fortfahren", + "deviceSetup_linkTroubleshoot_title": "Wenn du das Armband oder die Uhr nicht verbinden kannst", + "deviceSetup_contactUs": "Kontaktiere uns", + "deviceSetup_accountData_info": "Wir benötigen diese Angaben, um das Konto zu erstellen und Taschengeld sowie Ausgaben zu verwalten", + "deviceSetup_startWithOneKid_info": "Starte mit einem Kind, später kannst du weitere hinzufügen", + "deviceSetup_firstAllowance_title": "Du kannst ihnen jetzt das erste Taschengeld geben, damit sie es auf ihrer Uhr nutzen können", + "deviceSetup_addAnotherKid": "Ein weiteres Kind hinzufügen", + "deviceSetup_start": "Los geht's!", + "deviceSetup_giveFirstAllowance": "Gib das erste Taschengeld", + "deviceSetup_scanQr": "QR scannen", + "deviceSetup_scanQr_hint": "Richte den QR-Code innerhalb des Rahmens aus" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 9e4de259..584432c6 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -119,8 +119,8 @@ "passwordRulesSubtitle": "Minimum 8 characters, including an uppercase letter, a number, and a special character", "accountCreatedTitle": "Account created", "accountCreatedForLabel": "You created the account for:", - "accountCreatedEmailVerificationSentLabel": "We’ve sent a verification email to:", - "accountCreatedChildSetupHint": "Create your child’s account and enter their \nfirst allowance to use it with their watch", + "accountCreatedEmailVerificationSentLabel": "We've sent a verification email to:", + "accountCreatedChildSetupHint": "Create your child's account and enter their \nfirst allowance to use it with their watch", "accountCreatedContinue": "Continue", "secretCodeTitle": "Setup instructions", "secretCodeStep1Title": "Download an authenticator app", @@ -130,5 +130,34 @@ "secretCodeKeyCopied": "Key copied", "secretCodeStep3Title": "Copy the generated code", "secretCodeStep3Body": "After scanning the QR code or entering the key in the authenticator app, copy the generated 6-digit code and enter it on the next screen.", - "secretCodeConfigure": "Set up" + "secretCodeConfigure": "Set up", + "deviceSetup_intro_title": "Add your child", + "deviceSetup_intro_subtitle": "Track their spending while they learn responsible financial habits", + "deviceSetup_intro_step_1": "Create their profile", + "deviceSetup_intro_step_2": "Link their watch and band", + "deviceSetup_intro_step_3": "Top up their piggy bank", + "deviceSetup_intro_ready_title": "And you're all set so they can have their money!", + "deviceSetup_intro_remember_prefix": "Remember you need to have a", + "deviceSetup_intro_plan_name": "SaveFamily Plan", + "deviceSetup_intro_web_prefix": "If you don't have it yet, you can get it through ", + "deviceSetup_intro_web_link": "our website", + "deviceSetup_linkInfo_title": "Link their band and watch", + "deviceSetup_linkInfo_item1_prefix": "Scan the ", + "deviceSetup_linkInfo_item1_boldWord": "band", + "deviceSetup_linkInfo_item1_subtitle": "Your child will be able to make payments", + "deviceSetup_linkInfo_item2_prefix": "Scan the ", + "deviceSetup_linkInfo_item2_boldWord": "watch", + "deviceSetup_linkInfo_item2_subtitle": "You'll be able to see the expenses made", + "deviceSetup_watchCode_orInsert": "Or enter the watch code", + "deviceSetup_watchCode_continueWithCode": "Continue with code", + "deviceSetup_linkTroubleshoot_title": "If you can't link their band or watch", + "deviceSetup_contactUs": "Contact us", + "deviceSetup_accountData_info": "We need this information to create their account and manage their allowances and spending", + "deviceSetup_startWithOneKid_info": "Start with one child; you can add more later", + "deviceSetup_firstAllowance_title": "You can now give them their first allowance so they can start enjoying it on their watch", + "deviceSetup_addAnotherKid": "Add another child", + "deviceSetup_start": "Start!", + "deviceSetup_giveFirstAllowance": "Give their first allowance", + "deviceSetup_scanQr": "Scan QR", + "deviceSetup_scanQr_hint": "Center the QR inside the frame" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index 8beabf1a..76a7dc45 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -130,5 +130,34 @@ "secretCodeKeyCopied": "Llave copiada", "secretCodeStep3Title": "Copia el código generado", "secretCodeStep3Body": "Después de escanear el código QR o introducir la llave en la aplicación de autenticación, copia el código generado de 6 dígitos e introdúcelo en la siguiente pantalla.", - "secretCodeConfigure": "Configurar" + "secretCodeConfigure": "Configurar", + "deviceSetup_intro_title": "Añade a tu peque", + "deviceSetup_intro_subtitle": "Controla su gasto a la vez que aprende hábitos financieros responsables", + "deviceSetup_intro_step_1": "Crea su perfil", + "deviceSetup_intro_step_2": "Vincula su reloj y su correa", + "deviceSetup_intro_step_3": "Carga su hucha", + "deviceSetup_intro_ready_title": "¡Y todo listo para que tenga su dinero!", + "deviceSetup_intro_remember_prefix": "Recuerda que necesitas tener un", + "deviceSetup_intro_plan_name": "Plan SaveFamily", + "deviceSetup_intro_web_prefix": "Si aún no lo tienes, puedes conseguirlo a través de ", + "deviceSetup_intro_web_link": "nuestra web", + "deviceSetup_linkInfo_title": "Vincula su correa y su reloj", + "deviceSetup_linkInfo_item1_prefix": "Escanea la ", + "deviceSetup_linkInfo_item1_boldWord": "correa", + "deviceSetup_linkInfo_item1_subtitle": "El peque podrá realizar pagos", + "deviceSetup_linkInfo_item2_prefix": "Escanea el ", + "deviceSetup_linkInfo_item2_boldWord": "reloj", + "deviceSetup_linkInfo_item2_subtitle": "Visualizarás los gastos que se hagan", + "deviceSetup_watchCode_orInsert": "O inserta el código del reloj", + "deviceSetup_watchCode_continueWithCode": "Continuar con código", + "deviceSetup_linkTroubleshoot_title": "Si no consigues vincular su correa o reloj", + "deviceSetup_contactUs": "Contáctanos", + "deviceSetup_accountData_info": "Necesitamos estos datos para crear su cuenta y gestionar sus pagas y gastos", + "deviceSetup_startWithOneKid_info": "Comienza con un peque; luego podrás agregar más", + "deviceSetup_firstAllowance_title": "Ya puedes darle su primera paga para que empiece a disfrutarla en su reloj", + "deviceSetup_addAnotherKid": "Añadir otro peque", + "deviceSetup_start": "¡Empezar!", + "deviceSetup_giveFirstAllowance": "Dale su primera paga", + "deviceSetup_scanQr": "Escanear QR", + "deviceSetup_scanQr_hint": "Centra el QR dentro del recuadro" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index ae8be5a0..e25a04f0 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -120,15 +120,44 @@ "accountCreatedTitle": "Compte créé", "accountCreatedForLabel": "Vous avez créé le compte pour :", "accountCreatedEmailVerificationSentLabel": "Nous avons envoyé un e-mail de vérification à :", - "accountCreatedChildSetupHint": "Créez le compte de votre enfant et saisissez sa \npremière allocation pour l’utiliser avec sa montre", + "accountCreatedChildSetupHint": "Créez le compte de votre enfant et saisissez sa \npremière allocation pour l'utiliser avec sa montre", "accountCreatedContinue": "Continuer", "secretCodeTitle": "Instructions de configuration", - "secretCodeStep1Title": "Téléchargez une application d’authentification", - "secretCodeStep1Body": "Assurez-vous d’avoir Google Authenticator sur votre appareil.", + "secretCodeStep1Title": "Téléchargez une application d'authentification", + "secretCodeStep1Body": "Assurez-vous d'avoir Google Authenticator sur votre appareil.", "secretCodeStep2Title": "Scannez le code QR ou copiez la clé", - "secretCodeStep2Body": "Scannez le code QR ci-dessous avec l’application d’authentification pour vérifier l’appareil.\n\nOu copiez la clé et saisissez-la manuellement dans l’application d’authentification.", + "secretCodeStep2Body": "Scannez le code QR ci-dessous avec l'application d'authentification pour vérifier l'appareil.\n\nOu copiez la clé et saisissez-la manuellement dans l'application d'authentification.", "secretCodeKeyCopied": "Clé copiée", "secretCodeStep3Title": "Copiez le code généré", - "secretCodeStep3Body": "Après avoir scanné le code QR ou saisi la clé dans l’application d’authentification, copiez le code à 6 chiffres généré et saisissez-le sur l’écran suivant.", - "secretCodeConfigure": "Configurer" + "secretCodeStep3Body": "Après avoir scanné le code QR ou saisi la clé dans l'application d'authentification, copiez le code à 6 chiffres généré et saisissez-le sur l'écran suivant.", + "secretCodeConfigure": "Configurer", + "deviceSetup_intro_title": "Ajoutez votre enfant", + "deviceSetup_intro_subtitle": "Suivez ses dépenses tout en l'aidant à adopter des habitudes financières responsables", + "deviceSetup_intro_step_1": "Créez son profil", + "deviceSetup_intro_step_2": "Associez sa montre et son bracelet", + "deviceSetup_intro_step_3": "Alimentez sa cagnotte", + "deviceSetup_intro_ready_title": "Et voilà, tout est prêt pour qu'il/elle ait son argent !", + "deviceSetup_intro_remember_prefix": "N'oubliez pas que vous devez avoir un", + "deviceSetup_intro_plan_name": "Plan SaveFamily", + "deviceSetup_intro_web_prefix": "Si vous ne l'avez pas encore, vous pouvez l'obtenir via ", + "deviceSetup_intro_web_link": "notre site web", + "deviceSetup_linkInfo_title": "Associez son bracelet et sa montre", + "deviceSetup_linkInfo_item1_prefix": "Scanne le ", + "deviceSetup_linkInfo_item1_boldWord": "bracelet", + "deviceSetup_linkInfo_item1_subtitle": "Votre enfant pourra effectuer des paiements", + "deviceSetup_linkInfo_item2_prefix": "Scanne la ", + "deviceSetup_linkInfo_item2_boldWord": "montre", + "deviceSetup_linkInfo_item2_subtitle": "Vous verrez les dépenses effectuées", + "deviceSetup_watchCode_orInsert": "Ou saisissez le code de la montre", + "deviceSetup_watchCode_continueWithCode": "Continuer avec un code", + "deviceSetup_linkTroubleshoot_title": "Si vous n'arrivez pas à associer son bracelet ou sa montre", + "deviceSetup_contactUs": "Contactez-nous", + "deviceSetup_accountData_info": "Nous avons besoin de ces informations pour créer son compte et gérer ses allocations et dépenses", + "deviceSetup_startWithOneKid_info": "Commencez avec un enfant, vous pourrez en ajouter d'autres ensuite", + "deviceSetup_firstAllowance_title": "Vous pouvez maintenant lui donner sa première allocation pour qu'il/elle commence à en profiter sur sa montre", + "deviceSetup_addAnotherKid": "Ajouter un autre enfant", + "deviceSetup_start": "Commencer!", + "deviceSetup_giveFirstAllowance": "Donner sa première allocation", + "deviceSetup_scanQr": "Scanner le QR", + "deviceSetup_scanQr_hint": "Place le QR au centre du cadre" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index d1c54076..939a1f52 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -118,17 +118,46 @@ "stepAddressTitle": "Il tuo indirizzo", "passwordRulesSubtitle": "Password minima di 8 caratteri, con una maiuscola, un numero e un carattere speciale", "accountCreatedTitle": "Account creato", - "accountCreatedForLabel": "Hai creato l’account per:", - "accountCreatedEmailVerificationSentLabel": "Abbiamo inviato un’email di verifica a:", - "accountCreatedChildSetupHint": "Crea l’account del tuo bambino e inserisci la sua \nprima paghetta per usarlo con il suo orologio", + "accountCreatedForLabel": "Hai creato l'account per:", + "accountCreatedEmailVerificationSentLabel": "Abbiamo inviato un'email di verifica a:", + "accountCreatedChildSetupHint": "Crea l'account del tuo bambino e inserisci la sua \nprima paghetta per usarlo con il suo orologio", "accountCreatedContinue": "Continua", "secretCodeTitle": "Istruzioni di configurazione", - "secretCodeStep1Title": "Scarica un’app di autenticazione", + "secretCodeStep1Title": "Scarica un'app di autenticazione", "secretCodeStep1Body": "Assicurati di avere Google Authenticator sul tuo dispositivo.", "secretCodeStep2Title": "Scansiona il codice QR o copia la chiave", - "secretCodeStep2Body": "Scansiona il codice QR qui sotto con l’app di autenticazione per verificare il dispositivo.\n\nOppure copia la chiave e inseriscila manualmente nell’app di autenticazione.", + "secretCodeStep2Body": "Scansiona il codice QR qui sotto con l'app di autenticazione per verificare il dispositivo.\n\nOppure copia la chiave e inseriscila manualmente nell'app di autenticazione.", "secretCodeKeyCopied": "Chiave copiata", "secretCodeStep3Title": "Copia il codice generato", - "secretCodeStep3Body": "Dopo aver scansionato il codice QR o inserito la chiave nell’app di autenticazione, copia il codice a 6 cifre generato e inseriscilo nella schermata successiva.", - "secretCodeConfigure": "Configura" + "secretCodeStep3Body": "Dopo aver scansionato il codice QR o inserito la chiave nell'app di autenticazione, copia il codice a 6 cifre generato e inseriscilo nella schermata successiva.", + "secretCodeConfigure": "Configura", + "deviceSetup_intro_title": "Aggiungi il tuo bambino", + "deviceSetup_intro_subtitle": "Controlla le sue spese mentre impara abitudini finanziarie responsabili", + "deviceSetup_intro_step_1": "Crea il suo profilo", + "deviceSetup_intro_step_2": "Collega il suo orologio e il cinturino", + "deviceSetup_intro_step_3": "Ricarica il suo salvadanaio", + "deviceSetup_intro_ready_title": "E tutto è pronto perché abbia i suoi soldi!", + "deviceSetup_intro_remember_prefix": "Ricorda che devi avere un", + "deviceSetup_intro_plan_name": "Piano SaveFamily", + "deviceSetup_intro_web_prefix": "Se non ce l'hai ancora, puoi ottenerlo tramite ", + "deviceSetup_intro_web_link": "il nostro sito web", + "deviceSetup_linkInfo_title": "Collega il cinturino e l'orologio", + "deviceSetup_linkInfo_item1_prefix": "Scansiona il ", + "deviceSetup_linkInfo_item1_boldWord": "cinturino", + "deviceSetup_linkInfo_item1_subtitle": "Il bambino potrà effettuare pagamenti", + "deviceSetup_linkInfo_item2_prefix": "Scansiona l'", + "deviceSetup_linkInfo_item2_boldWord": "orologio", + "deviceSetup_linkInfo_item2_subtitle": "Potrai visualizzare le spese effettuate", + "deviceSetup_watchCode_orInsert": "Oppure inserisci il codice dell'orologio", + "deviceSetup_watchCode_continueWithCode": "Continua con il codice", + "deviceSetup_linkTroubleshoot_title": "Se non riesci a collegare il cinturino o l'orologio", + "deviceSetup_contactUs": "Contactez-nous", + "deviceSetup_accountData_info": "Abbiamo bisogno di questi dati per creare il suo conto e gestire paghette e spese", + "deviceSetup_startWithOneKid_info": "Inizia con un bambino, poi potrai aggiungerne altri", + "deviceSetup_firstAllowance_title": "Ora puoi dargli la sua prima paghetta così potrà iniziare a usarla sul suo orologio", + "deviceSetup_addAnotherKid": "Aggiungi un altro bambino", + "deviceSetup_start": "Inizia!", + "deviceSetup_giveFirstAllowance": "Dagli la sua prima paghetta", + "deviceSetup_scanQr": "Scansiona QR", + "deviceSetup_scanQr_hint": "Centra il QR all’interno del riquadro" } \ No newline at end of file diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 4fb3a0a0..8dbae710 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -130,5 +130,34 @@ "secretCodeKeyCopied": "Chave copiada", "secretCodeStep3Title": "Copia o código gerado", "secretCodeStep3Body": "Depois de leres o código QR ou introduzires a chave na aplicação de autenticação, copia o código de 6 dígitos gerado e introduz-lo no ecrã seguinte.", - "secretCodeConfigure": "Configurar" + "secretCodeConfigure": "Configurar", + "deviceSetup_intro_title": "Adicione o seu filho", + "deviceSetup_intro_subtitle": "Acompanhe os gastos enquanto ele aprende hábitos financeiros responsáveis", + "deviceSetup_intro_step_1": "Crie o perfil dele", + "deviceSetup_intro_step_2": "Vincule o relógio e a pulseira", + "deviceSetup_intro_step_3": "Carregue o cofrinho dele", + "deviceSetup_intro_ready_title": "E pronto, tudo preparado para ele ter o dinheiro dele!", + "deviceSetup_intro_remember_prefix": "Lembre-se de que precisa de um", + "deviceSetup_intro_plan_name": "Plano SaveFamily", + "deviceSetup_intro_web_prefix": "Se ainda não tem, pode conseguir através do ", + "deviceSetup_intro_web_link": "nosso site", + "deviceSetup_linkInfo_title": "Vincula a pulseira e o relógio", + "deviceSetup_linkInfo_item1_prefix": "Digitaliza a ", + "deviceSetup_linkInfo_item1_boldWord": "pulseira", + "deviceSetup_linkInfo_item1_subtitle": "A criança poderá realizar pagamentos", + "deviceSetup_linkInfo_item2_prefix": "Digitaliza o ", + "deviceSetup_linkInfo_item2_boldWord": "relógio", + "deviceSetup_linkInfo_item2_subtitle": "Poderás visualizar os gastos efetuados", + "deviceSetup_watchCode_orInsert": "Ou introduz o código do relógio", + "deviceSetup_watchCode_continueWithCode": "Continuar com código", + "deviceSetup_linkTroubleshoot_title": "Se não conseguires vincular a pulseira ou o relógio", + "deviceSetup_contactUs": "Contacta-nos", + "deviceSetup_accountData_info": "Precisamos destes dados para criar a conta e gerir as mesadas e os gastos", + "deviceSetup_startWithOneKid_info": "Começa com uma criança; depois podes adicionar mais", + "deviceSetup_firstAllowance_title": "Agora já podes dar-lhe a primeira mesada para que comece a aproveitá-la no relógio", + "deviceSetup_addAnotherKid": "Adicionar outra criança", + "deviceSetup_start": "Começar!", + "deviceSetup_giveFirstAllowance": "Dá-lhe a primeira mesada", + "deviceSetup_scanQr": "Digitalizar QR", + "deviceSetup_scanQr_hint": "Centraliza o QR dentro da moldura" } \ No newline at end of file diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 74e9e434..91243e52 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -161,4 +161,50 @@ class I18n { static const String secretCodeStep3Title = 'secretCodeStep3Title'; static const String secretCodeStep3Body = 'secretCodeStep3Body'; static const String secretCodeConfigure = 'secretCodeConfigure'; + static const String deviceSetup_intro_title = 'deviceSetup_intro_title'; + static const String deviceSetup_intro_subtitle = 'deviceSetup_intro_subtitle'; + static const String deviceSetup_intro_step_1 = 'deviceSetup_intro_step_1'; + static const String deviceSetup_intro_step_2 = 'deviceSetup_intro_step_2'; + static const String deviceSetup_intro_step_3 = 'deviceSetup_intro_step_3'; + static const String deviceSetup_intro_ready_title = + 'deviceSetup_intro_ready_title'; + static const String deviceSetup_intro_remember_prefix = + 'deviceSetup_intro_remember_prefix'; + static const String deviceSetup_intro_plan_name = + 'deviceSetup_intro_plan_name'; + static const String deviceSetup_intro_web_prefix = + 'deviceSetup_intro_web_prefix'; + static const String deviceSetup_intro_web_link = 'deviceSetup_intro_web_link'; + static const String deviceSetup_linkInfo_title = 'deviceSetup_linkInfo_title'; + static const String deviceSetup_linkInfo_item1_prefix = + 'deviceSetup_linkInfo_item1_prefix'; + static const String deviceSetup_linkInfo_item1_boldWord = + 'deviceSetup_linkInfo_item1_boldWord'; + static const String deviceSetup_linkInfo_item1_subtitle = + 'deviceSetup_linkInfo_item1_subtitle'; + static const String deviceSetup_linkInfo_item2_prefix = + 'deviceSetup_linkInfo_item2_prefix'; + static const String deviceSetup_linkInfo_item2_boldWord = + 'deviceSetup_linkInfo_item2_boldWord'; + static const String deviceSetup_linkInfo_item2_subtitle = + 'deviceSetup_linkInfo_item2_subtitle'; + static const String deviceSetup_watchCode_orInsert = + 'deviceSetup_watchCode_orInsert'; + static const String deviceSetup_watchCode_continueWithCode = + 'deviceSetup_watchCode_continueWithCode'; + static const String deviceSetup_linkTroubleshoot_title = + 'deviceSetup_linkTroubleshoot_title'; + static const String deviceSetup_contactUs = 'deviceSetup_contactUs'; + static const String deviceSetup_accountData_info = + 'deviceSetup_accountData_info'; + static const String deviceSetup_startWithOneKid_info = + 'deviceSetup_startWithOneKid_info'; + static const String deviceSetup_firstAllowance_title = + 'deviceSetup_firstAllowance_title'; + static const String deviceSetup_addAnotherKid = 'deviceSetup_addAnotherKid'; + static const String deviceSetup_start = 'deviceSetup_start'; + static const String deviceSetup_giveFirstAllowance = + 'deviceSetup_giveFirstAllowance'; + static const String deviceSetup_scanQr = 'deviceSetup_scanQr'; + static const String deviceSetup_scanQr_hint = 'deviceSetup_scanQr_hint'; }