diff --git a/assets/images/finger.png b/assets/images/finger.png new file mode 100644 index 0000000..5f07100 Binary files /dev/null and b/assets/images/finger.png differ diff --git a/assets/images/secBg.jpg b/assets/images/secBg.jpg new file mode 100644 index 0000000..975493f Binary files /dev/null and b/assets/images/secBg.jpg differ diff --git a/lib/core/route_names.dart b/lib/core/route_names.dart index f880aa9..21f9469 100644 --- a/lib/core/route_names.dart +++ b/lib/core/route_names.dart @@ -4,5 +4,6 @@ const String ImageShowRoute = "ImageShowRoute"; const String PaymentViewRoute = "PaymentView"; const String HistoryViewRoute = "HistoryView"; const String InfoKkmViewRoute = "InfoKkmViewRoute"; +const String SettingsViewRoute = "SettingsViewRoute"; const String QrViewRoute = "QrViewRoute"; // Generate the views here diff --git a/lib/core/router.dart b/lib/core/router.dart index 574f5de..1af744f 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -3,6 +3,7 @@ import 'package:aman_kassa_flutter/views/history/history_view.dart'; import 'package:aman_kassa_flutter/views/info_kkm/info_kkm_view.dart'; import 'package:aman_kassa_flutter/views/payment/payment_view.dart'; import 'package:aman_kassa_flutter/views/qr_view/qr_view.dart'; +import 'package:aman_kassa_flutter/views/settings/settings_view.dart'; import './route_names.dart'; import 'package:aman_kassa_flutter/views/home/home_view.dart'; @@ -38,6 +39,11 @@ Route generateRoute(RouteSettings settings) { routeName: settings.name, viewToShow: InfoKkmView(), ); + case SettingsViewRoute: + return _getPageRoute( + routeName: settings.name, + viewToShow: SettingView(), + ); case QrViewRoute: ImageShowModel data = settings.arguments as ImageShowModel; return _getPageRoute( diff --git a/lib/redux/actions/setting_actions.dart b/lib/redux/actions/setting_actions.dart index da4953b..7ba6071 100644 --- a/lib/redux/actions/setting_actions.dart +++ b/lib/redux/actions/setting_actions.dart @@ -23,4 +23,22 @@ ThunkAction changeTradeTypeFromSetting(String tradeType) { return (Store store) async { store.dispatch(SetSettingStateAction(SettingState(tradeType: tradeType ))); }; +} + +ThunkAction changePinCodeFromSetting(String pinCode) { + return (Store store) async { + store.dispatch(SetSettingStateAction(SettingState(pinCode: pinCode))); + }; +} + +ThunkAction changePinLockedFromSetting(bool locked) { + return (Store store) async { + store.dispatch(SetSettingStateAction(SettingState(pinLocked: locked))); + }; +} + +ThunkAction changePinSkipFromSetting(bool skip) { + return (Store store) async { + store.dispatch(SetSettingStateAction(SettingState(pinSkip: skip))); + }; } \ No newline at end of file diff --git a/lib/redux/actions/user_actions.dart b/lib/redux/actions/user_actions.dart index 048b49d..72194e3 100644 --- a/lib/redux/actions/user_actions.dart +++ b/lib/redux/actions/user_actions.dart @@ -72,7 +72,7 @@ Future logoutAction(Store store) async { UserState( isLoading: false, isAuthenticated: false, - user: User(), + user: User() ), ), ); diff --git a/lib/redux/reducers/setting_reducer.dart b/lib/redux/reducers/setting_reducer.dart index 5e76b10..961ea2c 100644 --- a/lib/redux/reducers/setting_reducer.dart +++ b/lib/redux/reducers/setting_reducer.dart @@ -5,6 +5,9 @@ settingReducer(SettingState prevState, SetSettingStateAction action) { final payload = action.settingState; return prevState.copyWith( mode: payload.mode, - tradeType: payload.tradeType + tradeType: payload.tradeType, + pinCode: payload.pinCode, + pinLocked: payload.pinLocked, + pinSkip: payload.pinSkip, ); } diff --git a/lib/redux/state/setting_state.dart b/lib/redux/state/setting_state.dart index dd5bb1a..2fa437c 100644 --- a/lib/redux/state/setting_state.dart +++ b/lib/redux/state/setting_state.dart @@ -5,24 +5,38 @@ import 'package:meta/meta.dart'; class SettingState { final String mode; final String tradeType; + final String pinCode; + final bool pinLocked; + final bool pinSkip; - SettingState({this.mode, this.tradeType}); + + SettingState({this.mode, this.tradeType, this.pinCode, this.pinLocked, this.pinSkip}); //read hive factory SettingState.initial(SettingState payload) { return SettingState( mode: payload?.mode ?? SettingModeKassa, - tradeType: payload?.tradeType ?? SettingTradeTypeGood); + tradeType: payload?.tradeType ?? SettingTradeTypeGood, + pinCode: payload.pinCode ?? null, + pinLocked: payload.pinLocked ?? false, + pinSkip: false, + ); } //write hive SettingState copyWith({ @required mode, @required tradeType, + @required pinCode, + @required pinLocked, + @required pinSkip, }) { return SettingState( mode: mode ?? this.mode, tradeType: tradeType ?? this.tradeType, + pinCode: pinCode ?? this.pinCode, + pinLocked: pinLocked ?? this.pinLocked, + pinSkip: pinSkip ?? this.pinSkip ); } @@ -31,11 +45,20 @@ class SettingState { ? SettingState( tradeType: json['tradeType'], mode: json['mode'], + pinCode: json['pinCode'], + pinLocked: json['pinLocked'], + pinSkip: json['pinSkip'], ) : null; } dynamic toJson() { - return {"tradeType": tradeType, "mode": mode}; + return { + "tradeType": tradeType, + "mode": mode, + "pinCode": pinCode, + "pinLocked" : pinLocked, + "pinSkip" : pinSkip, + }; } } diff --git a/lib/redux/state/user_state.dart b/lib/redux/state/user_state.dart index d7a1a85..6dbf02e 100644 --- a/lib/redux/state/user_state.dart +++ b/lib/redux/state/user_state.dart @@ -16,6 +16,7 @@ class UserState { final Smena smena; final Money money; + UserState( {this.isError, this.isLoading, @@ -26,7 +27,8 @@ class UserState { this.user, this.loginFormMessage, this.smena, - this.money}); + this.money, + }); factory UserState.initial(UserState payload) => UserState( isLoading: false, @@ -73,7 +75,8 @@ class UserState { user: User.fromJson(json['user']), authenticateType: json['authenticateType'], login: json['login'], - password: json['password']) + password: json['password'], + ) : null; } diff --git a/lib/views/home/components/popup_menu.dart b/lib/views/home/components/popup_menu.dart index e588169..f500178 100644 --- a/lib/views/home/components/popup_menu.dart +++ b/lib/views/home/components/popup_menu.dart @@ -7,7 +7,7 @@ const List choices = const [ //const Choice(title: 'Помощь', icon: Icons.help, command: 'help'), const Choice( title: 'Информация о ККМ', icon: Icons.info_outline, command: 'infokkm'), - //const Choice(title: 'Язык', icon: Icons.language, command: 'language'), + const Choice(title: 'Настройки', icon: Icons.settings, command: 'settings'), const Choice(title: 'Выйти', icon: Icons.exit_to_app, command: 'exit') ]; diff --git a/lib/views/home/home_view.dart b/lib/views/home/home_view.dart index 72aab32..ea3b185 100644 --- a/lib/views/home/home_view.dart +++ b/lib/views/home/home_view.dart @@ -6,17 +6,21 @@ import 'package:aman_kassa_flutter/core/route_names.dart'; import 'package:aman_kassa_flutter/core/services/ApiService.dart'; import 'package:aman_kassa_flutter/core/services/DataService.dart'; import 'package:aman_kassa_flutter/core/services/navigator_service.dart'; +import 'package:aman_kassa_flutter/redux/actions/setting_actions.dart'; import 'package:aman_kassa_flutter/redux/actions/user_actions.dart'; import 'package:aman_kassa_flutter/redux/constants/setting_const.dart'; import 'package:aman_kassa_flutter/redux/state/setting_state.dart'; import 'package:aman_kassa_flutter/redux/store.dart'; import 'package:aman_kassa_flutter/shared/app_colors.dart'; import 'package:aman_kassa_flutter/views/home/components/header_title.dart'; +import 'package:aman_kassa_flutter/views/lockscreen/passcodescreen.dart'; +import 'package:aman_kassa_flutter/views/start_up/start_up_view.dart'; import 'package:aman_kassa_flutter/widgets/loader/Dialogs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:logger/logger.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import './tabs/KassaTab.dart'; import './tabs/AdditionalTab.dart'; @@ -30,7 +34,7 @@ class HomeView extends StatefulWidget { _HomeViewState createState() => _HomeViewState(); } -class _HomeViewState extends State { +class _HomeViewState extends State with WidgetsBindingObserver { Logger log = getLogger('HomeView'); PageController pageController; int selectedTabIndex; @@ -39,6 +43,68 @@ class _HomeViewState extends State { NavigatorService _navigatorService = locator(); final GlobalKey _keyLoader = new GlobalKey(); + final lastKnownStateKey = 'lastKnownStateKey'; + final backgroundedTimeKey = 'backgroundedTimeKey'; + final pinLockMillis = 2000; // 2 seconds + + Future _paused() async { + final sp = await SharedPreferences.getInstance(); + sp.setInt(lastKnownStateKey, AppLifecycleState.paused.index); + } + + Future _inactive() async { + final sp = await SharedPreferences.getInstance(); + final prevState = sp.getInt(lastKnownStateKey); + final prevStateIsNotPaused = prevState != null && + AppLifecycleState.values[prevState] != AppLifecycleState.paused; + if(prevStateIsNotPaused && Redux.store.state.settingState.pinSkip == false) { + // save App backgrounded time to Shared preferences + sp.setInt(backgroundedTimeKey, DateTime.now().millisecondsSinceEpoch); + } +// update previous state as inactive + sp.setInt(lastKnownStateKey, AppLifecycleState.inactive.index); + } + + Future _resumed() async { + final sp = await SharedPreferences.getInstance(); + final bgTime = sp.getInt(backgroundedTimeKey) ?? 0; + final allowedBackgroundTime = bgTime + pinLockMillis; + final shouldShowPIN = DateTime.now().millisecondsSinceEpoch > allowedBackgroundTime; + print(shouldShowPIN); + if(shouldShowPIN && bgTime > 0) { + Redux.store.dispatch(changePinLockedFromSetting(true)); + // show PIN screen + // Navigator.pushReplacement(context, MaterialPageRoute( + // builder: (_) => PassCodeScreen( title: 'Безопасность',))); + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => + WillPopScope( + onWillPop: () async { + return false; + }, + child: PassCodeScreen( title: 'Безопасность',) + ) + )); + + } + sp.remove(backgroundedTimeKey); // clean + sp.setInt(lastKnownStateKey, AppLifecycleState.resumed.index);// previous state + } + + _checkLockPin () async { + if ( Redux.store.state.settingState.pinLocked == true) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => + WillPopScope( + onWillPop: () async { + return false; + }, + child: PassCodeScreen( title: 'Безопасность',) + ) + )); + } + } + @override void initState() { super.initState(); @@ -46,10 +112,32 @@ class _HomeViewState extends State { pageController = new PageController(initialPage: selectedTabIndex); Redux.store.dispatch(checkSmena); _dataService.checkDbFill(Redux.store.state.userState.user); + WidgetsBinding.instance.addObserver(this); + _checkLockPin(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + print('state = $state'); + switch(state) { + case AppLifecycleState.resumed: + _resumed(); + break; + case AppLifecycleState.paused: + _paused(); + break; + case AppLifecycleState.inactive: + _inactive(); + break; + default: + break; + } } @override void dispose() { + WidgetsBinding.instance.removeObserver(this); pageController.dispose(); super.dispose(); } @@ -64,6 +152,8 @@ class _HomeViewState extends State { Navigator.of(_keyLoader.currentContext, rootNavigator: true).pop(); } else if (choice.command == 'infokkm') { _navigatorService.push(InfoKkmViewRoute); + } else if (choice.command == 'settings') { + _navigatorService.push(SettingsViewRoute); } } diff --git a/lib/views/home/tabs/KassaTab.dart b/lib/views/home/tabs/KassaTab.dart index 096c09d..928a10a 100644 --- a/lib/views/home/tabs/KassaTab.dart +++ b/lib/views/home/tabs/KassaTab.dart @@ -6,6 +6,7 @@ import 'package:aman_kassa_flutter/core/services/DataService.dart'; import 'package:aman_kassa_flutter/core/services/dialog_service.dart'; import 'package:aman_kassa_flutter/core/services/navigator_service.dart'; import 'package:aman_kassa_flutter/redux/actions/kassa_actions.dart'; +import 'package:aman_kassa_flutter/redux/actions/setting_actions.dart'; import 'package:aman_kassa_flutter/redux/constants/operation_const.dart'; import 'package:aman_kassa_flutter/redux/constants/setting_const.dart'; import 'package:aman_kassa_flutter/redux/state/kassa_state.dart'; @@ -183,6 +184,7 @@ class KassaTab extends StatelessWidget { Future scan() async { try { + Redux.store.dispatch(changePinSkipFromSetting(true)); var options = ScanOptions(strings: { "cancel": 'Отмена', "flash_on": 'Вкл фонарик', @@ -238,6 +240,8 @@ class KassaTab extends StatelessWidget { result.rawContent = 'Unknown error: $e'; _dialogService.showDialog(description: 'Неизвестная ошибка: $e'); } + } finally { + Redux.store.dispatch(changePinSkipFromSetting(false)); } } diff --git a/lib/views/lockscreen/passcodescreen.dart b/lib/views/lockscreen/passcodescreen.dart new file mode 100644 index 0000000..b75234d --- /dev/null +++ b/lib/views/lockscreen/passcodescreen.dart @@ -0,0 +1,79 @@ +import 'package:aman_kassa_flutter/core/route_names.dart'; +import 'package:aman_kassa_flutter/redux/actions/setting_actions.dart'; +import 'package:aman_kassa_flutter/redux/store.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_lock_screen/flutter_lock_screen.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:aman_kassa_flutter/core/locator.dart'; +import 'package:aman_kassa_flutter/core/services/navigator_service.dart'; + +class PassCodeScreen extends StatefulWidget { + PassCodeScreen({Key key, this.title}) : super(key: key); + + final String title; + + @override + _PassCodeScreenState createState() => new _PassCodeScreenState(); +} + +class _PassCodeScreenState extends State { + // bool isFingerprint = false; + NavigatorService _navigatorService = locator(); + final backgroundedTimeKey = 'backgroundedTimeKey'; + + + // Future biometrics() async { + // final LocalAuthentication auth = new LocalAuthentication(); + // bool authenticated = false; + // + // try { + // authenticated = await auth.authenticateWithBiometrics( + // localizedReason: 'Scan your fingerprint to authenticate', + // useErrorDialogs: true, + // stickyAuth: false); + // } on PlatformException catch (e) { + // print(e); + // } + // if (!mounted) return; + // if (authenticated) { + // setState(() { + // isFingerprint = true; + // }); + // } + // } + + @override + Widget build(BuildContext context) { + var myPass = []; + String _pinCode = Redux.store.state.settingState.pinCode; + for (var i = 0; i < _pinCode.length; i++) { + myPass.add(int.parse(_pinCode[i])); + } + return LockScreen( + title: "Введите ПИН-код", + passLength: myPass.length, + bgImage: "assets/images/secBg.jpg", + // fingerPrintImage: "assets/images/finger.png", + // showFingerPass: true, + // fingerFunction: biometrics, + // fingerVerify: isFingerprint, + borderColor: Colors.white, + showWrongPassDialog: true, + wrongPassContent: "Неверный код, повторите попытку", + wrongPassTitle: "Aman Kassa", + wrongPassCancelButtonText: "Отмена", + passCodeVerify: (passcode) async { + for (int i = 0; i < myPass.length; i++) { + if (passcode[i] != myPass[i]) { + return false; + } + } + + return true; + }, + onSuccess: () { + Redux.store.dispatch(changePinLockedFromSetting(false)); + _navigatorService.replace(HomeViewRoute); + }); + } +} \ No newline at end of file diff --git a/lib/views/settings/settings_view.dart b/lib/views/settings/settings_view.dart new file mode 100644 index 0000000..f7e427e --- /dev/null +++ b/lib/views/settings/settings_view.dart @@ -0,0 +1,96 @@ +import 'package:aman_kassa_flutter/core/locator.dart'; +import 'package:aman_kassa_flutter/core/services/dialog_service.dart'; +import 'package:aman_kassa_flutter/redux/actions/setting_actions.dart'; +import 'package:aman_kassa_flutter/redux/state/setting_state.dart'; +import 'package:aman_kassa_flutter/redux/store.dart'; +import 'package:aman_kassa_flutter/shared/app_colors.dart'; +import 'package:aman_kassa_flutter/shared/ui_helpers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SettingView extends StatefulWidget { + SettingView(); + + @override + _SettingViewState createState() => _SettingViewState(); +} + +class _SettingViewState extends State { + TextEditingController _pinController; + final DialogService _dialogService = locator(); + + @override + void initState() { + super.initState(); + SettingState state = Redux.store.state.settingState; + _pinController = new TextEditingController(text: state.pinCode); + } + + + + + @override + void dispose() { + _pinController.dispose(); + super.dispose(); + } + + void _setPinCode(BuildContext _context) async { + FocusScope.of(_context).unfocus(); + var value = _pinController.text; + if(value.isEmpty || value.length !=4){ + _dialogService.showDialog(description: 'Необходимо указать 4-х значный числовой код'); + } else { + await Redux.store.dispatch(changePinCodeFromSetting(_pinController.text)); + _dialogService.showDialog(description: 'Данные успешно сохранены'); + } + } + + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text('Настройка HalykPos'), + ), + body: SingleChildScrollView( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 14.0), + child: Column( + children: [ + verticalSpaceTiny, + Text( + 'Ддя блокировки приложения пин-кодом, укажите 4-ех значный числовой код', + style: TextStyle(fontSize: 15.0), + textAlign: TextAlign.center, + ), + verticalSpaceTiny, + TextField( + controller: _pinController, + maxLength: 4, + obscureText: true, + decoration: InputDecoration( + labelText: 'Пин-код', hintText: "Укажите пин-код"), + keyboardType: TextInputType.number, + ), + verticalSpaceMedium, + RaisedButton( + onPressed: () => this._setPinCode(context), + child: Text( + 'Cохранить настройки', + style: TextStyle(color: whiteColor, fontSize: 20.0), + ), + color: primaryColor, + padding: + const EdgeInsets.symmetric(vertical: 5.0, horizontal: 20.0), + ), + + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 861beca..69ca472 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -139,6 +139,20 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lock_screen: + dependency: "direct main" + description: + name: flutter_lock_screen + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.8" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.11" flutter_redux: dependency: "direct main" description: @@ -198,6 +212,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.16.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3+4" logger: dependency: "direct main" description: @@ -374,7 +395,7 @@ packages: source: hosted version: "0.3.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index fe85302..f924d38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,9 @@ dependencies: qr_flutter: ^3.2.0 mask_text_input_formatter: ^1.2.1 flutter_screenutil: ^2.3.1 + shared_preferences: ^0.5.12+4 + flutter_lock_screen: ^1.0.8 + local_auth: ^0.6.3+4 dev_dependencies: flutter_test: sdk: flutter @@ -40,8 +43,7 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - assets/images/logo.png - - assets/images/icon_large.png + - assets/images/ - assets/lang/en.json - assets/lang/ru.json - assets/google_fonts/