diff --git a/lib/app/bindings/initial_bindings.dart b/lib/app/bindings/initial_bindings.dart index ea48a0b..d9631c9 100644 --- a/lib/app/bindings/initial_bindings.dart +++ b/lib/app/bindings/initial_bindings.dart @@ -1,9 +1,11 @@ import 'package:get/get.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; class InitialBindings extends Bindings { @override void dependencies() { + Get.put(UserStorageService()); Get.putAsync(() => ApiClient().init()); } } diff --git a/lib/app/middleware/auth_middleware.dart b/lib/app/middleware/auth_middleware.dart index b486f35..2dcea74 100644 --- a/lib/app/middleware/auth_middleware.dart +++ b/lib/app/middleware/auth_middleware.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:get/get_navigation/src/routes/route_middleware.dart'; +import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; class AuthMiddleware extends GetMiddleware { @override RouteSettings? redirect(String? route) { - if (route != null) return RouteSettings(name: AppRoutes.loginPage); + final UserStorageService _storageService = Get.find(); + if (!_storageService.isLogged) { + return const RouteSettings(name: AppRoutes.loginPage); + } return null; } } diff --git a/lib/data/models/base/base_model.dart b/lib/data/models/base/base_model.dart new file mode 100644 index 0000000..9e1e6ea --- /dev/null +++ b/lib/data/models/base/base_model.dart @@ -0,0 +1,22 @@ +class BaseResponseModel { + final String message; + final T? data; + final dynamic meta; + + BaseResponseModel({ + required this.message, + this.data, + this.meta, + }); + + factory BaseResponseModel.fromJson( + Map json, + T Function(Map) fromJsonT, + ) { + return BaseResponseModel( + message: json['message'], + data: json['data'] != null ? fromJsonT(json['data']) : null, + meta: json['meta'], + ); + } +} diff --git a/lib/data/models/login/login_request_model.dart b/lib/data/models/login/login_request_model.dart new file mode 100644 index 0000000..c54a862 --- /dev/null +++ b/lib/data/models/login/login_request_model.dart @@ -0,0 +1,23 @@ +class LoginRequestModel { + final String email; + final String password; + + LoginRequestModel({ + required this.email, + required this.password, + }); + + factory LoginRequestModel.fromJson(Map json) { + return LoginRequestModel( + email: json['email'] ?? '', + password: json['password'] ?? '', + ); + } + + Map toJson() { + return { + 'email': email, + 'password': password, + }; + } +} diff --git a/lib/data/models/login/login_response_model.dart b/lib/data/models/login/login_response_model.dart new file mode 100644 index 0000000..0e2a110 --- /dev/null +++ b/lib/data/models/login/login_response_model.dart @@ -0,0 +1,55 @@ +class LoginResponseModel { + final String? id; + final String? googleId; + final String email; + final String name; + final DateTime? birthDate; + final String? picUrl; + final String? phone; + final String locale; + // final DateTime? createdAt; + // final DateTime? updatedAt; + + LoginResponseModel({ + this.id, + this.googleId, + required this.email, + required this.name, + this.birthDate, + this.picUrl, + this.phone, + this.locale = "en-US", + // this.createdAt, + // this.updatedAt, + }); + + factory LoginResponseModel.fromJson(Map json) { + return LoginResponseModel( + id: json['_id'], + googleId: json['google_id'], + email: json['email'], + name: json['name'], + birthDate: json['birth_date'] != null ? DateTime.parse(json['birth_date']) : null, + picUrl: json['pic_url'], + phone: json['phone'], + locale: json['locale'] ?? 'en-US', + // createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null, + // updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null, + ); + } + + Map toJson() { + return { + '_id': id, + 'google_id': googleId, + 'email': email, + 'name': name, + 'birth_date': birthDate?.toIso8601String(), + 'pic_url': picUrl, + 'phone': phone, + 'locale': locale, + // 'created_at': createdAt?.toIso8601String(), + // 'updated_at': updatedAt?.toIso8601String(), + }; + } +} diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 98163d6..260c8f0 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -1,6 +1,9 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:quiz_app/core/endpoint/api_endpoint.dart'; +import 'package:quiz_app/data/models/base/base_model.dart'; +import 'package:quiz_app/data/models/login/login_request_model.dart'; +import 'package:quiz_app/data/models/login/login_response_model.dart'; import 'package:quiz_app/data/models/register/register_request.dart'; import 'package:quiz_app/data/providers/dio_client.dart'; @@ -13,11 +16,47 @@ class AuthService extends GetxService { super.onInit(); } - Future register(RegisterRequestModel request) async { + Future register(RegisterRequestModel request) async { var data = await _dio.post( APIEndpoint.register, data: request.toJson(), ); - print(data); + if (data.statusCode == 200) { + return true; + } else { + throw Exception("Registration failed"); + } + } + + Future loginWithEmail(LoginRequestModel request) async { + final data = request.toJson(); + final response = await _dio.post(APIEndpoint.login, data: data); + + if (response.statusCode == 200) { + final baseResponse = BaseResponseModel.fromJson( + response.data, + (json) => LoginResponseModel.fromJson(json), + ); + return baseResponse.data!; + } else { + throw Exception("Login failed"); + } + } + + Future loginWithGoogle(String idToken) async { + final response = await _dio.post( + APIEndpoint.loginGoogle, + data: {"token_id": idToken}, + ); + + if (response.statusCode == 200) { + final baseResponse = BaseResponseModel.fromJson( + response.data, + (json) => LoginResponseModel.fromJson(json), + ); + return baseResponse.data!; + } else { + throw Exception("Google login failed"); + } } } diff --git a/lib/data/services/user_storage_service.dart b/lib/data/services/user_storage_service.dart new file mode 100644 index 0000000..71e599e --- /dev/null +++ b/lib/data/services/user_storage_service.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; +import 'package:quiz_app/data/models/login/login_response_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class UserStorageService { + static const _userKey = 'user_data'; + bool isLogged = false; + + Future saveUser(LoginResponseModel user) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_userKey, jsonEncode(user.toJson())); + } + + Future loadUser() async { + final prefs = await SharedPreferences.getInstance(); + final jsonString = prefs.getString(_userKey); + + if (jsonString == null) return null; + return LoginResponseModel.fromJson(jsonDecode(jsonString)); + } + + Future clearUser() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_userKey); + } + + Future isLoggedIn() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.containsKey(_userKey); + } +} diff --git a/lib/feature/login/bindings/login_binding.dart b/lib/feature/login/bindings/login_binding.dart index 8ca14ce..dce6efd 100644 --- a/lib/feature/login/bindings/login_binding.dart +++ b/lib/feature/login/bindings/login_binding.dart @@ -1,10 +1,13 @@ import 'package:get/get_core/get_core.dart'; import 'package:get/get_instance/get_instance.dart'; +import 'package:quiz_app/data/services/auth_service.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; import 'package:quiz_app/feature/login/controllers/login_controller.dart'; class LoginBinding extends Bindings { @override void dependencies() { - Get.lazyPut(() => LoginController()); + Get.lazyPut(() => AuthService()); + Get.lazyPut(() => LoginController(Get.find(), Get.find())); } } diff --git a/lib/feature/login/controllers/login_controller.dart b/lib/feature/login/controllers/login_controller.dart index b22e887..dfc4297 100644 --- a/lib/feature/login/controllers/login_controller.dart +++ b/lib/feature/login/controllers/login_controller.dart @@ -1,21 +1,26 @@ -import 'dart:convert'; import 'package:get/get.dart'; -import 'package:http/http.dart' as http; import 'package:google_sign_in/google_sign_in.dart'; import 'package:flutter/material.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; -import 'package:quiz_app/core/endpoint/api_endpoint.dart'; import 'package:quiz_app/core/utils/logger.dart'; +import 'package:quiz_app/data/models/login/login_request_model.dart'; +import 'package:quiz_app/data/models/login/login_response_model.dart'; +import 'package:quiz_app/data/services/auth_service.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; class LoginController extends GetxController { + final AuthService _authService; + final UserStorageService _userStorageService; + + LoginController(this._authService, this._userStorageService); final TextEditingController emailController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); var isPasswordHidden = true.obs; - var isLoading = false.obs; // Loading state for UI + var isLoading = false.obs; final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: ['email', 'profile', 'openid'], - ); // Singleton instance + ); void togglePasswordVisibility() { isPasswordHidden.value = !isPasswordHidden.value; @@ -34,19 +39,18 @@ class LoginController extends GetxController { try { isLoading.value = true; - var response = await http.post( - Uri.parse("${APIEndpoint.baseUrl}${APIEndpoint.login}"), - body: jsonEncode({"email": email, "password": password}), - headers: {"Content-Type": "application/json"}, + LoginResponseModel response = await _authService.loginWithEmail( + LoginRequestModel( + email: email, + password: password, + ), ); - if (response.statusCode == 200) { - var data = jsonDecode(response.body); - } else { - var errorMsg = jsonDecode(response.body)['message'] ?? "Invalid credentials"; - logC.i(errorMsg); - Get.snackbar("Error", errorMsg); - } + await _userStorageService.saveUser(response); + + _userStorageService.isLogged = true; + + Get.toNamed(AppRoutes.homePage); } catch (e, stackTrace) { logC.e(e, stackTrace: stackTrace); Get.snackbar("Error", "Failed to connect to server"); @@ -63,10 +67,6 @@ class LoginController extends GetxController { return; } - logC.i("Google User ID: ${googleUser.id}"); - logC.i("Google User Email: ${googleUser.email}"); - logC.i("Google User Display Name: ${googleUser.displayName}"); - final GoogleSignInAuthentication googleAuth = await googleUser.authentication; if (googleAuth.idToken == null || googleAuth.idToken!.isEmpty) { @@ -75,25 +75,13 @@ class LoginController extends GetxController { } String idToken = googleAuth.idToken!; - logC.i("Google ID Token: $idToken"); - // Send ID Token to backend - var response = await http.post( - Uri.parse("${APIEndpoint.baseUrl}${APIEndpoint.loginGoogle}"), - body: jsonEncode({"token_id": idToken}), - headers: {"Content-Type": "application/json"}, - ); + final response = await _authService.loginWithGoogle(idToken); + await _userStorageService.saveUser(response); - if (response.statusCode == 200) { - var data = jsonDecode(response.body); + _userStorageService.isLogged = true; - Get.snackbar("Success", "Google login successful!"); - // logC.i("Backend Auth Token: $backendToken"); - } else { - var errorMsg = jsonDecode(response.body)['message'] ?? "Google login failed"; - Get.snackbar("Error", errorMsg); - logC.i(errorMsg); - } + Get.toNamed(AppRoutes.homePage); } catch (e, stackTrace) { logC.e("Google Sign-In Error: $e", stackTrace: stackTrace); Get.snackbar("Error", "Google sign-in error"); diff --git a/lib/feature/splash_screen/presentation/splash_screen_page.dart b/lib/feature/splash_screen/presentation/splash_screen_page.dart index 5819aef..c5c2dc5 100644 --- a/lib/feature/splash_screen/presentation/splash_screen_page.dart +++ b/lib/feature/splash_screen/presentation/splash_screen_page.dart @@ -2,21 +2,36 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:quiz_app/app/routes/app_pages.dart'; import 'package:quiz_app/component/app_name.dart'; +import 'package:quiz_app/data/services/user_storage_service.dart'; class SplashScreenView extends StatelessWidget { const SplashScreenView({super.key}); + void _navigate() async { + final storageService = Get.find(); + final isLoggedIn = await storageService.isLoggedIn(); + storageService.isLogged = isLoggedIn; + + await Future.delayed(const Duration(seconds: 2)); + + if (isLoggedIn) { + Get.offNamed(AppRoutes.homePage); + } else { + Get.offNamed(AppRoutes.loginPage); + } + } + @override Widget build(BuildContext context) { - // Delay navigation after the first frame is rendered + // Jalankan navigasi setelah frame pertama selesai dirender WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(const Duration(seconds: 2), () { - Get.offNamed(AppRoutes.homePage); - }); + _navigate(); }); - return Scaffold( - body: Center(child: AppName()), + return const Scaffold( + body: Center( + child: AppName(), + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index b76549f..299040d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -161,7 +177,7 @@ packages: source: hosted version: "0.12.4+3" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f @@ -248,6 +264,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -256,6 +304,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -341,6 +445,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0b93ba6..c731b77 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: google_sign_in: ^6.2.2 flutter_dotenv: ^5.2.1 dio: ^5.8.0+1 + shared_preferences: ^2.5.3 dev_dependencies: flutter_test: