added device setup flow, qr reader, createChildProfile models and cookies packages

This commit is contained in:
2026-01-20 07:37:29 +01:00
parent b90d1f635c
commit 80b0750f62
71 changed files with 3267 additions and 626 deletions

View File

@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:label="SaveFamily"
android:name="${applicationName}"

View File

@@ -2,20 +2,40 @@ PODS:
- Flutter (1.0.0)
- flutter_native_splash (2.4.3):
- Flutter
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e

View File

@@ -199,6 +199,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
437F5EA1E5D92D7C421FD996 /* [CP] Embed Pods Frameworks */,
791C3CA41F1AAEE1267769C8 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -325,6 +326,23 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
791C3CA41F1AAEE1267769C8 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

View File

@@ -47,6 +47,8 @@
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para escanear códigos QR</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>

View File

@@ -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<void> 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<PlatformApp>
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(

View File

@@ -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) {

View File

@@ -0,0 +1,18 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/widgets.dart';
final appLifecycleStateProvider =
NotifierProvider<AppLifecycleNotifier, AppLifecycleState>(
AppLifecycleNotifier.new,
);
class AppLifecycleNotifier extends Notifier<AppLifecycleState> {
@override
AppLifecycleState build() {
return AppLifecycleState.resumed;
}
void setState(AppLifecycleState newState) {
state = newState;
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
final permissionsProvider =
NotifierProvider<PermissionsNotifier, PermissionsState>(
PermissionsNotifier.new,
);
class PermissionsNotifier extends Notifier<PermissionsState> {
@override
PermissionsState build() {
final initialState = PermissionsState();
checkPermissions();
return initialState;
}
Future<void> 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<bool> requestCameraAccess() async {
final status = await Permission.camera.request();
state = state.copyWith(camera: status);
_checkPerssionState(status);
return status == PermissionStatus.granted;
}
Future<bool> 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,
);
}
}

View File

@@ -0,0 +1,2 @@
export 'app_state_provider.dart';
export 'permissions/permissions_provider.dart';

View File

@@ -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"

View File

@@ -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

View File

@@ -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';

View File

@@ -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<MeUserModel> getMe();
Future<void> requestPhoneCode({required String phone});
Future<void> verifyPhoneCode({required String phone, required String code});
@@ -11,12 +15,25 @@ abstract class AuthRemoteDatasource {
Future<void> twoFALogin({required String token, required String code});
Future<String> signUp({required SignUpRequestEntity request});
Future<TwoFASecretEntity> generateTwoFASignUp({required String token});
Future<TwoFASecretResponseModel> generateTwoFASignUp({required String token});
Future<void> verifyTwoFACodeSignUp({
required String token,
required String code,
});
Future<String> requestPasswordReset({String? phone, String? email});
Future<String> requestPasswordReset({required String email});
Future<void> recoverPassword({required newPassword, required token});
Future<String> 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,
});
}

View File

@@ -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<MeUserModel> getMe() async {
try {
final response = await _repository.get<Map<String, dynamic>>('/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<void> requestPhoneCode({required String phone}) async {
try {
@@ -127,7 +145,9 @@ class AuthRemoteDatasourceImpl implements AuthRemoteDatasource {
}
@override
Future<TwoFASecretEntity> generateTwoFASignUp({required String token}) async {
Future<TwoFASecretResponseModel> generateTwoFASignUp({
required String token,
}) async {
try {
final response = await _repository.post<Map<String, dynamic>>(
'/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<String> requestPasswordReset({
String? phone,
String? email
}) async {
Future<String> requestPasswordReset({required String email}) async {
try {
if (phone == null && email == null) {
throw FormatException("No phone or email address given");
}
late final Map<String, dynamic> body;
body = {'email': email};
// late final Map<String, dynamic> 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<Map<String, dynamic>>(
final response = await _repository.put<String>(
'/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: <String, dynamic>{'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<String> 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<Map<String, dynamic>>(
'/auth/child-profiles',
body: <String, dynamic>{
'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');
}
}
}

View File

@@ -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<String, dynamic> 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<String, dynamic> json) =>
_$MeUserModelFromJson(json);
}

View File

@@ -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>(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<GetMeResponseModel> get copyWith => _$GetMeResponseModelCopyWithImpl<GetMeResponseModel>(this as GetMeResponseModel, _$identity);
/// Serializes this GetMeResponseModel to a JSON map.
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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<MeUserModel> get copyWith => _$MeUserModelCopyWithImpl<MeUserModel>(this as MeUserModel, _$identity);
/// Serializes this MeUserModel to a JSON map.
Map<String, dynamic> 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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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<String, dynamic> 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<String, dynamic> 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

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_me_response_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_GetMeResponseModel _$GetMeResponseModelFromJson(Map<String, dynamic> json) =>
_GetMeResponseModel(
item: MeUserModel.fromJson(json['item'] as Map<String, dynamic>),
);
Map<String, dynamic> _$GetMeResponseModelToJson(_GetMeResponseModel instance) =>
<String, dynamic>{'item': instance.item};
_MeUserModel _$MeUserModelFromJson(Map<String, dynamic> 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<String, dynamic> _$MeUserModelToJson(_MeUserModel instance) =>
<String, dynamic>{
'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,
};

View File

@@ -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<TwoFASecretEntity> generateTwoFASignUp({required String token}) {
Future<TwoFASecretResponseModel> generateTwoFASignUp({
required String token,
}) {
return _remote.generateTwoFASignUp(token: token);
}
@@ -47,8 +50,8 @@ class AuthRepositoryImpl implements AuthRepository {
}
@override
Future<String> requestPasswordReset({String? phone, String? email}) {
return _remote.requestPasswordReset(phone: phone, email: email);
Future<String> 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<String> 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,
);
}
}

View File

@@ -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<String> login({required String email, required String password});
Future<void> twoFactor({required String token, required String code});
Future<String> requestPasswordReset({String phone, String email});
Future<String> requestPasswordReset({required String email});
Future<void> recoverPassword({
required String newPassword,
@@ -18,9 +19,22 @@ abstract class AuthRepository {
Future<String> signUp({required SignUpRequestEntity request});
Future<TwoFASecretEntity> generateTwoFASignUp({required String token});
Future<TwoFASecretResponseModel> generateTwoFASignUp({required String token});
Future<void> verifyTwoFACodeSignUp({
required String token,
required String code,
});
Future<String> 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,
});
}

View File

@@ -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<void> buildPage(BuildContext context, GoRouterState state) {
final NavigationContract navigationContract = GetIt.I<NavigationContract>();
return MaterialPage<void>(
key: state.pageKey,
child: DeviceSignupScreen(navigationContract: navigationContract),
child: DeviceSetupScreen(navigationContract: navigationContract),
);
}
}

View File

@@ -0,0 +1,15 @@
abstract class CreateChildProfileUseCase {
Future<String> 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,
});
}

View File

@@ -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<String> 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,
);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1 @@
enum AddKidMainStep { linkDevice, profile, success }

View File

@@ -0,0 +1 @@
enum AddKidStep { intro, linkInfo, scanStrap, scanWatch, profile, success }

View File

@@ -0,0 +1 @@
enum ScanLinkStep { strap, watch }

View File

@@ -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<CreateChildProfileUseCase>((ref) {
final authRepository = ref.read(authRepositoryProvider);
return CreateChildProfileUseCaseImpl(authRepository);
});

View File

@@ -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<QrScannerScreen> createState() => _QrScannerScreenState();
}
class _QrScannerScreenState extends State<QrScannerScreen> {
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;
}

View File

@@ -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, DeviceSetupViewState>(
DeviceSetupViewModel.new,
);
class DeviceSetupViewModel extends Notifier<DeviceSetupViewState> {
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<void> 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();
}
}

View File

@@ -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;
}

View File

@@ -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>(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<DeviceSetupViewState> get copyWith => _$DeviceSetupViewStateCopyWithImpl<DeviceSetupViewState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@@ -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();
}
}
}

View File

@@ -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,
),
),
],
),
),
),
],
);
}
}

View File

@@ -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,
),
),
],
),
),
],
);
}
}

View File

@@ -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,
),
),
),
],
),
),
],
);
}
}

View File

@@ -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<String>(
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,
),
],
),
],
);
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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,
),
),
),
),
],
],
),
),
);
}
}

View File

@@ -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)),
],
),
),
],
);
}
}

View File

@@ -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),
),
),
);
}
}

View File

@@ -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<String> 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,
),
),
);
}
}

View File

@@ -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,
),
],
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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<Widget>.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)
),
],
),
),
);
}
}

View File

@@ -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<DeviceSignupScreen> createState() =>
DeviceSignupScreenState(navigationContract);
}
class DeviceSignupScreenState extends ConsumerState<DeviceSignupScreen> {
late int currentStep;
final NavigationContract navigationContract;
DeviceSignupScreenState(this.navigationContract);
@override
void initState() {
currentStep = 0;
}
@override
Widget build(BuildContext context) {
return getSteps()[currentStep];
}
List<Widget> 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),
];
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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"),
]
)
],
),
],
);
}
}

View File

@@ -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<LinkWatchScreen> createState() => LinkWatchScreenState();
}
class LinkWatchScreenState extends ConsumerState<LinkWatchScreen>{
@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)
],
)
],
);
}
}

View File

@@ -0,0 +1,5 @@
import 'package:auth/src/core/data/models/get_me_response_model.dart';
abstract class GetMeUserUseCase {
Future<MeUserModel> getMe();
}

View File

@@ -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<String> login({required String email, required String password}) {
return _repository.login(email: email, password: password);
}
@override
Future<MeUserModel> getMe() {
// TODO: implement getMe
throw UnimplementedError();
}
}

View File

@@ -1,7 +1,8 @@
abstract class RecoverPasswordUseCase {
Future<String> requestEmail({required String email});
Future<String> requestSms({required String phone});
Future<void> recoverPassword({required String newPassword, required String token});
}
Future<void> recoverPassword({
required String newPassword,
required String token,
});
}

View File

@@ -12,12 +12,10 @@ class RecoverPasswordUseCaseImpl implements RecoverPasswordUseCase {
}
@override
Future<String> requestSms({required String phone}) async {
return await _repository.requestPasswordReset(phone: phone);
}
@override
Future<void> recoverPassword({required String newPassword, required String token}) async {
Future<void> recoverPassword({
required String newPassword,
required String token,
}) async {
await _repository.recoverPassword(newPassword: newPassword, token: token);
}
}

View File

@@ -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),

View File

@@ -6,9 +6,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/recover_password_provider.dart';
final recoverPasswordViewModelProvider =
NotifierProvider.autoDispose<RecoverPasswordViewModel, RecoverPasswordViewState>(
RecoverPasswordViewModel.new,
);
NotifierProvider.autoDispose<
RecoverPasswordViewModel,
RecoverPasswordViewState
>(RecoverPasswordViewModel.new);
class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
late final RecoverPasswordUseCase _recoverPasswordUseCase;
@@ -104,27 +105,18 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
}
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<void> requestRecovery() async {
final trimmedNumber = state.phoneNumber.trim();
final email = state.email.trim();
state = state.copyWith(
@@ -135,8 +127,6 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
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<RecoverPasswordViewState> {
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<RecoverPasswordViewState> {
}
}
Future<void> 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<void> recoverPassword() async {
//final String fullPhone = state.newDialCode + state.newPhoneNumber;
final String password = state.password;
@@ -244,17 +208,13 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
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<RecoverPasswordViewState> {
newPhoneNumberController.removeListener(_onNewPhoneNumberChanged);
newPhoneNumberController.dispose();
}
}
}

View File

@@ -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<TwoFASecretEntity> generateTwoFASignUp({required String token}) {
return _repository.generateTwoFASignUp(token: token);
return _repository
.generateTwoFASignUp(token: token)
.then((model) => model.toEntity());
}
}

View File

@@ -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:

View File

@@ -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<Kid> kids, WidgetRef ref) {
return Column(
spacing: 20,
children: List<Widget>.generate(kids.length, (int index) {

View File

@@ -30,7 +30,7 @@ class _SplashScreenState extends State<SplashScreen> {
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,

View File

@@ -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<Widget>.generate(total, (index){
...List<Widget>.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),
),
);
}),
]
],
);
}
}
}

View File

@@ -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';

View File

@@ -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,
),

View File

@@ -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:

View File

@@ -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"
}

View File

@@ -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": "Weve sent a verification email to:",
"accountCreatedChildSetupHint": "Create your childs 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"
}

View File

@@ -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"
}

View File

@@ -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 lutiliser 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 dauthentification",
"secretCodeStep1Body": "Assurez-vous davoir 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 lapplication dauthentification pour vérifier lappareil.\n\nOu copiez la clé et saisissez-la manuellement dans lapplication dauthentification.",
"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 lapplication dauthentification, 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"
}

View File

@@ -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 laccount per:",
"accountCreatedEmailVerificationSentLabel": "Abbiamo inviato unemail di verifica a:",
"accountCreatedChildSetupHint": "Crea laccount 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 unapp 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 lapp di autenticazione per verificare il dispositivo.\n\nOppure copia la chiave e inseriscila manualmente nellapp 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 nellapp 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 allinterno del riquadro"
}

View File

@@ -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"
}

View File

@@ -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';
}