diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c1ce832f..0d7f146e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:showWhenLocked="true" android:turnScreenOn="true" + android:enableOnBackInvokedCallback="true" android:usesCleartextTraffic="true"> "$_baseUrl/MobileAuth/Login"; // web login - static get checkLoginValidation => "$_baseUrl/Account/Authenticate "; // web login + static get checkLoginValidation => "$_baseUrl/Account/Authenticate"; // web login // static get login => "$_baseUrl/MobileAuth/LoginIntegration"; // mobile login static get register => "$_baseUrl/handle/create/user"; // post static get updateProfile => "$_baseUrl/update/user/profile"; // post diff --git a/lib/controllers/providers/settings/app_settings.dart b/lib/controllers/providers/settings/app_settings.dart index 5251aaee..587bb562 100644 --- a/lib/controllers/providers/settings/app_settings.dart +++ b/lib/controllers/providers/settings/app_settings.dart @@ -4,6 +4,7 @@ class ASettings { static final String host = "host"; static final String language = "language"; static final String theme = "theme"; + static final String localAuth = "local_auth"; static final String speechToText = "speech_to_text"; static final String rememberMe = "remember_me"; static final String userName = "username"; diff --git a/lib/controllers/providers/settings/setting_provider.dart b/lib/controllers/providers/settings/setting_provider.dart index 79439f9a..4758e232 100644 --- a/lib/controllers/providers/settings/setting_provider.dart +++ b/lib/controllers/providers/settings/setting_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:local_auth/local_auth.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; @@ -94,10 +95,21 @@ class SettingProvider extends ChangeNotifier { notifyListeners(); } + bool isLocalAuthEnable = false; + + Future setAuth(bool status) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + isLocalAuthEnable = status; + prefs.setBool(ASettings.localAuth, isLocalAuthEnable); + notifyListeners(); + } + String _theme; String get theme => _theme; + bool get localAuth => isLocalAuthEnable; + Future setDarkTheme(bool value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); _theme = value ? "dark" : "light"; @@ -139,6 +151,12 @@ class SettingProvider extends ChangeNotifier { _theme = 'light'; } + if (prefs.containsKey(ASettings.localAuth)) { + isLocalAuthEnable = prefs.getBool(ASettings.localAuth); + } else { + isLocalAuthEnable = false; + } + if (prefs.containsKey(ASettings.speechToText)) { _speechToText = prefs.getString(ASettings.speechToText); } else { @@ -169,4 +187,20 @@ class SettingProvider extends ChangeNotifier { isLoaded = true; notifyListeners(); } + + Future checkUserTokenValidation(String token) async { + Response response; + bool isValid = false; + try { + Map body = {}; + response = await ApiManager.instance.post(URLs.checkLoginValidation + "?token=$token", body: body); + if (response.statusCode >= 200 && response.statusCode < 300) { + isValid = true; + } + + return isValid; + } catch (error) { + return isValid; + } + } } diff --git a/lib/new_views/pages/land_page/land_page.dart b/lib/new_views/pages/land_page/land_page.dart index 76a74993..6706bc46 100644 --- a/lib/new_views/pages/land_page/land_page.dart +++ b/lib/new_views/pages/land_page/land_page.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; +import 'package:test_sa/controllers/providers/settings/app_settings.dart'; import 'package:test_sa/extensions/context_extension.dart'; +import 'package:test_sa/extensions/int_extensions.dart'; +import 'package:test_sa/extensions/string_extensions.dart'; +import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/models/enums/user_types.dart'; +import 'package:test_sa/new_views/app_style/app_color.dart'; +import 'package:test_sa/new_views/common_widgets/app_filled_button.dart'; import 'package:test_sa/new_views/pages/land_page/calendar_page.dart'; import 'package:test_sa/new_views/pages/land_page/my_request/my_requests_page.dart'; +import 'package:test_sa/new_views/pages/settings_page.dart'; import 'package:test_sa/views/widgets/equipment/single_device_picker.dart'; import '../../../controllers/providers/settings/setting_provider.dart'; @@ -36,6 +44,55 @@ class _LandPageState extends State { super.initState(); } + void checkLocalAuth() async { + if (await Provider.of(context, listen: false).auth.isDeviceSupported()) { + SharedPreferences prefs = await SharedPreferences.getInstance(); + if (!prefs.containsKey(ASettings.localAuth)) { + showModalBottomSheet( + context: context, + useSafeArea: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20)), + ), + padding: EdgeInsets.symmetric(horizontal: 24.toScreenWidth, vertical: 24.toScreenHeight), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40.toScreenWidth, + height: 5.toScreenHeight, + decoration: BoxDecoration(color: AppColor.neutral40, borderRadius: BorderRadius.circular(30)), + ), + 16.height, + Align( + alignment: AlignmentDirectional.centerStart, + child: "Fingerprint/Face ID".addTranslation.heading3(context).custom(fontWeight: FontWeight.w600), + ), + 16.height, + "fingerprint".toLottieAsset(height: 180), + 16.height, + Align( + alignment: AlignmentDirectional.centerStart, + child: "Let's set up Face ID/Fingerprint for quick and secure logins in the future".bodyText(context).custom(fontWeight: FontWeight.w500), + ), + 24.height, + AppFilledButton( + onPressed: () { + Navigator.pop(context); + Navigator.of(context).pushNamed(SettingsPage.id); + }, + label: "Enable".addTranslation), + ], + )), + ); + } + } + } + @override Widget build(BuildContext context) { if (_userProvider == null) { @@ -49,6 +106,7 @@ class _LandPageState extends State { const MyAssetsPage(fromBottomBar: true), if (_userProvider.user.type == UsersTypes.engineer) const CalendarPage(), ]; + checkLocalAuth(); } return WillPopScope( diff --git a/lib/new_views/pages/settings_page.dart b/lib/new_views/pages/settings_page.dart index e9cf4c7a..f23b99da 100644 --- a/lib/new_views/pages/settings_page.dart +++ b/lib/new_views/pages/settings_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_advanced_switch/flutter_advanced_switch.dart'; import 'package:local_auth/local_auth.dart'; @@ -24,16 +26,44 @@ class _SettingsPageState extends State { ValueNotifier langController, themeController; SettingProvider _settingProvider; + bool localAuth = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + @override void dispose() { super.dispose(); langController.dispose(); themeController.dispose(); + // authController.dispose(); + } + + void checkForLocalAuth(bool buttonState) async { + bool authStatus = await _settingProvider.auth.authenticate( + localizedReason: Platform.isAndroid ? "Scan your fingerprint to authenticate" : "", + options: AuthenticationOptions(biometricOnly: Platform.isAndroid), + ); + if (authStatus) { + localAuth = !localAuth; + await _settingProvider.setAuth(localAuth); + // authController.value = _settingProvider.localAuth == buttonState.toString(); + setState(() {}); + } else { + // authController.value = !buttonState; + } } @override Widget build(BuildContext context) { - _settingProvider ??= Provider.of(context, listen: false); + if (_settingProvider == null) { + _settingProvider = Provider.of(context, listen: false); + localAuth = _settingProvider.localAuth; + } + langController ??= ValueNotifier(_settingProvider.language == "ar") ..addListener(() async { /// TODO : uncomment the below lines to support the another language @@ -45,17 +75,18 @@ class _SettingsPageState extends State { await _settingProvider.setDarkTheme(_settingProvider.theme == "light"); themeController.value = _settingProvider.theme == "light"; }); + + // authController ??= ValueNotifier(_settingProvider.localAuth == "false") + // ..addListener(() async { + // print("authController.value:${authController.value}"); // - // _settingProvider.auth.getAvailableBiometrics().then((value) { - // value.forEach((element) => print(element.name)); - // print("face:${value.contains(BiometricType.face)}"); - // print("bio:${value.contains(BiometricType.fingerprint)}"); - // print("iris:${value.contains(BiometricType.iris)}"); - // print("weak:${value.contains(BiometricType.weak)}"); - // print("strong:${value.contains(BiometricType.strong)}"); - // - // _settingProvider.auth.canCheckBiometrics.then((value) => print(value)); - // }); + // // if (authController.value) { + // // checkForLocalAuth(); + // // } else { + // // await _settingProvider.setAuth(_settingProvider.localAuth == "false"); + // // authController.value = _settingProvider.localAuth == "false"; + // // } + // }); return Scaffold( appBar: DefaultAppBar(title: context.translation.settings), @@ -100,40 +131,50 @@ class _SettingsPageState extends State { ), ], ), - // 16.height, - // const Divider().defaultStyle(context), - // 16.height, - // FutureBuilder( - // future: _settingProvider.auth.isDeviceSupported(), - // builder: (cxt, snapshot) { - // bool isDeviceSupported = false; - // if (snapshot.hasData) { - // isDeviceSupported = snapshot.data; - // } - // - // if (!isDeviceSupported) { - // return Text("Your device did not support Fingerprint/Face ID.", style: AppTextStyles.tinyFont.copyWith(fontSize: 11, color: Colors.red)); - // } - // - // return Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // context.translation.lightTheme.heading5(context), - // AdvancedSwitch( - // controller: themeController, - // activeColor: AppColor.green50.withOpacity(0.5), - // inactiveColor: AppColor.neutral10, - // thumb: CircleAvatar(backgroundColor: _settingProvider.theme == "light" ? AppColor.green50 : AppColor.neutral20), - // borderRadius: const BorderRadius.all(Radius.circular(30)), - // width: 42.toScreenWidth, - // height: 24.toScreenHeight, - // disabledOpacity: 1, - // ), - // ], - // ); - // }), + 16.height, + const Divider().defaultStyle(context), + 16.height, + FutureBuilder( + future: _settingProvider.auth.isDeviceSupported(), + builder: (cxt, snapshot) { + bool isDeviceSupported = false; + if (snapshot.hasData) { + isDeviceSupported = snapshot.data; + } + + if (!isDeviceSupported) { + return Text("Your device did not support Fingerprint/Face ID.", style: AppTextStyles.tinyFont.copyWith(fontSize: 11, color: Colors.red)); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Fingerprint/Face ID".heading5(context), + customSwitch(localAuth).onPress(() { + checkForLocalAuth(localAuth); + }), + ], + ); + }), ], ).toShadowContainer(context).paddingAll(16), ); } + + Widget customSwitch(bool enable) { + return Container( + width: 42.toScreenWidth, + height: 24.toScreenHeight, + alignment: enable ? Alignment.centerRight : Alignment.centerLeft, + decoration: BoxDecoration( + color: enable ? AppColor.green50.withOpacity(0.5) : AppColor.neutral10, + borderRadius: const BorderRadius.all(Radius.circular(30)), + ), + padding: const EdgeInsets.all(2), + child: Container( + height: 20.toScreenHeight, + width: 20.toScreenWidth, + decoration: BoxDecoration(shape: BoxShape.circle, color: enable ? AppColor.green50 : AppColor.neutral20), + )); + } } diff --git a/lib/new_views/pages/splash_page.dart b/lib/new_views/pages/splash_page.dart index 41824a81..818a3e5f 100644 --- a/lib/new_views/pages/splash_page.dart +++ b/lib/new_views/pages/splash_page.dart @@ -1,16 +1,15 @@ +import 'dart:io'; + import 'package:firebase_core/firebase_core.dart'; import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter/material.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; -import 'package:test_sa/controllers/notification/notification_manger.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart'; import 'package:test_sa/controllers/providers/settings/setting_provider.dart'; -import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart'; -import 'package:test_sa/models/service_request/service_request.dart'; import 'package:test_sa/new_views/pages/land_page/land_page.dart'; import 'package:test_sa/new_views/pages/login_page.dart'; -import 'package:test_sa/views/pages/user/requests/service_request_details.dart'; import '../../models/size_config.dart'; @@ -27,7 +26,7 @@ class _SplashPageState extends State { SettingProvider _settingProvider; UserProvider _userProvider; - bool isnotificationCame=false; + bool loading = false; @override void initState() { @@ -50,34 +49,84 @@ class _SplashPageState extends State { super.initState(); } + void checkTokenValidity(String token) async { + setState(() { + loading = true; + }); + + bool isValid = await _settingProvider.checkUserTokenValidation(token); + setState(() { + loading = false; + }); + if (isValid && _settingProvider.isLocalAuthEnable) { + bool isSuccess = await checkDualAuthentication(); + if (isSuccess) { + _userProvider.user = _settingProvider.user; + Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => true); + } else { + Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true); + } + } else { + Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true); + } + } + + Future checkDualAuthentication() async { + return await _settingProvider.auth.authenticate( + localizedReason: Platform.isAndroid ? "Scan your fingerprint to authenticate" : "", + options: AuthenticationOptions(biometricOnly: Platform.isAndroid), + ); + } + @override Widget build(BuildContext context) { SizeConfig.init(context); _settingProvider = Provider.of(context, listen: false); _userProvider = Provider.of(context, listen: false); return Scaffold( - body: SizedBox( - width: MediaQuery.of(context).size.width / 1.1, - child: FlareActor( - "assets/rives/atoms_splash.flr", - fit: BoxFit.contain, - animation: "splash", - callback: (animation) async { - Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true); - if (_settingProvider.isLoaded && (_settingProvider.user?.isLiveToken ?? false)) { - _userProvider.user = _settingProvider.user; - Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => true); - // if(isnotificationCame) - // Navigator.of(context).push(MaterialPageRoute( - // builder: (_) => ServiceRequestDetailsPage( - // serviceRequest: ServiceRequest(id: "72348"), - // ))); - /// The below line for the new design - // Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => true); - } - }, - ), - ).center, + body: Stack( + alignment: Alignment.center, + children: [ + if (loading) + const Positioned( + bottom: 100, + child: SizedBox( + width: 36, + height: 36, + child: CircularProgressIndicator( + color: Color(0xff4A8DB7), + strokeWidth: 3, + ), + )), + SizedBox( + width: MediaQuery.of(context).size.width / 1.1, + child: FlareActor( + "assets/rives/atoms_splash.flr", + fit: BoxFit.contain, + animation: "splash", + callback: (animation) async { + if (_settingProvider.isLoaded && (_settingProvider.user != null)) { + checkTokenValidity(_settingProvider.user.token); + } else { + Navigator.of(context).pushNamedAndRemoveUntil(LoginPage.routeName, (routes) => true); + } + + // if (_settingProvider.isLoaded && (_settingProvider.user?.isLiveToken ?? false)) { + // _userProvider.user = _settingProvider.user; + // Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => true); + // // if(isnotificationCame) + // // Navigator.of(context).push(MaterialPageRoute( + // // builder: (_) => ServiceRequestDetailsPage( + // // serviceRequest: ServiceRequest(id: "72348"), + // // ))); + // /// The below line for the new design + // // Navigator.of(context).pushNamedAndRemoveUntil(LandPage.routeName, (routes) => true); + // } + }, + ), + ).center, + ], + ), ); } }