Merge branch 'master' of tzw/fcs into master

This commit is contained in:
2024-10-04 13:56:54 +06:30
committed by Gogs
18 changed files with 340 additions and 68 deletions

View File

@@ -18,6 +18,7 @@
"back.button_confirm":"Are you sure you want to continue without submitting changes?",
"btn.clear":"Clear Filter",
"btn.filter":"Filter",
"btn.exit_confirm":"Are you sure you want to exit?",
"Buttons End ================================================================":"",
"Offline Start ================================================================":"",
@@ -104,6 +105,7 @@
"home.invitation.request.msg":"We are working on your invitation request!",
"home.search":"Enter tracking number",
"home.search.btn":"Search",
"home.pin.logout.confirm":"Are you sure want to logout?",
"Home End ================================================================":"",
"Invite Start ================================================================":"",

View File

@@ -17,6 +17,7 @@
"back.button_confirm":"Are you sure you want to continue without submitting changes?",
"btn.clear":"Clear Filter",
"btn.filter":"Filter",
"btn.exit_confirm":"Are you sure you want to exit?",
"Buttons End ================================================================":"",
"Offline Start ================================================================":"",
@@ -104,6 +105,7 @@
"home.invitation.request.msg":"ဖိတ်ကြားမှု တောင်းဆိုသည်ကို လုပ်ဆောင်နေပါသည်!",
"home.search":"Tracking number ရိုက်ထည့်ပါ",
"home.search.btn":"ရှာမည်",
"home.pin.logout.confirm":"အကောင့်ထွက်ရန်သေချာပြီလား?",
"Home End ================================================================":"",
"Invite Start ================================================================":"",

View File

@@ -35,6 +35,7 @@ import 'pages/carton/model/package_selection_model.dart';
import 'pages/carton/model/sender_selection_model.dart';
import 'pages/carton/model/shipment_selection_model.dart';
import 'pages/delivery/model/delivery_model.dart';
import 'pages/signin/pinlogin_page.dart';
class App extends StatefulWidget {
final String title;
@@ -121,6 +122,7 @@ class _AppState extends State<App> {
'/welcome': (_) => WelcomePage(),
'/home': (_) => HomePage(),
'/language_selection': (context) => InitialLanguageSelectionPage(),
'/pin_login': (_) => PinLoginPage()
};
return routes;
}

View File

@@ -83,6 +83,7 @@ const privilege_receiving = "rc";
const privilege_pickup = "pku";
const privilege_collect = "col";
const privilege_report = "rpt";
const privilege_pin ="pin";
// Pickup types
const shipment_local_pickup = "Local pickup";

View File

@@ -12,6 +12,7 @@ import 'package:fcs/helpers/firebase_helper.dart';
import 'package:firebase_auth/firebase_auth.dart' as fb;
import 'package:logging/logging.dart';
import '../../helpers/shared_pref.dart';
import '../services/services.dart';
class AuthFb {
@@ -108,13 +109,15 @@ class AuthFb {
Future<void> signoutStart() async {
await userListener?.cancel();
await userAuthListener?.cancel();
await _pinUserListener?.cancel();
}
Future<void> signoutEnd() async {
await SharedPref.setPinLockOn(false);
await _fb.signOut();
}
Future<void> _addUserToStream({bool refreshIdToken = false}) async {
Future<User?> _addUserToStream({bool refreshIdToken = false}) async {
fb.User? firebaseUser = _fb.currentUser;
if (firebaseUser == null) return null;
Map<dynamic, dynamic>? claims =
@@ -128,15 +131,20 @@ class AuthFb {
user = await _getUserFromFirestore(cid);
}
if (user == null) {
controller.add(null);
return;
_addUser(null);
return null;
}
loadUserClaim(claims, user);
controller.add(user);
_addUser(user);
return user;
}
loadUserClaim(Map claims, User user) {
if (pinLoginUser != null) {
user.fcsID = pinLoginUser?.fcsID;
}
// add privileges
String? privileges = claims["pr"];
if (privileges != null && privileges != "") {
@@ -252,9 +260,9 @@ class AuthFb {
// get privilege from claim
Map<dynamic, dynamic> claims = await getClaims(refreshIdToken: true);
loadUserClaim(claims, user);
controller.add(user);
_addUser(user);
} catch (e) {
controller.add(null);
_addUser(null);
}
});
}
@@ -286,12 +294,23 @@ class AuthFb {
log.info("_startAuthListener: $user");
if (_logIn) {
controller.add(user);
_addUser(user);
}
}
});
}
_addUser(User? user) {
if (user == null) {
pinLoginUser = null;
}
if (pinLoginUser != null) {
controller.add(pinLoginUser);
} else {
controller.add(user);
}
}
Stream<User?> user() {
// ignore: close_sinks
StreamSubscription<fb.User?>? authListener;
@@ -301,7 +320,7 @@ class AuthFb {
authListener = _fb.authStateChanges().listen((firebaseUser) async {
_logIn = firebaseUser != null;
if (firebaseUser == null) {
controller.add(null);
_addUser(null);
} else {
_addUserToStream(refreshIdToken: true);
_startUserListener();
@@ -319,4 +338,51 @@ class AuthFb {
return controller.stream;
}
User? pinLoginUser;
StreamSubscription<DocumentSnapshot>? _pinUserListener;
Future<void> pinLogin(
{required String currentUserId,
required String fcsID,
required String pin}) async {
var data = await requestAPI("/pin/login", "POST",
token: await getToken(), payload: {'fcs_id': fcsID, 'pin': pin});
var userId = data['user_id'] ?? '';
if (userId == currentUserId) {
//logout
logoutPinAccount();
await SharedPref.setPinLockOn(false);
}
var pinToken = data['pin_token'] ?? '';
// Get user for pin login
_listenPinUser(userId: userId, pinToken: pinToken);
}
Future<void> _listenPinUser(
{required String userId, required String pinToken}) async {
Stream<DocumentSnapshot> snapshot = FirebaseFirestore.instance
.collection(user_collection)
.doc(userId)
.snapshots();
_pinUserListener?.cancel();
_pinUserListener = snapshot.listen((snap) async {
User user = User.fromMap(snap.data() as Map<String, dynamic>, snap.id);
pinLoginUser = user;
if (pinLoginUser != null) {
pinLoginUser!.pinToken = pinToken;
}
await _addUserToStream(refreshIdToken: true);
});
}
Future<void> logoutPinAccount() async {
_pinUserListener?.cancel();
pinLoginUser = null;
await _addUserToStream(refreshIdToken: true);
}
}

View File

@@ -74,4 +74,23 @@ class AuthServiceImp implements AuthService {
Future<void> deleteAccount() {
return authFb.deleteAccount();
}
@override
Future<void> pinLogin(
{required String currentUserId,
required String fcsID,
required String pin}) {
return authFb.pinLogin(
currentUserId: currentUserId, fcsID: fcsID, pin: pin);
}
@override
User? getPinLoginUser() {
return authFb.pinLoginUser;
}
@override
Future<void> logoutPinAccount() {
return authFb.logoutPinAccount();
}
}

View File

@@ -15,4 +15,10 @@ abstract class AuthService {
Stream<User?> getUserStream();
Stream<Setting> getSetting();
Future<void> deleteAccount();
Future<void> pinLogin(
{required String currentUserId,
required String fcsID,
required String pin});
User? getPinLoginUser();
Future<void> logoutPinAccount();
}

View File

@@ -21,6 +21,7 @@ class User {
String? preferCurrency;
bool enablePinLogin;
String? pinDigit;
List<String> privileges = [];
String get initial =>
name != null && name != "" ? name!.substring(0, 1) : "?";
@@ -48,7 +49,9 @@ class User {
String get getFcsUnseenCount =>
fcsUnseenCount > 100 ? "99+" : fcsUnseenCount.toString();
List<String> privileges = [];
// for pin login
String? pinToken;
bool get isPinLogin => pinToken!=null;
String get phone => phoneNumber != null && phoneNumber!.startsWith("959")
? "0${phoneNumber!.substring(2)}"
@@ -117,7 +120,9 @@ class User {
userUnseenCount: map['user_unseen_count'] ?? 0,
fcsUnseenCount: map['fcs_unseen_count'] ?? 0,
preferCurrency: map['preferred_currency'],
lastMessageTime: _date == null ? null : _date.toDate());
lastMessageTime: _date == null ? null : _date.toDate(),
enablePinLogin: map['enable_pin_login'] ?? false,
pinDigit: map['pin'] ?? '');
}
bool diffPrivileges(User another) {

View File

@@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_icons_null_safety/flutter_icons_null_safety.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_vector_icons/flutter_vector_icons.dart' as vector;
class Privilege {
String id;
@@ -49,6 +50,8 @@ class Privilege {
iconData = MaterialCommunityIcons.layers;
} else if (this.id == privilege_report) {
iconData = Feather.file_text;
} else if (this.id == privilege_pin) {
iconData = vector.MaterialCommunityIcons.account_lock_outline;
} else {
iconData = MaterialCommunityIcons.account_question;
}

View File

@@ -4,6 +4,8 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:fcs/data/services/services.dart';
import 'package:fcs/domain/entities/user.dart';
import 'package:fcs/domain/vo/status.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
@@ -28,6 +30,10 @@ Future<dynamic> requestAPI(String path, method,
if (token != null) {
headers["Token"] = token;
}
User? pinLoginUser = Services.instance.authService.getPinLoginUser();
if (pinLoginUser != null) {
headers["pin_token"] = pinLoginUser.pinToken;
}
if (devInfo.deviceID != null) {
headers["Device"] = devInfo.deviceID ?? "" + ":" + deviceName;
}

View File

@@ -108,4 +108,14 @@ class SharedPref {
static Future<void> clearRecentSearch(String key) async {
return await _remove(key);
}
static Future<bool?> getPinLockOn() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getBool('pin_lock');
}
static Future<void> setPinLockOn(bool isLockOn) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('pin_lock', isLockOn);
}
}

View File

@@ -203,9 +203,27 @@ class _HomePageState extends State<HomePage> {
super.dispose();
}
_logoutPinAccount() async {
setState(() {
_isLoading = true;
});
try {
await context.read<MainModel>().logoutPinAccount();
Navigator.pushNamedAndRemoveUntil(context, "/pin_login", (r) => false);
} catch (e) {
showMsgDialog(context, "Error", e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
User? user = Provider.of<MainModel>(context).user;
var mainModel = context.watch<MainModel>();
User? user = mainModel.user;
if (user == null) {
Future.microtask(
@@ -213,7 +231,7 @@ class _HomePageState extends State<HomePage> {
return Container();
}
login = Provider.of<MainModel>(context).isLogin();
login = mainModel.isLogin();
LanguageModel languageModel = Provider.of<LanguageModel>(context);
final faqBtn = TaskButton("faq.btn",
@@ -357,7 +375,7 @@ class _HomePageState extends State<HomePage> {
selectedColor: Colors.white,
color: Colors.blue,
children: <Widget>[
Icon(MaterialCommunityIcons.account_tie,size: 25),
Icon(MaterialCommunityIcons.account_tie, size: 25),
],
onPressed: (i) => this.setState(() {
isFcs[0] = !isFcs[0];
@@ -390,8 +408,11 @@ class _HomePageState extends State<HomePage> {
final pinLoginBtn = IconButton(
onPressed: () {
Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) => PinLoginPage()));
Navigator.pushAndRemoveUntil(
context,
CupertinoPageRoute(
builder: (BuildContext context) => PinLoginPage()),
(r) => false);
},
iconSize: 25,
icon: Icon(
@@ -400,6 +421,19 @@ class _HomePageState extends State<HomePage> {
),
);
final pinLogoutBtn = IconButton(
onPressed: () {
showConfirmDialog(context, "home.pin.logout.confirm", () async {
await _logoutPinAccount();
});
},
iconSize: 25,
icon: Icon(
MaterialCommunityIcons.lock_open_variant_outline,
color: buttonColor,
),
);
var searchInput = Row(children: [
Expanded(
child: Padding(
@@ -478,7 +512,7 @@ class _HomePageState extends State<HomePage> {
profileBtn,
]
: <Widget>[
pinLoginBtn,
mainModel.isPinLogin ? pinLogoutBtn : pinLoginBtn,
fcsToggle,
profileBtn,
]

View File

@@ -17,6 +17,7 @@ class MainModel extends ChangeNotifier {
String? messagingToken;
User? user;
User? _fbUser;
PackageInfo? packageInfo;
set setMessaginToken(token) {
@@ -29,6 +30,9 @@ class MainModel extends ChangeNotifier {
bool isLoaded = false;
bool isOnline = false;
bool isFirstLaunch = false;
bool isLockOn = false;
bool get isPinLogin => user?.isPinLogin ?? false;
MainModel() {
NetworkConnectivity.instance.statusStream.listen((data) {
@@ -83,6 +87,7 @@ class MainModel extends ChangeNotifier {
await _listenSetting();
this.isFirstLaunch = await SharedPref.isFirstLaunch() ?? true;
this.packageInfo = await PackageInfo.fromPlatform();
this.isLockOn = await SharedPref.getPinLockOn() ?? false;
userListener?.cancel();
userListener =
@@ -91,6 +96,17 @@ class MainModel extends ChangeNotifier {
bool diffPrivilege =
_user != null && (user == null || user!.diffPrivileges(_user));
bool loggingOut = user != null && _user == null;
if (_user != null) {
if (!_user.isPinLogin) {
_fbUser = _user;
}
}
if ((_fbUser?.id == _user?.id)) {
_user?.pinToken = null;
}
user = _user;
if (_user != null) {
@@ -101,6 +117,7 @@ class MainModel extends ChangeNotifier {
}
}
}
if (loggingOut) {
for (final m in models) {
m.logout();
@@ -196,4 +213,13 @@ class MainModel extends ChangeNotifier {
Future<void> deleteAccount() async {
return await Services.instance.authService.deleteAccount();
}
Future<void> pinLogin({required String fcsID, required String pin}) async {
await Services.instance.authService
.pinLogin(fcsID: fcsID, pin: pin, currentUserId: _fbUser?.id ?? '');
}
Future<void> logoutPinAccount() async {
await Services.instance.authService.logoutPinAccount();
}
}

View File

@@ -53,7 +53,11 @@ class _SplashScreenState extends State<SplashScreen> {
if (mainModel.isFirstLaunch) {
page = "/language_selection";
} else if (mainModel.isLogin()) {
if (mainModel.isLockOn) {
page = "/pin_login";
} else {
page = "/home";
}
} else {
page = "/welcome";
}

View File

@@ -129,12 +129,17 @@ class _ProfileState extends State<Profile> {
],
);
final logoutbutton = fcsButton(
context, getLocalString(context, "profile.logout"), callack: () {
showConfirmDialog(context, "profile.logout.confirm", () async {
final logoutbutton =
fcsButton(context, getLocalString(context, "profile.logout"),
callack: mainModel.isPinLogin
? null
: () {
showConfirmDialog(context, "profile.logout.confirm",
() async {
await _logout();
});
}, iconData: Icons.exit_to_app);
},
iconData: Icons.exit_to_app);
return LocalProgress(
inAsyncCall: _isLoading,
@@ -355,8 +360,12 @@ class _ProfileState extends State<Profile> {
}
_showToast(String title) {
final ScaffoldMessengerState scaffold =
key.currentState as ScaffoldMessengerState;
ScaffoldMessengerState? scaffold = key.currentState;
if (scaffold == null) {
scaffold = ScaffoldMessenger.of(context);
}
scaffold.showSnackBar(
SnackBar(
content: Text('copied "$title" data to clipboard'),

View File

@@ -1,13 +1,18 @@
import 'package:fcs/helpers/theme.dart';
import 'package:fcs/pages/main/model/language_model.dart';
import 'package:fcs/pages/main/model/main_model.dart';
import 'package:fcs/pages/main/util.dart';
import 'package:fcs/pages/widgets/local_button.dart';
import 'package:fcs/pages/widgets/local_text.dart';
import 'package:fcs/pages/widgets/progress.dart';
import 'package:flutter/material.dart';
import 'package:pin_input_text_field/pin_input_text_field.dart';
import 'package:provider/provider.dart';
import '../../helpers/shared_pref.dart';
import '../../localization/app_translations.dart';
class PinLoginPage extends StatefulWidget {
//final User user;
const PinLoginPage({super.key});
@override
@@ -18,10 +23,77 @@ class _PinLoginPageState extends State<PinLoginPage> {
bool _isLoading = false;
String pin = "";
String prefixText = "FCS-";
//late User _user;
final _formKey = GlobalKey<FormState>();
TextEditingController _fcsIdCtl = new TextEditingController();
@override
void initState() {
_init();
super.initState();
}
_init() async {
await SharedPref.setPinLockOn(true);
}
Future<bool> _onWillPop() async {
// Show the confirmation dialog
return (await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Center(
child:
LocalText(context, 'btn.exit_confirm', color: primaryColor),
),
content: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.grey[300],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0))),
child: Text(
AppTranslations.of(context)!.text('btn.cancel'),
style: Provider.of<LanguageModel>(context).isEng
? TextStyle(color: primaryColor)
: TextStyle(
fontFamily: 'Myanmar3', color: primaryColor),
),
onPressed: () {
Navigator.of(context).pop(false);
}),
SizedBox(
width: 0,
),
TextButton(
style: TextButton.styleFrom(
backgroundColor: primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5.0))),
child: Text(AppTranslations.of(context)!.text('btn.ok'),
style: Provider.of<LanguageModel>(context).isEng
? TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold)
: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontFamily: 'Myanmar3')),
onPressed: () {
Navigator.of(context).pop(true);
})
],
),
),
),
)) ??
false;
}
Widget build(BuildContext context) {
final fcsIdBox = TextFormField(
controller: _fcsIdCtl,
@@ -121,7 +193,11 @@ class _PinLoginPageState extends State<PinLoginPage> {
callBack: _login,
),
);
return LocalProgress(
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: _onWillPop,
child: LocalProgress(
inAsyncCall: _isLoading,
child: new Scaffold(
body: Form(
@@ -157,6 +233,7 @@ class _PinLoginPageState extends State<PinLoginPage> {
],
),
)),
),
);
}
@@ -171,14 +248,14 @@ class _PinLoginPageState extends State<PinLoginPage> {
return;
}
String fcsId = "$prefixText${_fcsIdCtl.text}";
print(fcsId);
setState(() {
_isLoading = true;
});
try {
Navigator.pop(context, true);
await context.read<MainModel>().pinLogin(fcsID: fcsId, pin: pin);
Navigator.pushNamedAndRemoveUntil(context, "/home", (r) => false);
} catch (e) {
showMsgDialog(context, "Error", e.toString());
} finally {

View File

@@ -81,7 +81,7 @@ class StaffModel extends BaseModel {
Future<void> updatePin(
{required String userID,
required bool enablePin,
required int? pin}) async {
required String? pin}) async {
await request("/employee/pin", "PUT",
payload: {"id": userID, "enable_pin_login": enablePin, "pin": pin},
token: await getToken());

View File

@@ -310,7 +310,7 @@ class _StaffPinEditorState extends State<StaffPinEditor> {
await context.read<StaffModel>().updatePin(
userID: _staff.id!,
enablePin: _enablePinLogin,
pin: _enablePinLogin ? int.parse(_confirmPin) : null);
pin: _enablePinLogin ? _confirmPin : null);
Navigator.pop(context, true);
} catch (e) {
showMsgDialog(context, "Error", e.toString());