From 823f1aa52577066450df7bc4938f674474712eb9 Mon Sep 17 00:00:00 2001 From: sindhu Date: Sat, 15 Feb 2025 17:17:19 +0700 Subject: [PATCH] step 7 : proses login dan refactor struktur folder --- lib/app/constant.dart | 11 ++- lib/app/route.dart | 6 +- lib/main.dart | 4 +- lib/model/auth_model.dart | 80 ++++++++++++++++ lib/provider/auth_provider.dart | 0 lib/provider/current_user_provider.dart | 4 + lib/repository/auth_repository.dart | 23 +++++ lib/screen/{ => home}/home_screen.dart | 28 +++++- lib/screen/login/login_provider.dart | 88 ++++++++++++++++++ lib/screen/{ => login}/login_screen.dart | 66 +++++++++++-- lib/screen/splash/splash_screen.dart | 112 +++++++++++++++++++++++ lib/screen/splash_screen.dart | 59 ------------ lib/widget/customsnackbarwidget.dart | 70 ++++++++++++++ 13 files changed, 471 insertions(+), 80 deletions(-) create mode 100644 lib/model/auth_model.dart create mode 100644 lib/provider/auth_provider.dart create mode 100644 lib/provider/current_user_provider.dart create mode 100644 lib/repository/auth_repository.dart rename lib/screen/{ => home}/home_screen.dart (78%) create mode 100644 lib/screen/login/login_provider.dart rename lib/screen/{ => login}/login_screen.dart (84%) create mode 100644 lib/screen/splash/splash_screen.dart delete mode 100644 lib/screen/splash_screen.dart create mode 100644 lib/widget/customsnackbarwidget.dart diff --git a/lib/app/constant.dart b/lib/app/constant.dart index c2e76dc..bcb4a89 100644 --- a/lib/app/constant.dart +++ b/lib/app/constant.dart @@ -13,12 +13,17 @@ class Constant { static double designWidthPhone = 390; // color theme + static Color inputanGrey = const Color(0xff637381); + static Color bgButton = const Color(0xFF0098DA); + static Color bgRed = const Color(0xffFF4842).withOpacity(0.08); + static Color bgGrey = const Color(0xffF9F9F9); + static Color bgBlue = Colors.blue; + static Color textTrueBlack = const Color(0xff000000); static Color textBlack = const Color(0xff212B36); static Color textLightGrey = const Color(0xff919EAB); static Color textOrange = const Color(0xffF15A29); static Color textDarkGrey = const Color(0xff637381); - static Color bgAddressPresensi = const Color.fromRGBO(241, 90, 41, 0.08); static Color textWhite = Color(0xffFDFDFD); static Color textRed = Color(0xffFF4842); // background upload file @@ -67,8 +72,4 @@ class Constant { fontWeight: FontWeight.w500, ); } - - static Color inputanGrey = const Color(0xff637381); - static Color bgButton = const Color(0xFF0098DA); - static Color bgRed = const Color(0xffFF4842).withOpacity(0.08); } diff --git a/lib/app/route.dart b/lib/app/route.dart index f4368a1..e356692 100644 --- a/lib/app/route.dart +++ b/lib/app/route.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:scanktpflutter/screen/home_screen.dart'; +import 'package:scanktpflutter/screen/home/home_screen.dart'; -import '../screen/login_screen.dart'; -import '../screen/splash_screen.dart'; +import '../screen/login/login_screen.dart'; +import '../screen/splash/splash_screen.dart'; const splashRoute = "/splashRoute"; const loginRoute = "/loginRoute"; diff --git a/lib/main.dart b/lib/main.dart index dfa0e90..e8d7a6d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,9 +31,7 @@ class MyApp extends StatelessWidget { }, ), debugShowCheckedModeBanner: false, - // initialRoute: splashRoute, - // initialRoute: loginRoute, - initialRoute: homeRoute, + initialRoute: splashRoute, onGenerateRoute: AppRoute.generateRoute, ); } diff --git a/lib/model/auth_model.dart b/lib/model/auth_model.dart new file mode 100644 index 0000000..7ff5b52 --- /dev/null +++ b/lib/model/auth_model.dart @@ -0,0 +1,80 @@ +class AuthModel { + final String token; + final UserModel model; + + AuthModel({ + required this.token, + required this.model, + }); + + Map toJson() { + return { + 'token': token, + 'model': model.toJson(), + }; + } +} + +class UserModel { + final String userId; + final String username; + final String groupDashboard; + final String defaultSampleStationId; + final String staffName; + final String isCourier; + final String timeAutoLogout; + final String ip; + final String agent; + final String version; + final String lastLogin; + final int satelliteId; + + UserModel({ + required this.userId, + required this.username, + required this.groupDashboard, + required this.defaultSampleStationId, + required this.staffName, + required this.isCourier, + required this.timeAutoLogout, + required this.ip, + required this.agent, + required this.version, + required this.lastLogin, + required this.satelliteId, + }); + + factory UserModel.fromJson(Map json) { + return UserModel( + userId: json['M_UserID'] ?? '', + username: json['M_UserUsername'] ?? '', + groupDashboard: json['M_UserGroupDashboard'] ?? '', + defaultSampleStationId: json['M_UserDefaultT_SampleStationID'] ?? '', + staffName: json['M_StaffName'] ?? '', + isCourier: json['is_courier'] ?? '', + timeAutoLogout: json['time_autologout'] ?? '', + ip: json['ip'] ?? '', + agent: json['agent'] ?? '', + version: json['version'] ?? '', + lastLogin: json['last-login'] ?? '', + satelliteId: json['M_SatelliteID'] ?? 0, + ); + } + + Map toJson() { + return { + 'M_UserID': userId, + 'M_UserUsername': username, + 'M_UserGroupDashboard': groupDashboard, + 'M_UserDefaultT_SampleStationID': defaultSampleStationId, + 'M_StaffName': staffName, + 'is_courier': isCourier, + 'time_autologout': timeAutoLogout, + 'ip': ip, + 'agent': agent, + 'version': version, + 'last-login': lastLogin, + 'M_SatelliteID': satelliteId, + }; + } +} diff --git a/lib/provider/auth_provider.dart b/lib/provider/auth_provider.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/provider/current_user_provider.dart b/lib/provider/current_user_provider.dart new file mode 100644 index 0000000..e358c65 --- /dev/null +++ b/lib/provider/current_user_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../model/auth_model.dart'; + +final currentUserProvider = StateProvider((ref) => null); \ No newline at end of file diff --git a/lib/repository/auth_repository.dart b/lib/repository/auth_repository.dart new file mode 100644 index 0000000..3754b74 --- /dev/null +++ b/lib/repository/auth_repository.dart @@ -0,0 +1,23 @@ +import '../model/auth_model.dart'; +import 'base_repository.dart'; + +class AuthRepository extends BaseRepository { + AuthRepository({required super.dio}); + + Future login({ + required String username, + required String host, + required String password, + }) async { + final param = {"username": username, "password": password}; + // final service = "${Constant.baseUrl}xauth/login"; + final service = "http://${host}/one-api/v1/system/auth/login"; + final resp = await post(param: param, service: service); + + final result = AuthModel( + token: resp["data"]["token"], + model: UserModel.fromJson(resp["data"]["user"]), + ); + return result; + } +} diff --git a/lib/screen/home_screen.dart b/lib/screen/home/home_screen.dart similarity index 78% rename from lib/screen/home_screen.dart rename to lib/screen/home/home_screen.dart index c8bb525..568ec24 100644 --- a/lib/screen/home_screen.dart +++ b/lib/screen/home/home_screen.dart @@ -1,20 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../app/constant.dart'; +import '../../app/constant.dart'; +import '../../app/route.dart'; +import '../../provider/current_user_provider.dart'; class HomeScreen extends HookConsumerWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final currentUser = ref.watch(currentUserProvider); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final userID = currentUser?.model.userId ?? "0"; + if (userID == "0") { + // not logged in + Navigator.of(context) + .pushNamedAndRemoveUntil(loginRoute, (route) => false); + return; + } + }); + return () {}; + }, [currentUser]); + + final username = currentUser?.model.username ?? "-"; + return GestureDetector( onTap: () { FocusManager.instance.primaryFocus!.unfocus(); }, child: Scaffold( resizeToAvoidBottomInset: true, - backgroundColor: Constant.textWhite, + backgroundColor: Constant.bgGrey, body: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -31,6 +51,7 @@ class HomeScreen extends HookConsumerWidget { y: 34, ), ), + Text(username), // button scan ktp Padding( padding: EdgeInsets.only( @@ -63,8 +84,7 @@ class HomeScreen extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons - .scanner, + Icons.scanner, color: Constant.textWhite, size: 24, ), diff --git a/lib/screen/login/login_provider.dart b/lib/screen/login/login_provider.dart new file mode 100644 index 0000000..95d0c67 --- /dev/null +++ b/lib/screen/login/login_provider.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../app/constant.dart'; +import '../../model/auth_model.dart'; +import '../../provider/current_user_provider.dart'; +import '../../provider/dio_provider.dart'; +import '../../repository/auth_repository.dart'; +import '../../repository/base_repository.dart'; + +// 3. state provider +final loginProvider = StateNotifierProvider( + (ref) => LoginNotifier(ref: ref)); + +// 2. notifier +class LoginNotifier extends StateNotifier { + final Ref ref; + LoginNotifier({required this.ref}) : super(LoginStateInit()); + void login({ + required String username, + required String host, + required String password, + }) async { + try { + state = LoginStateLoading(); + final resp = await AuthRepository( + dio: ref.read(dioProvider), + ).login( + username: username, + host: host, + password: password, + ); + + // print(resp); + state = LoginStateDone(model: resp); + //Simpan ke token + final shared = await SharedPreferences.getInstance(); + final token = jsonEncode({ + "date": DateTime.now().toString(), + "model": resp.model, + "token": resp.token + }); + await shared.setString(Constant.bearerName, token); + ref.read(currentUserProvider.notifier).state = resp; + + // print(shared.getString(Constant.bearerName)); + } catch (e) { + if (e is BaseRepositoryException) { + state = LoginStateError(message: e.message); + } else { + state = LoginStateError(message: e.toString()); + } + } + } +} + +// 1. state +abstract class LoginState extends Equatable { + final DateTime date; + const LoginState(this.date); + @override + List get props => [date]; +} + +class LoginStateInit extends LoginState { + LoginStateInit() : super(DateTime.now()); +} + +class LoginStateLoading extends LoginState { + LoginStateLoading() : super(DateTime.now()); +} + +class LoginStateError extends LoginState { + final String message; + LoginStateError({ + required this.message, + }) : super(DateTime.now()); +} + +class LoginStateDone extends LoginState { + final AuthModel model; + LoginStateDone({ + required this.model, + }) : super(DateTime.now()); +} diff --git a/lib/screen/login_screen.dart b/lib/screen/login/login_screen.dart similarity index 84% rename from lib/screen/login_screen.dart rename to lib/screen/login/login_screen.dart index 1b5fa43..31d4f15 100644 --- a/lib/screen/login_screen.dart +++ b/lib/screen/login/login_screen.dart @@ -1,9 +1,17 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:scanktpflutter/app/route.dart'; +import 'package:scanktpflutter/model/auth_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -import '../app/constant.dart'; +import '../../app/constant.dart'; +import '../../provider/current_user_provider.dart'; +import '../../widget/customsnackbarwidget.dart'; +import 'login_provider.dart'; class LoginScreen extends HookConsumerWidget { const LoginScreen({super.key}); @@ -26,6 +34,51 @@ class LoginScreen extends HookConsumerWidget { text: "", ); + final isLoading = useState(false); + final isSuccess = useState(false); + + // proses login + ref.listen(loginProvider, (prev, next) { + if (next is LoginStateLoading) { + isLoading.value = true; + } else if (next is LoginStateError) { + isLoading.value = false; + // errorMessage.value = next.message; + snackbarWidget( + context, + next.message, + snackbarType.error, + Duration(seconds: 3), + ); + } else if (next is LoginStateDone) { + isLoading.value = false; + isSuccess.value = true; + ref.read(currentUserProvider.notifier).state = next.model; + Navigator.of(context) + .pushNamedAndRemoveUntil(homeRoute, (route) => false); + } + }); + + void login() { + if (usernameCtr.text.isEmpty || + passwordCtr.text.isEmpty || + hostCtr.text.isEmpty) { + snackbarWidget( + context, + 'Inputan wajib diisi', + snackbarType.error, + Duration(seconds: 3), + ); + } else { + // print('proses login'); + ref.read(loginProvider.notifier).login( + username: usernameCtr.text, + host: hostCtr.text, + password: passwordCtr.text, + ); + } + } + return GestureDetector( onTap: () { FocusManager.instance.primaryFocus!.unfocus(); @@ -133,7 +186,6 @@ class LoginScreen extends HookConsumerWidget { ), child: TextField( controller: usernameCtr, - onTap: () {}, decoration: InputDecoration( hintText: "Masukkan Username", enabledBorder: const OutlineInputBorder( @@ -195,7 +247,6 @@ class LoginScreen extends HookConsumerWidget { child: TextField( controller: passwordCtr, obscureText: true, - onTap: () {}, decoration: InputDecoration( hintText: "Masukkan Password", enabledBorder: const OutlineInputBorder( @@ -256,7 +307,6 @@ class LoginScreen extends HookConsumerWidget { ), child: TextField( controller: hostCtr, - onTap: () {}, decoration: InputDecoration( hintText: "Masukkan Host", enabledBorder: const OutlineInputBorder( @@ -301,7 +351,11 @@ class LoginScreen extends HookConsumerWidget { y: 48, ), child: ElevatedButton( - onPressed: () {}, + onPressed: () async { + (isLoading.value || (isSuccess.value == true)) + ? null + : login(); + }, style: ElevatedButton.styleFrom( backgroundColor: Constant.bgButton, shape: RoundedRectangleBorder( @@ -311,7 +365,7 @@ class LoginScreen extends HookConsumerWidget { shadowColor: Constant.bgButton.withOpacity(0.24), ), child: Text( - 'LOGIN', + (isLoading.value) ? 'Loading...' : 'LOGIN', style: Constant.titleButton500(context: context).copyWith( color: Constant.textWhite, ), diff --git a/lib/screen/splash/splash_screen.dart b/lib/screen/splash/splash_screen.dart new file mode 100644 index 0000000..a424b4b --- /dev/null +++ b/lib/screen/splash/splash_screen.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../app/constant.dart'; +import '../../app/route.dart'; +import '../../model/auth_model.dart'; +import '../../provider/current_user_provider.dart'; + +class SplashScreen extends HookConsumerWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ + SystemUiOverlay.bottom, + ]); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final shared = await SharedPreferences.getInstance(); + final bearerString = shared.getString(Constant.bearerName); + + if (bearerString == null || bearerString == "null") { + Timer(const Duration(seconds: 3), () { + Navigator.of(context).pushNamedAndRemoveUntil( + loginRoute, + (route) => false, + ); + }); + return; + } + + final xmodel = jsonDecode(bearerString); + if (xmodel == null) return; + + final authModel = AuthModel( + token: xmodel["token"], + model: UserModel( + userId: xmodel["model"]['M_UserID'], + username: xmodel["model"]['M_UserUsername'], + groupDashboard: xmodel["model"]['M_UserGroupDashboard'], + defaultSampleStationId: xmodel["model"] + ['M_UserDefaultT_SampleStationID'], + staffName: xmodel["model"]['M_StaffName'], + isCourier: xmodel["model"]['is_courier'], + timeAutoLogout: xmodel["model"]['time_autologout'], + ip: xmodel["model"]['ip'], + agent: xmodel["model"]['agent'], + version: xmodel["model"]['version'], + lastLogin: xmodel["model"]['last-login'], + satelliteId: xmodel["model"]['M_SatelliteID'], + ), + ); + + ref.read(currentUserProvider.notifier).state = authModel; + + Timer(const Duration(seconds: 3), () { + Navigator.of(context).pushNamedAndRemoveUntil( + homeRoute, + (route) => false, + ); + }); + }); + return () {}; + }, []); + + return Scaffold( + backgroundColor: Constant.textWhite, + body: Column( + children: [ + // Bagian atas + Align( + alignment: Alignment.topRight, + child: SizedBox( + width: Constant.getActualXPhone(context: context, x: 246), + child: Image.asset( + 'images/splashatas.png', + fit: BoxFit.fitWidth, + ), + ), + ), + Spacer(), + // Logo di tengah + Center( + child: Image.asset( + 'images/logo.png', + width: Constant.getActualXPhone(context: context, x: 164), + ), + ), + Spacer(), + // Bagian bawah + Align( + alignment: Alignment.bottomLeft, + child: SizedBox( + width: Constant.getActualXPhone(context: context, x: 246), + child: Image.asset( + 'images/splashbawah.png', + fit: BoxFit.fitWidth, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screen/splash_screen.dart b/lib/screen/splash_screen.dart deleted file mode 100644 index e215649..0000000 --- a/lib/screen/splash_screen.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../app/constant.dart'; - -class SplashScreen extends HookConsumerWidget { - const SplashScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [ - SystemUiOverlay.bottom, - ]); - - return Scaffold( - backgroundColor: Constant.textWhite, - body: Column( - children: [ - // atas - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - width: Constant.getActualXPhone(context: context, x: 246), - child: Image.asset( - 'images/splashatas.png', - fit: BoxFit.fitWidth, - ), - ), - ], - ), - Spacer(), - // logo - Center( - child: Image.asset( - 'images/logo.png', - width: Constant.getActualXPhone(context: context, x: 164), - ), - ), - Spacer(), - // bawah - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: Constant.getActualXPhone(context: context, x: 246), - child: Image.asset( - 'images/splashbawah.png', - fit: BoxFit.fitWidth, - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/widget/customsnackbarwidget.dart b/lib/widget/customsnackbarwidget.dart new file mode 100644 index 0000000..901e90e --- /dev/null +++ b/lib/widget/customsnackbarwidget.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:top_snackbar_flutter/custom_snack_bar.dart'; +import 'package:top_snackbar_flutter/top_snack_bar.dart'; + +enum snackbarType { error, info, success, warning } + +snackbarWidget( + BuildContext context, + String msg, + snackbarType tipe, + Duration displayDuration, +) { + switch (tipe) { + case snackbarType.error: + return showTopSnackBar( + animationDuration: Duration(milliseconds: 900), + displayDuration: displayDuration, + dismissType: DismissType.onTap, + Overlay.of(context), + CustomSnackBar.error( + message: msg, + ), + ); + + case snackbarType.success: + return showTopSnackBar( + animationDuration: Duration(milliseconds: 900), + displayDuration: displayDuration, + dismissType: DismissType.onTap, + Overlay.of(context), + CustomSnackBar.success( + message: msg, + ), + ); + + case snackbarType.info: + return showTopSnackBar( + animationDuration: Duration(milliseconds: 900), + displayDuration: displayDuration, + dismissType: DismissType.onTap, + Overlay.of(context), + CustomSnackBar.info( + message: msg, + ), + ); + + case snackbarType.warning: + return showTopSnackBar( + animationDuration: Duration(milliseconds: 900), + displayDuration: displayDuration, + dismissType: DismissType.onTap, + Overlay.of(context), + CustomSnackBar.info( + backgroundColor: Colors.orangeAccent, + message: msg, + ), + ); + + default: + return showTopSnackBar( + animationDuration: Duration(milliseconds: 900), + displayDuration: Duration(seconds: 1), + dismissType: DismissType.onTap, + Overlay.of(context), + CustomSnackBar.info( + message: msg, + ), + ); + } +} \ No newline at end of file