/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; import '../../providers/global/prefs_provider.dart'; import '../../providers/global/secure_store_provider.dart'; import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; // import 'package:stackwallet/providers/global/should_show_lockscreen_on_resume_state_provider.dart'; import '../../utilities/assets.dart'; import '../../utilities/biometrics.dart'; import '../../utilities/flutter_secure_storage_interface.dart'; import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/custom_pin_put/custom_pin_put.dart'; import '../../widgets/shake/shake.dart'; import '../home_view/home_view.dart'; import '../wallet_view/wallet_view.dart'; class LockscreenView extends ConsumerStatefulWidget { const LockscreenView({ super.key, required this.routeOnSuccess, required this.biometricsAuthenticationTitle, required this.biometricsLocalizedReason, required this.biometricsCancelButtonString, this.showBackButton = false, this.popOnSuccess = false, this.isInitialAppLogin = false, this.routeOnSuccessArguments, this.biometrics = const Biometrics(), this.onSuccess, this.customKeyLabel = "Button", }); static const String routeName = "/lockscreen"; final String routeOnSuccess; final Object? routeOnSuccessArguments; final bool showBackButton; final bool popOnSuccess; final bool isInitialAppLogin; final String biometricsAuthenticationTitle; final String biometricsLocalizedReason; final String biometricsCancelButtonString; final Biometrics biometrics; final VoidCallback? onSuccess; final String customKeyLabel; @override ConsumerState createState() => _LockscreenViewState(); } class _LockscreenViewState extends ConsumerState { late final ShakeController _shakeController; late final bool _autoPin; late int _attempts; bool _attemptLock = false; late Duration _timeout; static const maxAttemptsBeforeThrottling = 3; Timer? _timer; Future _onUnlock() async { final now = DateTime.now().toUtc(); ref.read(prefsChangeNotifierProvider).lastUnlocked = now.millisecondsSinceEpoch ~/ 1000; // if (widget.isInitialAppLogin) { // ref.read(hasAuthenticatedOnStartStateProvider.state).state = true; // ref.read(shouldShowLockscreenOnResumeStateProvider.state).state = true; // } widget.onSuccess?.call(); if (widget.popOnSuccess) { Navigator.of(context).pop(widget.routeOnSuccessArguments); } else { final loadIntoWallet = widget.routeOnSuccess == HomeView.routeName && widget.routeOnSuccessArguments is String; if (loadIntoWallet) { final walletId = widget.routeOnSuccessArguments as String; final wallet = ref.read(pWallets).getWallet(walletId); final Future loadFuture; if (wallet is CwBasedInterface) { loadFuture = wallet.init().then((value) async => await (wallet).open()); } else { loadFuture = wallet.init(); } await showLoading( opaqueBG: true, whileFuture: loadFuture, context: context, message: "Loading ${wallet.info.coin.prettyName} wallet...", ); } if (mounted) { unawaited( Navigator.of(context).pushReplacementNamed( widget.routeOnSuccess, arguments: widget.routeOnSuccessArguments, ), ); if (loadIntoWallet) { final walletId = widget.routeOnSuccessArguments as String; unawaited( Navigator.of(context).pushNamed( WalletView.routeName, arguments: walletId, ), ); } } } } Future _checkUseBiometrics() async { if (!ref.read(prefsChangeNotifierProvider).isInitialized) { await ref.read(prefsChangeNotifierProvider).init(); } final bool useBiometrics = ref.read(prefsChangeNotifierProvider).useBiometrics; final title = widget.biometricsAuthenticationTitle; final localizedReason = widget.biometricsLocalizedReason; final cancelButtonText = widget.biometricsCancelButtonString; if (useBiometrics) { if (await biometrics.authenticate( title: title, localizedReason: localizedReason, cancelButtonText: cancelButtonText, )) { // check if initial log in // if (widget.routeOnSuccess == "/mainview") { // await logIn(await walletsService.networkName, currentWalletName, // await walletsService.getWalletId(currentWalletName)); // } unawaited(_onUnlock()); } // leave this commented to enable pin fall back should biometrics not work properly // else { // Navigator.pop(context); // } } } @override void didChangeDependencies() { if (widget.isInitialAppLogin) { // unawaited(Assets.precache(context)); } super.didChangeDependencies(); } @override void initState() { _shakeController = ShakeController(); _secureStore = ref.read(secureStoreProvider); biometrics = widget.biometrics; _attempts = 0; _timeout = Duration.zero; _autoPin = ref.read(prefsChangeNotifierProvider).autoPin; if (_autoPin) { _pinTextController.addListener(_onPinChangedAutologinCheck); } _checkUseBiometrics(); super.initState(); } @override dispose() { // _shakeController.dispose(); _pinTextController.removeListener(_onPinChangedAutologinCheck); super.dispose(); } BoxDecoration get _pinPutDecoration { return BoxDecoration( color: Theme.of(context).extension()!.infoItemIcons, border: Border.all( width: 1, color: Theme.of(context).extension()!.infoItemIcons, ), borderRadius: BorderRadius.circular(6), ); } final FocusNode _pinFocusNode = FocusNode(); late SecureStorageInterface _secureStore; late Biometrics biometrics; int pinCount = 1; final _pinTextController = TextEditingController(); final Mutex _autoPinCheckLock = Mutex(); void _onPinChangedAutologinCheck() async { if (mounted) { await _autoPinCheckLock.acquire(); } try { if (_autoPin && _pinTextController.text.length >= 4) { final storedPin = await _secureStore.read(key: 'stack_pin'); if (_pinTextController.text == storedPin) { await Future.delayed( const Duration(milliseconds: 200), ); unawaited(_onUnlock()); } } } finally { _autoPinCheckLock.release(); } } void _onSubmitPin(String pin) async { _attempts++; if (_attempts > maxAttemptsBeforeThrottling) { _attemptLock = true; switch (_attempts) { case 4: _timeout = const Duration(seconds: 30); break; case 5: _timeout = const Duration(seconds: 60); break; case 6: _timeout = const Duration(minutes: 5); break; case 7: _timeout = const Duration(minutes: 10); break; case 8: _timeout = const Duration(minutes: 20); break; case 9: _timeout = const Duration(minutes: 30); break; default: _timeout = const Duration(minutes: 60); } _timer?.cancel(); _timer = Timer(_timeout, () { _attemptLock = false; _attempts = 0; }); } if (_attemptLock) { String prettyTime = ""; if (_timeout.inSeconds >= 60) { prettyTime += "${_timeout.inMinutes} minutes"; } else { prettyTime += "${_timeout.inSeconds} seconds"; } unawaited( showFloatingFlushBar( type: FlushBarType.warning, message: "Incorrect PIN entered too many times. Please wait $prettyTime", context: context, iconAsset: Assets.svg.alertCircle, ), ); await Future.delayed( const Duration(milliseconds: 100), ); _pinTextController.text = ''; return; } final storedPin = await _secureStore.read(key: 'stack_pin'); if (storedPin == pin) { await Future.delayed( const Duration(milliseconds: 200), ); unawaited(_onUnlock()); } else { unawaited(_shakeController.shake()); if (mounted) { unawaited( showFloatingFlushBar( type: FlushBarType.warning, message: "Incorrect PIN. Please try again", context: context, iconAsset: Assets.svg.alertCircle, ), ); } await Future.delayed( const Duration(milliseconds: 100), ); _pinTextController.text = ''; } } Widget get _body => Background( child: SafeArea( child: Scaffold( extendBodyBehindAppBar: true, backgroundColor: Theme.of(context).extension()!.background, appBar: AppBar( leading: widget.showBackButton ? AppBarBackButton( onPressed: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); await Future.delayed( const Duration(milliseconds: 70), ); } if (mounted) { Navigator.of(context).pop(); } }, ) : Container(), actions: [ // check prefs and hide if user has biometrics toggle off? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only( right: 16.0, ), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (ref .read(prefsChangeNotifierProvider) .useBiometrics == true) CustomTextButton( text: "Use biometrics", onTap: () async { await _checkUseBiometrics(); }, ), ], ), ), ], ), ], ), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Shake( animationDuration: const Duration(milliseconds: 700), animationRange: 12, controller: _shakeController, child: Center( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Center( child: Text( "Enter PIN", style: STextStyles.pageTitleH1(context), ), ), const SizedBox( height: 52, ), CustomPinPut( fieldsCount: pinCount, eachFieldHeight: 12, eachFieldWidth: 12, textStyle: STextStyles.label(context).copyWith( fontSize: 1, ), focusNode: _pinFocusNode, controller: _pinTextController, useNativeKeyboard: false, obscureText: "", inputDecoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, fillColor: Theme.of(context) .extension()! .background, counterText: "", ), submittedFieldDecoration: _pinPutDecoration, isRandom: ref .read(prefsChangeNotifierProvider) .randomizePIN, onSubmit: (pin) { if (!_autoPinCheckLock.isLocked) { _onSubmitPin(pin); } }, ), ], ), ), ), ], ), ), ), ); @override Widget build(BuildContext context) { return widget.showBackButton ? _body : WillPopScope( onWillPop: () async { return widget.showBackButton; }, child: _body, ); } }