diff --git a/assets/images/apple_logo.svg b/assets/images/apple_logo.svg new file mode 100644 index 000000000..b3f28ce8a --- /dev/null +++ b/assets/images/apple_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/google_logo.png b/assets/images/google_logo.png deleted file mode 100644 index 750c9dabe..000000000 Binary files a/assets/images/google_logo.png and /dev/null differ diff --git a/assets/images/google_logo.svg b/assets/images/google_logo.svg new file mode 100644 index 000000000..2a29a62bd --- /dev/null +++ b/assets/images/google_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 0c67376eb..a812db506 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,5 +1,10 @@ - + + com.apple.developer.applesignin + + Default + + diff --git a/ios/Runner/RunnerDebug.entitlements b/ios/Runner/RunnerDebug.entitlements index 0c67376eb..a812db506 100644 --- a/ios/Runner/RunnerDebug.entitlements +++ b/ios/Runner/RunnerDebug.entitlements @@ -1,5 +1,10 @@ - + + com.apple.developer.applesignin + + Default + + diff --git a/lib/data/di/service_locator.config.dart b/lib/data/di/service_locator.config.dart index 5a84e1c24..9f610c2bd 100644 --- a/lib/data/di/service_locator.config.dart +++ b/lib/data/di/service_locator.config.dart @@ -168,6 +168,7 @@ extension GetItInjectableX on _i1.GetIt { gh<_i6.DesktopAuthManager>(), gh<_i11.FirebaseFirestore>(), gh<_i9.FirebaseAuth>(), + gh<_i26.AccountService>(), )); gh.factory<_i29.EditSpaceBloc>(() => _i29.EditSpaceBloc( gh<_i22.SpaceService>(), diff --git a/lib/data/l10n/app_en.arb b/lib/data/l10n/app_en.arb index 96f3a6238..ae7516150 100644 --- a/lib/data/l10n/app_en.arb +++ b/lib/data/l10n/app_en.arb @@ -4,9 +4,8 @@ "sign_in_description_text": "Help you to manage your office leaves and employees easily with unity.", "sign_in_title_text": "Effortless Leave\nManagement with Unity", - "login_button_text": "Sign in with Google", - "company_name": "Canopas Software", - "company_subtitle": "www.canopas.com", + "google_login_button_text": "Sign in with Google", + "apple_login_button_text": "Sign in with Apple", "settings_tag": "Settings", "members_tag": "Members", diff --git a/lib/data/services/account_service.dart b/lib/data/services/account_service.dart index ae16a98e7..e2f82142c 100644 --- a/lib/data/services/account_service.dart +++ b/lib/data/services/account_service.dart @@ -19,7 +19,7 @@ class AccountService { fromFirestore: Account.fromFireStore, toFirestore: (Account user, _) => user.toJson()); - Future getUser(firebase_auth.User authData) async { + Future getUser(firebase_auth.User authData, {String? name}) async { final userDataDoc = await _accountsDb.doc(authData.uid).get(); final Account user; final Account? userData = userDataDoc.data(); @@ -29,7 +29,7 @@ class AccountService { user = Account( uid: authData.uid, email: authData.email!, - name: authData.displayName); + name:name?? authData.displayName); await _accountsDb.doc(authData.uid).set(user); } await _setUserSession(authData.uid); diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 109670b93..ddd7bbc5a 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -1,19 +1,29 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:crypto/crypto.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:injectable/injectable.dart'; import 'package:oauth2/oauth2.dart'; +import 'package:projectunity/data/services/account_service.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; +import '../model/account/account.dart'; import '../state_manager/auth/desktop/desktop_auth_manager.dart'; @LazySingleton() class AuthService { final DesktopAuthManager _desktopAuthManager; final FirebaseFirestore fireStore; + final AccountService _accountService; final firebase_auth.FirebaseAuth firebaseAuth; - AuthService(this._desktopAuthManager, this.fireStore, this.firebaseAuth); + AuthService(this._desktopAuthManager, this.fireStore, this.firebaseAuth, + this._accountService); Future signInWithGoogle() async { final GoogleSignIn googleSignIn = GoogleSignIn(); @@ -46,7 +56,7 @@ class AuthService { idToken: googleSignInAuthentication.idToken, ); - user = await _signInWithCredentials(credential); + user = await signInWithCredentials(credential); await googleSignIn.signOut(); } } catch (e) { @@ -63,7 +73,7 @@ class AuthService { idToken: credentials.idToken, accessToken: credentials.accessToken); - user = await _signInWithCredentials(authCredential); + user = await signInWithCredentials(authCredential); await _desktopAuthManager.signOutFromGoogle(credentials.accessToken); } on Exception { @@ -73,8 +83,9 @@ class AuthService { return user; } - Future _signInWithCredentials( - firebase_auth.AuthCredential authCredential) async { + Future signInWithCredentials( + firebase_auth.AuthCredential authCredential, + ) async { firebase_auth.User? user; try { final firebase_auth.UserCredential userCredential = @@ -86,7 +97,49 @@ class AuthService { return user; } - Future signOutWithGoogle() async { + Future signInWithApple() async { + final rawNounce = generateNonce(); + final nounce = sha256ofString(rawNounce); + + final appleCredential = await SignInWithApple.getAppleIDCredential(scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName + ], nonce: nounce); + + final oAuthCredential = firebase_auth.OAuthProvider("apple.com").credential( + idToken: appleCredential.identityToken, rawNonce: rawNounce); + + final name = appleCredential.givenName; + final authUser = await signInWithCredentials(oAuthCredential); + if (name != null) { + return await setUser(authUser, name: name); + } + return await setUser(authUser); + } + + Future setUser(firebase_auth.User? authUser, {String? name}) async { + if (authUser != null) { + final Account user = await _accountService.getUser(authUser, name: name); + return user; + } + return null; + } + + String generateNounce([int length = 32]) { + const charset = + '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; + final random = Random.secure(); + return List.generate(length, (_) => charset[random.nextInt(charset.length)]) + .join(); + } + + String sha256ofString(String input) { + final bytes = utf8.encode(input); + final digest = sha256.convert(bytes); + return digest.toString(); + } + + Future signOut() async { try { await firebaseAuth.signOut(); return true; diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index e820e17c7..13101aaab 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -15,6 +15,9 @@ class $AssetsImagesGen { /// File path: assets/images/app_logo.svg String get appLogo => 'assets/images/app_logo.svg'; + /// File path: assets/images/apple_logo.svg + String get appleLogo => 'assets/images/apple_logo.svg'; + /// File path: assets/images/calendar_filled.svg String get calendarFilled => 'assets/images/calendar_filled.svg'; @@ -22,9 +25,8 @@ class $AssetsImagesGen { AssetGenImage get emptyState => const AssetGenImage('assets/images/empty_state.png'); - /// File path: assets/images/google_logo.png - AssetGenImage get googleLogo => - const AssetGenImage('assets/images/google_logo.png'); + /// File path: assets/images/google_logo.svg + String get googleLogo => 'assets/images/google_logo.svg'; /// File path: assets/images/home_filled.svg String get homeFilled => 'assets/images/home_filled.svg'; @@ -54,6 +56,7 @@ class $AssetsImagesGen { /// List of all assets List get values => [ appLogo, + appleLogo, calendarFilled, emptyState, googleLogo, diff --git a/lib/style/other/smart_scroll_view.dart b/lib/style/other/smart_scroll_view.dart new file mode 100644 index 000000000..d477f9b62 --- /dev/null +++ b/lib/style/other/smart_scroll_view.dart @@ -0,0 +1,36 @@ + +// A scrollview that supports Spacer to fill the remaining space +import 'package:flutter/cupertino.dart'; + +class SmartScrollView extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry? padding; + final ScrollController? controller; + + const SmartScrollView({ + super.key, + required this.child, + this.padding, + this.controller, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + controller: controller, + padding: padding, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - (padding?.vertical ?? 0), + ), + child: IntrinsicHeight( + child: child, + ), + ), + ); + }, + ); + } +} \ No newline at end of file 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 5b5799075..4bb05a9a4 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_bloc.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_bloc.dart @@ -1,13 +1,20 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:crypto/crypto.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:injectable/injectable.dart'; import 'package:projectunity/data/services/account_service.dart'; +import 'package:projectunity/ui/sign_in/widget/apple_signin_button.dart'; import '../../../data/core/exception/error_const.dart'; import '../../../data/model/account/account.dart'; import '../../../data/provider/user_state.dart'; import '../../../data/services/auth_service.dart'; import 'sign_in_view_event.dart'; import 'sign_in_view_state.dart'; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; @Injectable() class SignInBloc extends Bloc { @@ -19,23 +26,49 @@ class SignInBloc extends Bloc { this._userStateNotifier, this._authService, this._accountService, - ) : super(SignInInitialState()) { - on(_signIn); + ) : super(const SignInState()) { + on(_googleSignIn); + on(_appleSignIn); + } + + Future check(Emitter emit) async { + final isAvailable = await SignInWithApple.isAvailable(); + emit(state.copyWith(appleSignInAvailable: isAvailable)); } - Future _signIn(SignInEvent event, Emitter emit) async { - emit(SignInLoadingState()); + Future _googleSignIn( + SignInEvent event, Emitter emit) async { try { + emit(state.copyWith(googleSignInLoading: true)); firebase_auth.User? authUser = await _authService.signInWithGoogle(); if (authUser != null) { final Account user = await _accountService.getUser(authUser); await _userStateNotifier.setUser(user); - emit(SignInSuccessState()); + emit(state.copyWith(googleSignInLoading: false, signInSuccess: true)); } else { - emit(SignInInitialState()); + emit(state.copyWith(googleSignInLoading: false)); } } on Exception { - emit(SignInFailureState(error: firesbaseAuthError)); + emit(state.copyWith( + googleSignInLoading: false, error: firesbaseAuthError)); + } + } + + Future _appleSignIn( + AppleSignInEvent event, Emitter emit) async { + try { + emit(state.copyWith(appleSignInLoading: true)); + Account? user = await _authService.signInWithApple(); + if (user != null) { + await _userStateNotifier.setUser(user); + emit(state.copyWith(appleSignInLoading: false, signInSuccess: true)); + } else { + emit(state.copyWith(appleSignInLoading: false)); + } + } on Exception catch (e) { + emit( + state.copyWith(appleSignInLoading: false, error: firesbaseAuthError)); + throw Exception(e.toString()); } } } diff --git a/lib/ui/sign_in/bloc/sign_in_view_event.dart b/lib/ui/sign_in/bloc/sign_in_view_event.dart index c58a605ca..23909f59d 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_event.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_event.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; -class SignInEvent extends Equatable { - @override - List get props => []; -} +abstract class SignInEvent{} + +class GoogleSignInEvent extends SignInEvent {} +class AppleSignInEvent extends SignInEvent {} + 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 c362138bf..da5aac34d 100644 --- a/lib/ui/sign_in/bloc/sign_in_view_state.dart +++ b/lib/ui/sign_in/bloc/sign_in_view_state.dart @@ -1,27 +1,18 @@ import 'package:equatable/equatable.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -abstract class SignInState extends Equatable {} +part "sign_in_view_state.freezed.dart"; -class SignInInitialState extends SignInState { - @override - List get props => []; -} -class SignInLoadingState extends SignInState { - @override - List get props => []; +@freezed +class SignInState with _$SignInState{ + const factory SignInState({ + @Default(false) appleSignInAvailable, + @Default(false) googleSignInLoading, + @Default(false) appleSignInLoading, + @Default(false) signInSuccess, + String? error +}) = _SignInState; } -class SignInFailureState extends SignInState { - final String error; - - SignInFailureState({required this.error}); - @override - List get props => [error]; -} - -class SignInSuccessState extends SignInState { - @override - List get props => []; -} 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 new file mode 100644 index 000000000..041c6f94f --- /dev/null +++ b/lib/ui/sign_in/bloc/sign_in_view_state.freezed.dart @@ -0,0 +1,225 @@ +// 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 'sign_in_view_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 _$SignInState { + dynamic get appleSignInAvailable => throw _privateConstructorUsedError; + dynamic get googleSignInLoading => throw _privateConstructorUsedError; + dynamic get appleSignInLoading => throw _privateConstructorUsedError; + dynamic get signInSuccess => throw _privateConstructorUsedError; + String? get error => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $SignInStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SignInStateCopyWith<$Res> { + factory $SignInStateCopyWith( + SignInState value, $Res Function(SignInState) then) = + _$SignInStateCopyWithImpl<$Res, SignInState>; + @useResult + $Res call( + {dynamic appleSignInAvailable, + dynamic googleSignInLoading, + dynamic appleSignInLoading, + dynamic signInSuccess, + String? error}); +} + +/// @nodoc +class _$SignInStateCopyWithImpl<$Res, $Val extends SignInState> + implements $SignInStateCopyWith<$Res> { + _$SignInStateCopyWithImpl(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? appleSignInAvailable = freezed, + Object? googleSignInLoading = freezed, + Object? appleSignInLoading = freezed, + Object? signInSuccess = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + appleSignInAvailable: freezed == appleSignInAvailable + ? _value.appleSignInAvailable + : appleSignInAvailable // ignore: cast_nullable_to_non_nullable + as dynamic, + googleSignInLoading: freezed == googleSignInLoading + ? _value.googleSignInLoading + : googleSignInLoading // ignore: cast_nullable_to_non_nullable + as dynamic, + appleSignInLoading: freezed == appleSignInLoading + ? _value.appleSignInLoading + : appleSignInLoading // ignore: cast_nullable_to_non_nullable + as dynamic, + signInSuccess: freezed == signInSuccess + ? _value.signInSuccess + : signInSuccess // ignore: cast_nullable_to_non_nullable + as dynamic, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SignInStateImplCopyWith<$Res> + implements $SignInStateCopyWith<$Res> { + factory _$$SignInStateImplCopyWith( + _$SignInStateImpl value, $Res Function(_$SignInStateImpl) then) = + __$$SignInStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {dynamic appleSignInAvailable, + dynamic googleSignInLoading, + dynamic appleSignInLoading, + dynamic signInSuccess, + String? error}); +} + +/// @nodoc +class __$$SignInStateImplCopyWithImpl<$Res> + extends _$SignInStateCopyWithImpl<$Res, _$SignInStateImpl> + implements _$$SignInStateImplCopyWith<$Res> { + __$$SignInStateImplCopyWithImpl( + _$SignInStateImpl _value, $Res Function(_$SignInStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? appleSignInAvailable = freezed, + Object? googleSignInLoading = freezed, + Object? appleSignInLoading = freezed, + Object? signInSuccess = freezed, + Object? error = freezed, + }) { + return _then(_$SignInStateImpl( + appleSignInAvailable: freezed == appleSignInAvailable + ? _value.appleSignInAvailable! + : appleSignInAvailable, + googleSignInLoading: freezed == googleSignInLoading + ? _value.googleSignInLoading! + : googleSignInLoading, + appleSignInLoading: freezed == appleSignInLoading + ? _value.appleSignInLoading! + : appleSignInLoading, + signInSuccess: + freezed == signInSuccess ? _value.signInSuccess! : signInSuccess, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc + +class _$SignInStateImpl implements _SignInState { + const _$SignInStateImpl( + {this.appleSignInAvailable = false, + this.googleSignInLoading = false, + this.appleSignInLoading = false, + this.signInSuccess = false, + this.error}); + + @override + @JsonKey() + final dynamic appleSignInAvailable; + @override + @JsonKey() + final dynamic googleSignInLoading; + @override + @JsonKey() + final dynamic appleSignInLoading; + @override + @JsonKey() + final dynamic signInSuccess; + @override + final String? error; + + @override + String toString() { + return 'SignInState(appleSignInAvailable: $appleSignInAvailable, googleSignInLoading: $googleSignInLoading, appleSignInLoading: $appleSignInLoading, signInSuccess: $signInSuccess, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SignInStateImpl && + const DeepCollectionEquality() + .equals(other.appleSignInAvailable, appleSignInAvailable) && + const DeepCollectionEquality() + .equals(other.googleSignInLoading, googleSignInLoading) && + const DeepCollectionEquality() + .equals(other.appleSignInLoading, appleSignInLoading) && + const DeepCollectionEquality() + .equals(other.signInSuccess, signInSuccess) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(appleSignInAvailable), + const DeepCollectionEquality().hash(googleSignInLoading), + const DeepCollectionEquality().hash(appleSignInLoading), + const DeepCollectionEquality().hash(signInSuccess), + error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SignInStateImplCopyWith<_$SignInStateImpl> get copyWith => + __$$SignInStateImplCopyWithImpl<_$SignInStateImpl>(this, _$identity); +} + +abstract class _SignInState implements SignInState { + const factory _SignInState( + {final dynamic appleSignInAvailable, + final dynamic googleSignInLoading, + final dynamic appleSignInLoading, + final dynamic signInSuccess, + final String? error}) = _$SignInStateImpl; + + @override + dynamic get appleSignInAvailable; + @override + dynamic get googleSignInLoading; + @override + dynamic get appleSignInLoading; + @override + dynamic get signInSuccess; + @override + String? get error; + @override + @JsonKey(ignore: true) + _$$SignInStateImplCopyWith<_$SignInStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/sign_in/sign_in_screen.dart b/lib/ui/sign_in/sign_in_screen.dart index f17a48c28..519d18a12 100644 --- a/lib/ui/sign_in/sign_in_screen.dart +++ b/lib/ui/sign_in/sign_in_screen.dart @@ -1,9 +1,13 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.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/ui/sign_in/widget/sign_in_button.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/utils/const/image_constant.dart'; import '../../data/di/service_locator.dart'; import '../widget/error_snack_bar.dart'; @@ -35,66 +39,63 @@ class SignInScreenState extends State { return AppPage( backGroundColor: context.colorScheme.surface, body: BlocListener( - listener: (context, state) { - if (state is SignInFailureState) { - showSnackBar(context: context, error: state.error); - } - }, - child: SingleChildScrollView( - child: Column( - children: [ - Container( - constraints: const BoxConstraints(minHeight: 400), - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Padding( - padding: const EdgeInsets.all(20).copyWith(bottom: 60), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(height: 20), - Flexible( - child: Image.asset(ImageConst.loginPageVectorImage, - width: MediaQuery.of(context).size.width * 0.8), - ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - context.l10n.sign_in_title_text, - textAlign: TextAlign.center, - style: AppTextStyle.style24.copyWith( - color: context.colorScheme.textPrimary, - overflow: TextOverflow.fade, - fontWeight: FontWeight.w700, - ), - ), + listenWhen: (previous,current)=>current.error != null, + listener: (context, state) { + if (state.error != null) { + showSnackBar(context: context, error: state.error); + } + }, + child: SafeArea( + child: SmartScrollView( + padding: const EdgeInsets.all(16).copyWith(bottom: 50), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(ImageConst.loginPageVectorImage, + fit: BoxFit.cover, + ), + const SizedBox(height: 50,), + Text( + context.l10n.sign_in_title_text, + textAlign: TextAlign.center, + style: AppTextStyle.style24.copyWith( + color: context.colorScheme.textPrimary, + overflow: TextOverflow.fade, + fontWeight: FontWeight.w700, ), - Flexible( - child: Padding( - padding: const EdgeInsets.only( - left: 20.0, right: 20, top: 20, bottom: 40), - child: Text( - context.l10n.sign_in_description_text, - style: AppTextStyle.style16.copyWith( - color: context.colorScheme.textSecondary), - overflow: TextOverflow.fade, - textAlign: TextAlign.center, - ), - ), + ), + Padding( + padding: const EdgeInsets.only( + left: 20.0, right: 20, top: 20, bottom: 40), + child: Text( + context.l10n.sign_in_description_text, + style: AppTextStyle.style16 + .copyWith(color: context.colorScheme.textSecondary), + overflow: TextOverflow.fade, + textAlign: TextAlign.center, ), - ], - ), - const SignInButton() - ], + ), + + ], + + ), ), - ), + + + + const GoogleSignInButton(), + const SizedBox( + height: 20, + ), + const AppleSignInButton() + ], ), - ], - ), - ), - ), + ), + )), ); } } diff --git a/lib/ui/sign_in/widget/apple_signin_button.dart b/lib/ui/sign_in/widget/apple_signin_button.dart new file mode 100644 index 000000000..dd291ee6f --- /dev/null +++ b/lib/ui/sign_in/widget/apple_signin_button.dart @@ -0,0 +1,51 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:projectunity/data/core/extensions/context_extension.dart'; +import '../../../gen/assets.gen.dart'; +import '../../../style/app_text_style.dart'; +import '../../../style/other/app_button.dart'; +import '../bloc/sign_in_view_bloc.dart'; +import '../bloc/sign_in_view_event.dart'; +import '../bloc/sign_in_view_state.dart'; + +class AppleSignInButton extends StatelessWidget { + const AppleSignInButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => previous.appleSignInLoading!=current.appleSignInLoading, + builder: (context, state) { + return AppButton( + onTap: () => context.read().add(AppleSignInEvent()), + loading: state.appleSignInLoading, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.surface), + child: SvgPicture.asset(Assets.images.appleLogo)), + const SizedBox( + width: 20, + ), + Flexible( + child: Text( + context.l10n.apple_login_button_text, + style: AppTextStyle.style18 + .copyWith(color: context.colorScheme.surface), + overflow: TextOverflow.clip, + ), + ), + const SizedBox(width: 35), + ], + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/ui/sign_in/widget/sign_in_button.dart b/lib/ui/sign_in/widget/google_signin_button.dart similarity index 69% rename from lib/ui/sign_in/widget/sign_in_button.dart rename to lib/ui/sign_in/widget/google_signin_button.dart index a8d552dfa..aafc502f8 100644 --- a/lib/ui/sign_in/widget/sign_in_button.dart +++ b/lib/ui/sign_in/widget/google_signin_button.dart @@ -1,45 +1,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:projectunity/data/core/extensions/context_extension.dart'; import 'package:projectunity/style/app_text_style.dart'; import 'package:projectunity/style/other/app_button.dart'; import '../../../data/core/utils/const/image_constant.dart'; +import '../../../gen/assets.gen.dart'; import '../bloc/sign_in_view_bloc.dart'; import '../bloc/sign_in_view_event.dart'; import '../bloc/sign_in_view_state.dart'; -class SignInButton extends StatelessWidget { - const SignInButton({ +class GoogleSignInButton extends StatelessWidget { + const GoogleSignInButton({ super.key, }); @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (previous, current) => - previous is SignInLoadingState || current is SignInLoadingState, + buildWhen: (previous, current) => previous.googleSignInLoading != current.googleSignInLoading, builder: (context, state) { return AppButton( - onTap: () => context.read().add(SignInEvent()), - loading: state is SignInLoadingState, + onTap: () => context.read().add(GoogleSignInEvent()), + loading: state.googleSignInLoading, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( + padding: const EdgeInsets.all(5), decoration: BoxDecoration( shape: BoxShape.circle, color: context.colorScheme.surface), - child: const Image( - image: AssetImage(ImageConst.googleLogoImage), - fit: BoxFit.contain, - width: 35, - height: 35)), + child: SvgPicture.asset( + Assets.images.googleLogo, + )), const SizedBox( width: 20, ), Flexible( child: Text( - context.l10n.login_button_text, + context.l10n.google_login_button_text, style: AppTextStyle.style18 .copyWith(color: context.colorScheme.surface), overflow: TextOverflow.clip, diff --git a/lib/ui/space/join_space/bloc/join_space_bloc.dart b/lib/ui/space/join_space/bloc/join_space_bloc.dart index 7e8bed96c..5cd5e3391 100644 --- a/lib/ui/space/join_space/bloc/join_space_bloc.dart +++ b/lib/ui/space/join_space/bloc/join_space_bloc.dart @@ -135,7 +135,7 @@ class JoinSpaceBloc extends Bloc { SignOutEvent event, Emitter emit) async { emit(state.copyWith(signOutStatus: Status.loading)); try { - bool isLogOut = await _authService.signOutWithGoogle(); + bool isLogOut = await _authService.signOut(); if (isLogOut) { await _userManager.removeAll(); emit(state.copyWith(signOutStatus: Status.success)); diff --git a/pubspec.lock b/pubspec.lock index d1a4a90a4..58e1b24f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -250,7 +250,7 @@ packages: source: hosted version: "0.3.3+8" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab @@ -1189,8 +1189,32 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - simple_gesture_detector: + sign_in_with_apple: dependency: "direct main" + description: + name: sign_in_with_apple + sha256: "3dcc472f6d3ef6915828b4aa536da14df20a6d1fee65c02eaa7e75b5c4f3f599" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + sign_in_with_apple_platform_interface: + dependency: transitive + description: + name: sign_in_with_apple_platform_interface + sha256: a5883edee09ed6be19de19e7d9f618a617fe41a6fa03f76d082dfb787e9ea18d + url: "https://pub.dev" + source: hosted + version: "1.0.0" + sign_in_with_apple_web: + dependency: transitive + description: + name: sign_in_with_apple_web + sha256: ffe74793bc31e7e7cc7e149fe14711a59e50a9cbbb3198e1f67fda92492f7b54 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + simple_gesture_detector: + dependency: transitive description: name: simple_gesture_detector sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 @@ -1532,4 +1556,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.19.1" diff --git a/pubspec.yaml b/pubspec.yaml index 13a1e8ba4..6c5bf92a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: cloud_firestore: ^4.5.2 collection: ^1.17.0 connectivity_plus: ^5.0.2 + crypto: device_info_plus: ^9.1.2 equatable: ^2.0.5 freezed: ^2.4.7 @@ -42,9 +43,9 @@ dependencies: json_annotation: ^4.8.1 oauth2: ^2.0.2 package_info_plus: ^5.0.1 - simple_gesture_detector: rxdart: ^0.27.7 shared_preferences: ^2.1.1 + sign_in_with_apple: ^6.0.0 sticky_headers: ^0.3.0+2 table_calendar: ^3.0.7 url_launcher: ^6.1.11 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 417a111cf..61303bf13 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 @@ -127,16 +127,22 @@ class MockAccountService extends _i1.Mock implements _i8.AccountService { ) as _i3.DeviceInfoProvider); @override - _i9.Future<_i4.Account> getUser(_i10.User? authData) => (super.noSuchMethod( + _i9.Future<_i4.Account> getUser( + _i10.User? authData, { + String? name, + }) => + (super.noSuchMethod( Invocation.method( #getUser, [authData], + {#name: name}, ), returnValue: _i9.Future<_i4.Account>.value(_FakeAccount_2( this, Invocation.method( #getUser, [authData], + {#name: name}, ), )), ) as _i9.Future<_i4.Account>); diff --git a/test/unit_test/authentication/login/login_bloc_test.dart b/test/unit_test/authentication/login/login_bloc_test.dart index 4939ce95b..5020dd373 100644 --- a/test/unit_test/authentication/login/login_bloc_test.dart +++ b/test/unit_test/authentication/login/login_bloc_test.dart @@ -36,15 +36,16 @@ void main() { bloc = SignInBloc(userStateNotifier, authService, accountService); }); + group("Log in with google test", () { test("Set initial state on cancel login test", () async { when(authService.signInWithGoogle()).thenAnswer((_) async => null); - bloc.add(SignInEvent()); + bloc.add(GoogleSignInEvent()); expect( bloc.stream, emitsInOrder([ - SignInLoadingState(), - SignInInitialState(), + const SignInState(googleSignInLoading: true), + const SignInState(googleSignInLoading: false), ])); }); @@ -53,12 +54,12 @@ void main() { when(authUser.email).thenAnswer((realInvocation) => user.email); when(authService.signInWithGoogle()).thenAnswer((_) async => authUser); when(accountService.getUser(authUser)).thenAnswer((_) async => user); - bloc.add(SignInEvent()); + bloc.add(GoogleSignInEvent()); expect( bloc.stream, emitsInOrder([ - SignInLoadingState(), - SignInSuccessState(), + const SignInState(googleSignInLoading: true), + const SignInState(googleSignInLoading: false,signInSuccess: true), ])); await untilCalled(userStateNotifier.setUser(user)); verify(userStateNotifier.setUser(user)).called(1); 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 3d126737c..6b6c18689 100644 --- a/test/unit_test/authentication/login/login_bloc_test.mocks.dart +++ b/test/unit_test/authentication/login/login_bloc_test.mocks.dart @@ -9,13 +9,13 @@ import 'dart:ui' as _i13; import 'package:cloud_firestore/cloud_firestore.dart' as _i2; import 'package:firebase_auth/firebase_auth.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i11; +import 'package:mockito/src/dummies.dart' as _i9; import 'package:projectunity/data/model/account/account.dart' as _i5; import 'package:projectunity/data/model/employee/employee.dart' as _i6; import 'package:projectunity/data/model/space/space.dart' as _i12; import 'package:projectunity/data/provider/device_info.dart' as _i4; -import 'package:projectunity/data/provider/user_state.dart' as _i10; -import 'package:projectunity/data/services/account_service.dart' as _i9; +import 'package:projectunity/data/provider/user_state.dart' as _i11; +import 'package:projectunity/data/services/account_service.dart' as _i10; import 'package:projectunity/data/services/auth_service.dart' as _i7; // ignore_for_file: type=lint @@ -181,9 +181,73 @@ class MockAuthService extends _i1.Mock implements _i7.AuthService { ) as _i8.Future<_i3.User?>); @override - _i8.Future signOutWithGoogle() => (super.noSuchMethod( + _i8.Future<_i3.User?> signInWithCredentials( + _i3.AuthCredential? authCredential) => + (super.noSuchMethod( + Invocation.method( + #signInWithCredentials, + [authCredential], + ), + returnValue: _i8.Future<_i3.User?>.value(), + ) as _i8.Future<_i3.User?>); + + @override + _i8.Future<_i5.Account?> signInWithApple() => (super.noSuchMethod( Invocation.method( - #signOutWithGoogle, + #signInWithApple, + [], + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + _i8.Future<_i5.Account?> setUser( + _i3.User? authUser, { + String? name, + }) => + (super.noSuchMethod( + Invocation.method( + #setUser, + [authUser], + {#name: name}, + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + String generateNounce([int? length = 32]) => (super.noSuchMethod( + Invocation.method( + #generateNounce, + [length], + ), + returnValue: _i9.dummyValue( + this, + Invocation.method( + #generateNounce, + [length], + ), + ), + ) as String); + + @override + String sha256ofString(String? input) => (super.noSuchMethod( + Invocation.method( + #sha256ofString, + [input], + ), + returnValue: _i9.dummyValue( + this, + Invocation.method( + #sha256ofString, + [input], + ), + ), + ) as String); + + @override + _i8.Future signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, [], ), returnValue: _i8.Future.value(false), @@ -193,7 +257,7 @@ class MockAuthService extends _i1.Mock implements _i7.AuthService { /// A class which mocks [AccountService]. /// /// See the documentation for Mockito's code generation for more information. -class MockAccountService extends _i1.Mock implements _i9.AccountService { +class MockAccountService extends _i1.Mock implements _i10.AccountService { MockAccountService() { _i1.throwOnMissingStub(this); } @@ -217,16 +281,22 @@ class MockAccountService extends _i1.Mock implements _i9.AccountService { ) as _i4.DeviceInfoProvider); @override - _i8.Future<_i5.Account> getUser(_i3.User? authData) => (super.noSuchMethod( + _i8.Future<_i5.Account> getUser( + _i3.User? authData, { + String? name, + }) => + (super.noSuchMethod( Invocation.method( #getUser, [authData], + {#name: name}, ), returnValue: _i8.Future<_i5.Account>.value(_FakeAccount_3( this, Invocation.method( #getUser, [authData], + {#name: name}, ), )), ) as _i8.Future<_i5.Account>); @@ -300,21 +370,21 @@ class MockAccountService extends _i1.Mock implements _i9.AccountService { /// A class which mocks [UserStateNotifier]. /// /// See the documentation for Mockito's code generation for more information. -class MockUserStateNotifier extends _i1.Mock implements _i10.UserStateNotifier { +class MockUserStateNotifier extends _i1.Mock implements _i11.UserStateNotifier { MockUserStateNotifier() { _i1.throwOnMissingStub(this); } @override - _i10.UserState get state => (super.noSuchMethod( + _i11.UserState get state => (super.noSuchMethod( Invocation.getter(#state), - returnValue: _i10.UserState.authenticated, - ) as _i10.UserState); + returnValue: _i11.UserState.authenticated, + ) as _i11.UserState); @override String get employeeId => (super.noSuchMethod( Invocation.getter(#employeeId), - returnValue: _i11.dummyValue( + returnValue: _i9.dummyValue( this, Invocation.getter(#employeeId), ), @@ -519,7 +589,7 @@ class MockUser extends _i1.Mock implements _i3.User { @override String get uid => (super.noSuchMethod( Invocation.getter(#uid), - returnValue: _i11.dummyValue( + returnValue: _i9.dummyValue( this, Invocation.getter(#uid), ), diff --git a/test/unit_test/shared/drawer/drawer_test.mocks.dart b/test/unit_test/shared/drawer/drawer_test.mocks.dart index 5addd28d2..43a093b7f 100644 --- a/test/unit_test/shared/drawer/drawer_test.mocks.dart +++ b/test/unit_test/shared/drawer/drawer_test.mocks.dart @@ -502,16 +502,22 @@ class MockAccountService extends _i1.Mock implements _i13.AccountService { ) as _i4.DeviceInfoProvider); @override - _i8.Future<_i5.Account> getUser(_i14.User? authData) => (super.noSuchMethod( + _i8.Future<_i5.Account> getUser( + _i14.User? authData, { + String? name, + }) => + (super.noSuchMethod( Invocation.method( #getUser, [authData], + {#name: name}, ), returnValue: _i8.Future<_i5.Account>.value(_FakeAccount_3( this, Invocation.method( #getUser, [authData], + {#name: name}, ), )), ) as _i8.Future<_i5.Account>); diff --git a/test/unit_test/space/join_space/join_space_test.dart b/test/unit_test/space/join_space/join_space_test.dart index 023a02b61..2979d7e5c 100644 --- a/test/unit_test/space/join_space/join_space_test.dart +++ b/test/unit_test/space/join_space/join_space_test.dart @@ -230,7 +230,7 @@ void main() { }); group("sign out test ", () { test("sign out successful test with navigation test", () async { - when(authService.signOutWithGoogle()) + when(authService.signOut()) .thenAnswer((_) => Future(() => true)); bloc.add(SignOutEvent()); expect( @@ -244,7 +244,7 @@ void main() { }); test("sign out failure test", () { - when(authService.signOutWithGoogle()) + when(authService.signOut()) .thenAnswer((_) => Future(() => false)); bloc.add(SignOutEvent()); expect( @@ -257,7 +257,7 @@ void main() { }); test("sign out failure test on exception", () { - when(authService.signOutWithGoogle()).thenThrow(Exception(signOutError)); + when(authService.signOut()).thenThrow(Exception(signOutError)); bloc.add(SignOutEvent()); expect( bloc.stream, 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 6a988bb89..66eeb475e 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 @@ -489,16 +489,22 @@ class MockAccountService extends _i1.Mock implements _i15.AccountService { ) as _i4.DeviceInfoProvider); @override - _i8.Future<_i5.Account> getUser(_i6.User? authData) => (super.noSuchMethod( + _i8.Future<_i5.Account> getUser( + _i6.User? authData, { + String? name, + }) => + (super.noSuchMethod( Invocation.method( #getUser, [authData], + {#name: name}, ), returnValue: _i8.Future<_i5.Account>.value(_FakeAccount_3( this, Invocation.method( #getUser, [authData], + {#name: name}, ), )), ) as _i8.Future<_i5.Account>); @@ -725,9 +731,73 @@ class MockAuthService extends _i1.Mock implements _i17.AuthService { ) as _i8.Future<_i6.User?>); @override - _i8.Future signOutWithGoogle() => (super.noSuchMethod( + _i8.Future<_i6.User?> signInWithCredentials( + _i6.AuthCredential? authCredential) => + (super.noSuchMethod( + Invocation.method( + #signInWithCredentials, + [authCredential], + ), + returnValue: _i8.Future<_i6.User?>.value(), + ) as _i8.Future<_i6.User?>); + + @override + _i8.Future<_i5.Account?> signInWithApple() => (super.noSuchMethod( + Invocation.method( + #signInWithApple, + [], + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + _i8.Future<_i5.Account?> setUser( + _i6.User? authUser, { + String? name, + }) => + (super.noSuchMethod( + Invocation.method( + #setUser, + [authUser], + {#name: name}, + ), + returnValue: _i8.Future<_i5.Account?>.value(), + ) as _i8.Future<_i5.Account?>); + + @override + String generateNounce([int? length = 32]) => (super.noSuchMethod( + Invocation.method( + #generateNounce, + [length], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #generateNounce, + [length], + ), + ), + ) as String); + + @override + String sha256ofString(String? input) => (super.noSuchMethod( + Invocation.method( + #sha256ofString, + [input], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #sha256ofString, + [input], + ), + ), + ) as String); + + @override + _i8.Future signOut() => (super.noSuchMethod( Invocation.method( - #signOutWithGoogle, + #signOut, [], ), returnValue: _i8.Future.value(false),