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