add pin randomization

This commit is contained in:
Matthew Fosse 2024-09-20 20:36:39 -07:00
parent 32e119e24f
commit 3e17d31435
9 changed files with 133 additions and 51 deletions

BIN
assets/images/shuffle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -33,6 +33,7 @@ class PreferencesKey {
static const currentTheme = 'current_theme'; static const currentTheme = 'current_theme';
static const displayActionListModeKey = 'display_list_mode'; static const displayActionListModeKey = 'display_list_mode';
static const currentPinLength = 'current_pin_length'; static const currentPinLength = 'current_pin_length';
static const randomizePinCode = 'randomize_pin_code';
static const currentLanguageCode = 'language_code'; static const currentLanguageCode = 'language_code';
static const currentSeedPhraseLength = 'current_seed_phrase_length'; static const currentSeedPhraseLength = 'current_seed_phrase_length';
static const currentDefaultSettingsMigrationVersion = static const currentDefaultSettingsMigrationVersion =

View file

@ -19,9 +19,7 @@ abstract class AuthPageState<T extends StatefulWidget> extends State<T> {
} }
class AuthPage extends StatefulWidget { class AuthPage extends StatefulWidget {
AuthPage(this.authViewModel, AuthPage(this.authViewModel, {required this.onAuthenticationFinished, this.closable = true});
{required this.onAuthenticationFinished,
this.closable = true});
final AuthViewModel authViewModel; final AuthViewModel authViewModel;
final OnAuthenticationFinished onAuthenticationFinished; final OnAuthenticationFinished onAuthenticationFinished;
@ -34,16 +32,14 @@ class AuthPage extends StatefulWidget {
class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> { class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
final _key = GlobalKey<ScaffoldState>(); final _key = GlobalKey<ScaffoldState>();
final _pinCodeKey = GlobalKey<PinCodeState>(); final _pinCodeKey = GlobalKey<PinCodeState>();
final _backArrowImageDarkTheme = final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png');
Image.asset('assets/images/close_button.png');
ReactionDisposer? _reaction; ReactionDisposer? _reaction;
Flushbar<void>? _authBar; Flushbar<void>? _authBar;
Flushbar<void>? _progressBar; Flushbar<void>? _progressBar;
@override @override
void initState() { void initState() {
_reaction ??= _reaction ??= reaction((_) => widget.authViewModel.state, (ExecutionState state) {
reaction((_) => widget.authViewModel.state, (ExecutionState state) {
if (state is ExecutedSuccessfullyState) { if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onAuthenticationFinished(true, this); widget.onAuthenticationFinished(true, this);
@ -54,9 +50,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
if (state is IsExecutingState) { if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// null duration to make it indefinite until its disposed // null duration to make it indefinite until its disposed
_authBar = _authBar = createBar<void>(S.of(context).authentication, duration: null)..show(context);
createBar<void>(S.of(context).authentication, duration: null)
..show(context);
}); });
} }
@ -64,8 +58,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear(); _pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar); dismissFlushBar(_authBar);
showBar<void>( showBar<void>(context, S.of(context).failed_authentication(state.error));
context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this); widget.onAuthenticationFinished(false, this);
}); });
@ -75,8 +68,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear(); _pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar); dismissFlushBar(_authBar);
showBar<void>( showBar<void>(context, S.of(context).failed_authentication(state.error));
context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this); widget.onAuthenticationFinished(false, this);
}); });
@ -102,8 +94,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
@override @override
void changeProcessText(String text) { void changeProcessText(String text) {
dismissFlushBar(_authBar); dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null) _progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
..show(_key.currentContext!);
} }
@override @override
@ -134,25 +125,33 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
key: _key, key: _key,
appBar: CupertinoNavigationBar( appBar: CupertinoNavigationBar(
leading: widget.closable leading: widget.closable
? Container( ? Container(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: SizedBox( child: SizedBox(
height: 37, height: 37,
width: 37, width: 37,
child: InkWell( child: InkWell(
onTap: () => Navigator.of(context).pop(), onTap: () => Navigator.of(context).pop(),
child: _backArrowImageDarkTheme, child: _backArrowImageDarkTheme,
), ),
)) ))
: Container(), : Container(),
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
border: null), border: null),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
body: PinCode((pin, _) => widget.authViewModel.auth(password: pin), body: PinCode(
(_) => null, widget.authViewModel.pinLength, false, _pinCodeKey)); (pin, _) => widget.authViewModel.auth(password: pin),
(_) => null,
widget.authViewModel.setPinRandomized,
widget.authViewModel.pinLength,
widget.authViewModel.pinRandomized,
false,
_pinCodeKey,
),
);
} }
void dismissFlushBar(Flushbar<dynamic>? bar) { void dismissFlushBar(Flushbar<dynamic>? bar) {

View file

@ -5,15 +5,20 @@ class PinCode extends PinCodeWidget {
PinCode( PinCode(
void Function(String pin, PinCodeState state) onFullPin, void Function(String pin, PinCodeState state) onFullPin,
void Function(String pin) onChangedPin, void Function(String pin) onChangedPin,
void Function(bool) setPinRandomized,
int initialPinLength, int initialPinLength,
bool initialPinRandomized,
bool hasLengthSwitcher, bool hasLengthSwitcher,
Key key) Key key)
: super( : super(
key: key, key: key,
onFullPin: onFullPin, onFullPin: onFullPin,
onChangedPin: onChangedPin, onChangedPin: onChangedPin,
hasLengthSwitcher: hasLengthSwitcher, hasLengthSwitcher: hasLengthSwitcher,
initialPinLength: initialPinLength); initialPinLength: initialPinLength,
initialPinRandomized: initialPinRandomized,
setPinRandomized: setPinRandomized,
);
@override @override
PinCodeState createState() => PinCodeState(); PinCodeState createState() => PinCodeState();

View file

@ -14,14 +14,18 @@ class PinCodeWidget extends StatefulWidget {
required this.initialPinLength, required this.initialPinLength,
required this.onChangedPin, required this.onChangedPin,
required this.hasLengthSwitcher, required this.hasLengthSwitcher,
required this.setPinRandomized,
required this.initialPinRandomized,
this.onChangedPinLength, this.onChangedPinLength,
}) : super(key: key); }) : super(key: key);
final void Function(String pin, PinCodeState state) onFullPin; final void Function(String pin, PinCodeState state) onFullPin;
final void Function(String pin) onChangedPin; final void Function(String pin) onChangedPin;
final void Function(int length)? onChangedPinLength; final void Function(int length)? onChangedPinLength;
final void Function(bool) setPinRandomized;
final bool hasLengthSwitcher; final bool hasLengthSwitcher;
final int initialPinLength; final int initialPinLength;
final bool initialPinRandomized;
@override @override
State<StatefulWidget> createState() => PinCodeState(); State<StatefulWidget> createState() => PinCodeState();
@ -44,6 +48,8 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
String title; String title;
double _aspectRatio; double _aspectRatio;
Flushbar<void>? _progressBar; Flushbar<void>? _progressBar;
late List<int> numbers = [];
bool randomizePin = false;
int currentPinLength() => pin.length; int currentPinLength() => pin.length;
@ -54,6 +60,12 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
pin = ''; pin = '';
title = S.current.enter_your_pin; title = S.current.enter_your_pin;
_aspectRatio = 0; _aspectRatio = 0;
randomizePin = widget.initialPinRandomized;
numbers = List.generate(10, (index) => index);
if (randomizePin) {
numbers.shuffle();
}
WidgetsBinding.instance.addPostFrameCallback(_afterLayout); WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
} }
@ -118,6 +130,10 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
'assets/images/face.png', 'assets/images/face.png',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
); );
final shuffleImage = Image.asset(
'assets/images/shuffle.png',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
);
return RawKeyboardListener( return RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
@ -144,8 +160,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
Spacer(flex: 3), Spacer(flex: 3),
Container( Container(
width: 180, width: 180,
@ -162,7 +177,9 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
shape: BoxShape.circle, shape: BoxShape.circle,
color: isFilled color: isFilled
? Theme.of(context).extension<CakeTextTheme>()!.titleColor ? Theme.of(context).extension<CakeTextTheme>()!.titleColor
: Theme.of(context).extension<PinCodeTheme>()!.indicatorsColor : Theme.of(context)
.extension<PinCodeTheme>()!
.indicatorsColor
.withOpacity(0.25), .withOpacity(0.25),
)); ));
}), }),
@ -208,9 +225,26 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
const double marginLeft = 15; const double marginLeft = 15;
if (index == 9) { if (index == 9) {
// Empty container // randomize button
return Container( return MergeSemantics(
margin: EdgeInsets.only(left: marginLeft, right: marginRight), 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) { } else if (index == 10) {
index = 0; index = 0;
@ -225,7 +259,8 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
child: TextButton( child: TextButton(
onPressed: () => _pop(), onPressed: () => _pop(),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor:
Theme.of(context).colorScheme.background,
shape: CircleBorder(), shape: CircleBorder(),
), ),
child: deleteIconImage, child: deleteIconImage,
@ -240,16 +275,18 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
return Container( return Container(
margin: EdgeInsets.only(left: marginLeft, right: marginRight), margin: EdgeInsets.only(left: marginLeft, right: marginRight),
child: TextButton( child: TextButton(
onPressed: () => _push(index), onPressed: () => _push(numbers[index]),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
shape: CircleBorder(), shape: CircleBorder(),
), ),
child: Text('$index', child: Text('${numbers[index]}',
style: TextStyle( style: TextStyle(
fontSize: 30.0, fontSize: 30.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)), color: Theme.of(context)
.extension<CakeTextTheme>()!
.titleColor)),
), ),
); );
}), }),
@ -290,6 +327,18 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
setState(() => pin = pin.substring(0, pin.length - 1)); 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() { String _changePinLengthText() {
return S.current.use + return S.current.use +
(pinLength == PinCodeState.fourPinLength (pinLength == PinCodeState.fourPinLength

View file

@ -22,6 +22,8 @@ class SetupPinCodePage extends BasePage {
Widget body(BuildContext context) => PinCodeWidget( Widget body(BuildContext context) => PinCodeWidget(
key: pinCodeStateKey, key: pinCodeStateKey,
hasLengthSwitcher: true, hasLengthSwitcher: true,
setPinRandomized: pinCodeViewModel.setPinRandomized,
initialPinRandomized: pinCodeViewModel.pinRandomized,
onFullPin: (String pin, PinCodeState<PinCodeWidget> state) async { onFullPin: (String pin, PinCodeState<PinCodeWidget> state) async {
if (pinCodeViewModel.isOriginalPinCodeFull && if (pinCodeViewModel.isOriginalPinCodeFull &&
!pinCodeViewModel.isRepeatedPinCodeFull) { !pinCodeViewModel.isRepeatedPinCodeFull) {

View file

@ -73,6 +73,7 @@ abstract class SettingsStoreBase with Store {
required ExchangeApiMode initialExchangeStatus, required ExchangeApiMode initialExchangeStatus,
required ThemeBase initialTheme, required ThemeBase initialTheme,
required int initialPinLength, required int initialPinLength,
required bool initialRandomizePinCode,
required String initialLanguageCode, required String initialLanguageCode,
required SyncMode initialSyncMode, required SyncMode initialSyncMode,
required bool initialSyncAll, required bool initialSyncAll,
@ -150,6 +151,7 @@ abstract class SettingsStoreBase with Store {
exchangeStatus = initialExchangeStatus, exchangeStatus = initialExchangeStatus,
currentTheme = initialTheme, currentTheme = initialTheme,
pinCodeLength = initialPinLength, pinCodeLength = initialPinLength,
randomizePinCode = initialRandomizePinCode,
languageCode = initialLanguageCode, languageCode = initialLanguageCode,
shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet, shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet,
shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact, shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact,
@ -369,6 +371,9 @@ abstract class SettingsStoreBase with Store {
reaction((_) => pinCodeLength, reaction((_) => pinCodeLength,
(int pinLength) => sharedPreferences.setInt(PreferencesKey.currentPinLength, pinLength)); (int pinLength) => sharedPreferences.setInt(PreferencesKey.currentPinLength, pinLength));
reaction((_) => randomizePinCode,
(bool randomizePinCode) => sharedPreferences.setBool(PreferencesKey.randomizePinCode, randomizePinCode));
reaction( reaction(
(_) => languageCode, (_) => languageCode,
(String languageCode) => (String languageCode) =>
@ -683,6 +688,9 @@ abstract class SettingsStoreBase with Store {
@observable @observable
int pinCodeLength; int pinCodeLength;
@observable
bool randomizePinCode;
@observable @observable
PinCodeRequiredDuration pinTimeOutDuration; PinCodeRequiredDuration pinTimeOutDuration;
@ -931,6 +939,8 @@ abstract class SettingsStoreBase with Store {
pinLength = defaultPinLength; pinLength = defaultPinLength;
} }
final randomizePinCode = sharedPreferences.getBool(PreferencesKey.randomizePinCode) ?? false;
final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ??
await LanguageService.localeDetection(); await LanguageService.localeDetection();
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
@ -1171,6 +1181,7 @@ abstract class SettingsStoreBase with Store {
initialTheme: savedTheme, initialTheme: savedTheme,
actionlistDisplayMode: actionListDisplayMode, actionlistDisplayMode: actionListDisplayMode,
initialPinLength: pinLength, initialPinLength: pinLength,
initialRandomizePinCode: randomizePinCode,
pinTimeOutDuration: pinCodeTimeOutDuration, pinTimeOutDuration: pinCodeTimeOutDuration,
seedPhraseLength: seedPhraseWordCount, seedPhraseLength: seedPhraseWordCount,
initialLanguageCode: savedLanguageCode, initialLanguageCode: savedLanguageCode,
@ -1327,6 +1338,8 @@ abstract class SettingsStoreBase with Store {
} }
pinCodeLength = pinLength; pinCodeLength = pinLength;
randomizePinCode = sharedPreferences.getBool(PreferencesKey.randomizePinCode) ?? false;
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode; languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
shouldShowYatPopup = shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup; sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;

View file

@ -30,8 +30,15 @@ abstract class AuthViewModelBase with Store {
int get pinLength => _settingsStore.pinCodeLength; int get pinLength => _settingsStore.pinCodeLength;
bool get pinRandomized => _settingsStore.randomizePinCode;
bool get isBiometricalAuthenticationAllowed => _settingsStore.allowBiometricalAuthentication; bool get isBiometricalAuthenticationAllowed => _settingsStore.allowBiometricalAuthentication;
@action
void setPinRandomized(bool randomized) {
_settingsStore.randomizePinCode = randomized;
}
@observable @observable
int _failureCounter; int _failureCounter;
@ -121,4 +128,4 @@ abstract class AuthViewModelBase with Store {
_authService.saveLastAuthTime(); _authService.saveLastAuthTime();
} }
} }
} }

View file

@ -67,4 +67,10 @@ class SetupPinCodeViewModel {
await _authService.setPassword(repeatedPinCode); await _authService.setPassword(repeatedPinCode);
_settingsStore.pinCodeLength = pinCodeLength; _settingsStore.pinCodeLength = pinCodeLength;
} }
bool get pinRandomized => _settingsStore.randomizePinCode;
void setPinRandomized(bool randomized) {
_settingsStore.randomizePinCode = randomized;
}
} }