diff --git a/lib/blocs/auth_bloc/auth_bloc.dart b/lib/blocs/auth_bloc/auth_bloc.dart index e075453..b27f877 100644 --- a/lib/blocs/auth_bloc/auth_bloc.dart +++ b/lib/blocs/auth_bloc/auth_bloc.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:user_repository/user_repository.dart'; @@ -7,6 +10,9 @@ part 'auth_state.dart'; class AuthBloc extends Bloc { final UserRepository _userRepository; + final PhoneVerificationService phoneVerificationService = + PhoneVerificationService(); + String? _verificationId; AuthBloc({required UserRepository userRepository}) : _userRepository = userRepository, @@ -14,6 +20,9 @@ class AuthBloc extends Bloc { on(_onAuthEventLoginOAuth); on(_onAuthEventVerifyOAuth); on(_onAuthEventAddEmailAndPassword); + on(_updatePassword); + on(_linkWithPhoneNumber); + on(_linkWithPhoneNumberOtp); } void _onAuthEventLoginOAuth( @@ -39,10 +48,6 @@ class AuthBloc extends Bloc { } else { emit(const AuthStateVerifyOAuth(true)); } - - (isVerified) - ? emit(AuthStateSuccess()) - : emit(const AuthStateVerifyOAuth(true)); } catch (e) { emit(const AuthStateFailure()); } @@ -69,4 +74,161 @@ class AuthBloc extends Bloc { emit(const AuthStateFailure()); } } + + void _updatePassword( + AuthEventUpdatePassword event, Emitter emit) async { + emit(AuthStateProcess()); + try { + final error = await _userRepository.updatePassword( + event.actualPassword, event.password); + if (error == null) { + emit(AuthStateSuccess()); + } else { + switch (error) { + case UpdatePassworErros.credentialsWrong: + emit(const AuthStateFailure(message: "Contrasen虄a incorrecta")); + break; + case UpdatePassworErros.userNotFound: + emit(const AuthStateFailure(message: "Usuario no encontrado")); + break; + case UpdatePassworErros.unknown: + emit(const AuthStateFailure(message: "Error inesperado. 馃ジ")); + break; + } + } + } catch (e) { + emit(const AuthStateFailure(message: "Error inesperado. 馃ジ")); + } + } + + void _linkWithPhoneNumber( + LinkWithPhoneNumber event, Emitter emit) async { + emit(AuthStateProcess()); + + try { + await for (PhoneAuthEvent event + in phoneVerificationService.verifyPhoneNumber(event.phoneNumber)) { + switch (event.type) { + case PhoneAuthEventType.verificationCompleted: + AuthCredential credential = event.data; + print('Verificaci贸n completada. Credencial: $credential'); + break; + case PhoneAuthEventType.verificationFailed: + FirebaseAuthException exception = event.data; + print('Verificaci贸n fallida. Excepci贸n: $exception'); + emit(AuthStateFailure(message: "Error inesperado. 馃ジ $exception")); + return; // Detener la ejecuci贸n aqu铆 + case PhoneAuthEventType.codeAutoRetrievalTimeout: + String verificationId = event.data; + print( + 'Tiempo de espera agotado para recuperar el c贸digo. ID: $verificationId'); + emit(const AuthStateFailure( + message: "Tiempo de espera agotado. 馃ジ", + )); + break; + case PhoneAuthEventType.codeSent: + Map eventData = event.data; + String verificationId = eventData['verificationId']; + int? resendToken = eventData['resendToken']; + print( + 'C贸digo enviado. ID: $verificationId, resendToken: $resendToken'); + _verificationId = verificationId; + emit(const AuthStateVerifyOAuth(false)); + break; + } + } + } catch (e) { + emit(const AuthStateFailure(message: "Error inesperado. PUTA 馃ジ")); + } + } + + void _linkWithPhoneNumber3( + LinkWithPhoneNumber event, Emitter emit) async { + emit(AuthStateProcess()); + + try { + StreamSubscription subscription = phoneVerificationService + .verifyPhoneNumber(event.phoneNumber) + .listen((event) async { + switch (event.type) { + case PhoneAuthEventType.verificationCompleted: + AuthCredential credential = event.data; + print('Verificaci贸n completada. Credencial: $credential'); + break; + case PhoneAuthEventType.verificationFailed: + FirebaseAuthException exception = event.data; + print('Verificaci贸n fallida. Excepci贸n: $exception'); + break; + case PhoneAuthEventType.codeAutoRetrievalTimeout: + String verificationId = event.data; + print( + 'Tiempo de espera agotado para recuperar el c贸digo. ID: $verificationId'); + break; + case PhoneAuthEventType.codeSent: + Map eventData = event.data; + String verificationId = eventData['verificationId']; + int? resendToken = eventData['resendToken']; + print( + 'C贸digo enviado. ID: $verificationId, resendToken: $resendToken'); + _verificationId = verificationId; + emit(const AuthStateVerifyOAuth(false)); + break; + } + + if (event.type == PhoneAuthEventType.verificationFailed) { + // Detener la suscripci贸n si la verificaci贸n falla + // subscription.cancel(); + emit(const AuthStateFailure(message: "Error inesperado. 馃ジ")); + } + }); + + // await _userRepository + } catch (e) { + emit(const AuthStateFailure(message: "Error inesperado. PUTA 馃ジ")); + } + } + + void _linkWithPhoneNumberOtp( + LinkWithPhoneNumberOtp event, Emitter emit) async { + emit(AuthStateProcess()); + try { + final bool isVerified = await _userRepository.linkWithOTP( + event.phoneNumber, _verificationId!, event.code); + + if (isVerified) { + emit(AuthStateSuccess()); + } else { + emit(const AuthStateVerifyOAuth(true)); + } + } catch (e) { + if (e is FirebaseAuthException) { + switch (e.code) { + case "invalid-verification-code": + emit(const AuthStateFailure( + message: "Co虂digo de verificaci贸n incorrecto. 馃ジ", + )); + break; + + case "provider-already-linked": + emit(const AuthStateFailure( + message: "Cuenta ya vinculada. 馃ジ", + )); + break; + + case "credential-already-in-use": + emit(const AuthStateFailure( + message: + "Este numero ya se encuentra registrado con otra cuenta. 馃ジ", + )); + break; + + default: + emit(AuthStateFailure(message: "Error inesperado. 馃ジ $e")); + break; + } + } else { + emit(AuthStateFailure(message: "Error inesperado. 馃ジ $e")); + } + } + } } diff --git a/lib/blocs/auth_bloc/auth_event.dart b/lib/blocs/auth_bloc/auth_event.dart index 72e4559..7dd24bc 100644 --- a/lib/blocs/auth_bloc/auth_event.dart +++ b/lib/blocs/auth_bloc/auth_event.dart @@ -31,3 +31,39 @@ class AuthEventAddEmailAndPassword extends AuthEvent { @override List get props => [email, password]; } + +class AuthEventUpdatePassword extends AuthEvent { + final String email; + final String actualPassword; + final String password; + + const AuthEventUpdatePassword({ + required this.email, + required this.actualPassword, + required this.password, + }); + + @override + List get props => [password]; +} + +class LinkWithPhoneNumber extends AuthEvent { + final String phoneNumber; + const LinkWithPhoneNumber({required this.phoneNumber}); + + @override + List get props => [phoneNumber]; +} + +class LinkWithPhoneNumberOtp extends AuthEvent { + final String phoneNumber; + final String code; + + const LinkWithPhoneNumberOtp({ + required this.phoneNumber, + required this.code, + }); + + @override + List get props => [phoneNumber, code]; +} diff --git a/lib/blocs/authentication_bloc/authentication_bloc.dart b/lib/blocs/authentication_bloc/authentication_bloc.dart index f63a213..8f9f082 100644 --- a/lib/blocs/authentication_bloc/authentication_bloc.dart +++ b/lib/blocs/authentication_bloc/authentication_bloc.dart @@ -8,15 +8,13 @@ import 'package:user_repository/user_repository.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; -class AuthenticationBloc - extends Bloc { +class AuthenticationBloc extends Bloc { final UserRepository userRepository; late final StreamSubscription _userSubscription; AuthenticationBloc({required UserRepository myUserRepository}) : userRepository = myUserRepository, - super(const AuthenticationState.unknown()) { - _userSubscription = userRepository.streamUser().listen((authUser) { + super(const AuthenticationState.unknown()) {_userSubscription = userRepository.streamUser().listen((authUser) { add(AuthenticationUserChanged(authUser)); }); on(_onAuthenticationUserChanged); @@ -32,8 +30,7 @@ class AuthenticationBloc ); } - void _onAuthenticationLogoutRequested(AuthenticationLogoutRequested event, - Emitter emit) async { + void _onAuthenticationLogoutRequested(AuthenticationLogoutRequested event, Emitter emit) async { await userRepository.logOut(); emit(const AuthenticationState.unauthenticated()); } diff --git a/lib/blocs/my_user_bloc/my_user_event.dart b/lib/blocs/my_user_bloc/my_user_event.dart index cdf22dd..dd31143 100644 --- a/lib/blocs/my_user_bloc/my_user_event.dart +++ b/lib/blocs/my_user_bloc/my_user_event.dart @@ -7,14 +7,6 @@ abstract class MyUserEvent extends Equatable { List get props => []; } -// class GetMyUser extends MyUserEvent { -// final String myUserId; - -// const GetMyUser({required this.myUserId}); - -// @override -// List get props => [myUserId]; -// } class UserChanged extends MyUserEvent { final MyUser? user; diff --git a/lib/components/general_drawer_header.dart b/lib/components/general_drawer_header.dart index d9b7374..5d47b60 100644 --- a/lib/components/general_drawer_header.dart +++ b/lib/components/general_drawer_header.dart @@ -9,30 +9,33 @@ class GeneralDrawerHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final user = context.read().state.user!; - return ListTile( - onTap: () { - // Navigator.pop(context); - Navigator.push( - context, - CupertinoPageRoute( - builder: (BuildContext context) { - return const ProfileScreen(); + return BlocBuilder( + builder: (context, state) { + if (state.status == MyUserStatus.success) { + final user = state.user!; + return ListTile( + onTap: () { + Navigator.push( + context, + CupertinoPageRoute( + builder: (BuildContext context) { + return const ProfileScreen(); + }, + ), + ); }, - ), - ); + title: Text(user.name ?? '',style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text(user.drawerLabel,style: const TextStyle(fontSize: 12)), + leading: pictureWidget(user.picture, context), + trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black), + contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), + ); + } else if (state.status == MyUserStatus.failure) { + return const Text('Error obteniendo datos del usuario'); + } else { + return const CircularProgressIndicator(); + } }, - title: Text( - user.name ?? '', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - user.drawerLabel, - style: const TextStyle(fontSize: 12), - ), - leading: pictureWidget(user.picture, context), - trailing: const Icon(Icons.keyboard_arrow_right, color: Colors.black), - contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), ); } diff --git a/lib/screens/authentication/otp_auth_screen.dart b/lib/screens/authentication/otp_auth_screen.dart index b109ac0..a68849d 100644 --- a/lib/screens/authentication/otp_auth_screen.dart +++ b/lib/screens/authentication/otp_auth_screen.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; @@ -16,6 +17,7 @@ class OtpAuthScreen extends StatefulWidget { class _OtpAuthScreenState extends State { late final AuthBloc authBloc; + late String verificationCode = ''; @override void initState() { @@ -47,7 +49,7 @@ class _OtpAuthScreenState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.1), - const Text('Te enviaremos un C贸digo de verificaci贸n a'), + const Text('Te enviaremos un c贸digo de verificaci贸n a'), Text( widget.phoneNumber, style: const TextStyle( @@ -60,14 +62,18 @@ class _OtpAuthScreenState extends State { numberOfFields: 6, borderColor: const Color(0xFF512DA8), showFieldAsBox: true, - onCodeChanged: (String code) {}, + onCodeChanged: (String code) { + verificationCode = code; + }, onSubmit: (String verificationCode) { authBloc.add(AuthEventVerifyOAuth(code: verificationCode)); }, ), Expanded(child: Container()), GeneralPrimaryButton( - onPressed: () {}, + onPressed: () { + authBloc.add(AuthEventVerifyOAuth(code: verificationCode)); + }, label: 'Continuar', ), const SizedBox(height: 20), diff --git a/lib/screens/authentication/welcome_screen.dart b/lib/screens/authentication/welcome_screen.dart index 6285eec..b604def 100644 --- a/lib/screens/authentication/welcome_screen.dart +++ b/lib/screens/authentication/welcome_screen.dart @@ -85,32 +85,25 @@ class WelcomeScreen extends StatelessWidget { onChanged: (phone) { _phoneNumber = phone.completeNumber; }, - decoration: - GeneralInputDecoration.getCustomDecoration( + decoration: GeneralInputDecoration.getCustomDecoration( context: context, hintText: 'Ingresa tu numero', errorMsg: _errorMsg, ), ), ), - Text( - 'Un c贸digo ser谩 enviado a este numero de celular.', + Text('Un c贸digo ser谩 enviado a este numero de celular.', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13.0, - color: Theme.of(context).colorScheme.onBackground, - ), + style: TextStyle(fontSize: 13.0,color: Theme.of(context).colorScheme.onBackground), ), GeneralPrimaryButton( onPressed: () { final phoneNumber = _phoneNumber; if (phoneNumber != null && phoneNumber.isNotEmpty) { - Navigator.push( - context, + Navigator.push(context, CupertinoPageRoute( - builder: (context) => - OtpAuthScreen(phoneNumber: phoneNumber), + builder: (context) => OtpAuthScreen(phoneNumber: phoneNumber), ), ); } diff --git a/lib/screens/profile/profile_phone_screen.dart b/lib/screens/profile/profile_phone_screen.dart deleted file mode 100644 index 8dd3f11..0000000 --- a/lib/screens/profile/profile_phone_screen.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; - -class ProfilePhoneScreen extends StatefulWidget { - const ProfilePhoneScreen({super.key}); - - @override - State createState() => _ProfilePhoneScreenState(); -} - -class _ProfilePhoneScreenState extends State { - final TextEditingController _phoneController = TextEditingController(); - - @override - void dispose() { - _phoneController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.status == MyUserStatus.success) { - _phoneController.text = state.user!.phone ?? ''; - - return Scaffold( - appBar: AppBar( - title: Text( - _phoneController.text.isEmpty - ? 'Agregar telefono' - : 'Actualizar telefono', - ), - ), - body: const Placeholder(), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); - } -} diff --git a/lib/screens/profile/profile_email_screen.dart b/lib/screens/profile/profile_register_email_screen.dart similarity index 52% rename from lib/screens/profile/profile_email_screen.dart rename to lib/screens/profile/profile_register_email_screen.dart index e4ee89b..6183a95 100644 --- a/lib/screens/profile/profile_email_screen.dart +++ b/lib/screens/profile/profile_register_email_screen.dart @@ -1,23 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; import 'package:injector/injector.dart'; import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/components/general_primary_button.dart'; -import 'package:prosappco/screens/authentication/sign_in_screen.dart'; -class ProfileEmailScreen extends StatefulWidget { - const ProfileEmailScreen({super.key}); +class ProfileRegisterEmailScreen extends StatefulWidget { + const ProfileRegisterEmailScreen({super.key}); @override - State createState() => _ProfileEmailScreenState(); + State createState() => + _ProfileRegisterEmailScreenState(); } -class _ProfileEmailScreenState extends State { +class _ProfileRegisterEmailScreenState extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); late final AuthBloc authBloc; + late String verificationCode; @override void initState() { @@ -32,28 +34,83 @@ class _ProfileEmailScreenState extends State { super.dispose(); } - void _showLoginModal(BuildContext context) { + void _showLoginModal(BuildContext context, MyUserState state) { + final authBlocDialog = Injector.appInstance.get(); + final phone = state.user?.phone ?? ''; + authBlocDialog.add(AuthEventLoginOAuth(phone: phone)); showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Requiere inicio de sesi贸n reciente'), - content: const Text('Por favor, inicie sesi贸n nuevamente para continuar.'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (context) => authBlocDialog, + child: BlocConsumer( + listener: (context, state) { + if (state is AuthStateSuccess) { + // here update email and password autentication + authBloc.add( + AuthEventAddEmailAndPassword( + email: _emailController.text, + password: _passwordController.text, + ), + ); + } else if (state is AuthStateVerifyOAuth && state.isWrongCode) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('El C贸digo es incorrecto'), + )); + } + }, + builder: (context, state) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Te enviaremos un c贸digo \n de verificaci贸n a', + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Text( + phone, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + OtpTextField( + numberOfFields: 6, + fieldWidth: 35, + borderColor: const Color(0xFF512DA8), + // showFieldAsBox: true, + onCodeChanged: (String code) { + verificationCode = code; + }, + onSubmit: (String verificationCode) { + authBlocDialog.add( + AuthEventVerifyOAuth(code: verificationCode)); + }, + ), + const SizedBox(height: 30), + GeneralPrimaryButton( + onPressed: () { + authBlocDialog.add( + AuthEventVerifyOAuth(code: verificationCode)); + }, + label: 'Continuar', + ), + ], + ), + ); }, - child: const Text('OK'), ), - ], - ); - }, - ); + ); + }); } @override Widget build(BuildContext context) { + // _showLoginModal(context); return BlocProvider( create: (context) => authBloc, child: BlocListener( @@ -66,9 +123,11 @@ class _ProfileEmailScreenState extends State { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('El correo ya existe'), )); + + Navigator.of(context).pop(); } if (state is AuthStateRequiresRecentLogin) { - _showLoginModal(context); + // _showLoginModal(context, state); } }, child: BlocBuilder( @@ -78,11 +137,7 @@ class _ProfileEmailScreenState extends State { return Scaffold( appBar: AppBar( - title: Text( - _emailController.text.isEmpty - ? 'Agregar correo' - : 'Actualizar correo', - ), + title: const Text('Agregar correo'), ), body: Center( // Centro del contenido @@ -145,6 +200,33 @@ class _ProfileEmailScreenState extends State { return null; }, ), + const SizedBox(height: 20), + TextFormField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Confirmar contrasen虄a', + prefixIcon: Icon(Icons.lock_rounded), + hintText: 'Contrasen虄a', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.red, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su contrasen虄a'; + } + return null; + }, + ), const SizedBox(height: 30), GeneralPrimaryButton( onPressed: () { @@ -155,17 +237,9 @@ class _ProfileEmailScreenState extends State { if (_passwordController.text.isEmpty) { return; } - - context.read().add( - AuthEventAddEmailAndPassword( - email: _emailController.text, - password: _passwordController.text, - ), - ); + _showLoginModal(context, state); }, - label: _emailController.text.isEmpty - ? 'Registrar' - : 'Actualizar', + label: 'Guardar', ), ], ), diff --git a/lib/screens/profile/profile_register_phone_screen.dart b/lib/screens/profile/profile_register_phone_screen.dart new file mode 100644 index 0000000..c97b2bc --- /dev/null +++ b/lib/screens/profile/profile_register_phone_screen.dart @@ -0,0 +1,279 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; +import 'package:injector/injector.dart'; +import 'package:intl_phone_field/intl_phone_field.dart'; +import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart'; +import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; +import 'package:prosappco/components/general_input_decoration.dart'; +import 'package:prosappco/components/general_primary_button.dart'; +import 'package:prosappco/screens/authentication/otp_auth_screen.dart'; + +class ProfileRegisterPhoneScreen extends StatefulWidget { + const ProfileRegisterPhoneScreen({super.key}); + + @override + State createState() => + _ProfileRegisterPhoneScreenState(); +} + +class _ProfileRegisterPhoneScreenState + extends State { + final TextEditingController _actualPasswordController = + TextEditingController(); + late final AuthBloc authBloc; + late String verificationCode; + + @override + void initState() { + super.initState(); + authBloc = Injector.appInstance.get(); + } + + @override + void dispose() { + super.dispose(); + } + + void _showLoginModal(BuildContext context, MyUserState state) { + final authBlocDialog = Injector.appInstance.get(); + final phone = state.user?.phone ?? ''; + authBlocDialog.add(AuthEventLoginOAuth(phone: phone)); + showDialog( + context: context, + builder: (BuildContext context) { + return BlocProvider( + create: (context) => authBlocDialog, + child: BlocConsumer( + listener: (context, state) { + if (state is AuthStateSuccess) { + // here update email and password autentication + // authBloc.add( + // AuthEventAddEmailAndPassword( + // email: _emailController.text, + // password: _passwordController.text, + // ), + // ); + } else if (state is AuthStateVerifyOAuth && state.isWrongCode) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('El C贸digo es incorrecto'), + )); + } + }, + builder: (context, state) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Requiere autenticaci贸n porfavor ingresa tu contrase帽a', + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Text( + phone, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + GeneralPrimaryButton( + onPressed: () { + // authBlocDialog.add(AuthEventVerifyOAuth(code: verificationCode)); + }, + label: 'Continuar', + ), + ], + ), + ); + }, + ), + ); + }); + } + + String? _phoneNumber; + String? _errorMsg; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => authBloc, + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + return BlocBuilder( + builder: (context, stateUser) { + if (stateUser.status == MyUserStatus.success) { + return Scaffold( + appBar: AppBar( + title: const Text('Agregar celular'), + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...switchContent(context, state), + ], + ), + ), + ), + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + }, + ), + ); + } + + switchContent( + BuildContext context, + AuthState state, + ) { + if (state is AuthStateInitial) { + return getContent(context); + } + + if (state is AuthStateVerifyOAuth) { + return [ + OtpTextField( + numberOfFields: 6, + fieldWidth: 35, + borderColor: const Color(0xFF512DA8), + showFieldAsBox: true, + onCodeChanged: (String code) { + verificationCode = code; + }, + onSubmit: (String verificationCode) { + authBloc.add(LinkWithPhoneNumberOtp( + phoneNumber: _phoneNumber!, code: verificationCode)); + }, + ), + ]; + } + + if (state is AuthStateSuccess) { + return [ + const Text('Tu celular ha sido agregado'), + ]; + } + + if (state is AuthStateFailure) { + return [ + Text(state.message ?? "Error inesperado. 馃ジ"), + ]; + } + + return [ + const CircularProgressIndicator(), + ]; + } + + getContent(BuildContext context) { + double width = MediaQuery.of(context).size.width; + + return [ + SizedBox( + width: double.infinity, + child: Text( + 'Numero de celular', + style: TextStyle( + fontSize: width * 0.045, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ), + IntlPhoneField( + initialCountryCode: 'CO', + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + validator: (value) { + if (value == null) { + return 'Ingresa un numero'; + } else { + return null; + } + }, + onChanged: (phone) { + _phoneNumber = phone.completeNumber; + log('xd ${_phoneNumber!}'); + }, + decoration: GeneralInputDecoration.getCustomDecoration( + context: context, + hintText: 'Ingresa tu numero', + errorMsg: _errorMsg, + ), + ), + const SizedBox(height: 10), + // TextFormField( + // controller: _actualPasswordController, + // obscureText: true, + // decoration: const InputDecoration( + // labelText: 'Contrasen虄a actual', + // prefixIcon: Icon(Icons.lock_rounded), + // hintText: 'Contrasen虄a', + // border: OutlineInputBorder( + // borderRadius: BorderRadius.all( + // Radius.circular(10.0), + // )), + // errorBorder: OutlineInputBorder( + // borderSide: BorderSide(color: Colors.red), + // ), + // focusedErrorBorder: OutlineInputBorder( + // borderSide: BorderSide(color: Colors.red, width: 2.0), + // ), + // ), + // validator: (value) { + // if (value == null || value.isEmpty) { + // return 'Por favor, ingrese su contrasen虄a'; + // } + // return null; + // }, + // ), + const SizedBox(height: 15), + Center( + child: Text( + 'Un c贸digo ser谩 enviado a este numero de celular.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13.0, + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ), + const SizedBox(height: 10), + Center( + child: GeneralPrimaryButton( + onPressed: () async { + final phoneNumber = _phoneNumber; + + if (phoneNumber == null || phoneNumber.isEmpty) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Por favor, ingresa un numero'), + )); + return; + } + + authBloc.add(LinkWithPhoneNumber( + phoneNumber: phoneNumber, + )); + }, + label: 'Enviar c贸digo', + ), + ), + ]; + } +} diff --git a/lib/screens/profile/profile_screen.dart b/lib/screens/profile/profile_screen.dart index a07fb82..8b62071 100644 --- a/lib/screens/profile/profile_screen.dart +++ b/lib/screens/profile/profile_screen.dart @@ -11,8 +11,9 @@ import 'package:prosappco/blocs/profile_bloc/profile_bloc.dart'; import 'package:prosappco/components/birthday_picker.dart'; import 'package:prosappco/components/gender_dropdown.dart'; import 'package:prosappco/screens/profile/components/profile_item.dart'; -import 'package:prosappco/screens/profile/profile_email_screen.dart'; -import 'package:prosappco/screens/profile/profile_phone_screen.dart'; +import 'package:prosappco/screens/profile/profile_register_email_screen.dart'; +import 'package:prosappco/screens/profile/profile_register_phone_screen.dart'; +import 'package:prosappco/screens/profile/profile_update_password_screen.dart'; class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key}); @@ -149,42 +150,54 @@ class _ProfileScreenState extends State { : const SizedBox(), const SizedBox(height: 20), ProfileItem( - title: 'Iniciar sesi贸n con correo', + title: 'Configurar inicio de sesi贸n con correo', subtitle: _emailController.text, leading: Icons.email_rounded, onTap: () { if (state.user!.name == null || state.user!.name == '') { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar( - content: Text('Por favor, ingrese su nombre'), - )); - + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su nombre'))); return; } Navigator.push( context, CupertinoPageRoute( - builder: (context) => - const ProfileEmailScreen(), - ), + builder: (context) => + _emailController.text.isEmpty + ? ProfileRegisterEmailScreen() + : ProfileUpdatePasswordScreen( + email: _emailController.text)), ); }, ), const SizedBox(height: 20), ProfileItem( - title: 'Iniciar sesi贸n con tele虂fono', + title: 'Configurar inicio de sesi贸n con celular', subtitle: _phoneController.text, leading: Icons.phone_iphone_rounded, onTap: () { - Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => - const ProfilePhoneScreen(), - ), - ); + if (state.user!.name == null || + state.user!.name == '') { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Por favor, ingrese su nombre'))); + return; + } + + if (_phoneController.text.isEmpty) { + Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => + const ProfileRegisterPhoneScreen(), + ), + ); + } }, ), const SizedBox(height: 60.0), @@ -210,42 +223,30 @@ class _ProfileScreenState extends State { return; } - // if (enableLoginWithEmail) { - // if (_newEmailController.text.isEmpty) { - // return; - // } + if (_nameController.text.isEmpty) { + ScaffoldMessenger.of(context).clearSnackBars(); - // if (_passwordController.text.isEmpty) { - // return; - // } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Por favor, ingrese su nombre')), + ); - // context.read().add( - // AuthEventAddEmailAndPassword( - // email: _newEmailController.text, - // password: _passwordController.text, - // ), - // ); - - // _emailController.text = _newEmailController.text; - // } + return; + } final myUser = state.user!.copyWith( name: _nameController.text, + nickname: _nameController.text.trim().toLowerCase(), email: _emailController.text, phone: _phoneController.text, birthday: _birthdayController.text, gender: _genderController.text, - nickname: _nameController.text.trim().toLowerCase(), ); - context.read().add( - UpdateUserInfo( - myUser: myUser, - filePicture: _imageFile?.path, - ), - ); + context + .read() + .add(UpdateUserInfo(myUser: myUser, filePicture: _imageFile?.path)); - // Navigator.pop(context); + Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, diff --git a/lib/screens/profile/profile_update_password_screen.dart b/lib/screens/profile/profile_update_password_screen.dart new file mode 100644 index 0000000..952ab61 --- /dev/null +++ b/lib/screens/profile/profile_update_password_screen.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injector/injector.dart'; +import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart'; +import 'package:prosappco/components/general_primary_button.dart'; + +class ProfileUpdatePasswordScreen extends StatefulWidget { + final String email; + + const ProfileUpdatePasswordScreen({super.key, required this.email}); + + @override + State createState() => + _ProfileUpdatePasswordScreenState(); +} + +class _ProfileUpdatePasswordScreenState + extends State { + final TextEditingController _actualPasswordController = + TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + @override + Widget build(BuildContext context) { + final authBloc = Injector.appInstance.get(); + return BlocProvider( + create: (context) => authBloc, + child: BlocConsumer( + listener: (context, state) { + if (state is AuthStateSuccess) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Se actualizo la contrase帽a.馃コ'), + )); + } else if (state is AuthStateFailure) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(state.message ?? 'Error inesperado. 馃ジ'), + )); + } + }, + builder: (context, state) { + return Scaffold( + appBar: AppBar( + title: const Text('Actualizar contrase帽a'), + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _actualPasswordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Contrasen虄a actual', + prefixIcon: Icon(Icons.lock_rounded), + hintText: 'Contrasen虄a', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.red, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su contrasen虄a'; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Nueva contrasen虄a', + prefixIcon: Icon(Icons.lock_rounded), + hintText: 'Contrasen虄a', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.red, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su contrasen虄a'; + } + return null; + }, + ), + const SizedBox(height: 20), + TextFormField( + controller: _confirmPasswordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Confirmar contrasen虄a', + prefixIcon: Icon(Icons.lock_rounded), + hintText: 'Contrasen虄a', + border: OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + )), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.red, width: 2.0), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Por favor, ingrese su contrasen虄a'; + } + return null; + }, + ), + const SizedBox(height: 30), + GeneralPrimaryButton( + onPressed: () { + if (_actualPasswordController.text.isEmpty) { + return; + } + + if (_passwordController.text.isEmpty) { + return; + } + + if (_confirmPasswordController.text.isEmpty) { + return; + } + + if (_passwordController.text != + _confirmPasswordController.text) { + return; + } + + if (_actualPasswordController.text == + _passwordController.text && + _actualPasswordController.text == + _confirmPasswordController.text) { + return; + } + + authBloc.add(AuthEventUpdatePassword( + email: widget.email, + password: _passwordController.text, + actualPassword: _actualPasswordController.text)); + }, + label: 'Actualizar contrase帽a', + ), + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/packages/user_repository/lib/src/repositories/firebase_user_repository.dart b/packages/user_repository/lib/src/repositories/firebase_user_repository.dart index 9ff29e2..ebab2d2 100644 --- a/packages/user_repository/lib/src/repositories/firebase_user_repository.dart +++ b/packages/user_repository/lib/src/repositories/firebase_user_repository.dart @@ -167,6 +167,14 @@ class FirebaseUserRepository implements UserRepository { await _firebaseAuth.currentUser!.updateEmail(email); await _firebaseAuth.currentUser!.updatePassword(password); + final user = await getMyUser(_firebaseAuth.currentUser!.uid); + if (user == null) { + return "user-not-found"; + } + + final newUser = user.copyWith(email: email); + await updateUserInfo(newUser); + return null; } catch (e) { if (e is FirebaseAuthException && e.code == 'requires-recent-login') { @@ -181,6 +189,95 @@ class FirebaseUserRepository implements UserRepository { } } + @override + addPhoneAuthCredential(String password, String phoneNumber, + {required Future Function(FirebaseAuthException) verificationFailed, + required Future Function(String) codeSent, + required Future Function(String) codeAutoRetrievalTimeout}) async { + await _firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + timeout: const Duration(seconds: 60), + verificationCompleted: (AuthCredential authCredential) async { + // La verificaci贸n se complet贸 autom谩ticamente. + // TODO: Revisar si es necesario. + }, + verificationFailed: (FirebaseAuthException authException) async { + // La verificaci贸n fall贸. + // throw authException; + log('verificationFailed: $authException'); + await verificationFailed(authException); + }, + codeAutoRetrievalTimeout: (String verificationId) async { + // Tiempo de espera agotado para la recuperaci贸n autom谩tica del c贸digo. + // throw 'timeout'; + log(verificationId); + await codeAutoRetrievalTimeout(verificationId); + }, + codeSent: (String verificationId, int? resendToken) async { + await codeSent(verificationId); + }, + ); + } + + @override + Future linkWithOTP( + String phoneNumber, String verificationId, String code) async { + try { + var phoneAuthCredential = PhoneAuthProvider.credential( + verificationId: verificationId, smsCode: code); + + User? userAuth = FirebaseAuth.instance.currentUser; + + if (userAuth == null) { + throw 'User not found'; + } + + final user = await getMyUser(_firebaseAuth.currentUser!.uid); + if (user == null) { + throw "user-not-found"; + } + + await userAuth.linkWithCredential(phoneAuthCredential); + final newUser = user.copyWith(phone: phoneNumber); + await updateUserInfo(newUser); + return true; + } catch (e) { + if (e is FirebaseAuthException) { + if (e.code == 'invalid-verification-code') { + return false; + } else { + rethrow; + } + } else { + rethrow; + } + } + } + + @override + Future updatePassword( + String password, String newPassword) async { + try { + User? user = FirebaseAuth.instance.currentUser; + + if (user == null) { + return UpdatePassworErros.userNotFound; + } + + // Verificar la autenticaci贸n reciente + await user.reauthenticateWithCredential(EmailAuthProvider.credential( + email: user.email!, + password: password, + )); + + await _firebaseAuth.currentUser!.updatePassword(newPassword); + return null; + } catch (e) { + log(e.toString()); + return UpdatePassworErros.unknown; + } + } + // Sign out @override Future logOut() async { diff --git a/packages/user_repository/lib/src/repositories/user_repo.dart b/packages/user_repository/lib/src/repositories/user_repo.dart index e994e87..9486467 100644 --- a/packages/user_repository/lib/src/repositories/user_repo.dart +++ b/packages/user_repository/lib/src/repositories/user_repo.dart @@ -1,3 +1,5 @@ +import 'package:firebase_auth/firebase_auth.dart'; + import '../../user_repository.dart'; abstract class UserRepository { @@ -8,6 +10,9 @@ abstract class UserRepository { Future addEmailAndPassword(String email, String password); + Future updatePassword( + String password, String newPassword); + Future logOut(); Future signUp(MyUser myUser, String password); @@ -16,6 +21,17 @@ abstract class UserRepository { Future verifyOTP(String code); + Future addPhoneAuthCredential(String password, String phoneNumber, + { + required Future Function(FirebaseAuthException) verificationFailed, + required Future Function(String) codeSent, + required Future Function(String) codeAutoRetrievalTimeout + + }); + + Future linkWithOTP( + String phoneNumber, String verificationId, String code); + Future resetPassword(String email); Future setUserData(MyUser user); @@ -28,3 +44,5 @@ abstract class UserRepository { Future createUser(MyUser myUser); } + +enum UpdatePassworErros { credentialsWrong, userNotFound, unknown } diff --git a/packages/user_repository/lib/src/services/phone_verification_service.dart b/packages/user_repository/lib/src/services/phone_verification_service.dart new file mode 100644 index 0000000..1f9a02b --- /dev/null +++ b/packages/user_repository/lib/src/services/phone_verification_service.dart @@ -0,0 +1,76 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; + +class PhoneVerificationService { + final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; + + Stream verifyPhoneNumber(String phoneNumber) async* { + final StreamController phoneAuthController = + StreamController(); + + _firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + timeout: const Duration(seconds: 60), + verificationCompleted: (AuthCredential authCredential) async { + phoneAuthController + .add(PhoneAuthEvent.verificationCompleted(authCredential)); + }, + verificationFailed: (FirebaseAuthException authException) async { + phoneAuthController + .add(PhoneAuthEvent.verificationFailed(authException)); + phoneAuthController.close(); + }, + codeAutoRetrievalTimeout: (String verificationId) async { + phoneAuthController + .add(PhoneAuthEvent.codeAutoRetrievalTimeout(verificationId)); + }, + codeSent: (String verificationId, int? resendToken) async { + phoneAuthController + .add(PhoneAuthEvent.codeSent(verificationId, resendToken)); + }, + ); + + await for (PhoneAuthEvent event in phoneAuthController.stream) { + yield event; + if (event.type == PhoneAuthEventType.verificationFailed) { + await phoneAuthController.close(); + break; + } + } + } +} + +enum PhoneAuthEventType { + verificationCompleted, + verificationFailed, + codeAutoRetrievalTimeout, + codeSent, +} + +class PhoneAuthEvent { + final PhoneAuthEventType type; + final dynamic data; + + PhoneAuthEvent(this.type, this.data); + + static PhoneAuthEvent verificationCompleted(AuthCredential authCredential) { + return PhoneAuthEvent( + PhoneAuthEventType.verificationCompleted, authCredential); + } + + static PhoneAuthEvent verificationFailed( + FirebaseAuthException authException) { + return PhoneAuthEvent(PhoneAuthEventType.verificationFailed, authException); + } + + static PhoneAuthEvent codeAutoRetrievalTimeout(String verificationId) { + return PhoneAuthEvent( + PhoneAuthEventType.codeAutoRetrievalTimeout, verificationId); + } + + static PhoneAuthEvent codeSent(String verificationId, int? resendToken) { + return PhoneAuthEvent(PhoneAuthEventType.codeSent, + {'verificationId': verificationId, 'resendToken': resendToken}); + } +} diff --git a/packages/user_repository/lib/user_repository.dart b/packages/user_repository/lib/user_repository.dart index e8d2c7c..f0f141e 100644 --- a/packages/user_repository/lib/user_repository.dart +++ b/packages/user_repository/lib/user_repository.dart @@ -2,5 +2,6 @@ library user_repository; export 'src/models/models.dart'; export 'src/entities/entities.dart'; +export 'src/services/phone_verification_service.dart'; export 'src/repositories/user_repo.dart'; export 'src/repositories/firebase_user_repository.dart';