diff --git a/lib/app_router.dart b/lib/app_router.dart index 60e3fc99..fcf8f881 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -5,6 +5,7 @@ import 'package:projectunity/ui/admin/forms/form_list/form_list_screen.dart'; import 'package:projectunity/ui/admin/leaves/leave_screen/admin_leaves_screen.dart'; import 'package:projectunity/ui/shared/dashboard/navigation_item.dart'; import 'package:projectunity/ui/shared/profile/view_profile/view_profle_screen.dart'; +import 'package:projectunity/ui/sign_in/setup_profile/setup_profile_screen.dart'; import 'package:projectunity/ui/sign_in/sign_in_screen.dart'; import 'package:projectunity/ui/user/leaves/detail/user_leave_detail_screen.dart'; import 'package:projectunity/ui/user/leaves/leaves_screen/user_leave_screen.dart'; @@ -30,6 +31,7 @@ import 'ui/user/leaves/apply_leave/apply_leave_screen.dart'; import 'ui/user/members/detail/user_employee_detail_screen.dart'; import 'ui/user/members/members_screen/user_members_screen.dart'; import 'ui/widget/error/page_not_found_screen.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; @Injectable() class AppRouter { @@ -57,6 +59,15 @@ class AppRouter { : Routes.userHome, navigatorKey: _rootNavigatorKey, routes: [ + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: Routes.setupProfile, + name: Routes.setupProfile, + pageBuilder: (context, state) { + final firebase_auth.User user = + state.extra as firebase_auth.User; + return CupertinoPage(child: SetUpProfilePage(user: user)); + }), GoRoute( parentNavigatorKey: _rootNavigatorKey, path: Routes.login, @@ -370,8 +381,9 @@ class AppRouter { redirect: (context, GoRouterState state) { final location = state.matchedLocation; final loggingIn = location == Routes.login; + final setupProfile = location == Routes.setupProfile; if (_userManager.state == UserState.unauthenticated) { - return loggingIn ? null : Routes.login; + return loggingIn || setupProfile ? null : Routes.login; } if (_userManager.state == UserState.authenticated && !location.contains(Routes.joinSpace)) { @@ -410,6 +422,7 @@ abstract class Routes { static const login = '/login'; static const joinSpace = '/spaces'; static const createSpace = 'create-space'; + static const setupProfile = '/setup-profile'; static const adminHome = '/admin/home'; static const inviteMember = 'invite'; diff --git a/lib/data/di/service_locator.config.dart b/lib/data/di/service_locator.config.dart index 3b29259e..d7e94e5d 100644 --- a/lib/data/di/service_locator.config.dart +++ b/lib/data/di/service_locator.config.dart @@ -24,13 +24,13 @@ import 'package:projectunity/data/bloc/network/network_connection_bloc.dart' import 'package:projectunity/data/bloc/user_state/space_change_notifier.dart' as _i21; import 'package:projectunity/data/bloc/user_state/user_state_controller_bloc.dart' - as _i54; + as _i55; import 'package:projectunity/data/core/functions/shared_function.dart' as _i3; -import 'package:projectunity/data/di/app_module.dart' as _i61; +import 'package:projectunity/data/di/app_module.dart' as _i62; import 'package:projectunity/data/pref/user_preference.dart' as _i24; import 'package:projectunity/data/provider/device_info.dart' as _i8; import 'package:projectunity/data/provider/user_state.dart' as _i25; -import 'package:projectunity/data/repo/employee_repo.dart' as _i52; +import 'package:projectunity/data/repo/employee_repo.dart' as _i53; import 'package:projectunity/data/repo/form_repo.dart' as _i31; import 'package:projectunity/data/repo/leave_repo.dart' as _i34; import 'package:projectunity/data/services/account_service.dart' as _i26; @@ -46,58 +46,60 @@ import 'package:projectunity/data/services/storage_service.dart' as _i23; import 'package:projectunity/data/state_manager/auth/desktop/desktop_auth_manager.dart' as _i6; import 'package:projectunity/ui/admin/forms/create_form/bloc/create_form_bloc.dart' - as _i48; + as _i49; import 'package:projectunity/ui/admin/forms/form_list/bloc/admin_form_list_bloc.dart' - as _i44; + as _i45; import 'package:projectunity/ui/admin/home/home_screen/bloc/admin_home_bloc.dart' - as _i57; + as _i58; import 'package:projectunity/ui/admin/home/invite_member/bloc/invite_member_bloc.dart' as _i32; import 'package:projectunity/ui/admin/leaves/details/bloc/admin_leave_details_bloc.dart' - as _i45; + as _i46; import 'package:projectunity/ui/admin/leaves/leave_screen/bloc%20/admin_leaves_bloc.dart' - as _i58; + as _i59; import 'package:projectunity/ui/admin/members/detail/bloc/employee_detail_bloc.dart' - as _i60; + as _i61; import 'package:projectunity/ui/admin/members/details_leaves/bloc/admin_employee_details_leave_bloc.dart' - as _i43; + as _i44; import 'package:projectunity/ui/admin/members/edit_employee/bloc/admin_edit_employee_bloc.dart' - as _i42; + as _i43; import 'package:projectunity/ui/admin/members/list/bloc/member_list_bloc.dart' - as _i59; + as _i60; import 'package:projectunity/ui/shared/appbar_drawer/drawer/bloc/app_drawer_bloc.dart' - as _i50; + as _i51; import 'package:projectunity/ui/shared/events/bloc/celebrations_bloc.dart' - as _i47; + as _i48; import 'package:projectunity/ui/shared/profile/edit_profile/bloc/employee_edit_profile_bloc.dart' - as _i51; + as _i52; import 'package:projectunity/ui/shared/profile/view_profile/bloc/view_profile_bloc.dart' - as _i55; -import 'package:projectunity/ui/shared/who_is_out_card/bloc/who_is_out_card_bloc.dart' as _i56; -import 'package:projectunity/ui/sign_in/bloc/sign_in_view_bloc.dart' as _i35; +import 'package:projectunity/ui/shared/who_is_out_card/bloc/who_is_out_card_bloc.dart' + as _i57; +import 'package:projectunity/ui/sign_in/bloc/sign_in_view_bloc.dart' as _i36; +import 'package:projectunity/ui/sign_in/setup_profile/bloc/setup_profile_bloc.dart' + as _i35; import 'package:projectunity/ui/space/create_space/bloc/create_workspace_bloc.dart' - as _i49; + as _i50; import 'package:projectunity/ui/space/edit_space/bloc/edit_space_bloc.dart' as _i29; import 'package:projectunity/ui/space/join_space/bloc/join_space_bloc.dart' as _i33; import 'package:projectunity/ui/user/forms/form_list_screen/bloc/user_forms_list_screen_bloc.dart' - as _i37; -import 'package:projectunity/ui/user/home/home_screen/bloc/user_home_bloc.dart' as _i38; +import 'package:projectunity/ui/user/home/home_screen/bloc/user_home_bloc.dart' + as _i39; import 'package:projectunity/ui/user/leaves/apply_leave/bloc/apply_leave_bloc.dart' - as _i46; + as _i47; import 'package:projectunity/ui/user/leaves/detail/bloc/user_leave_detail_bloc.dart' - as _i41; + as _i42; import 'package:projectunity/ui/user/leaves/leaves_screen/bloc/leave_count/user_leave_count_bloc.dart' - as _i40; + as _i41; import 'package:projectunity/ui/user/leaves/leaves_screen/bloc/leaves/user_leave_bloc.dart' - as _i39; + as _i40; import 'package:projectunity/ui/user/members/detail/bloc/user_employee_detail_bloc.dart' - as _i36; + as _i37; import 'package:projectunity/ui/user/members/members_screen/bloc/user_members_bloc.dart' - as _i53; + as _i54; import 'package:projectunity/ui/widget/pick_profile_image/bloc/pick_image_bloc.dart' as _i19; import 'package:shared_preferences/shared_preferences.dart' as _i20; @@ -204,123 +206,127 @@ extension GetItInjectableX on _i1.GetIt { gh<_i16.LeaveService>(), gh<_i25.UserStateNotifier>(), )); - gh.factory<_i35.SignInBloc>(() => _i35.SignInBloc( + gh.factory<_i35.SetupProfileBloc>(() => _i35.SetupProfileBloc( + gh<_i26.AccountService>(), + gh<_i25.UserStateNotifier>(), + )); + gh.factory<_i36.SignInBloc>(() => _i36.SignInBloc( gh<_i25.UserStateNotifier>(), gh<_i28.AuthService>(), gh<_i26.AccountService>(), )); - gh.factory<_i36.UserEmployeeDetailBloc>( - () => _i36.UserEmployeeDetailBloc(gh<_i34.LeaveRepo>())); - gh.factory<_i37.UserFormListBloc>( - () => _i37.UserFormListBloc(gh<_i31.FormRepo>())); - gh.factory<_i38.UserHomeBloc>(() => _i38.UserHomeBloc( + gh.factory<_i37.UserEmployeeDetailBloc>( + () => _i37.UserEmployeeDetailBloc(gh<_i34.LeaveRepo>())); + gh.factory<_i38.UserFormListBloc>( + () => _i38.UserFormListBloc(gh<_i31.FormRepo>())); + gh.factory<_i39.UserHomeBloc>(() => _i39.UserHomeBloc( gh<_i25.UserStateNotifier>(), gh<_i34.LeaveRepo>(), )); - gh.factory<_i39.UserLeaveBloc>(() => _i39.UserLeaveBloc( + gh.factory<_i40.UserLeaveBloc>(() => _i40.UserLeaveBloc( gh<_i25.UserStateNotifier>(), gh<_i34.LeaveRepo>(), )); - gh.factory<_i40.UserLeaveCountBloc>(() => _i40.UserLeaveCountBloc( + gh.factory<_i41.UserLeaveCountBloc>(() => _i41.UserLeaveCountBloc( gh<_i34.LeaveRepo>(), gh<_i25.UserStateNotifier>(), )); - gh.factory<_i41.UserLeaveDetailBloc>( - () => _i41.UserLeaveDetailBloc(gh<_i34.LeaveRepo>())); - gh.factory<_i42.AdminEditEmployeeDetailsBloc>( - () => _i42.AdminEditEmployeeDetailsBloc( + gh.factory<_i42.UserLeaveDetailBloc>( + () => _i42.UserLeaveDetailBloc(gh<_i34.LeaveRepo>())); + gh.factory<_i43.AdminEditEmployeeDetailsBloc>( + () => _i43.AdminEditEmployeeDetailsBloc( gh<_i30.EmployeeService>(), gh<_i25.UserStateNotifier>(), gh<_i23.StorageService>(), )); - gh.factory<_i43.AdminEmployeeDetailsLeavesBLoc>( - () => _i43.AdminEmployeeDetailsLeavesBLoc(gh<_i34.LeaveRepo>())); - gh.factory<_i44.AdminFormListBloc>( - () => _i44.AdminFormListBloc(gh<_i31.FormRepo>())); - gh.factory<_i45.AdminLeaveDetailsBloc>(() => _i45.AdminLeaveDetailsBloc( + gh.factory<_i44.AdminEmployeeDetailsLeavesBLoc>( + () => _i44.AdminEmployeeDetailsLeavesBLoc(gh<_i34.LeaveRepo>())); + gh.factory<_i45.AdminFormListBloc>( + () => _i45.AdminFormListBloc(gh<_i31.FormRepo>())); + gh.factory<_i46.AdminLeaveDetailsBloc>(() => _i46.AdminLeaveDetailsBloc( gh<_i34.LeaveRepo>(), gh<_i18.NotificationService>(), )); - gh.factory<_i46.ApplyLeaveBloc>(() => _i46.ApplyLeaveBloc( + gh.factory<_i47.ApplyLeaveBloc>(() => _i47.ApplyLeaveBloc( gh<_i25.UserStateNotifier>(), gh<_i34.LeaveRepo>(), gh<_i18.NotificationService>(), gh<_i3.AppFunctions>(), )); - gh.factory<_i47.CelebrationsBloc>( - () => _i47.CelebrationsBloc(gh<_i30.EmployeeService>())); - gh.factory<_i48.CreateFormBloc>(() => _i48.CreateFormBloc( + gh.factory<_i48.CelebrationsBloc>( + () => _i48.CelebrationsBloc(gh<_i30.EmployeeService>())); + gh.factory<_i49.CreateFormBloc>(() => _i49.CreateFormBloc( gh<_i31.FormRepo>(), gh<_i14.ImagePicker>(), gh<_i23.StorageService>(), gh<_i25.UserStateNotifier>(), )); - gh.factory<_i49.CreateSpaceBLoc>(() => _i49.CreateSpaceBLoc( + gh.factory<_i50.CreateSpaceBLoc>(() => _i50.CreateSpaceBLoc( gh<_i22.SpaceService>(), gh<_i25.UserStateNotifier>(), gh<_i30.EmployeeService>(), gh<_i14.ImagePicker>(), gh<_i23.StorageService>(), )); - gh.factory<_i50.DrawerBloc>(() => _i50.DrawerBloc( + gh.factory<_i51.DrawerBloc>(() => _i51.DrawerBloc( gh<_i22.SpaceService>(), gh<_i25.UserStateNotifier>(), gh<_i26.AccountService>(), gh<_i30.EmployeeService>(), )); - gh.factory<_i51.EmployeeEditProfileBloc>(() => _i51.EmployeeEditProfileBloc( + gh.factory<_i52.EmployeeEditProfileBloc>(() => _i52.EmployeeEditProfileBloc( gh<_i30.EmployeeService>(), gh<_i24.UserPreference>(), gh<_i25.UserStateNotifier>(), gh<_i23.StorageService>(), )); - gh.lazySingleton<_i52.EmployeeRepo>( - () => _i52.EmployeeRepo( + gh.lazySingleton<_i53.EmployeeRepo>( + () => _i53.EmployeeRepo( gh<_i30.EmployeeService>(), gh<_i25.UserStateNotifier>(), gh<_i10.FirebaseCrashlytics>(), ), dispose: (i) => i.dispose(), ); - gh.factory<_i53.UserEmployeesBloc>( - () => _i53.UserEmployeesBloc(gh<_i52.EmployeeRepo>())); - gh.factory<_i54.UserStateControllerBloc>(() => _i54.UserStateControllerBloc( - gh<_i52.EmployeeRepo>(), + gh.factory<_i54.UserEmployeesBloc>( + () => _i54.UserEmployeesBloc(gh<_i53.EmployeeRepo>())); + gh.factory<_i55.UserStateControllerBloc>(() => _i55.UserStateControllerBloc( + gh<_i53.EmployeeRepo>(), gh<_i25.UserStateNotifier>(), gh<_i22.SpaceService>(), gh<_i21.SpaceChangeNotifier>(), )); - gh.factory<_i55.ViewProfileBloc>(() => _i55.ViewProfileBloc( + gh.factory<_i56.ViewProfileBloc>(() => _i56.ViewProfileBloc( gh<_i25.UserStateNotifier>(), - gh<_i52.EmployeeRepo>(), + gh<_i53.EmployeeRepo>(), )); - gh.factory<_i56.WhoIsOutCardBloc>(() => _i56.WhoIsOutCardBloc( - gh<_i52.EmployeeRepo>(), + gh.factory<_i57.WhoIsOutCardBloc>(() => _i57.WhoIsOutCardBloc( + gh<_i53.EmployeeRepo>(), gh<_i34.LeaveRepo>(), )); - gh.factory<_i57.AdminHomeBloc>(() => _i57.AdminHomeBloc( + gh.factory<_i58.AdminHomeBloc>(() => _i58.AdminHomeBloc( gh<_i34.LeaveRepo>(), - gh<_i52.EmployeeRepo>(), + gh<_i53.EmployeeRepo>(), )); - gh.factory<_i58.AdminLeavesBloc>(() => _i58.AdminLeavesBloc( + gh.factory<_i59.AdminLeavesBloc>(() => _i59.AdminLeavesBloc( gh<_i34.LeaveRepo>(), - gh<_i52.EmployeeRepo>(), + gh<_i53.EmployeeRepo>(), )); - gh.factory<_i59.AdminMembersBloc>(() => _i59.AdminMembersBloc( - gh<_i52.EmployeeRepo>(), + gh.factory<_i60.AdminMembersBloc>(() => _i60.AdminMembersBloc( + gh<_i53.EmployeeRepo>(), gh<_i15.InvitationService>(), gh<_i25.UserStateNotifier>(), )); - gh.factory<_i60.EmployeeDetailBloc>(() => _i60.EmployeeDetailBloc( + gh.factory<_i61.EmployeeDetailBloc>(() => _i61.EmployeeDetailBloc( gh<_i26.AccountService>(), gh<_i22.SpaceService>(), gh<_i25.UserStateNotifier>(), gh<_i30.EmployeeService>(), gh<_i34.LeaveRepo>(), - gh<_i52.EmployeeRepo>(), + gh<_i53.EmployeeRepo>(), )); return this; } } -class _$AppModule extends _i61.AppModule {} +class _$AppModule extends _i62.AppModule {} diff --git a/lib/data/l10n/app_en.arb b/lib/data/l10n/app_en.arb index 39c1621a..78c751c7 100644 --- a/lib/data/l10n/app_en.arb +++ b/lib/data/l10n/app_en.arb @@ -430,6 +430,8 @@ "apple_sign_in_error_message": "Oops! It looks like you haven't granted permission to access your email. Please check and grant permission to access your email address", "sign_out_alert": "Are you sure you want to sign out?", + "setup_profile_title": "Setup Profile", + "submit_button_tag": "Submit", "deactivate_user_account_alert": "Are you sure you want to deactivate {username}'s account? This action cannot be undone.", "@deactivate_user_account_alert": { diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart index 26602e3d..de14d1cf 100644 --- a/lib/data/services/account_service.dart +++ b/lib/data/services/account_service.dart @@ -54,6 +54,11 @@ class AccountService { return null; } + Future setUserAccount(Account user) async { + await _accountsDb.doc(user.uid).set(user); + await _setUserSession(user.uid); + } + Future _setUserSession(String uid) async { final Session? session = await deviceInfoProvider.getDeviceInfo(); if (session != null) { diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 3582faa3..5a10adfc 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -103,7 +103,6 @@ class AuthService { credential = await firebase_auth.FirebaseAuth.instance .signInWithProvider(appleProvider); } - return credential.user; } diff --git a/lib/ui/sign_in/bloc/sign_in_view_bloc.dart b/lib/ui/sign_in/bloc/sign_in_view_bloc.dart index 7b3b3fce..f105dd0c 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_bloc.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_bloc.dart @@ -54,7 +54,9 @@ class SignInBloc extends Bloc { final Account? user = await _accountService.getAppleUser(authUser); if (user == null) { emit(state.copyWith( - appleSignInLoading: false, error: appleSigninError)); + appleSignInLoading: false, + error: appleSigninError, + firebaseAuthUser: authUser)); return; } await _userStateNotifier.setUser(user); diff --git a/lib/ui/sign_in/bloc/sign_in_view_state.dart b/lib/ui/sign_in/bloc/sign_in_view_state.dart index 3b0acf94..3664d215 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_state.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_state.dart @@ -9,5 +9,6 @@ class SignInState with _$SignInState { @Default(false) googleSignInLoading, @Default(false) appleSignInLoading, @Default(false) signInSuccess, + Object? firebaseAuthUser, String? error}) = _SignInState; } diff --git a/lib/ui/sign_in/bloc/sign_in_view_state.freezed.dart b/lib/ui/sign_in/bloc/sign_in_view_state.freezed.dart index 041c6f94..af33c469 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_state.freezed.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_state.freezed.dart @@ -20,6 +20,7 @@ mixin _$SignInState { dynamic get googleSignInLoading => throw _privateConstructorUsedError; dynamic get appleSignInLoading => throw _privateConstructorUsedError; dynamic get signInSuccess => throw _privateConstructorUsedError; + Object? get firebaseAuthUser => throw _privateConstructorUsedError; String? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -38,6 +39,7 @@ abstract class $SignInStateCopyWith<$Res> { dynamic googleSignInLoading, dynamic appleSignInLoading, dynamic signInSuccess, + Object? firebaseAuthUser, String? error}); } @@ -58,6 +60,7 @@ class _$SignInStateCopyWithImpl<$Res, $Val extends SignInState> Object? googleSignInLoading = freezed, Object? appleSignInLoading = freezed, Object? signInSuccess = freezed, + Object? firebaseAuthUser = freezed, Object? error = freezed, }) { return _then(_value.copyWith( @@ -77,6 +80,9 @@ class _$SignInStateCopyWithImpl<$Res, $Val extends SignInState> ? _value.signInSuccess : signInSuccess // ignore: cast_nullable_to_non_nullable as dynamic, + firebaseAuthUser: freezed == firebaseAuthUser + ? _value.firebaseAuthUser + : firebaseAuthUser, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -98,6 +104,7 @@ abstract class _$$SignInStateImplCopyWith<$Res> dynamic googleSignInLoading, dynamic appleSignInLoading, dynamic signInSuccess, + Object? firebaseAuthUser, String? error}); } @@ -116,6 +123,7 @@ class __$$SignInStateImplCopyWithImpl<$Res> Object? googleSignInLoading = freezed, Object? appleSignInLoading = freezed, Object? signInSuccess = freezed, + Object? firebaseAuthUser = freezed, Object? error = freezed, }) { return _then(_$SignInStateImpl( @@ -130,6 +138,9 @@ class __$$SignInStateImplCopyWithImpl<$Res> : appleSignInLoading, signInSuccess: freezed == signInSuccess ? _value.signInSuccess! : signInSuccess, + firebaseAuthUser: freezed == firebaseAuthUser + ? _value.firebaseAuthUser + : firebaseAuthUser, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -146,6 +157,7 @@ class _$SignInStateImpl implements _SignInState { this.googleSignInLoading = false, this.appleSignInLoading = false, this.signInSuccess = false, + this.firebaseAuthUser, this.error}); @override @@ -161,11 +173,13 @@ class _$SignInStateImpl implements _SignInState { @JsonKey() final dynamic signInSuccess; @override + final Object? firebaseAuthUser; + @override final String? error; @override String toString() { - return 'SignInState(appleSignInAvailable: $appleSignInAvailable, googleSignInLoading: $googleSignInLoading, appleSignInLoading: $appleSignInLoading, signInSuccess: $signInSuccess, error: $error)'; + return 'SignInState(appleSignInAvailable: $appleSignInAvailable, googleSignInLoading: $googleSignInLoading, appleSignInLoading: $appleSignInLoading, signInSuccess: $signInSuccess, firebaseAuthUser: $firebaseAuthUser, error: $error)'; } @override @@ -181,6 +195,8 @@ class _$SignInStateImpl implements _SignInState { .equals(other.appleSignInLoading, appleSignInLoading) && const DeepCollectionEquality() .equals(other.signInSuccess, signInSuccess) && + const DeepCollectionEquality() + .equals(other.firebaseAuthUser, firebaseAuthUser) && (identical(other.error, error) || other.error == error)); } @@ -191,6 +207,7 @@ class _$SignInStateImpl implements _SignInState { const DeepCollectionEquality().hash(googleSignInLoading), const DeepCollectionEquality().hash(appleSignInLoading), const DeepCollectionEquality().hash(signInSuccess), + const DeepCollectionEquality().hash(firebaseAuthUser), error); @JsonKey(ignore: true) @@ -206,6 +223,7 @@ abstract class _SignInState implements SignInState { final dynamic googleSignInLoading, final dynamic appleSignInLoading, final dynamic signInSuccess, + final Object? firebaseAuthUser, final String? error}) = _$SignInStateImpl; @override @@ -217,6 +235,8 @@ abstract class _SignInState implements SignInState { @override dynamic get signInSuccess; @override + Object? get firebaseAuthUser; + @override String? get error; @override @JsonKey(ignore: true) diff --git a/lib/ui/sign_in/setup_profile/bloc/setup_profile_bloc.dart b/lib/ui/sign_in/setup_profile/bloc/setup_profile_bloc.dart new file mode 100644 index 00000000..c11f0f80 --- /dev/null +++ b/lib/ui/sign_in/setup_profile/bloc/setup_profile_bloc.dart @@ -0,0 +1,62 @@ +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:projectunity/data/model/account/account.dart'; +import 'package:projectunity/data/services/account_service.dart'; +import 'package:projectunity/ui/sign_in/setup_profile/bloc/setup_profile_event.dart'; +import 'package:projectunity/ui/sign_in/setup_profile/bloc/setup_profile_state.dart'; + +import '../../../../data/core/mixin/input_validation.dart'; +import '../../../../data/provider/user_state.dart'; + +@Injectable() +class SetupProfileBloc extends Bloc + with InputValidationMixin { + final AccountService _accountService; + final UserStateNotifier _userStateNotifier; + SetupProfileBloc(this._accountService, this._userStateNotifier) + : super(const SetupProfileState()) { + on(_validName); + on(_validEmail); + on(_onSubmitProfile); + } + + void _validName(NameChangedEvent event, Emitter emit) { + if (validInputLength(event.name)) { + emit(state.copyWith(nameError: false)); + if (!state.emailError) { + emit(state.copyWith(buttonEnabled: true)); + } + } else { + emit(state.copyWith(nameError: true, buttonEnabled: false)); + } + } + + void _validEmail(EmailChangedEvent event, Emitter emit) { + if (validEmail(event.email)) { + emit(state.copyWith(emailError: false)); + if (!state.emailError) { + emit(state.copyWith(buttonEnabled: true)); + } + } else { + emit(state.copyWith(emailError: true, buttonEnabled: false)); + } + } + + void _onSubmitProfile( + SubmitProfileEvent event, Emitter emit) async { + try { + emit(state.copyWith(isSubmitting: true)); + final user = + Account(uid: event.uid, email: state.email, name: state.name); + + await _accountService.setUserAccount(user); + await _userStateNotifier.setUser(user); + emit(state.copyWith(isSubmitting: false, isSuccess: true)); + } catch (error, stackTrace) { + FirebaseCrashlytics.instance + .recordError(error, stackTrace, reason: "Error setting up profile"); + emit(state.copyWith(isSubmitting: false, error: error.toString())); + } + } +} diff --git a/lib/ui/sign_in/setup_profile/bloc/setup_profile_event.dart b/lib/ui/sign_in/setup_profile/bloc/setup_profile_event.dart new file mode 100644 index 00000000..d6c6f8fe --- /dev/null +++ b/lib/ui/sign_in/setup_profile/bloc/setup_profile_event.dart @@ -0,0 +1,16 @@ +abstract class SetUpProfileEvent {} + +class NameChangedEvent extends SetUpProfileEvent { + final String name; + NameChangedEvent({required this.name}); +} + +class EmailChangedEvent extends SetUpProfileEvent { + final String email; + EmailChangedEvent({required this.email}); +} + +class SubmitProfileEvent extends SetUpProfileEvent { + final String uid; + SubmitProfileEvent({required this.uid}); +} diff --git a/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.dart b/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.dart new file mode 100644 index 00000000..c174bd2f --- /dev/null +++ b/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'setup_profile_state.freezed.dart'; + +@freezed +class SetupProfileState with _$SetupProfileState { + const factory SetupProfileState({ + @Default("") String name, + @Default("") String email, + @Default(false) bool nameError, + @Default(false) bool emailError, + @Default(false) bool buttonEnabled, + @Default(false) bool isSubmitting, + @Default(false) bool isSuccess, + String? error, + }) = _SetupProfileState; +} diff --git a/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.freezed.dart b/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.freezed.dart new file mode 100644 index 00000000..1324fd29 --- /dev/null +++ b/lib/ui/sign_in/setup_profile/bloc/setup_profile_state.freezed.dart @@ -0,0 +1,290 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// 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 'setup_profile_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SetupProfileState { + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + bool get nameError => throw _privateConstructorUsedError; + bool get emailError => throw _privateConstructorUsedError; + bool get buttonEnabled => throw _privateConstructorUsedError; + bool get isSubmitting => throw _privateConstructorUsedError; + bool get isSuccess => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $SetupProfileStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SetupProfileStateCopyWith<$Res> { + factory $SetupProfileStateCopyWith( + SetupProfileState value, $Res Function(SetupProfileState) then) = + _$SetupProfileStateCopyWithImpl<$Res, SetupProfileState>; + @useResult + $Res call( + {String name, + String email, + bool nameError, + bool emailError, + bool buttonEnabled, + bool isSubmitting, + bool isSuccess, + String? error}); +} + +/// @nodoc +class _$SetupProfileStateCopyWithImpl<$Res, $Val extends SetupProfileState> + implements $SetupProfileStateCopyWith<$Res> { + _$SetupProfileStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? email = null, + Object? nameError = null, + Object? emailError = null, + Object? buttonEnabled = null, + Object? isSubmitting = null, + Object? isSuccess = null, + Object? error = freezed, + }) { + return _then(_value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + nameError: null == nameError + ? _value.nameError + : nameError // ignore: cast_nullable_to_non_nullable + as bool, + emailError: null == emailError + ? _value.emailError + : emailError // ignore: cast_nullable_to_non_nullable + as bool, + buttonEnabled: null == buttonEnabled + ? _value.buttonEnabled + : buttonEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isSubmitting: null == isSubmitting + ? _value.isSubmitting + : isSubmitting // ignore: cast_nullable_to_non_nullable + as bool, + isSuccess: null == isSuccess + ? _value.isSuccess + : isSuccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SetupProfileStateImplCopyWith<$Res> + implements $SetupProfileStateCopyWith<$Res> { + factory _$$SetupProfileStateImplCopyWith(_$SetupProfileStateImpl value, + $Res Function(_$SetupProfileStateImpl) then) = + __$$SetupProfileStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String name, + String email, + bool nameError, + bool emailError, + bool buttonEnabled, + bool isSubmitting, + bool isSuccess, + String? error}); +} + +/// @nodoc +class __$$SetupProfileStateImplCopyWithImpl<$Res> + extends _$SetupProfileStateCopyWithImpl<$Res, _$SetupProfileStateImpl> + implements _$$SetupProfileStateImplCopyWith<$Res> { + __$$SetupProfileStateImplCopyWithImpl(_$SetupProfileStateImpl _value, + $Res Function(_$SetupProfileStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? email = null, + Object? nameError = null, + Object? emailError = null, + Object? buttonEnabled = null, + Object? isSubmitting = null, + Object? isSuccess = null, + Object? error = freezed, + }) { + return _then(_$SetupProfileStateImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + nameError: null == nameError + ? _value.nameError + : nameError // ignore: cast_nullable_to_non_nullable + as bool, + emailError: null == emailError + ? _value.emailError + : emailError // ignore: cast_nullable_to_non_nullable + as bool, + buttonEnabled: null == buttonEnabled + ? _value.buttonEnabled + : buttonEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isSubmitting: null == isSubmitting + ? _value.isSubmitting + : isSubmitting // ignore: cast_nullable_to_non_nullable + as bool, + isSuccess: null == isSuccess + ? _value.isSuccess + : isSuccess // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$SetupProfileStateImpl implements _SetupProfileState { + const _$SetupProfileStateImpl( + {this.name = "", + this.email = "", + this.nameError = false, + this.emailError = false, + this.buttonEnabled = false, + this.isSubmitting = false, + this.isSuccess = false, + this.error}); + + @override + @JsonKey() + final String name; + @override + @JsonKey() + final String email; + @override + @JsonKey() + final bool nameError; + @override + @JsonKey() + final bool emailError; + @override + @JsonKey() + final bool buttonEnabled; + @override + @JsonKey() + final bool isSubmitting; + @override + @JsonKey() + final bool isSuccess; + @override + final String? error; + + @override + String toString() { + return 'SetupProfileState(name: $name, email: $email, nameError: $nameError, emailError: $emailError, buttonEnabled: $buttonEnabled, isSubmitting: $isSubmitting, isSuccess: $isSuccess, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SetupProfileStateImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.nameError, nameError) || + other.nameError == nameError) && + (identical(other.emailError, emailError) || + other.emailError == emailError) && + (identical(other.buttonEnabled, buttonEnabled) || + other.buttonEnabled == buttonEnabled) && + (identical(other.isSubmitting, isSubmitting) || + other.isSubmitting == isSubmitting) && + (identical(other.isSuccess, isSuccess) || + other.isSuccess == isSuccess) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, name, email, nameError, + emailError, buttonEnabled, isSubmitting, isSuccess, error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SetupProfileStateImplCopyWith<_$SetupProfileStateImpl> get copyWith => + __$$SetupProfileStateImplCopyWithImpl<_$SetupProfileStateImpl>( + this, _$identity); +} + +abstract class _SetupProfileState implements SetupProfileState { + const factory _SetupProfileState( + {final String name, + final String email, + final bool nameError, + final bool emailError, + final bool buttonEnabled, + final bool isSubmitting, + final bool isSuccess, + final String? error}) = _$SetupProfileStateImpl; + + @override + String get name; + @override + String get email; + @override + bool get nameError; + @override + bool get emailError; + @override + bool get buttonEnabled; + @override + bool get isSubmitting; + @override + bool get isSuccess; + @override + String? get error; + @override + @JsonKey(ignore: true) + _$$SetupProfileStateImplCopyWith<_$SetupProfileStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/sign_in/setup_profile/setup_profile_screen.dart b/lib/ui/sign_in/setup_profile/setup_profile_screen.dart new file mode 100644 index 00000000..f7c8ef2c --- /dev/null +++ b/lib/ui/sign_in/setup_profile/setup_profile_screen.dart @@ -0,0 +1,115 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:projectunity/data/configs/space_constant.dart'; +import 'package:projectunity/data/core/extensions/context_extension.dart'; +import 'package:projectunity/style/app_page.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; +import 'package:projectunity/ui/sign_in/setup_profile/bloc/setup_profile_state.dart'; + +import '../../../app_router.dart'; +import '../../../data/di/service_locator.dart'; +import '../../../style/other/app_button.dart'; +import '../../widget/employee_details_textfield.dart'; +import '../../widget/error_snack_bar.dart'; +import 'bloc/setup_profile_bloc.dart'; +import 'bloc/setup_profile_event.dart'; + +class SetUpProfilePage extends StatelessWidget { + final firebase_auth.User user; + const SetUpProfilePage({super.key, required this.user}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => getIt(), + child: SetupProfileScreen(user: user), + ); + } +} + +class SetupProfileScreen extends StatefulWidget { + final firebase_auth.User user; + const SetupProfileScreen({super.key, required this.user}); + + @override + State createState() => _SetupProfileScreenState(); +} + +class _SetupProfileScreenState extends State { + late final TextEditingController nameController; + late final TextEditingController emailController; + + @override + void initState() { + super.initState(); + nameController = TextEditingController(text: widget.user.displayName); + emailController = TextEditingController(text: widget.user.email); + } + + @override + Widget build(BuildContext context) { + return AppPage( + backGroundColor: context.colorScheme.surface, + title: context.l10n.setup_profile_title, + body: SafeArea( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: primaryHorizontalSpacing), + child: BlocListener( + listener: (BuildContext context, SetupProfileState state) { + if (state.error != null) { + showSnackBar(context: context, error: state.error); + context.goNamed(Routes.login); + } + }, + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + FieldTitle(title: context.l10n.employee_name_tag), + BlocBuilder( + builder: (context, state) { + return FieldEntry( + controller: nameController, + hintText: context.l10n.employee_name_tag, + onChanged: (value) => context + .read() + .add(NameChangedEvent(name: value)), + errorText: state.nameError + ? context.l10n.admin_home_add_member_error_name + : null, + ); + }), + FieldTitle(title: context.l10n.employee_email_tag), + BlocBuilder( + builder: (context, state) { + return FieldEntry( + controller: emailController, + hintText: context.l10n.employee_email_tag, + onChanged: (value) => context + .read() + .add(EmailChangedEvent(email: value)), + errorText: state.emailError + ? context.l10n.admin_home_add_member_error_email + : null, + ); + }), + const Spacer(), + BlocBuilder( + builder: (context, state) { + return AppButton( + backgroundColor: state.buttonEnabled + ? context.colorScheme.primary + : context.colorScheme.primary.withOpacity(0.5), + loading: state.isSubmitting, + tag: context.l10n.submit_button_tag, + onTap: () => context + .read() + .add(SubmitProfileEvent(uid: widget.user.uid)), + ); + }), + const SizedBox(height: 16), + ]), + ), + ))); + } +} diff --git a/lib/ui/sign_in/sign_in_screen.dart b/lib/ui/sign_in/sign_in_screen.dart index 6295c26a..6c598d63 100644 --- a/lib/ui/sign_in/sign_in_screen.dart +++ b/lib/ui/sign_in/sign_in_screen.dart @@ -5,13 +5,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:projectunity/app_router.dart'; import 'package:projectunity/data/core/extensions/context_extension.dart'; import 'package:projectunity/style/app_page.dart'; import 'package:projectunity/style/app_text_style.dart'; import 'package:projectunity/style/other/smart_scroll_view.dart'; import 'package:projectunity/ui/sign_in/widget/apple_signin_button.dart'; import 'package:projectunity/ui/sign_in/widget/google_signin_button.dart'; -import '../../data/core/exception/error_const.dart'; import '../../data/core/utils/const/image_constant.dart'; import '../../data/di/service_locator.dart'; import '../widget/error_snack_bar.dart'; @@ -62,10 +63,9 @@ class SignInScreenState extends State { listenWhen: (previous, current) => current.error != null, listener: (context, state) { if (state.error != null) { - if (state.error == appleSigninError) { - showSnackBar( - context: context, - msg: context.l10n.apple_sign_in_error_message); + if (state.firebaseAuthUser != null) { + context.goNamed(Routes.setupProfile, + extra: state.firebaseAuthUser); } else { showSnackBar(context: context, error: state.error); } diff --git a/lib/ui/widget/employee_details_textfield.dart b/lib/ui/widget/employee_details_textfield.dart index 564bf287..ee0f528b 100644 --- a/lib/ui/widget/employee_details_textfield.dart +++ b/lib/ui/widget/employee_details_textfield.dart @@ -30,6 +30,7 @@ class FieldEntry extends StatelessWidget { final List? inputFormatters; final TextEditingController? controller; final int? maxLength; + final String? labelText; const FieldEntry( {super.key, @@ -40,7 +41,8 @@ class FieldEntry extends StatelessWidget { this.controller, this.keyboardType, this.maxLength, - this.inputFormatters}); + this.inputFormatters, + this.labelText}); @override Widget build(BuildContext context) { @@ -57,6 +59,7 @@ class FieldEntry extends StatelessWidget { AppTextStyle.style16.copyWith(color: context.colorScheme.textPrimary), textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( + labelText: labelText, isCollapsed: true, contentPadding: const EdgeInsets.all(primaryHorizontalSpacing), fillColor: context.colorScheme.containerNormal, diff --git a/test/unit_test/admin/member/detail/employee_detail_bloc_test.mocks.dart b/test/unit_test/admin/member/detail/employee_detail_bloc_test.mocks.dart index 417a111c..f5bcd62f 100644 --- a/test/unit_test/admin/member/detail/employee_detail_bloc_test.mocks.dart +++ b/test/unit_test/admin/member/detail/employee_detail_bloc_test.mocks.dart @@ -141,6 +141,26 @@ class MockAccountService extends _i1.Mock implements _i8.AccountService { )), ) as _i9.Future<_i4.Account>); + @override + _i9.Future<_i4.Account?> getAppleUser(_i10.User? authData) => + (super.noSuchMethod( + Invocation.method( + #getAppleUser, + [authData], + ), + returnValue: _i9.Future<_i4.Account?>.value(), + ) as _i9.Future<_i4.Account?>); + + @override + _i9.Future setUserAccount(_i4.Account? user) => (super.noSuchMethod( + Invocation.method( + #setUserAccount, + [user], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override _i9.Future updateSpaceOfUser({ required String? spaceID, diff --git a/test/unit_test/authentication/login/login_bloc_test.mocks.dart b/test/unit_test/authentication/login/login_bloc_test.mocks.dart index 3ec1bb85..8ea78c6d 100644 --- a/test/unit_test/authentication/login/login_bloc_test.mocks.dart +++ b/test/unit_test/authentication/login/login_bloc_test.mocks.dart @@ -251,6 +251,26 @@ class MockAccountService extends _i1.Mock implements _i9.AccountService { )), ) as _i8.Future<_i5.Account>); + @override + _i8.Future<_i5.Account?> getAppleUser(_i3.User? authData) => + (super.noSuchMethod( + Invocation.method( + #getAppleUser, + [authData], + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + _i8.Future setUserAccount(_i5.Account? user) => (super.noSuchMethod( + Invocation.method( + #setUserAccount, + [user], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override _i8.Future updateSpaceOfUser({ required String? spaceID, diff --git a/test/unit_test/shared/drawer/drawer_test.mocks.dart b/test/unit_test/shared/drawer/drawer_test.mocks.dart index 5addd28d..ac369166 100644 --- a/test/unit_test/shared/drawer/drawer_test.mocks.dart +++ b/test/unit_test/shared/drawer/drawer_test.mocks.dart @@ -516,6 +516,26 @@ class MockAccountService extends _i1.Mock implements _i13.AccountService { )), ) as _i8.Future<_i5.Account>); + @override + _i8.Future<_i5.Account?> getAppleUser(_i14.User? authData) => + (super.noSuchMethod( + Invocation.method( + #getAppleUser, + [authData], + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + _i8.Future setUserAccount(_i5.Account? user) => (super.noSuchMethod( + Invocation.method( + #setUserAccount, + [user], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override _i8.Future updateSpaceOfUser({ required String? spaceID, diff --git a/test/unit_test/space/join_space/join_space_test.mocks.dart b/test/unit_test/space/join_space/join_space_test.mocks.dart index 2b4cd8e5..2df0fb89 100644 --- a/test/unit_test/space/join_space/join_space_test.mocks.dart +++ b/test/unit_test/space/join_space/join_space_test.mocks.dart @@ -503,6 +503,26 @@ class MockAccountService extends _i1.Mock implements _i15.AccountService { )), ) as _i8.Future<_i5.Account>); + @override + _i8.Future<_i5.Account?> getAppleUser(_i6.User? authData) => + (super.noSuchMethod( + Invocation.method( + #getAppleUser, + [authData], + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + _i8.Future setUserAccount(_i5.Account? user) => (super.noSuchMethod( + Invocation.method( + #setUserAccount, + [user], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + @override _i8.Future updateSpaceOfUser({ required String? spaceID,