From 82ab874a93b3b368bb48d536122ae4534b0339fc Mon Sep 17 00:00:00 2001 From: jihye Date: Sun, 17 Dec 2023 15:57:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20[=EA=B3=B5=ED=86=B5]=20get=5Fit?= =?UTF-8?q?=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=9C=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=ED=99=98=EA=B2=BD=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- windows/flutter/generated_plugin_registrant.cc | 3 --- windows/flutter/generated_plugins.cmake | 1 - 2 files changed, 4 deletions(-) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d141b74..1a82e7d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 29944d5..fa8a39b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - firebase_auth firebase_core ) From bc2561a28cf4cc464c32cd934648f1765247a9a7 Mon Sep 17 00:00:00 2001 From: Yellowtoast Date: Wed, 27 Dec 2023 18:01:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20[=EC=8A=A4=ED=94=8C=EB=9E=98?= =?UTF-8?q?=EC=8B=9C]=20=EC=8A=A4=ED=94=8C=EB=9E=98=EC=8B=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=9D=B8=EC=A6=9D=EC=A0=95=EB=B3=B4=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#29?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/user_api.dart | 3 +- lib/api/user_api.g.dart | 3 +- lib/app/di/app_binding.dart | 2 +- lib/app/di/modules/user_di.dart | 18 +++- lib/app/env/flavors.dart | 3 + lib/app/router/app_router.dart | 16 ++- lib/app/router/app_router.g.dart | 2 +- lib/core/network_handling/app_dio.dart | 2 +- .../exceptions/custom_exception.dart | 26 +++++ .../interceptors/token_interceptor.dart | 17 ++- lib/core/services/local_storage_service.dart | 100 ++++++++++++++++++ lib/core/services/toast_service.dart | 18 ++++ .../auth/data/phone_auth_repository.dart | 2 +- lib/features/pet/data/pet_repository.dart | 2 +- .../data/local/user_local_data_source.dart | 4 + .../local/user_local_data_source_impl.dart | 18 ++++ .../data/remote/user_remote_data_source.dart | 2 +- .../remote/user_remote_data_source_impl.dart | 12 ++- .../user/repositories/user_repository.dart | 11 +- .../repositories/user_repository_impl.dart | 30 ++++-- .../user/usecases/get_user_data_use_case.dart | 3 +- .../usecases/get_user_token_use_case.dart | 13 +++ .../update_user_token_local_use_case.dart | 13 +++ lib/features/user/user.dart | 8 ++ .../pages/splash/splash_event.dart | 33 ++++++ .../pages/splash/splash_page.dart | 20 +++- .../providers/user/user_data_provider.dart | 34 +++++- .../providers/user/user_data_provider.g.dart | 16 ++- lib/presentation/widgets/toast/app_toast.dart | 53 ++++++++++ pubspec.yaml | 1 + 30 files changed, 445 insertions(+), 40 deletions(-) create mode 100644 lib/core/network_handling/exceptions/custom_exception.dart create mode 100644 lib/core/services/local_storage_service.dart create mode 100644 lib/core/services/toast_service.dart create mode 100644 lib/features/user/data/local/user_local_data_source.dart create mode 100644 lib/features/user/data/local/user_local_data_source_impl.dart create mode 100644 lib/features/user/usecases/get_user_token_use_case.dart create mode 100644 lib/features/user/usecases/update_user_token_local_use_case.dart create mode 100644 lib/presentation/pages/splash/splash_event.dart create mode 100644 lib/presentation/widgets/toast/app_toast.dart diff --git a/lib/api/user_api.dart b/lib/api/user_api.dart index b14329b..c489db2 100644 --- a/lib/api/user_api.dart +++ b/lib/api/user_api.dart @@ -1,4 +1,4 @@ -import 'package:dio/dio.dart'; +import 'package:dio/dio.dart' hide Headers; import 'package:pets_next_door_flutter/features/user/data/models/user_data_model.dart'; import 'package:retrofit/retrofit.dart'; @@ -9,5 +9,6 @@ abstract class UserAPI { factory UserAPI(Dio dio, {String baseUrl}) = _UserAPI; @GET("/users/me") + @Headers({"requiresToken": true}) Future getUserData(); } diff --git a/lib/api/user_api.g.dart b/lib/api/user_api.g.dart index a42c5a0..4b8a8cc 100644 --- a/lib/api/user_api.g.dart +++ b/lib/api/user_api.g.dart @@ -22,7 +22,8 @@ class _UserAPI implements UserAPI { Future getUserData() async { const _extra = {}; final queryParameters = {}; - final _headers = {}; + final _headers = {r'requiresToken': true}; + _headers.removeWhere((k, v) => v == null); final Map? _data = null; final _result = await _dio .fetch>(_setStreamType(Options( diff --git a/lib/app/di/app_binding.dart b/lib/app/di/app_binding.dart index 484eece..76e81d7 100644 --- a/lib/app/di/app_binding.dart +++ b/lib/app/di/app_binding.dart @@ -5,7 +5,7 @@ final class AppBinder { /// 'Splash' 단계에서 우선적으로 Binding 해야되는 모듈들은 /// 아래 메소드에서 처리합 - static void _initTopPriority() {} + static Future _initTopPriority() async {} static void init() { _initTopPriority(); diff --git a/lib/app/di/modules/user_di.dart b/lib/app/di/modules/user_di.dart index 7dec4c6..a55762d 100644 --- a/lib/app/di/modules/user_di.dart +++ b/lib/app/di/modules/user_di.dart @@ -1,15 +1,20 @@ import 'package:get_it/get_it.dart'; import 'package:pets_next_door_flutter/app/di/feature_di_interface.dart'; +import 'package:pets_next_door_flutter/features/user/data/local/user_local_data_source.dart'; +import 'package:pets_next_door_flutter/features/user/data/local/user_local_data_source_impl.dart'; import 'package:pets_next_door_flutter/features/user/data/remote/user_remote_data_source_impl.dart'; import 'package:pets_next_door_flutter/features/user/repositories/user_repository_impl.dart'; import 'package:pets_next_door_flutter/features/user/user.dart'; final class UserDependencyInjection extends FeatureDependencyInjection { @override - void dataSources() { + Future dataSources() async { GetIt.I.registerLazySingleton( UserRemoteDataSourceImpl.new, ); + + GetIt.I.registerLazySingleton( + UserLocalDataSourceImpl.new); } @override @@ -17,6 +22,7 @@ final class UserDependencyInjection extends FeatureDependencyInjection { GetIt.I.registerLazySingleton( () => UserRepositoryImpl( userRemoteDataSource, + userLocalDataSource, ), ); } @@ -25,7 +31,15 @@ final class UserDependencyInjection extends FeatureDependencyInjection { void useCases() { GetIt.I ..registerFactory( - () => GetUserDataUseCase( + () => GetUserDataUseCase(userRepository), + ) + ..registerFactory( + () => GetUserTokenUseCase( + userRepository, + ), + ) + ..registerFactory( + () => UpdateUserTokenLocalUseCase( userRepository, ), ); diff --git a/lib/app/env/flavors.dart b/lib/app/env/flavors.dart index ee79bbe..8b058a8 100644 --- a/lib/app/env/flavors.dart +++ b/lib/app/env/flavors.dart @@ -7,6 +7,7 @@ import 'package:pets_next_door_flutter/app/env/firebase_options_dev.dart' as dev; import 'package:pets_next_door_flutter/app/env/firebase_options_prod.dart' as prod; +import 'package:pets_next_door_flutter/core/services/local_storage_service.dart'; enum BuildType { development, @@ -45,6 +46,8 @@ class Flavor { javaScriptAppKey: dotenv.env['JAVASCRIPT_APP_KEY'], ); + await LocalStorageService.init(); + // 앱 DI 실행 AppBinder.init(); } diff --git a/lib/app/router/app_router.dart b/lib/app/router/app_router.dart index 9480133..0935657 100644 --- a/lib/app/router/app_router.dart +++ b/lib/app/router/app_router.dart @@ -9,6 +9,7 @@ import 'package:pets_next_door_flutter/presentation/pages/pet/register_pet_page. import 'package:pets_next_door_flutter/presentation/pages/pet/steps/breed_search_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/sign_in/sign_in_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/sign_up/phone_auth_view.dart'; +import 'package:pets_next_door_flutter/presentation/pages/splash/splash_page.dart'; import 'package:pets_next_door_flutter/presentation/pages/user/user_profile_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/user/user_view.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,13 +17,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_router.g.dart'; // private navigators -final _rootNavigatorKey = GlobalKey(); +final rootNavigatorKey = GlobalKey(); final _homeNavigatorKey = GlobalKey(debugLabel: 'home'); final _gatherNavigatorKey = GlobalKey(debugLabel: 'gather'); final _chatNavigatorKey = GlobalKey(debugLabel: 'chat'); final _userNavigatorKey = GlobalKey(debugLabel: 'user'); enum AppRoute { + splash, signIn, phoneAuth, home, @@ -38,13 +40,21 @@ enum AppRoute { // ignore: unsupported_provider_value GoRouter goRouter(GoRouterRef ref) { return GoRouter( - initialLocation: '/home', - navigatorKey: _rootNavigatorKey, + initialLocation: '/${AppRoute.splash}', + navigatorKey: rootNavigatorKey, debugLogDiagnostics: true, redirect: (context, state) { return null; }, routes: [ + GoRoute( + path: '/${AppRoute.splash}', + name: AppRoute.splash.name, + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: SplashPage(), + ), + ), GoRoute( path: '/signIn', name: AppRoute.signIn.name, diff --git a/lib/app/router/app_router.g.dart b/lib/app/router/app_router.g.dart index 06f1e82..c51162c 100644 --- a/lib/app/router/app_router.g.dart +++ b/lib/app/router/app_router.g.dart @@ -6,7 +6,7 @@ part of 'app_router.dart'; // RiverpodGenerator // ************************************************************************** -String _$goRouterHash() => r'2e9b9f1a0fb84fb3ae32a077c839450e17ae24f8'; +String _$goRouterHash() => r'f6ad5be65b55994d92dbf4db62fec0f7c33453f3'; /// See also [goRouter]. @ProviderFor(goRouter) diff --git a/lib/core/network_handling/app_dio.dart b/lib/core/network_handling/app_dio.dart index 721a01f..e46ad86 100644 --- a/lib/core/network_handling/app_dio.dart +++ b/lib/core/network_handling/app_dio.dart @@ -17,7 +17,7 @@ abstract class AppDio { static Dio? _instance; - static Dio getInstance() => _instance ??= _AppDio(); + static Dio get instance => _instance ??= _AppDio(); } class _AppDio with DioMixin implements Dio { diff --git a/lib/core/network_handling/exceptions/custom_exception.dart b/lib/core/network_handling/exceptions/custom_exception.dart new file mode 100644 index 0000000..8aee85b --- /dev/null +++ b/lib/core/network_handling/exceptions/custom_exception.dart @@ -0,0 +1,26 @@ +sealed class CustomException implements Exception { + const CustomException(this.code, this.message); + + final String code; + final String message; + + @override + String toString() => '$code: $message'; +} + +class UnAuthorizedException extends CustomException { + const UnAuthorizedException() : super('000000', '유저 인증 정보를 불러올 수 없습니다.'); +} + +class AlreadyExistUserDataException extends CustomException { + const AlreadyExistUserDataException() : super('100001', '이미 유저 데이터가 존재합니다.'); +} + +class NoUserDataException extends CustomException { + const NoUserDataException() : super('100002', '유저 데이터가 존재하지않습니다.'); +} + +class LocalDataNotUpdatedException extends CustomException { + const LocalDataNotUpdatedException() + : super('100003', '로컬 데이터를 업데이트 할 수 없습니다.'); +} diff --git a/lib/core/network_handling/interceptors/token_interceptor.dart b/lib/core/network_handling/interceptors/token_interceptor.dart index a058c1d..c815e33 100644 --- a/lib/core/network_handling/interceptors/token_interceptor.dart +++ b/lib/core/network_handling/interceptors/token_interceptor.dart @@ -1,16 +1,23 @@ import 'package:dio/dio.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:pets_next_door_flutter/presentation/providers/user/user_auth_provider.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:pets_next_door_flutter/features/user/user.dart'; class TokenInterceptor implements Interceptor { @override - void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - final isUserAuthorized = ProviderContainer().read(isUserAuthorizedProvider); + Future onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { + final requiresToken = (options.headers['requiresToken'] ?? true) as bool; + + if (!requiresToken) return handler.next(options); + + final isUserAuthorized = (FirebaseAuth.instance.currentUser != null); if (isUserAuthorized) { - final token = ''; + final token = await getUserTokenUseCase.call(); options.headers['Authorization'] = 'Bearer $token'; } + + return handler.next(options); } @override diff --git a/lib/core/services/local_storage_service.dart b/lib/core/services/local_storage_service.dart new file mode 100644 index 0000000..3c76e79 --- /dev/null +++ b/lib/core/services/local_storage_service.dart @@ -0,0 +1,100 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/** + * SharedPreferences로 로컬 스토리지 사용하는 서비스 + * 반드시 init 후에 사용해야 함. + * */ +final class LocalStorageService implements SharedPreferences { + static late final SharedPreferences _sharedPreference; + + static SharedPreferences get instance => _sharedPreference; + + static Future init() async => + _sharedPreference = await SharedPreferences.getInstance(); + + @override + Future clear() { + return _sharedPreference.clear(); + } + + @override + @Deprecated('deprecated 된 메소드') + Future commit() { + return _sharedPreference.commit(); + } + + @override + bool containsKey(String key) { + return _sharedPreference.containsKey(key); + } + + @override + Object? get(String key) { + return _sharedPreference.get(key); + } + + @override + bool? getBool(String key) { + return _sharedPreference.getBool(key); + } + + @override + double? getDouble(String key) { + return _sharedPreference.getDouble(key); + } + + @override + int? getInt(String key) { + return _sharedPreference.getInt(key); + } + + @override + Set getKeys() { + return _sharedPreference.getKeys(); + } + + @override + String? getString(String key) { + return _sharedPreference.getString(key); + } + + @override + List? getStringList(String key) { + return _sharedPreference.getStringList(key); + } + + @override + Future reload() { + return _sharedPreference.reload(); + } + + @override + Future remove(String key) { + return _sharedPreference.remove(key); + } + + @override + Future setBool(String key, bool value) { + return _sharedPreference.setBool(key, value); + } + + @override + Future setDouble(String key, double value) { + return _sharedPreference.setDouble(key, value); + } + + @override + Future setInt(String key, int value) { + return _sharedPreference.setInt(key, value); + } + + @override + Future setString(String key, String value) { + return _sharedPreference.setString(key, value); + } + + @override + Future setStringList(String key, List value) { + return _sharedPreference.setStringList(key, value); + } +} diff --git a/lib/core/services/toast_service.dart b/lib/core/services/toast_service.dart new file mode 100644 index 0000000..1409236 --- /dev/null +++ b/lib/core/services/toast_service.dart @@ -0,0 +1,18 @@ +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:pets_next_door_flutter/app/router/app_router.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/toast/app_toast.dart'; + +class ToastService { + ToastService._(); + + static final FToast _fToast = FToast() + ..init(rootNavigatorKey.currentContext!); + + static void show(CustomToast toast) { + _fToast + ..removeQueuedCustomToasts() + ..showToast( + child: toast, + ); + } +} diff --git a/lib/features/auth/data/phone_auth_repository.dart b/lib/features/auth/data/phone_auth_repository.dart index 724a20a..0bbc8df 100644 --- a/lib/features/auth/data/phone_auth_repository.dart +++ b/lib/features/auth/data/phone_auth_repository.dart @@ -53,6 +53,6 @@ final phoneAuthRepositoryProvider = Provider((ref) { ); return DioAuthRepository( api: PNDAuthAPI(apiKey), - client: AppDio.getInstance(), + client: AppDio.instance, ); }); diff --git a/lib/features/pet/data/pet_repository.dart b/lib/features/pet/data/pet_repository.dart index 4bcea8f..146bfcf 100644 --- a/lib/features/pet/data/pet_repository.dart +++ b/lib/features/pet/data/pet_repository.dart @@ -65,7 +65,7 @@ final petRepositoryProvider = Provider((ref) { return PetRepository( api: PNDPetAPI(apiBaseUrl), - client: AppDio.getInstance(), + client: AppDio.instance, ); }); diff --git a/lib/features/user/data/local/user_local_data_source.dart b/lib/features/user/data/local/user_local_data_source.dart new file mode 100644 index 0000000..f4c42ed --- /dev/null +++ b/lib/features/user/data/local/user_local_data_source.dart @@ -0,0 +1,4 @@ +abstract interface class UserLocalDataSource { + String? getUserToken(); + Future updateUserToken({required String? token}); +} diff --git a/lib/features/user/data/local/user_local_data_source_impl.dart b/lib/features/user/data/local/user_local_data_source_impl.dart new file mode 100644 index 0000000..5ca3745 --- /dev/null +++ b/lib/features/user/data/local/user_local_data_source_impl.dart @@ -0,0 +1,18 @@ +import 'package:pets_next_door_flutter/core/services/local_storage_service.dart'; +import 'package:pets_next_door_flutter/features/user/data/local/user_local_data_source.dart'; + +final class UserLocalDataSourceImpl implements UserLocalDataSource { + final LocalStorageService _localStorage = LocalStorageService(); + + @override + String? getUserToken() { + return _localStorage.getString('token'); + } + + @override + Future updateUserToken({required String? token}) async { + if (token == null) return _localStorage.remove('token'); + + return _localStorage.setString('token', token); + } +} diff --git a/lib/features/user/data/remote/user_remote_data_source.dart b/lib/features/user/data/remote/user_remote_data_source.dart index 33d8cb1..475e6dc 100644 --- a/lib/features/user/data/remote/user_remote_data_source.dart +++ b/lib/features/user/data/remote/user_remote_data_source.dart @@ -1,5 +1,5 @@ import 'package:pets_next_door_flutter/features/user/data/models/user_data_model.dart'; abstract interface class UserRemoteDataSource { - Future getUserData(String uid); + Future getUserData(); } diff --git a/lib/features/user/data/remote/user_remote_data_source_impl.dart b/lib/features/user/data/remote/user_remote_data_source_impl.dart index c40c114..4539a2c 100644 --- a/lib/features/user/data/remote/user_remote_data_source_impl.dart +++ b/lib/features/user/data/remote/user_remote_data_source_impl.dart @@ -1,15 +1,17 @@ -import 'package:dio/dio.dart'; import 'package:pets_next_door_flutter/api/user_api.dart'; +import 'package:pets_next_door_flutter/app/env/flavors.dart'; +import 'package:pets_next_door_flutter/core/network_handling/app_dio.dart'; import 'package:pets_next_door_flutter/features/user/data/models/user_data_model.dart'; import 'package:pets_next_door_flutter/features/user/data/remote/user_remote_data_source.dart'; final class UserRemoteDataSourceImpl implements UserRemoteDataSource { - // TODO: 아직 Dio의 네트워크 핸들링 로직이 없어서, 임시로 baseUrl ''로 넣음, 추후에 flavor에 따라 변경되도록 구현 필요 - final UserAPI _userAPI = UserAPI(Dio(), baseUrl: ''); + final UserAPI _userAPI = UserAPI( + AppDio.instance, + baseUrl: Flavor.apiUrl, + ); @override - Future getUserData(String uid) async { - // TODO: 후에 API에서 header 정보로 자동으로 주는지 uid 값을 파라미터로 받는지 확인 필요 + Future getUserData() async { final userData = await _userAPI.getUserData(); return userData; diff --git a/lib/features/user/repositories/user_repository.dart b/lib/features/user/repositories/user_repository.dart index 367ffc5..f703832 100644 --- a/lib/features/user/repositories/user_repository.dart +++ b/lib/features/user/repositories/user_repository.dart @@ -1,7 +1,16 @@ +import 'package:pets_next_door_flutter/core/utils/result.dart'; import 'package:pets_next_door_flutter/features/user/entities/user_data_entity.dart'; abstract interface class UserRepository { // TODO: 회원가입 기능에서 구현할 예정 Future createUserData(UserDataEntity data); - Future getUserData(); + + /// 서버에서 유저 정보를 가져옴 + Future> getUserData(); + + /// 로컬에 유저 토큰 업데이트 + Future updateUserTokenLocal({required String? token}); + + /// 로컬에 저장된 유저 토큰 가져오기 + String? getUserToken(); } diff --git a/lib/features/user/repositories/user_repository_impl.dart b/lib/features/user/repositories/user_repository_impl.dart index 113ce97..afe0e71 100644 --- a/lib/features/user/repositories/user_repository_impl.dart +++ b/lib/features/user/repositories/user_repository_impl.dart @@ -1,4 +1,5 @@ -import 'package:firebase_auth/firebase_auth.dart'; +import 'package:pets_next_door_flutter/core/utils/result.dart'; +import 'package:pets_next_door_flutter/features/user/data/local/user_local_data_source.dart'; import 'package:pets_next_door_flutter/features/user/data/remote/user_remote_data_source.dart'; import 'package:pets_next_door_flutter/features/user/entities/user_data_entity.dart'; import 'package:pets_next_door_flutter/features/user/repositories/user_repository.dart'; @@ -6,26 +7,37 @@ import 'package:pets_next_door_flutter/features/user/repositories/user_repositor final class UserRepositoryImpl implements UserRepository { const UserRepositoryImpl( this._userRemoteDataSource, + this._userLocalDataSource, ); final UserRemoteDataSource _userRemoteDataSource; + final UserLocalDataSource _userLocalDataSource; + @override Future createUserData(UserDataEntity data) async { // TODO: 추후 회원가입 로직에서 구현예정 } @override - Future getUserData() async { - final uid = FirebaseAuth.instance.currentUser!.uid; - - var userData = await _userRemoteDataSource.getUserData(uid); + Future> getUserData() async { + try { + final userData = await _userRemoteDataSource.getUserData(); - if (userData == null) { - return null; + // remote data source에서 받아온 모델을 앱에서 사용하는 모델로 변환 + return Result.success(UserDataEntity.fromModel(userData)); + } on Exception catch (e) { + return Result.failure(e); } + } - // remote data source에서 받아온 모델을 앱에서 사용하는 모델로 변환 - return UserDataEntity.fromModel(userData); + @override + String? getUserToken() { + return _userLocalDataSource.getUserToken(); + } + + @override + Future updateUserTokenLocal({required String? token}) async { + return _userLocalDataSource.updateUserToken(token: token); } } diff --git a/lib/features/user/usecases/get_user_data_use_case.dart b/lib/features/user/usecases/get_user_data_use_case.dart index fe249a6..a09d3e8 100644 --- a/lib/features/user/usecases/get_user_data_use_case.dart +++ b/lib/features/user/usecases/get_user_data_use_case.dart @@ -1,3 +1,4 @@ +import 'package:pets_next_door_flutter/core/utils/result.dart'; import 'package:pets_next_door_flutter/features/user/entities/user_data_entity.dart'; import 'package:pets_next_door_flutter/features/user/repositories/user_repository.dart'; @@ -8,7 +9,7 @@ final class GetUserDataUseCase { final UserRepository _userRepository; - Future call() async { + Future> call() async { return _userRepository.getUserData(); } } diff --git a/lib/features/user/usecases/get_user_token_use_case.dart b/lib/features/user/usecases/get_user_token_use_case.dart new file mode 100644 index 0000000..612929c --- /dev/null +++ b/lib/features/user/usecases/get_user_token_use_case.dart @@ -0,0 +1,13 @@ +import 'package:pets_next_door_flutter/features/user/repositories/user_repository.dart'; + +final class GetUserTokenUseCase { + const GetUserTokenUseCase( + this._userRepository, + ); + + final UserRepository _userRepository; + + String? call() { + return _userRepository.getUserToken(); + } +} diff --git a/lib/features/user/usecases/update_user_token_local_use_case.dart b/lib/features/user/usecases/update_user_token_local_use_case.dart new file mode 100644 index 0000000..ef0c046 --- /dev/null +++ b/lib/features/user/usecases/update_user_token_local_use_case.dart @@ -0,0 +1,13 @@ +import 'package:pets_next_door_flutter/features/user/repositories/user_repository.dart'; + +final class UpdateUserTokenLocalUseCase { + const UpdateUserTokenLocalUseCase( + this._userRepository, + ); + + final UserRepository _userRepository; + + Future call({required String? newToken}) async { + return _userRepository.updateUserTokenLocal(token: newToken); + } +} diff --git a/lib/features/user/user.dart b/lib/features/user/user.dart index 1b8708d..5b57fa2 100644 --- a/lib/features/user/user.dart +++ b/lib/features/user/user.dart @@ -1,12 +1,20 @@ import 'package:pets_next_door_flutter/app/di/locator.dart'; +import 'package:pets_next_door_flutter/features/user/data/local/user_local_data_source.dart'; import 'package:pets_next_door_flutter/features/user/data/remote/user_remote_data_source.dart'; import 'package:pets_next_door_flutter/features/user/repositories/user_repository.dart'; import 'package:pets_next_door_flutter/features/user/usecases/get_user_data_use_case.dart'; +import 'package:pets_next_door_flutter/features/user/usecases/get_user_token_use_case.dart'; +import 'package:pets_next_door_flutter/features/user/usecases/update_user_token_local_use_case.dart'; export 'data/remote/user_remote_data_source.dart'; export 'repositories/user_repository.dart'; export 'usecases/get_user_data_use_case.dart'; +export 'usecases/get_user_token_use_case.dart'; +export 'usecases/update_user_token_local_use_case.dart'; final userRemoteDataSource = locator(); +final userLocalDataSource = locator(); final userRepository = locator(); final getUserDataUseCase = locator(); +final getUserTokenUseCase = locator(); +final updateUserTokenUseCase = locator(); diff --git a/lib/presentation/pages/splash/splash_event.dart b/lib/presentation/pages/splash/splash_event.dart new file mode 100644 index 0000000..0b8137c --- /dev/null +++ b/lib/presentation/pages/splash/splash_event.dart @@ -0,0 +1,33 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:pets_next_door_flutter/app/router/app_router.dart'; +import 'package:pets_next_door_flutter/presentation/providers/user/user_auth_provider.dart'; +import 'package:pets_next_door_flutter/presentation/providers/user/user_data_provider.dart'; + +abstract interface class _SplashEvent { + Future routeByUserAuthAndData(WidgetRef ref); +} + +mixin class SplashEvent implements _SplashEvent { + @override + Future routeByUserAuthAndData(WidgetRef ref) async { + final isLoggedIn = ref.read(isUserAuthorizedProvider); + + if (!isLoggedIn) { + ref.context.goNamed(AppRoute.signIn.name); + return; + } + + await ref.read(userDataProvider.future).then( + (userData) { + if (userData != null) { + ref.context.goNamed(AppRoute.home.name); + } else { + ref.context.goNamed(AppRoute.signIn.name); + } + }, + ).onError((error, stackTrace) { + ref.context.goNamed(AppRoute.signIn.name); + }); + } +} diff --git a/lib/presentation/pages/splash/splash_page.dart b/lib/presentation/pages/splash/splash_page.dart index 9cb9825..905984d 100644 --- a/lib/presentation/pages/splash/splash_page.dart +++ b/lib/presentation/pages/splash/splash_page.dart @@ -1,9 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/presentation/pages/splash/splash_event.dart'; -class SplashPage extends StatelessWidget { +class SplashPage extends ConsumerStatefulWidget { const SplashPage({super.key}); + @override + SplashPageState createState() => SplashPageState(); +} + +class SplashPageState extends ConsumerState with SplashEvent { + @override + void initState() { + super.initState(); + // TODO: 스플래시 화면이 짧게 지나가는 현상을 방지하기 위해 추가, 디자인 변경되면 바뀔 수 있음 + Future.delayed(Duration(milliseconds: 500)).then( + (_) async { + await routeByUserAuthAndData(ref); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/presentation/providers/user/user_data_provider.dart b/lib/presentation/providers/user/user_data_provider.dart index 4dfa555..c2c673a 100644 --- a/lib/presentation/providers/user/user_data_provider.dart +++ b/lib/presentation/providers/user/user_data_provider.dart @@ -1,6 +1,9 @@ +import 'package:pets_next_door_flutter/core/network_handling/exceptions/custom_exception.dart'; +import 'package:pets_next_door_flutter/core/services/toast_service.dart'; import 'package:pets_next_door_flutter/features/user/entities/user_data_entity.dart'; import 'package:pets_next_door_flutter/features/user/user.dart'; import 'package:pets_next_door_flutter/presentation/providers/user/user_auth_provider.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/toast/app_toast.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user_data_provider.g.dart'; @@ -9,14 +12,37 @@ part 'user_data_provider.g.dart'; class UserData extends _$UserData { @override FutureOr build() async { - final userAuth = ref.watch(userAuthProvider); + try { + final userAuth = ref.watch(userAuthProvider); - if (userAuth == null) throw Exception('로그인 필요'); + if (userAuth == null) throw const UnAuthorizedException(); - final userData = await getUserDataUseCase.call(); + final userData = await getUserDataUseCase.call(); - return userData; + return userData.fold( + onSuccess: (userData) => userData, + onFailure: (e) => throw e, + ); + } catch (e) { + ToastService.show(NormalToast(message: '$e')); + rethrow; + } } void updateUserData() {} } + +@riverpod +Future userToken(UserTokenRef ref) async { + final userToken = await ref + .watch(userDataProvider.future) + .then((userData) => userData?.uid); + + final updateSucceed = await updateUserTokenUseCase.call(newToken: userToken); + + if (updateSucceed) { + return getUserTokenUseCase.call(); + } else { + throw LocalDataNotUpdatedException(); + } +} diff --git a/lib/presentation/providers/user/user_data_provider.g.dart b/lib/presentation/providers/user/user_data_provider.g.dart index ac04a90..0ac58e8 100644 --- a/lib/presentation/providers/user/user_data_provider.g.dart +++ b/lib/presentation/providers/user/user_data_provider.g.dart @@ -6,7 +6,21 @@ part of 'user_data_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$userDataHash() => r'9e00e111b5a3aae812ceb2df6e53518b30b83ca2'; +String _$userTokenHash() => r'1170b0e699e542d1611cd7f7208d0976abcb0b87'; + +/// See also [userToken]. +@ProviderFor(userToken) +final userTokenProvider = AutoDisposeFutureProvider.internal( + userToken, + name: r'userTokenProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userTokenHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UserTokenRef = AutoDisposeFutureProviderRef; +String _$userDataHash() => r'0587e70e7b6971b9fc900e5134c57fe9c8008edc'; /// See also [UserData]. @ProviderFor(UserData) diff --git a/lib/presentation/widgets/toast/app_toast.dart b/lib/presentation/widgets/toast/app_toast.dart new file mode 100644 index 0000000..07e1984 --- /dev/null +++ b/lib/presentation/widgets/toast/app_toast.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/core/constants/text_style.dart'; + +final TextStyle _baseTextStyle = AppTextStyle.pretendardStyle( + 12, + 18, +); + +class CustomToast extends StatelessWidget { + const CustomToast({ + super.key, + required this.message, + this.backgroundColor, + this.borderRadius, + this.textStyle, + }); + + final String message; + final Color? backgroundColor; + final double? borderRadius; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints(maxWidth: 300), + margin: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.symmetric(vertical: 13), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius ?? 5), + ), + alignment: Alignment.center, + child: Text( + message, + style: textStyle ?? _baseTextStyle, + ), + ); + } +} + +class NormalToast extends CustomToast { + NormalToast({ + super.key, + required super.message, + }) : super( + backgroundColor: AppColor.of.gray90, + textStyle: _baseTextStyle.copyWith( + color: Colors.white, + ), + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 77d4021..b6f138e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: intl: ^0.18.1 freezed_annotation: ^2.4.1 flutter_dotenv: ^5.1.0 # 환경변수 설정 + shared_preferences: ^2.2.2 # 로컬 캐싱 # 앱 정보 change_app_package_name: ^1.1.0