diff --git a/lib/providers/all_providers.dart b/lib/providers/all_providers.dart index 6a2f9b1..6da8582 100644 --- a/lib/providers/all_providers.dart +++ b/lib/providers/all_providers.dart @@ -6,22 +6,22 @@ import '../services/networking/api_service.dart'; //repository imports import '../services/repositories/auth_repository.dart'; +import '../services/repositories/bookings_repository.dart'; import '../services/repositories/movies_repository.dart'; +import '../services/repositories/payments_repository.dart'; import '../services/repositories/shows_repository.dart'; import '../services/repositories/theaters_repository.dart'; -import '../services/repositories/bookings_repository.dart'; -import '../services/repositories/payments_repository.dart'; //provider imports import 'auth_provider.dart'; -import 'movies_provider.dart'; -import 'shows_provider.dart'; -import 'theaters_provider.dart'; import 'bookings_provider.dart'; +import 'movies_provider.dart'; import 'payments_provider.dart'; +import 'shows_provider.dart'; //states import 'states/auth_state.dart'; +import 'theaters_provider.dart'; //service providers final _apiServiceProvider = Provider((ref) => ApiService()); @@ -43,17 +43,17 @@ final _showsRepositoryProvider = Provider((ref) { return ShowsRepository(apiService: _apiService); }); -final _theatersRepositoryProvider = Provider((ref){ +final _theatersRepositoryProvider = Provider((ref) { final _apiService = ref.watch(_apiServiceProvider); return TheatersRepository(apiService: _apiService); }); -final _bookingsRepositoryProvider = Provider((ref){ +final _bookingsRepositoryProvider = Provider((ref) { final _apiService = ref.watch(_apiServiceProvider); return BookingsRepository(apiService: _apiService); }); -final _paymentsRepositoryProvider = Provider((ref){ +final _paymentsRepositoryProvider = Provider((ref) { final _apiService = ref.watch(_apiServiceProvider); return PaymentsRepository(apiService: _apiService); }); @@ -62,7 +62,11 @@ final _paymentsRepositoryProvider = Provider((ref){ final authProvider = StateNotifierProvider((ref) { final _authRepository = ref.watch(_authRepositoryProvider); final _prefsService = ref.watch(_prefsServiceProvider); - return AuthProvider(_authRepository, _prefsService); + return AuthProvider( + reader: ref.read, + authRepository: _authRepository, + prefsService: _prefsService, + ); }); //data providers @@ -76,17 +80,19 @@ final showsProvider = Provider((ref) { return ShowsProvider(_showsRepository); }); -final theatersProvider = ChangeNotifierProvider((ref){ +final theatersProvider = ChangeNotifierProvider((ref) { final _theatersRepository = ref.watch(_theatersRepositoryProvider); return TheatersProvider(_theatersRepository); }); -final bookingsProvider = Provider((ref){ +final bookingsProvider = Provider((ref) { final _bookingsRepository = ref.watch(_bookingsRepositoryProvider); - return BookingsProvider(read: ref.read, bookingsRepository: _bookingsRepository); + return BookingsProvider( + read: ref.read, bookingsRepository: _bookingsRepository); }); -final paymentsProvider = Provider((ref){ +final paymentsProvider = Provider((ref) { final _paymentsRepository = ref.watch(_paymentsRepositoryProvider); - return PaymentsProvider(read: ref.read, paymentsRepository: _paymentsRepository); + return PaymentsProvider( + read: ref.read, paymentsRepository: _paymentsRepository); }); diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 811d8f1..c77fcac 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -1,28 +1,40 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -//enums +//Enums import '../enums/user_role_enum.dart'; -//models +//Models import '../models/user_model.dart'; -//services +//Services import '../services/local_storage/prefs_service.dart'; import '../services/networking/network_exception.dart'; import '../services/repositories/auth_repository.dart'; - -//states import 'states/auth_state.dart'; +//States +import 'states/future_state.dart'; + +final changePasswordStateProvider = StateProvider( + (ref) => const FutureState.idle(), +); + class AuthProvider extends StateNotifier { late UserModel? _currentUser; final AuthRepository _authRepository; final PrefsService _prefsService; + final Reader _reader; String _token = ""; String _password = ""; - AuthProvider(this._authRepository, this._prefsService) - : super(const AuthState.unauthenticated()) { + AuthProvider({ + required AuthRepository authRepository, + required PrefsService prefsService, + required Reader reader, + }) : _authRepository = authRepository, + _prefsService = prefsService, + _reader = reader, + super(const AuthState.unauthenticated()) { init(); } @@ -62,7 +74,7 @@ class AuthProvider extends StateNotifier { } } - void login({ + Future login({ required String email, required String password, }) async { @@ -81,7 +93,7 @@ class AuthProvider extends StateNotifier { } } - void register({ + Future register({ required String email, required String password, required String fullName, @@ -128,19 +140,21 @@ class AuthProvider extends StateNotifier { return result; } - Future changePassword({ - required String email, - required String oldPassword, - required String newPassword, - }) async { + Future changePassword({required String newPassword}) async { final data = { - "email": email, - "password": oldPassword, + "email": currentUserEmail, + "password": currentUserPassword, "new_password": newPassword, }; - final result = await _authRepository.sendChangePasswordData(data: data); - if (result) _updatePassword(newPassword); - return result; + final _changePasswordState = _reader(changePasswordStateProvider); + _changePasswordState.state = const FutureState.loading(); + try { + final result = await _authRepository.sendChangePasswordData(data: data); + _updatePassword(newPassword); + _changePasswordState.state = FutureState.data(data: result); + } on NetworkException catch (e) { + _changePasswordState.state = FutureState.failed(reason: e.message); + } } Future verifyOtp({required String email, required int otp}) async { diff --git a/lib/providers/states/future_state.dart b/lib/providers/states/future_state.dart new file mode 100644 index 0000000..d7b3bb5 --- /dev/null +++ b/lib/providers/states/future_state.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'future_state.freezed.dart'; + +@freezed +class FutureState with _$FutureState { + + const factory FutureState.idle() = IDLE; + + const factory FutureState.loading() = LOADING; + + const factory FutureState.data({required T data}) = DATA; + + const factory FutureState.failed({required String reason}) = FAILED; +} + + diff --git a/lib/routes/app_router.dart b/lib/routes/app_router.dart index 3b3b74c..2b23d12 100644 --- a/lib/routes/app_router.dart +++ b/lib/routes/app_router.dart @@ -12,6 +12,7 @@ import '../views/screens/ticket_summary_screen.dart'; import '../views/screens/payment_screen.dart'; import '../views/screens/confirmation_screen.dart'; import '../views/screens/user_bookings_screen.dart'; +import '../views/screens/change_password_screen.dart'; @MaterialAutoRouter( routes: [ @@ -27,6 +28,7 @@ import '../views/screens/user_bookings_screen.dart'; AutoRoute(page: PaymentScreen), AutoRoute(page: ConfirmationScreen), AutoRoute(page: UserBookingsScreen), + AutoRoute(page: ChangePasswordScreen), ], ) class $AppRouter{} diff --git a/lib/services/repositories/auth_repository.dart b/lib/services/repositories/auth_repository.dart index f8ec094..97ddc29 100644 --- a/lib/services/repositories/auth_repository.dart +++ b/lib/services/repositories/auth_repository.dart @@ -63,14 +63,14 @@ class AuthRepository { ); } - Future sendChangePasswordData({ + Future sendChangePasswordData({ required Map data, }) async { - return await _apiService.setData( + return await _apiService.setData( endpoint: ApiEndpoint.auth(AuthEndpoint.CHANGE_PASSWORD), data: data, requiresAuthToken: false, - converter: (response) => response["headers"]["success"] == 1, + converter: (response) => response["headers"]["message"], ); } diff --git a/lib/views/screens/change_password_screen.dart b/lib/views/screens/change_password_screen.dart new file mode 100644 index 0000000..84b4854 --- /dev/null +++ b/lib/views/screens/change_password_screen.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +//Helpers +import '../../helper/extensions/context_extensions.dart'; +import '../../helper/utils/constants.dart'; + +//Providers +import '../../providers/all_providers.dart'; +import '../../providers/auth_provider.dart'; +import '../../providers/states/future_state.dart'; + +//Widgets +import '../widgets/common/custom_dialog.dart'; +import '../widgets/common/scrollable_column.dart'; +import '../widgets/common/rounded_bottom_container.dart'; +import '../widgets/change_password/change_password_fields.dart'; +import '../widgets/change_password/save_password_button.dart'; + +class ChangePasswordScreen extends HookWidget { + const ChangePasswordScreen(); + + @override + Widget build(BuildContext context) { + final currentPasswordController = useTextEditingController(); + final newPasswordController = useTextEditingController(); + final cNewPasswordController = useTextEditingController(); + late final _formKey = useMemoized(() => GlobalKey()); + return Scaffold( + body: ProviderListener>>( + provider: changePasswordStateProvider, + onChange: (_, changePasswordStateController) async { + final changePasswordState = changePasswordStateController.state; + changePasswordState.maybeWhen( + data: (message) async { + currentPasswordController.clear(); + newPasswordController.clear(); + cNewPasswordController.clear(); + await showDialog( + context: context, + barrierColor: Constants.barrierColor.withOpacity(0.75), + builder: (ctx) => CustomDialog.alert( + title: "Change Password Success", + body: message, + buttonText: "Okay", + ), + ); + }, + failed: (reason) async => await showDialog( + context: context, + barrierColor: Constants.barrierColor.withOpacity(0.75), + builder: (ctx) => CustomDialog.alert( + title: "Change Password Failed", + body: reason, + buttonText: "Retry", + ), + ), + orElse: () {}, + ); + }, + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: ScrollableColumn( + children: [ + //Input card + Form( + key: _formKey, + child: RoundedBottomContainer( + children: [ + //Page name + Text( + "Your profile", + textAlign: TextAlign.center, + style: context.headline3.copyWith(fontSize: 22), + ), + + const SizedBox(height: 20), + + //Password fields + ChangePasswordFields( + currentPasswordController: currentPasswordController, + newPasswordController: newPasswordController, + cNewPasswordController: cNewPasswordController, + ), + ], + ), + ), + + const Spacer(), + + //Save Password Button + Padding( + padding: const EdgeInsets.fromLTRB(20, 40, 20, Constants.bottomInsets), + child: SavePasswordButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + final _authProv = context.read(authProvider.notifier); + _authProv.changePassword( + newPassword: newPasswordController.text, + ); + } + }, + ), + ), + + const SizedBox(height: 5), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/screens/login_screen.dart b/lib/views/screens/login_screen.dart index afc5a4d..0ffa92c 100644 --- a/lib/views/screens/login_screen.dart +++ b/lib/views/screens/login_screen.dart @@ -110,17 +110,16 @@ class LoginScreen extends HookWidget { //Login button Padding( - padding: const EdgeInsets.fromLTRB( - 20, 40, 20, Constants.bottomInsets), + padding: const EdgeInsets.fromLTRB(20, 40, 20, Constants.bottomInsets), child: CustomTextButton.gradient( width: double.infinity, onPressed: () async { if (formKey.currentState!.validate()) { formKey.currentState!.save(); context.read(authProvider.notifier).login( - email: emailController.text, - password: passwordController.text, - ); + email: emailController.text, + password: passwordController.text, + ); } }, gradient: Constants.buttonGradientOrange, diff --git a/lib/views/screens/user_bookings_screen.dart b/lib/views/screens/user_bookings_screen.dart index fd37dba..72ac107 100644 --- a/lib/views/screens/user_bookings_screen.dart +++ b/lib/views/screens/user_bookings_screen.dart @@ -33,7 +33,7 @@ class UserBookingsScreen extends StatelessWidget { const SizedBox(width: 20), - //Movie Title + //Page Title Expanded( child: Text( "Your bookings", diff --git a/lib/views/screens/welcome_screen.dart b/lib/views/screens/welcome_screen.dart index 835989a..3592ab9 100644 --- a/lib/views/screens/welcome_screen.dart +++ b/lib/views/screens/welcome_screen.dart @@ -10,6 +10,9 @@ import '../../helper/utils/constants.dart'; //Providers import '../../providers/all_providers.dart'; +//Routes +import '../../routes/app_router.gr.dart'; + //Widgets import '../widgets/welcome/user_profile_details.dart'; import '../widgets/welcome/view_bookings_button.dart'; @@ -30,20 +33,39 @@ class WelcomeScreen extends StatelessWidget { const SizedBox(height: 65), //Logout - RotatedBox( - quarterTurns: 2, - child: InkResponse( - radius: 26, - child: const Icon( - Icons.logout, - color: Constants.primaryColor, - size: 30, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + //Log out icon + RotatedBox( + quarterTurns: 2, + child: InkResponse( + radius: 26, + child: const Icon( + Icons.logout, + color: Constants.primaryColor, + size: 30, + ), + onTap: () { + context.read(authProvider.notifier).logout(); + context.router.popUntilRoot(); + }, + ), ), - onTap: () { - context.read(authProvider.notifier).logout(); - context.router.popUntilRoot(); - }, - ), + + //Edit profile icon + InkResponse( + radius: 26, + child: const Icon( + Icons.manage_accounts_sharp, + color: Constants.primaryColor, + size: 30, + ), + onTap: () { + context.router.push(const ChangePasswordScreenRoute()); + }, + ) + ], ), const SizedBox(height: 20), diff --git a/lib/views/widgets/change_password/change_password_fields.dart b/lib/views/widgets/change_password/change_password_fields.dart new file mode 100644 index 0000000..ce50de2 --- /dev/null +++ b/lib/views/widgets/change_password/change_password_fields.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +//Providers +import '../../../providers/all_providers.dart'; + +//Widgets +import '../common/custom_textfield.dart'; + +class ChangePasswordFields extends StatelessWidget { + final TextEditingController currentPasswordController; + final TextEditingController newPasswordController; + final TextEditingController cNewPasswordController; + + const ChangePasswordFields({ + required this.currentPasswordController, + required this.newPasswordController, + required this.cNewPasswordController, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + //Current Password Field + CustomTextField( + hintText: "Enter current password", + floatingText: "Current Password", + controller: currentPasswordController, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.next, + validator: (currPassword) { + final authProv = context.read(authProvider.notifier); + if (authProv.currentUserPassword == currPassword) return null; + return "Invalid current password!"; + }, + ), + + const SizedBox(height: 25), + + //New Password Field + CustomTextField( + hintText: "Type your password", + floatingText: "New Password", + controller: newPasswordController, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.next, + validator: (password) { + final authProv = context.read(authProvider.notifier); + if (password!.isEmpty) { + return "Please enter a password"; + } + else if(authProv.currentUserPassword == password) { + return "Current and new password can't be same"; + } + return null; + }, + ), + + const SizedBox(height: 25), + + //Confirm New Password Field + CustomTextField( + hintText: "Retype your password", + floatingText: "New Password", + controller: cNewPasswordController, + keyboardType: TextInputType.visiblePassword, + textInputAction: TextInputAction.done, + validator: (cPassword) { + if (newPasswordController.text.trim() == cPassword) return null; + return "Passwords don't match"; + }, + ), + ], + ); + } +} diff --git a/lib/views/widgets/change_password/save_password_button.dart b/lib/views/widgets/change_password/save_password_button.dart new file mode 100644 index 0000000..064b51b --- /dev/null +++ b/lib/views/widgets/change_password/save_password_button.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +//Helpers +import '../../../helper/utils/constants.dart'; + +//Providers +import '../../../providers/auth_provider.dart'; + +//Widgets +import '../common/custom_text_button.dart'; + +class SavePasswordButton extends StatelessWidget { + final VoidCallback onPressed; + + const SavePasswordButton({ + Key? key, + required this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomTextButton.gradient( + width: double.infinity, + onPressed: onPressed, + gradient: Constants.buttonGradientOrange, + child: Consumer( + builder: (context, watch, child) { + final _changePasswordState = watch(changePasswordStateProvider).state; + return _changePasswordState.maybeWhen( + loading: () => const Center( + child: SpinKitRing( + color: Colors.white, + size: 30, + lineWidth: 4, + duration: Duration(milliseconds: 1100), + ), + ), + orElse: () => child!, + ); + }, + child: const Center( + child: Text( + "SAVE", + style: TextStyle( + color: Colors.white, + fontSize: 15, + letterSpacing: 0.7, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/widgets/common/custom_textfield.dart b/lib/views/widgets/common/custom_textfield.dart index 3ee39d2..f19ffd2 100644 --- a/lib/views/widgets/common/custom_textfield.dart +++ b/lib/views/widgets/common/custom_textfield.dart @@ -2,13 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; - //Helpers import '../../../helper/extensions/context_extensions.dart'; import '../../../helper/utils/constants.dart'; class CustomTextField extends StatefulWidget { - final String floatingText, hintText; + final String? floatingText, hintText; final TextInputType keyboardType; final TextInputAction textInputAction; final TextEditingController controller; @@ -43,9 +42,9 @@ class CustomTextField extends StatefulWidget { color: Constants.textWhite80Color, ), this.fillColor = Constants.scaffoldColor, + this.floatingText, + this.hintText, required this.controller, - required this.floatingText, - required this.hintText, required this.keyboardType, required this.textInputAction, required this.validator, @@ -57,7 +56,7 @@ class CustomTextField extends StatefulWidget { class _CustomTextFieldState extends State { String? errorText; - bool showPassword = false; + bool hidePassword = true; bool get hasErrorText => errorText != null; @@ -108,15 +107,17 @@ class _CustomTextFieldState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ //Floating text - Text( - widget.floatingText, - style: widget.floatingStyle ?? context.bodyText1.copyWith( - color: Constants.textGreyColor, - fontSize: 17, + if (widget.floatingText != null) + Text( + widget.floatingText!, + style: widget.floatingStyle ?? + context.bodyText1.copyWith( + color: Constants.textGreyColor, + fontSize: 17, + ), ), - ), - const SizedBox(height: 2), + if (widget.floatingText != null) const SizedBox(height: 2), //TextField SizedBox( @@ -125,36 +126,36 @@ class _CustomTextFieldState extends State { controller: widget.controller, autofocus: widget.autofocus, maxLength: widget.maxLength, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.inputStyle, maxLengthEnforcement: MaxLengthEnforcement.enforced, textAlignVertical: TextAlignVertical.center, - showCursor: true, - obscureText: showPassword, - cursorColor: Colors.white, autovalidateMode: AutovalidateMode.disabled, + cursorColor: Colors.white, + obscureText: isPasswordField && hidePassword, + showCursor: true, validator: _onValidate, onSaved: _onSaved, onFieldSubmitted: _onFieldSubmitted, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction, - style: widget.inputStyle, decoration: InputDecoration( - contentPadding: const EdgeInsets.fromLTRB(17, 10, 1, 10), - isDense: true, - counterText: "", hintText: widget.hintText, hintStyle: widget.hintStyle, errorStyle: widget.errorStyle, + fillColor: widget.fillColor, + prefixIcon: widget.prefix, + contentPadding: const EdgeInsets.fromLTRB(17, 10, 1, 10), + isDense: true, + filled: true, + counterText: "", border: _normalBorder(), focusedBorder: _focusedBorder(), focusedErrorBorder: _focusedBorder(), - fillColor: widget.fillColor, - filled: true, - prefixIcon: widget.prefix, suffixIcon: isPasswordField ? InkWell( onTap: () { setState(() { - showPassword = !showPassword; + hidePassword = !hidePassword; }); }, child: const Icon( diff --git a/lib/views/widgets/user_bookings/user_bookings_list.dart b/lib/views/widgets/user_bookings/user_bookings_list.dart index c91e992..7bb8614 100644 --- a/lib/views/widgets/user_bookings/user_bookings_list.dart +++ b/lib/views/widgets/user_bookings/user_bookings_list.dart @@ -58,15 +58,15 @@ class UserBookingsList extends StatelessWidget { return SizedBox( height: 140, child: GestureDetector( - onTap: () {}, + onTap: () => onTap(context,booking), child: Stack( alignment: Alignment.bottomCenter, children: [ //Booking overview BookingSummaryRow( total: total, - title: booking.title, noOfSeats: noOfSeats, + title: booking.title, showDateTime: booking.show.showDatetime, showType: booking.show.showType, ),