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 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 =

View file

@ -19,9 +19,7 @@ abstract class AuthPageState<T extends StatefulWidget> extends State<T> {
}
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<AuthPage> {
final _key = GlobalKey<ScaffoldState>();
final _pinCodeKey = GlobalKey<PinCodeState>();
final _backArrowImageDarkTheme =
Image.asset('assets/images/close_button.png');
final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png');
ReactionDisposer? _reaction;
Flushbar<void>? _authBar;
Flushbar<void>? _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<AuthPage> {
if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// null duration to make it indefinite until its disposed
_authBar =
createBar<void>(S.of(context).authentication, duration: null)
..show(context);
_authBar = createBar<void>(S.of(context).authentication, duration: null)..show(context);
});
}
@ -64,8 +58,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
showBar<void>(context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
});
@ -75,8 +68,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
showBar<void>(context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
});
@ -102,8 +94,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
@override
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)
..show(_key.currentContext!);
_progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
}
@override
@ -134,25 +125,33 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
@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<dynamic>? bar) {

View file

@ -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();

View file

@ -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<StatefulWidget> createState() => PinCodeState();
@ -44,6 +48,8 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
String title;
double _aspectRatio;
Flushbar<void>? _progressBar;
late List<int> numbers = [];
bool randomizePin = false;
int currentPinLength() => pin.length;
@ -54,6 +60,12 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
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<T extends PinCodeWidget> extends State<T> {
'assets/images/face.png',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
);
final shuffleImage = Image.asset(
'assets/images/shuffle.png',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
);
return RawKeyboardListener(
focusNode: FocusNode(),
@ -144,8 +160,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
Spacer(flex: 3),
Container(
width: 180,
@ -162,7 +177,9 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
shape: BoxShape.circle,
color: isFilled
? Theme.of(context).extension<CakeTextTheme>()!.titleColor
: Theme.of(context).extension<PinCodeTheme>()!.indicatorsColor
: Theme.of(context)
.extension<PinCodeTheme>()!
.indicatorsColor
.withOpacity(0.25),
));
}),
@ -208,9 +225,26 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
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<T extends PinCodeWidget> extends State<T> {
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<T extends PinCodeWidget> extends State<T> {
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<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));
}
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

View file

@ -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<PinCodeWidget> state) async {
if (pinCodeViewModel.isOriginalPinCodeFull &&
!pinCodeViewModel.isRepeatedPinCodeFull) {

View file

@ -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;

View file

@ -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();
}
}
}
}

View file

@ -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;
}
}