diff --git a/lib/core/api_services.dart b/lib/core/api_services.dart index fa61503..8bf37d5 100644 --- a/lib/core/api_services.dart +++ b/lib/core/api_services.dart @@ -1,23 +1,71 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; class ApiService { final String baseUrl = dotenv.get('BASE_URL'); final String apiKey = dotenv.get('API_KEY'); + final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); static const Map _headers = { 'Content-Type': 'application/json', }; + Future _getAuthToken() async { + return await _secureStorage.read(key: 'token'); + } + + Future> _getHeaders() async { + final token = await _getAuthToken(); + return { + ..._headers, + 'x-api-key': apiKey, + if (token != null) 'Authorization': 'Bearer $token', + }; + } + + Map _processResponse(http.Response response) { + if (response.body.isEmpty) { + throw Exception('Empty response body'); + } + + try { + final responseJson = jsonDecode(response.body); + + switch (response.statusCode) { + case 200: + return responseJson; + case 400: + throw BadRequestException( + 'Bad request. The server could not process your request.', + ); + case 401: + throw UnauthorizedException( + 'Unauthorized. Please check your credentials.', + ); + case 404: + throw NotFoundException( + 'Not found. The requested resource could not be found.', + ); + case 500: + throw ServerException( + 'Internal server error. Please try again later.', + ); + default: + throw Exception('Failed with status code: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Error parsing response: $e'); + } + } + Future> get(String endpoint) async { try { + final headers = await _getHeaders(); final url = Uri.parse('$baseUrl$endpoint'); - final response = await http.get( - url, - headers: {..._headers, 'API_KEY': apiKey}, - ); + final response = await http.get(url, headers: headers); return _processResponse(response); } catch (e) { @@ -27,26 +75,24 @@ class ApiService { } } - Future> post(String endpoint, Map body) async { + Future> post( + String endpoint, + Map body, + ) async { try { + final headers = await _getHeaders(); final url = Uri.parse('$baseUrl$endpoint'); - - // Debugging URL dan Body Request + debugPrint('Request URL: $url'); debugPrint('Request Body: ${jsonEncode(body)}'); - debugPrint('API_KEY: $apiKey'); final response = await http.post( url, - headers: { - ..._headers, // Menggunakan _headers untuk Content-Type - 'x-api-key': apiKey, // Pastikan API_KEY dimasukkan dengan benar di sini - }, + headers: headers, body: jsonEncode(body), ); - debugPrint('Response: ${response.body}'); // Debugging Response - + debugPrint('Response: ${response.body}'); return _processResponse(response); } catch (e) { debugPrint('Error during API request: $e'); @@ -61,15 +107,22 @@ class ApiService { Map body, ) async { try { + final headers = await _getHeaders(); final url = Uri.parse('$baseUrl$endpoint'); + + debugPrint('Request URL: $url'); + debugPrint('Request Body: ${jsonEncode(body)}'); + final response = await http.put( url, - headers: {..._headers, 'API_KEY': apiKey}, + headers: headers, body: jsonEncode(body), ); + debugPrint('Response: ${response.body}'); return _processResponse(response); } catch (e) { + debugPrint('Error during API request: $e'); throw NetworkException( 'Failed to connect to the server. Please check your internet connection.', ); @@ -81,15 +134,22 @@ class ApiService { Map body, ) async { try { + final headers = await _getHeaders(); final url = Uri.parse('$baseUrl$endpoint'); + + debugPrint('Request URL: $url'); + debugPrint('Request Body: ${jsonEncode(body)}'); + final response = await http.patch( url, - headers: {..._headers, 'API_KEY': apiKey}, + headers: headers, body: jsonEncode(body), ); + debugPrint('Response: ${response.body}'); return _processResponse(response); } catch (e) { + debugPrint('Error during API request: $e'); throw NetworkException( 'Failed to connect to the server. Please check your internet connection.', ); @@ -98,11 +158,9 @@ class ApiService { Future> delete(String endpoint) async { try { + final headers = await _getHeaders(); final url = Uri.parse('$baseUrl$endpoint'); - final response = await http.delete( - url, - headers: {..._headers, 'API_KEY': apiKey}, - ); + final response = await http.delete(url, headers: headers); return _processResponse(response); } catch (e) { @@ -111,29 +169,6 @@ class ApiService { ); } } - - Map _processResponse(http.Response response) { - switch (response.statusCode) { - case 200: - return jsonDecode(response.body); - case 400: - throw BadRequestException( - 'Bad request. The server could not process your request.', - ); - case 401: - throw UnauthorizedException( - 'Unauthorized. Please check your credentials.', - ); - case 404: - throw NotFoundException( - 'Not found. The requested resource could not be found.', - ); - case 500: - throw ServerException('Internal server error. Please try again later.'); - default: - throw Exception('Failed with status code: ${response.statusCode}'); - } - } } class NetworkException implements Exception { diff --git a/lib/core/router.dart b/lib/core/router.dart index 06dc796..0e850ed 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -18,13 +18,6 @@ final router = GoRouter( builder: (context, state) => OnboardingPageScreen(), ), GoRoute(path: '/login', builder: (context, state) => LoginScreen()), - GoRoute( - path: '/navigasi', - builder: (context, state) { - dynamic data = state.extra; - return NavigationPage(data: data); - }, - ), GoRoute( path: '/verif-otp', builder: (context, state) { @@ -32,6 +25,17 @@ final router = GoRouter( return VerifotpScreen(phone: phone!); }, ), + GoRoute( + path: '/inputpin', + builder: (context, state) => OnboardingPageScreen(), + ), + GoRoute( + path: '/navigasi', + builder: (context, state) { + dynamic data = state.extra; + return NavigationPage(data: data); + }, + ), GoRoute(path: '/home', builder: (context, state) => HomeScreen()), GoRoute(path: '/activity', builder: (context, state) => ActivityScreen()), GoRoute( diff --git a/lib/model/auth_model.dart b/lib/model/auth_model.dart index 92c1640..8330b67 100644 --- a/lib/model/auth_model.dart +++ b/lib/model/auth_model.dart @@ -1,40 +1,27 @@ import 'package:rijig_mobile/core/api_services.dart'; +import 'package:rijig_mobile/model/response_model.dart'; class AuthModel { - final int status; - final String message; - - AuthModel({required this.status, required this.message}); - - factory AuthModel.fromJson(Map json) { - return AuthModel( - status: json['meta']?['status'] ?? 0, - message: json['meta']?['message'] ?? '', - ); - } -} - -class AuthService { final ApiService _apiService = ApiService(); - Future login(String phone) async { + Future login(String phone) async { try { var response = await _apiService.post('/authmasyarakat/auth', { 'phone': phone, }); - return AuthModel.fromJson(response); + return ResponseModel.fromJson(response); } catch (e) { rethrow; } } - Future> verifyOtp(String phone, String otp) async { + Future verifyOtp(String phone, String otp) async { try { var response = await _apiService.post('/authmasyarakat/verify-otp', { 'phone': phone, 'otp': otp, }); - return response; + return ResponseModel.fromJson(response); } catch (e) { rethrow; } diff --git a/lib/model/response_model.dart b/lib/model/response_model.dart new file mode 100644 index 0000000..334a489 --- /dev/null +++ b/lib/model/response_model.dart @@ -0,0 +1,15 @@ +class ResponseModel { + final int status; + final String message; + final Map? data; + + ResponseModel({required this.status, required this.message, this.data}); + + factory ResponseModel.fromJson(Map json) { + return ResponseModel( + status: json['meta']?['status'] ?? 0, + message: json['meta']?['message'] ?? '', + data: json['data'], + ); + } +} diff --git a/lib/model/userpin_model.dart b/lib/model/userpin_model.dart new file mode 100644 index 0000000..2783bf4 --- /dev/null +++ b/lib/model/userpin_model.dart @@ -0,0 +1,27 @@ +import 'package:rijig_mobile/core/api_services.dart'; +import 'package:rijig_mobile/model/response_model.dart'; + +class AuthService { + final ApiService _apiService = ApiService(); + + Future cekPinStatus(String userid) async { + try { + var response = await _apiService.get('/cek-pin-status'); + return ResponseModel.fromJson(response); + } catch (e) { + rethrow; + } + } + + Future> verifyOtp(String phone, String otp) async { + try { + var response = await _apiService.post('/authmasyarakat/verify-otp', { + 'phone': phone, + 'otp': otp, + }); + return response; + } catch (e) { + rethrow; + } + } +} diff --git a/lib/screen/auth/inputpin_screen.dart b/lib/screen/auth/inputpin_screen.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/screen/auth/login_screen.dart b/lib/screen/auth/login_screen.dart index 44232ab..a3cffb4 100644 --- a/lib/screen/auth/login_screen.dart +++ b/lib/screen/auth/login_screen.dart @@ -1,26 +1,42 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:rijig_mobile/core/guide.dart'; import 'package:rijig_mobile/core/router.dart'; import 'package:rijig_mobile/viewmodel/auth_vmod.dart'; import 'package:rijig_mobile/widget/buttoncard.dart'; import 'package:rijig_mobile/widget/formfiled.dart'; -class LoginScreen extends StatelessWidget { +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + LoginScreenState createState() => LoginScreenState(); +} + +class LoginScreenState extends State { final _phoneController = TextEditingController(); - LoginScreen({super.key}); + @override + void dispose() { + _phoneController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Padding( - padding: PaddingCustom().paddingHorizontalVertical(15, 30), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 30), child: Consumer( builder: (context, userVM, child) { - if (userVM.authModel?.status == 200) { - Future.delayed(Duration.zero, () { + debugPrint('AuthModel Status: ${userVM.authModel?.status}'); + + if (userVM.authModel?.status == 200 && !userVM.isLoading) { + debugPrint( + 'OTP Sent Successfully. Navigating to Verif-OTP Screen.', + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { router.go('/verif-otp', extra: _phoneController.text); }); } @@ -36,10 +52,20 @@ class LoginScreen extends StatelessWidget { keyboardType: TextInputType.phone, errorText: userVM.errorMessage, ), - SizedBox(height: 20), + const SizedBox(height: 20), userVM.isLoading - ? CircularProgressIndicator() + ? CardButtonOne( + textButton: 'Sending OTP...', + fontSized: 16, + colorText: Colors.white, + borderRadius: 12, + horizontal: double.infinity, + vertical: 50, + onTap: () {}, + loadingTrue: true, + usingRow: false, + ) : CardButtonOne( textButton: 'Send OTP', fontSized: 16, @@ -49,21 +75,27 @@ class LoginScreen extends StatelessWidget { vertical: 50, onTap: () { if (_phoneController.text.isNotEmpty) { + debugPrint( + 'Sending OTP to: ${_phoneController.text}', + ); userVM.login(_phoneController.text); } }, - loadingTrue: userVM.isLoading, + loadingTrue: false, usingRow: false, ), if (userVM.authModel != null) - Text( - userVM.authModel!.message, - style: TextStyle( - color: - userVM.authModel!.status == 200 - ? Colors.green - : Colors.red, + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + userVM.authModel!.message, + style: TextStyle( + color: + userVM.authModel!.status == 200 + ? Colors.green + : Colors.red, + ), ), ), ], diff --git a/lib/viewmodel/auth_vmod.dart b/lib/viewmodel/auth_vmod.dart index bb85b6d..a4b33ab 100644 --- a/lib/viewmodel/auth_vmod.dart +++ b/lib/viewmodel/auth_vmod.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:rijig_mobile/model/response_model.dart'; import 'package:rijig_mobile/model/auth_model.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AuthViewModel extends ChangeNotifier { - final AuthService _authService = AuthService(); + final AuthModel _authModel = AuthModel(); + final FlutterSecureStorage _secureStorage = FlutterSecureStorage(); bool isLoading = false; String? errorMessage; - AuthModel? authModel; + ResponseModel? authModel; Future login(String phone) async { try { @@ -14,10 +17,15 @@ class AuthViewModel extends ChangeNotifier { errorMessage = null; notifyListeners(); - authModel = await _authService.login(phone); + final response = await _authModel.login(phone); - if (authModel?.status != 200) { - errorMessage = authModel?.message ?? 'Failed to send OTP'; + if (response != null && response.status == 200) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isLoggedIn', false); + + authModel = response; + } else { + errorMessage = response?.message ?? 'Failed to send OTP'; } } catch (e) { errorMessage = 'Error: $e'; @@ -33,18 +41,28 @@ class AuthViewModel extends ChangeNotifier { errorMessage = null; notifyListeners(); - var response = await _authService.verifyOtp(phone, otp); + var response = await _authModel.verifyOtp(phone, otp); + + if (response != null && response.status == 200) { + await _secureStorage.write( + key: 'token', + value: response.data?['token'], + ); + await _secureStorage.write( + key: 'user_id', + value: response.data?['user_id'], + ); + await _secureStorage.write( + key: 'user_role', + value: response.data?['user_role'], + ); - if (response['meta'] != null && response['meta']['status'] == 200) { SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('token', response['data']['token']); - await prefs.setString('user_id', response['data']['user_id']); - await prefs.setString('user_role', response['data']['user_role']); await prefs.setBool('isLoggedIn', true); - authModel = AuthModel.fromJson(response['data']); + authModel = response; } else { - errorMessage = response['meta']?['message'] ?? 'Failed to verify OTP'; + errorMessage = response?.message ?? 'Failed to verify OTP'; } } catch (e) { errorMessage = 'Error: $e'; @@ -53,4 +71,31 @@ class AuthViewModel extends ChangeNotifier { notifyListeners(); } } + + Future getAuthToken() async { + return await _secureStorage.read(key: 'token'); + } + + Future getUserId() async { + return await _secureStorage.read(key: 'user_id'); + } + + Future getUserRole() async { + return await _secureStorage.read(key: 'user_role'); + } + + Future logout() async { + await _secureStorage.delete(key: 'token'); + await _secureStorage.delete(key: 'user_id'); + await _secureStorage.delete(key: 'user_role'); + + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove('isLoggedIn'); + notifyListeners(); + } + + Future isUserLoggedIn() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool('isLoggedIn') ?? false; + } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..37af1fe 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 6c6d5cc..b8794c2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -126,6 +126,54 @@ packages: url: "https://pub.dev" source: hosted version: "5.9.3" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -200,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -541,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.dev" + source: hosted + version: "5.12.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f669b4c..1207389 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: sdk: flutter flutter_dotenv: ^5.2.1 flutter_screenutil: ^5.9.3 + flutter_secure_storage: ^9.2.4 flutter_svg: ^2.1.0 gap: ^3.0.1 go_router: ^15.1.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..4fc759c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST