Compare commits

...

1 Commits

Author SHA1 Message Date
8dead99ae3 recover password and link watch data and domain 2026-01-09 13:07:06 +01:00
22 changed files with 271 additions and 229 deletions

View File

@@ -15,7 +15,7 @@ late final GoRouter appRouter;
void configureAppRouter() {
appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: AppRoutes.onboarding,
initialLocation: AppRoutes.login,
debugLogDiagnostics: true,
routes: [
GoRoute(

View File

@@ -832,7 +832,7 @@ packages:
source: hosted
version: "1.4.0"
utils:
dependency: "direct overridden"
dependency: "direct main"
description:
path: "../../packages/utils"
relative: true

View File

@@ -1,4 +1,4 @@
export 'src/features/device_sign_up/link_watch/create_profile_screen.dart';
export 'src/features/device_sign_up/presentation/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';

View File

@@ -16,7 +16,21 @@ abstract class AuthRemoteDatasource {
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

@@ -169,32 +169,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 +200,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

@@ -47,8 +47,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 +58,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

@@ -9,7 +9,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,
@@ -23,4 +23,17 @@ abstract class AuthRepository {
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,4 +1,4 @@
import 'package:auth/src/features/device_sign_up/device_signup_screen.dart';
import 'package:auth/src/features/device_sign_up/presentation/device_signup_screen.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';

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_sign_up/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

@@ -1,7 +1,7 @@
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/device_sign_up/presentation/add_kid_screen.dart';
import 'package:auth/src/features/device_sign_up/presentation/link_watch/link_watch_screen.dart';
import 'package:auth/src/features/device_sign_up/presentation/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';

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

@@ -45,7 +45,6 @@ class NewPasswordScreen extends ConsumerWidget {
labelSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
hint: '********',
controller: viewModel.passwordController,
// onVisibilityChanged: viewModel.togglePasswordVisible,
),
SizedBox(height: 16),
CustomTextField(
@@ -54,8 +53,6 @@ class NewPasswordScreen extends ConsumerWidget {
labelSize: SizeUtils.getByScreen(small: 14, big: 14, xl: 12),
hint: '********',
controller: viewModel.repeatedPasswordController,
// onVisibilityChanged: viewModel.togglePasswordVisible,
// color: viewState.equalPasswords ? const Color(0xFF4B4B4B) : const Color.fromRGBO(239, 17, 17, 1),
),
if (!viewState.equalPasswords) ...[
SizedBox(height: 4),
@@ -178,42 +175,6 @@ class NewPasswordScreen extends ConsumerWidget {
SizedBox(
height: SizeUtils.getByScreen(small: 32, big: 32, xl: 24),
),
Align(
alignment: Alignment.bottomLeft,
child: Text(
context.translate(I18n.mobilePhone),
style: TextStyle(
fontSize: SizeUtils.getByScreen(
small: 14,
big: 14,
xl: 12,
),
letterSpacing: 0,
),
),
),
SizedBox(height: 8),
Row(
spacing: 8,
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
width: 80,
onChanged: (country) {
viewModel.updateDialCode(
country.dialCode ?? viewState.dialCode,
);
},
),
Expanded(
child: CustomTextField(
hint: context.translate(I18n.phoneNumber),
keyboardType: TextInputType.number,
controller: viewModel.newPhoneNumberController,
),
),
],
),
if (viewState.errorMessage.isNotEmpty) ...[
SizedBox(height: 10),
Text(

View File

@@ -23,20 +23,28 @@ class RequestRecoveryScreen extends ConsumerWidget {
return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: Container(
margin: EdgeInsets.all(SizeUtils.getByScreen(small: 30, big: 30, xl: 20)),
margin: EdgeInsets.all(
SizeUtils.getByScreen(small: 30, big: 30, xl: 20),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.translate(I18n.recoverPasswordTitle),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: SizeUtils.getByScreen(small: 29, big: 29, xl: 26)),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: SizeUtils.getByScreen(small: 29, big: 28, xl: 26),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 24, big: 32)),
Text(
context.translate(I18n.recoverPasswordSubtitle),
textAlign: TextAlign.center,
style: TextStyle(letterSpacing: 0, fontSize: SizeUtils.getByScreen(small: 18, big: 18, xl: 16)),
style: TextStyle(
letterSpacing: 0,
fontSize: SizeUtils.getByScreen(small: 18, big: 18, xl: 16),
),
),
SizedBox(height: SizeUtils.getByScreen(small: 56, big: 48)),
CustomTextField(
@@ -44,49 +52,23 @@ class RequestRecoveryScreen extends ConsumerWidget {
hint: context.translate(I18n.email),
controller: viewModel.emailController,
),
SizedBox(height: SizeUtils.getByScreen(small: 40, big: 40, xl: 28)),
Align(
alignment: Alignment.bottomLeft,
child: Text(
context.translate(I18n.mobilePhone),
style: TextStyle(fontSize: 14, letterSpacing: 0),
),
SizedBox(
height: SizeUtils.getByScreen(small: 20, big: 20, xl: 28),
),
SizedBox(height: 8),
Row(
children: [
CountryPrefixPicker(
headerText: context.translate(I18n.selectYourCountry),
initialCountryCode: viewState.dialCode,
onChanged: (country) {
viewModel.updateDialCode(
country.dialCode ?? viewState.dialCode,
);
},
width: 80,
),
SizedBox(width: SizeUtils.getByScreen(small: 10, big: 10, xl: 6)),
Expanded(
child: CustomTextField(
hint: context.translate(I18n.phoneNumber),
keyboardType: TextInputType.number,
controller: viewModel.phoneNumberController,
),
),
],
),
SizedBox(height: SizeUtils.getByScreen(small: 40, big: 40, xl: 28)),
if (viewState.errorMessage.isNotEmpty) ...[
Text(
context.translate(viewState.errorMessage),
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 12,
),
Text(
context.translate(viewState.errorMessage),
textAlign: TextAlign.center,
style: const TextStyle(
color: Color.fromRGBO(239, 17, 17, 1),
fontSize: 12,
),
SizedBox(height: 40),
],
),
],
SizedBox(
height: SizeUtils.getByScreen(small: 10, big: 10, xl: 18),
),
Row(
children: [
Expanded(
@@ -96,17 +78,23 @@ class RequestRecoveryScreen extends ConsumerWidget {
size: SizeUtils.getByScreen(small: 16, big: 16, xl: 14),
),
),
SizedBox(width: SizeUtils.getByScreen(small: 20, big: 20, xl: 10)),
SizedBox(
width: SizeUtils.getByScreen(small: 20, big: 20, xl: 10),
),
Expanded(
child: PrimaryButton(
onPressed: () async {
await viewModel.requestRecovery();
final updatedState = ref.read(recoverPasswordViewModelProvider);
final updatedState = ref.read(
recoverPasswordViewModelProvider,
);
if (updatedState.recoveryRequested) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => SentScreen(navigationContract: navigationContract),
builder: (_) => SentScreen(
navigationContract: navigationContract,
),
),
);
}

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;
@@ -22,9 +23,6 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
RecoverPasswordViewState build() {
_recoverPasswordUseCase = ref.read(recoverPasswordUseCaseProvider);
phoneNumberController = TextEditingController();
phoneNumberController.addListener(_onPhoneNumberChanged);
emailController = TextEditingController();
emailController.addListener(_onEmailChanged);
@@ -34,32 +32,11 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
repeatedPasswordController = TextEditingController();
repeatedPasswordController.addListener(_onRepeatedPasswordChanged);
newPhoneNumberController = TextEditingController();
newPhoneNumberController.addListener(_onNewPhoneNumberChanged);
ref.onDispose(disposeControllers);
return const RecoverPasswordViewState();
}
void _onPhoneNumberChanged() {
final String raw = phoneNumberController.text;
state = state.copyWith(
phoneNumber: raw,
errorMessage: '',
recoveryRequested: false,
);
}
void _onNewPhoneNumberChanged() {
final String raw = newPhoneNumberController.text;
state = state.copyWith(
newPhoneNumber: raw,
errorMessage: '',
recoveryRequested: false,
);
}
void _onEmailChanged() {
final String raw = emailController.text;
state = state.copyWith(
@@ -103,28 +80,11 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
);
}
void updateDialCode(String dialCode) {
state = state.copyWith(
dialCode: dialCode,
errorMessage: '',
);
}
void updateNewDialCode(String dialCode) {
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 +95,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 +108,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 +132,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 +176,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(),
@@ -265,15 +193,11 @@ class RecoverPasswordViewModel extends Notifier<RecoverPasswordViewState> {
}
void disposeControllers() {
phoneNumberController.removeListener(_onPhoneNumberChanged);
phoneNumberController.dispose();
emailController.removeListener(_onPhoneNumberChanged);
emailController.removeListener(_onEmailChanged);
emailController.dispose();
passwordController.removeListener(_onPasswordChanged);
passwordController.dispose();
repeatedPasswordController.removeListener(_onRepeatedPasswordChanged);
repeatedPasswordController.dispose();
newPhoneNumberController.removeListener(_onNewPhoneNumberChanged);
newPhoneNumberController.dispose();
}
}
}