diff --git a/assets/images/shuffle.png b/assets/images/shuffle.png new file mode 100644 index 000000000..878938c06 Binary files /dev/null and b/assets/images/shuffle.png differ diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0c032a736..02cd4ae4a 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -33,6 +33,7 @@ class PreferencesKey { static const currentTheme = 'current_theme'; static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; + static const randomizePinCode = 'randomize_pin_code'; static const currentLanguageCode = 'language_code'; static const currentSeedPhraseLength = 'current_seed_phrase_length'; static const currentDefaultSettingsMigrationVersion = diff --git a/lib/src/screens/auth/auth_page.dart b/lib/src/screens/auth/auth_page.dart index d14a12527..469133c3f 100644 --- a/lib/src/screens/auth/auth_page.dart +++ b/lib/src/screens/auth/auth_page.dart @@ -19,9 +19,7 @@ abstract class AuthPageState extends State { } class AuthPage extends StatefulWidget { - AuthPage(this.authViewModel, - {required this.onAuthenticationFinished, - this.closable = true}); + AuthPage(this.authViewModel, {required this.onAuthenticationFinished, this.closable = true}); final AuthViewModel authViewModel; final OnAuthenticationFinished onAuthenticationFinished; @@ -34,16 +32,14 @@ class AuthPage extends StatefulWidget { class AuthPagePinCodeStateImpl extends AuthPageState { final _key = GlobalKey(); final _pinCodeKey = GlobalKey(); - final _backArrowImageDarkTheme = - Image.asset('assets/images/close_button.png'); + final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png'); ReactionDisposer? _reaction; Flushbar? _authBar; Flushbar? _progressBar; @override void initState() { - _reaction ??= - reaction((_) => widget.authViewModel.state, (ExecutionState state) { + _reaction ??= reaction((_) => widget.authViewModel.state, (ExecutionState state) { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { widget.onAuthenticationFinished(true, this); @@ -54,9 +50,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState { if (state is IsExecutingState) { WidgetsBinding.instance.addPostFrameCallback((_) { // null duration to make it indefinite until its disposed - _authBar = - createBar(S.of(context).authentication, duration: null) - ..show(context); + _authBar = createBar(S.of(context).authentication, duration: null)..show(context); }); } @@ -64,8 +58,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState { WidgetsBinding.instance.addPostFrameCallback((_) async { _pinCodeKey.currentState?.clear(); dismissFlushBar(_authBar); - showBar( - context, S.of(context).failed_authentication(state.error)); + showBar(context, S.of(context).failed_authentication(state.error)); widget.onAuthenticationFinished(false, this); }); @@ -75,8 +68,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState { WidgetsBinding.instance.addPostFrameCallback((_) async { _pinCodeKey.currentState?.clear(); dismissFlushBar(_authBar); - showBar( - context, S.of(context).failed_authentication(state.error)); + showBar(context, S.of(context).failed_authentication(state.error)); widget.onAuthenticationFinished(false, this); }); @@ -102,8 +94,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState { @override void changeProcessText(String text) { dismissFlushBar(_authBar); - _progressBar = createBar(text, duration: null) - ..show(_key.currentContext!); + _progressBar = createBar(text, duration: null)..show(_key.currentContext!); } @override @@ -134,25 +125,33 @@ class AuthPagePinCodeStateImpl extends AuthPageState { @override Widget build(BuildContext context) { return Scaffold( - key: _key, - appBar: CupertinoNavigationBar( - leading: widget.closable - ? Container( - padding: EdgeInsets.only(top: 10), - child: SizedBox( - height: 37, - width: 37, - child: InkWell( - onTap: () => Navigator.of(context).pop(), - child: _backArrowImageDarkTheme, - ), - )) - : Container(), - backgroundColor: Theme.of(context).colorScheme.background, - border: null), - resizeToAvoidBottomInset: false, - body: PinCode((pin, _) => widget.authViewModel.auth(password: pin), - (_) => null, widget.authViewModel.pinLength, false, _pinCodeKey)); + key: _key, + appBar: CupertinoNavigationBar( + leading: widget.closable + ? Container( + padding: EdgeInsets.only(top: 10), + child: SizedBox( + height: 37, + width: 37, + child: InkWell( + onTap: () => Navigator.of(context).pop(), + child: _backArrowImageDarkTheme, + ), + )) + : Container(), + backgroundColor: Theme.of(context).colorScheme.background, + border: null), + resizeToAvoidBottomInset: false, + body: PinCode( + (pin, _) => widget.authViewModel.auth(password: pin), + (_) => null, + widget.authViewModel.setPinRandomized, + widget.authViewModel.pinLength, + widget.authViewModel.pinRandomized, + false, + _pinCodeKey, + ), + ); } void dismissFlushBar(Flushbar? bar) { diff --git a/lib/src/screens/pin_code/pin_code.dart b/lib/src/screens/pin_code/pin_code.dart index 02a917531..35e2a2fb9 100644 --- a/lib/src/screens/pin_code/pin_code.dart +++ b/lib/src/screens/pin_code/pin_code.dart @@ -5,15 +5,20 @@ class PinCode extends PinCodeWidget { PinCode( void Function(String pin, PinCodeState state) onFullPin, void Function(String pin) onChangedPin, + void Function(bool) setPinRandomized, int initialPinLength, + bool initialPinRandomized, bool hasLengthSwitcher, Key key) : super( - key: key, - onFullPin: onFullPin, - onChangedPin: onChangedPin, - hasLengthSwitcher: hasLengthSwitcher, - initialPinLength: initialPinLength); + key: key, + onFullPin: onFullPin, + onChangedPin: onChangedPin, + hasLengthSwitcher: hasLengthSwitcher, + initialPinLength: initialPinLength, + initialPinRandomized: initialPinRandomized, + setPinRandomized: setPinRandomized, + ); @override PinCodeState createState() => PinCodeState(); diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 36328aee2..937ed7e70 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -14,14 +14,18 @@ class PinCodeWidget extends StatefulWidget { required this.initialPinLength, required this.onChangedPin, required this.hasLengthSwitcher, + required this.setPinRandomized, + required this.initialPinRandomized, this.onChangedPinLength, }) : super(key: key); final void Function(String pin, PinCodeState state) onFullPin; final void Function(String pin) onChangedPin; final void Function(int length)? onChangedPinLength; + final void Function(bool) setPinRandomized; final bool hasLengthSwitcher; final int initialPinLength; + final bool initialPinRandomized; @override State createState() => PinCodeState(); @@ -44,6 +48,8 @@ class PinCodeState extends State { String title; double _aspectRatio; Flushbar? _progressBar; + late List numbers = []; + bool randomizePin = false; int currentPinLength() => pin.length; @@ -54,6 +60,12 @@ class PinCodeState extends State { pin = ''; title = S.current.enter_your_pin; _aspectRatio = 0; + + randomizePin = widget.initialPinRandomized; + numbers = List.generate(10, (index) => index); + if (randomizePin) { + numbers.shuffle(); + } WidgetsBinding.instance.addPostFrameCallback(_afterLayout); } @@ -118,6 +130,10 @@ class PinCodeState extends State { 'assets/images/face.png', color: Theme.of(context).extension()!.titleColor, ); + final shuffleImage = Image.asset( + 'assets/images/shuffle.png', + color: Theme.of(context).extension()!.titleColor, + ); return RawKeyboardListener( focusNode: FocusNode(), @@ -144,8 +160,7 @@ class PinCodeState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, - color: - Theme.of(context).extension()!.titleColor)), + color: Theme.of(context).extension()!.titleColor)), Spacer(flex: 3), Container( width: 180, @@ -162,7 +177,9 @@ class PinCodeState extends State { shape: BoxShape.circle, color: isFilled ? Theme.of(context).extension()!.titleColor - : Theme.of(context).extension()!.indicatorsColor + : Theme.of(context) + .extension()! + .indicatorsColor .withOpacity(0.25), )); }), @@ -208,9 +225,26 @@ class PinCodeState extends State { const double marginLeft = 15; if (index == 9) { - // Empty container - return Container( - margin: EdgeInsets.only(left: marginLeft, right: marginRight), + // randomize button + return MergeSemantics( + child: Container( + margin: EdgeInsets.only(left: marginLeft, right: marginRight), + child: Semantics( + label: S.current.delete, + button: true, + onTap: () => _toggleRandomize(), + child: TextButton( + onPressed: () => _toggleRandomize(), + style: TextButton.styleFrom( + backgroundColor: randomizePin + ? Theme.of(context).colorScheme.onBackground + : Theme.of(context).colorScheme.background, + shape: CircleBorder(), + ), + child: shuffleImage, + ), + ), + ), ); } else if (index == 10) { index = 0; @@ -225,7 +259,8 @@ class PinCodeState extends State { child: TextButton( onPressed: () => _pop(), style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: + Theme.of(context).colorScheme.background, shape: CircleBorder(), ), child: deleteIconImage, @@ -240,16 +275,18 @@ class PinCodeState extends State { return Container( margin: EdgeInsets.only(left: marginLeft, right: marginRight), child: TextButton( - onPressed: () => _push(index), + onPressed: () => _push(numbers[index]), style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.background, shape: CircleBorder(), ), - child: Text('$index', + child: Text('${numbers[index]}', style: TextStyle( fontSize: 30.0, fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.titleColor)), + color: Theme.of(context) + .extension()! + .titleColor)), ), ); }), @@ -290,6 +327,18 @@ class PinCodeState extends State { setState(() => pin = pin.substring(0, pin.length - 1)); } + void _toggleRandomize() { + setState(() { + randomizePin = !randomizePin; + widget.setPinRandomized(randomizePin); + if (!randomizePin) { + numbers = List.generate(10, (index) => index); + } else { + numbers.shuffle(); + } + }); + } + String _changePinLengthText() { return S.current.use + (pinLength == PinCodeState.fourPinLength diff --git a/lib/src/screens/setup_pin_code/setup_pin_code.dart b/lib/src/screens/setup_pin_code/setup_pin_code.dart index 833fd9b60..e8638e822 100644 --- a/lib/src/screens/setup_pin_code/setup_pin_code.dart +++ b/lib/src/screens/setup_pin_code/setup_pin_code.dart @@ -22,6 +22,8 @@ class SetupPinCodePage extends BasePage { Widget body(BuildContext context) => PinCodeWidget( key: pinCodeStateKey, hasLengthSwitcher: true, + setPinRandomized: pinCodeViewModel.setPinRandomized, + initialPinRandomized: pinCodeViewModel.pinRandomized, onFullPin: (String pin, PinCodeState state) async { if (pinCodeViewModel.isOriginalPinCodeFull && !pinCodeViewModel.isRepeatedPinCodeFull) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6873ffde8..0f3b678f4 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -73,6 +73,7 @@ abstract class SettingsStoreBase with Store { required ExchangeApiMode initialExchangeStatus, required ThemeBase initialTheme, required int initialPinLength, + required bool initialRandomizePinCode, required String initialLanguageCode, required SyncMode initialSyncMode, required bool initialSyncAll, @@ -150,6 +151,7 @@ abstract class SettingsStoreBase with Store { exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, pinCodeLength = initialPinLength, + randomizePinCode = initialRandomizePinCode, languageCode = initialLanguageCode, shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet, shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact, @@ -369,6 +371,9 @@ abstract class SettingsStoreBase with Store { reaction((_) => pinCodeLength, (int pinLength) => sharedPreferences.setInt(PreferencesKey.currentPinLength, pinLength)); + reaction((_) => randomizePinCode, + (bool randomizePinCode) => sharedPreferences.setBool(PreferencesKey.randomizePinCode, randomizePinCode)); + reaction( (_) => languageCode, (String languageCode) => @@ -683,6 +688,9 @@ abstract class SettingsStoreBase with Store { @observable int pinCodeLength; + @observable + bool randomizePinCode; + @observable PinCodeRequiredDuration pinTimeOutDuration; @@ -931,6 +939,8 @@ abstract class SettingsStoreBase with Store { pinLength = defaultPinLength; } + final randomizePinCode = sharedPreferences.getBool(PreferencesKey.randomizePinCode) ?? false; + final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? await LanguageService.localeDetection(); final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -1171,6 +1181,7 @@ abstract class SettingsStoreBase with Store { initialTheme: savedTheme, actionlistDisplayMode: actionListDisplayMode, initialPinLength: pinLength, + initialRandomizePinCode: randomizePinCode, pinTimeOutDuration: pinCodeTimeOutDuration, seedPhraseLength: seedPhraseWordCount, initialLanguageCode: savedLanguageCode, @@ -1327,6 +1338,8 @@ abstract class SettingsStoreBase with Store { } pinCodeLength = pinLength; + randomizePinCode = sharedPreferences.getBool(PreferencesKey.randomizePinCode) ?? false; + languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode; shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup; diff --git a/lib/view_model/auth_view_model.dart b/lib/view_model/auth_view_model.dart index 6f6e29662..80cf573e3 100644 --- a/lib/view_model/auth_view_model.dart +++ b/lib/view_model/auth_view_model.dart @@ -30,8 +30,15 @@ abstract class AuthViewModelBase with Store { int get pinLength => _settingsStore.pinCodeLength; + bool get pinRandomized => _settingsStore.randomizePinCode; + bool get isBiometricalAuthenticationAllowed => _settingsStore.allowBiometricalAuthentication; + @action + void setPinRandomized(bool randomized) { + _settingsStore.randomizePinCode = randomized; + } + @observable int _failureCounter; @@ -121,4 +128,4 @@ abstract class AuthViewModelBase with Store { _authService.saveLastAuthTime(); } } -} \ No newline at end of file +} diff --git a/lib/view_model/setup_pin_code_view_model.dart b/lib/view_model/setup_pin_code_view_model.dart index ad503b7dc..29b00a13f 100644 --- a/lib/view_model/setup_pin_code_view_model.dart +++ b/lib/view_model/setup_pin_code_view_model.dart @@ -67,4 +67,10 @@ class SetupPinCodeViewModel { await _authService.setPassword(repeatedPinCode); _settingsStore.pinCodeLength = pinCodeLength; } + + bool get pinRandomized => _settingsStore.randomizePinCode; + + void setPinRandomized(bool randomized) { + _settingsStore.randomizePinCode = randomized; + } }