step 7 : proses login dan refactor struktur folder

This commit is contained in:
sindhu
2025-02-15 17:17:19 +07:00
parent c3d906653a
commit 823f1aa525
13 changed files with 471 additions and 80 deletions

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -31,9 +31,7 @@ class MyApp extends StatelessWidget {
},
),
debugShowCheckedModeBanner: false,
// initialRoute: splashRoute,
// initialRoute: loginRoute,
initialRoute: homeRoute,
initialRoute: splashRoute,
onGenerateRoute: AppRoute.generateRoute,
);
}

80
lib/model/auth_model.dart Normal file
View File

@@ -0,0 +1,80 @@
class AuthModel {
final String token;
final UserModel model;
AuthModel({
required this.token,
required this.model,
});
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
};
}
}

View File

View File

@@ -0,0 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../model/auth_model.dart';
final currentUserProvider = StateProvider<AuthModel?>((ref) => null);

View File

@@ -0,0 +1,23 @@
import '../model/auth_model.dart';
import 'base_repository.dart';
class AuthRepository extends BaseRepository {
AuthRepository({required super.dio});
Future<AuthModel> 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;
}
}

View File

@@ -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,
),

View File

@@ -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<LoginNotifier, LoginState>(
(ref) => LoginNotifier(ref: ref));
// 2. notifier
class LoginNotifier extends StateNotifier<LoginState> {
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<Object?> 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());
}

View File

@@ -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,
),

View File

@@ -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,
),
),
),
],
),
);
}
}

View File

@@ -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,
),
),
],
),
],
),
);
}
}

View File

@@ -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,
),
);
}
}