This commit is contained in:
Dain Nilsson 2024-03-11 16:38:11 +01:00
commit 2d43e526b7
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
32 changed files with 374 additions and 148 deletions

View File

@ -32,14 +32,18 @@ import '../state.dart';
final _log = Logger('desktop.fido.state');
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
(ref, _) => null,
final _pinProvider = StateProvider.family<String?, DevicePath>(
(ref, _) {
// Clear PIN if current device is changed
ref.watch(currentDeviceProvider);
return null;
},
);
final _sessionProvider =
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
(ref, devicePath) {
// Make sure the pinProvider is held for the duration of the session.
// Refresh state when PIN is changed
ref.watch(_pinProvider(devicePath));
return RpcNodeSession(
ref.watch(rpcProvider).requireValue, devicePath, ['fido', 'ctap2']);

View File

@ -256,7 +256,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
onFieldSubmitted: (_) {
_submit();
},
),
).init(),
)
]
],

View File

@ -44,7 +44,8 @@ class FidoPinDialog extends ConsumerStatefulWidget {
}
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
String _currentPin = '';
final _currentPinController = TextEditingController();
final _currentPinFocus = FocusNode();
String _newPin = '';
String _confirmPin = '';
String? _currentPinError;
@ -54,15 +55,28 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
bool _isObscureCurrent = true;
bool _isObscureNew = true;
bool _isObscureConfirm = true;
bool _isBlocked = false;
@override
void dispose() {
_currentPinController.dispose();
_currentPinFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final hasPin = widget.state.hasPin;
final isValid = _newPin.isNotEmpty &&
_newPin == _confirmPin &&
(!hasPin || _currentPin.isNotEmpty);
final minPinLength = widget.state.minPinLength;
final currentMinPinLen = !hasPin
? 0
// N.B. current PIN may be shorter than minimum if set before the minimum was increased
: (widget.state.forcePinChange ? 4 : widget.state.minPinLength);
final currentPinLenOk =
_currentPinController.text.length >= currentMinPinLen;
final newPinLenOk = _newPin.length >= minPinLength;
final isValid = currentPinLenOk && newPinLenOk && _newPin == _confirmPin;
return ResponsiveDialog(
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
@ -82,11 +96,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
Text(l10n.p_enter_current_pin_or_reset_no_puk),
AppTextFormField(
key: currentPin,
initialValue: _currentPin,
controller: _currentPinController,
focusNode: _currentPinFocus,
autofocus: true,
obscureText: _isObscureCurrent,
autofillHints: const [AutofillHints.password],
decoration: AppInputDecoration(
enabled: !_isBlocked,
border: const OutlineInputBorder(),
labelText: l10n.s_current_pin,
errorText: _currentIsWrong ? _currentPinError : null,
@ -108,10 +124,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentPin = value;
});
},
),
).init(),
],
Text(l10n.p_enter_new_fido2_pin(minPinLength)),
// TODO: Set max characters based on UTF-8 bytes
@ -124,7 +139,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_new_pin,
enabled: !hasPin || _currentPin.isNotEmpty,
enabled: !_isBlocked && currentPinLenOk,
errorText: _newIsWrong ? _newPinError : null,
errorMaxLines: 3,
prefixIcon: const Icon(Symbols.pin),
@ -146,7 +161,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_newPin = value;
});
},
),
).init(),
AppTextFormField(
key: confirmPin,
initialValue: _confirmPin,
@ -168,8 +183,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
tooltip:
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
),
enabled:
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
errorText: _newPin.length == _confirmPin.length &&
_newPin != _confirmPin
? l10n.l_pin_mismatch
: null,
helperText: '', // Prevents resizing when errorText shown
),
onChanged: (value) {
setState(() {
@ -181,7 +200,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
_submit();
}
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
@ -195,15 +214,9 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
void _submit() async {
final l10n = AppLocalizations.of(context)!;
final minPinLength = widget.state.minPinLength;
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
if (_newPin.length < minPinLength) {
setState(() {
_newPinError = l10n.l_new_pin_len(minPinLength);
_newIsWrong = true;
});
return;
}
final oldPin = _currentPinController.text.isNotEmpty
? _currentPinController.text
: null;
try {
final result = await ref
.read(fidoStateProvider(widget.devicePath).notifier)
@ -213,9 +226,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
showMessage(context, l10n.s_pin_set);
}, failed: (retries, authBlocked) {
setState(() {
_currentPinController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentPinController.text.length);
_currentPinFocus.requestFocus();
if (authBlocked) {
_currentPinError = l10n.l_pin_soft_locked;
_currentIsWrong = true;
_isBlocked = true;
} else {
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
_currentIsWrong = true;

View File

@ -37,11 +37,19 @@ class PinEntryForm extends ConsumerStatefulWidget {
class _PinEntryFormState extends ConsumerState<PinEntryForm> {
final _pinController = TextEditingController();
final _pinFocus = FocusNode();
bool _blocked = false;
int? _retries;
bool _pinIsWrong = false;
bool _isObscure = true;
@override
void dispose() {
_pinController.dispose();
_pinFocus.dispose();
super.dispose();
}
void _submit() async {
setState(() {
_pinIsWrong = false;
@ -51,8 +59,10 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
.read(fidoStateProvider(widget._deviceNode.path).notifier)
.unlock(_pinController.text);
result.whenOrNull(failed: (retries, authBlocked) {
_pinController.selection = TextSelection(
baseOffset: 0, extentOffset: _pinController.text.length);
_pinFocus.requestFocus();
setState(() {
_pinController.clear();
_pinIsWrong = true;
_retries = retries;
_blocked = authBlocked;
@ -92,6 +102,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
obscureText: _isObscure,
autofillHints: const [AutofillHints.password],
controller: _pinController,
focusNode: _pinFocus,
enabled: !_blocked && (_retries ?? 1) > 0,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
@ -116,7 +128,7 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
});
}, // Update state on change
onSubmitted: (_) => _submit(),
),
).init(),
),
ListTile(
leading: noFingerprints
@ -136,8 +148,12 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
key: unlockFido2WithPin,
icon: const Icon(Symbols.lock_open),
label: Text(l10n.s_unlock),
onPressed:
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
onPressed: !_pinIsWrong &&
_pinController.text.length >=
widget._state.minPinLength &&
!_blocked
? _submit
: null,
),
),
],

View File

@ -112,7 +112,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
_submit();
}
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -87,7 +87,7 @@ class _ManageLabelDialogState extends ConsumerState<ManageLabelDialog> {
onFieldSubmitted: (_) {
_submit();
},
)
).init()
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -233,6 +233,8 @@
"s_confirm_pin": "PIN bestätigen",
"s_confirm_puk": null,
"s_unblock_pin": null,
"l_pin_mismatch": null,
"l_puk_mismatch": null,
"l_new_pin_len": "Neue PIN muss mindestens {length} Zeichen lang sein",
"@l_new_pin_len": {
"placeholders": {
@ -309,6 +311,7 @@
"s_new_password": "Neues Passwort",
"s_current_password": "Aktuelles Passwort",
"s_confirm_password": "Passwort bestätigen",
"l_password_mismatch": null,
"s_wrong_password": "Falsches Passwort",
"s_remove_password": "Passwort entfernen",
"s_password_removed": "Passwort entfernt",

View File

@ -233,6 +233,8 @@
"s_confirm_pin": "Confirm PIN",
"s_confirm_puk": "Confirm PUK",
"s_unblock_pin": "Unblock PIN",
"l_pin_mismatch": "PINs do not match",
"l_puk_mismatch": "PUKs do not match",
"l_new_pin_len": "New PIN must be at least {length} characters",
"@l_new_pin_len": {
"placeholders": {
@ -309,6 +311,7 @@
"s_new_password": "New password",
"s_current_password": "Current password",
"s_confirm_password": "Confirm password",
"l_password_mismatch": "Passwords do not match",
"s_wrong_password": "Wrong password",
"s_remove_password": "Remove password",
"s_password_removed": "Password removed",

View File

@ -233,6 +233,8 @@
"s_confirm_pin": "Confirmez le PIN",
"s_confirm_puk": "Confirmez le PUK",
"s_unblock_pin": "Débloquer le PIN",
"l_pin_mismatch": null,
"l_puk_mismatch": null,
"l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères",
"@l_new_pin_len": {
"placeholders": {
@ -309,6 +311,7 @@
"s_new_password": "Nouveau mot de passe",
"s_current_password": "Mot de passe actuel",
"s_confirm_password": "Confirmez le mot de passe",
"l_password_mismatch": null,
"s_wrong_password": "Mauvais mot de passe",
"s_remove_password": "Supprimer le mot de passe",
"s_password_removed": "Mot de passe supprimé",

View File

@ -233,6 +233,8 @@
"s_confirm_pin": "PINの確認",
"s_confirm_puk": "PUKの確認",
"s_unblock_pin": "ブロックを解除",
"l_pin_mismatch": null,
"l_puk_mismatch": null,
"l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります",
"@l_new_pin_len": {
"placeholders": {
@ -309,6 +311,7 @@
"s_new_password": "新しいパスワード",
"s_current_password": "現在のパスワード",
"s_confirm_password": "パスワードを確認",
"l_password_mismatch": null,
"s_wrong_password": "間違ったパスワード",
"s_remove_password": "パスワードの削除",
"s_password_removed": "パスワードが削除されました",

View File

@ -233,6 +233,8 @@
"s_confirm_pin": "Potwierdź PIN",
"s_confirm_puk": "Potwierdź PUK",
"s_unblock_pin": "Odblokuj PIN",
"l_pin_mismatch": null,
"l_puk_mismatch": null,
"l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków",
"@l_new_pin_len": {
"placeholders": {
@ -309,6 +311,7 @@
"s_new_password": "Nowe hasło",
"s_current_password": "Aktualne hasło",
"s_confirm_password": "Potwierdź hasło",
"l_password_mismatch": null,
"s_wrong_password": "Błędne hasło",
"s_remove_password": "Usuń hasło",
"s_password_removed": "Hasło zostało usunięte",

View File

@ -390,7 +390,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
onSubmitted: (_) {
if (isValid) submit();
},
),
).init(),
AppTextField(
key: keys.nameField,
controller: _accountController,
@ -418,7 +418,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
onSubmitted: (_) {
if (isValid) submit();
},
),
).init(),
AppTextField(
key: keys.secretField,
controller: _secretController,
@ -460,7 +460,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
onSubmitted: (_) {
if (isValid) submit();
},
),
).init(),
const SizedBox(height: 8),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,

View File

@ -41,7 +41,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
}
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
String _currentPassword = '';
final _currentPasswordController = TextEditingController();
final _currentPasswordFocus = FocusNode();
String _newPassword = '';
String _confirmPassword = '';
bool _currentIsWrong = false;
@ -49,12 +50,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
bool _isObscureNew = true;
bool _isObscureConfirm = true;
@override
void dispose() {
_currentPasswordController.dispose();
_currentPasswordFocus.dispose();
super.dispose();
}
_submit() async {
FocusUtils.unfocus(context);
final result = await ref
.read(oathStateProvider(widget.path).notifier)
.setPassword(_currentPassword, _newPassword);
.setPassword(_currentPasswordController.text, _newPassword);
if (result) {
if (mounted) {
await ref.read(withContextProvider)((context) async {
@ -63,6 +71,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
});
}
} else {
_currentPasswordController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
_currentPasswordFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
@ -72,9 +83,10 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final isValid = _newPassword.isNotEmpty &&
final isValid = !_currentIsWrong &&
_newPassword.isNotEmpty &&
_newPassword == _confirmPassword &&
(!widget.state.hasKey || _currentPassword.isNotEmpty);
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
return ResponsiveDialog(
title: Text(
@ -98,6 +110,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
obscureText: _isObscureCurrent,
autofillHints: const [AutofillHints.password],
key: keys.currentPasswordField,
controller: _currentPasswordController,
focusNode: _currentPasswordFocus,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_current_password,
@ -121,21 +135,21 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentPassword = value;
});
},
),
).init(),
Wrap(
spacing: 4.0,
runSpacing: 8.0,
children: [
OutlinedButton(
key: keys.removePasswordButton,
onPressed: _currentPassword.isNotEmpty
onPressed: _currentPasswordController.text.isNotEmpty &&
!_currentIsWrong
? () async {
final result = await ref
.read(oathStateProvider(widget.path).notifier)
.unsetPassword(_currentPassword);
.unsetPassword(_currentPasswordController.text);
if (result) {
if (mounted) {
await ref.read(withContextProvider)(
@ -145,6 +159,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
});
}
} else {
_currentPasswordController.selection =
TextSelection(
baseOffset: 0,
extentOffset: _currentPasswordController
.text.length);
_currentPasswordFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
@ -193,7 +213,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
tooltip: _isObscureNew
? l10n.s_show_password
: l10n.s_hide_password),
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
enabled: !widget.state.hasKey ||
_currentPasswordController.text.isNotEmpty,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -206,7 +227,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
_submit();
}
},
),
).init(),
AppTextField(
key: keys.confirmPasswordField,
obscureText: _isObscureConfirm,
@ -227,9 +248,14 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
tooltip: _isObscureConfirm
? l10n.s_show_password
: l10n.s_hide_password),
enabled:
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
_newPassword.isNotEmpty,
enabled: (!widget.state.hasKey ||
_currentPasswordController.text.isNotEmpty) &&
_newPassword.isNotEmpty,
errorText: _newPassword.length == _confirmPassword.length &&
_newPassword != _confirmPassword
? l10n.l_password_mismatch
: null,
helperText: '', // Prevents resizing when errorText shown
),
textInputAction: TextInputAction.done,
onChanged: (value) {
@ -242,7 +268,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
_submit();
}
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -439,7 +439,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
Focus.of(context)
.focusInDirection(TraversalDirection.down);
},
),
).init(),
);
}),
),

View File

@ -193,7 +193,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
_issuer = value.trim();
});
},
),
).init(),
AppTextFormField(
initialValue: _name,
maxLength: nameRemaining,
@ -222,7 +222,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
_submit();
}
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -38,6 +38,7 @@ class UnlockForm extends ConsumerStatefulWidget {
class _UnlockFormState extends ConsumerState<UnlockForm> {
final _passwordController = TextEditingController();
final _passwordFocus = FocusNode();
bool _remember = false;
bool _passwordIsWrong = false;
bool _isObscure = true;
@ -51,9 +52,11 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
.unlock(_passwordController.text, remember: _remember);
if (!mounted) return;
if (!success) {
_passwordController.selection = TextSelection(
baseOffset: 0, extentOffset: _passwordController.text.length);
_passwordFocus.requestFocus();
setState(() {
_passwordIsWrong = true;
_passwordController.clear();
});
} else if (_remember && !remembered) {
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
@ -79,6 +82,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
child: AppTextField(
key: keys.passwordField,
controller: _passwordController,
focusNode: _passwordFocus,
autofocus: true,
obscureText: _isObscure,
autofillHints: const [AutofillHints.password],
@ -106,7 +110,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
_passwordIsWrong = false;
}), // Update state on change
onSubmitted: (_) => _submit(),
),
).init(),
),
const SizedBox(height: 3.0),
Column(
@ -143,7 +147,8 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
key: keys.unlockButton,
label: Text(l10n.s_unlock),
icon: const Icon(Symbols.lock_open),
onPressed: _passwordController.text.isNotEmpty
onPressed: _passwordController.text.isNotEmpty &&
!_passwordIsWrong
? _submit
: null,
),

View File

@ -166,7 +166,7 @@ class _ConfigureChalrespDialogState
_validateSecret = false;
});
},
),
).init(),
FilterChip(
label: Text(l10n.s_require_touch),
selected: _requireTouch,

View File

@ -127,6 +127,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
key: keys.secretField,
controller: _secretController,
obscureText: _isObscure,
autofocus: true,
autofillHints: isAndroid ? [] : const [AutofillHints.password],
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
@ -158,7 +159,7 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
_validateSecret = false;
});
},
),
).init(),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.0,

View File

@ -181,7 +181,7 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
_validatePassword = false;
});
},
),
).init(),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.0,

View File

@ -237,7 +237,7 @@ class _ConfigureYubiOtpDialogState
_validatePublicIdFormat = false;
});
},
),
).init(),
AppTextField(
key: keys.privateIdField,
controller: _privateIdController,
@ -274,7 +274,7 @@ class _ConfigureYubiOtpDialogState
_validatePrivateIdFormat = false;
});
},
),
).init(),
AppTextField(
key: keys.secretField,
controller: _secretController,
@ -311,7 +311,7 @@ class _ConfigureYubiOtpDialogState
_validateSecretFormat = false;
});
},
),
).init(),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.0,

View File

@ -35,8 +35,7 @@ abstract class PivStateNotifier extends ApplicationStateNotifier<PivState> {
bool storeKey = false,
});
Future<PinVerificationStatus> verifyPin(
String pin); //TODO: Maybe return authenticated?
Future<PinVerificationStatus> verifyPin(String pin);
Future<PinVerificationStatus> changePin(String pin, String newPin);
Future<PinVerificationStatus> changePuk(String puk, String newPuk);
Future<PinVerificationStatus> unblockPin(String puk, String newPin);

View File

@ -52,24 +52,26 @@ class ExportIntent extends Intent {
const ExportIntent(this.slot);
}
Future<bool> _authenticate(
BuildContext context, DevicePath devicePath, PivState pivState) async {
return await showBlurDialog(
context: context,
builder: (context) => pivState.protectedKey
? PinDialog(devicePath)
: AuthenticationDialog(
devicePath,
pivState,
),
) ??
false;
}
Future<bool> _authIfNeeded(
BuildContext context, DevicePath devicePath, PivState pivState) async {
Future<bool> _authIfNeeded(BuildContext context, WidgetRef ref,
DevicePath devicePath, PivState pivState) async {
if (pivState.needsAuth) {
return await _authenticate(context, devicePath, pivState);
if (pivState.protectedKey &&
pivState.metadata?.pinMetadata.defaultValue == true) {
final status = await ref
.read(pivStateProvider(devicePath).notifier)
.verifyPin(defaultPin);
return status.when(success: () => true, failure: (_) => false);
}
return await showBlurDialog(
context: context,
builder: (context) => pivState.protectedKey
? PinDialog(devicePath)
: AuthenticationDialog(
devicePath,
pivState,
),
) ??
false;
}
return true;
}
@ -96,21 +98,32 @@ class PivActions extends ConsumerWidget {
if (hasFeature(features.slotsGenerate))
GenerateIntent:
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
if (!pivState.protectedKey &&
!await withContext((context) =>
_authIfNeeded(context, devicePath, pivState))) {
//Verify management key and maybe PIN
if (!await withContext((context) =>
_authIfNeeded(context, ref, devicePath, pivState))) {
return false;
}
// Verify PIN, unless already done above
// TODO: Avoid asking for PIN if not needed?
final verified = await withContext((context) async =>
await showBlurDialog(
context: context,
builder: (context) => PinDialog(devicePath))) ??
false;
if (!pivState.protectedKey) {
bool verified;
if (pivState.metadata?.pinMetadata.defaultValue == true) {
final status = await ref
.read(pivStateProvider(devicePath).notifier)
.verifyPin(defaultPin);
verified =
status.when(success: () => true, failure: (_) => false);
} else {
verified = await withContext((context) async =>
await showBlurDialog(
context: context,
builder: (context) => PinDialog(devicePath))) ??
false;
}
if (!verified) {
return false;
if (!verified) {
return false;
}
}
return await withContext((context) async {
@ -158,8 +171,8 @@ class PivActions extends ConsumerWidget {
}),
if (hasFeature(features.slotsImport))
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
if (!await withContext(
(context) => _authIfNeeded(context, devicePath, pivState))) {
if (!await withContext((context) =>
_authIfNeeded(context, ref, devicePath, pivState))) {
return false;
}
@ -243,8 +256,8 @@ class PivActions extends ConsumerWidget {
if (hasFeature(features.slotsDelete))
DeleteIntent<PivSlot>:
CallbackAction<DeleteIntent<PivSlot>>(onInvoke: (intent) async {
if (!await withContext(
(context) => _authIfNeeded(context, devicePath, pivState))) {
if (!await withContext((context) =>
_authIfNeeded(context, ref, devicePath, pivState))) {
return false;
}

View File

@ -44,10 +44,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
bool _keyIsWrong = false;
bool _keyFormatInvalid = false;
final _keyController = TextEditingController();
final _keyFocus = FocusNode();
@override
void dispose() {
_keyController.dispose();
_keyFocus.dispose();
super.dispose();
}
@ -65,7 +67,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
actions: [
TextButton(
key: keys.unlockButton,
onPressed: _keyController.text.length == keyLen
onPressed: !_keyIsWrong && _keyController.text.length == keyLen
? () async {
if (keyFormatInvalid) {
setState(() {
@ -81,6 +83,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
if (status) {
navigator.pop(true);
} else {
_keyController.selection = TextSelection(
baseOffset: 0,
extentOffset: _keyController.text.length);
_keyFocus.requestFocus();
setState(() {
_keyIsWrong = true;
});
@ -88,6 +94,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
} on CancellationException catch (_) {
navigator.pop(false);
} catch (_) {
_keyController.selection = TextSelection(
baseOffset: 0,
extentOffset: _keyController.text.length);
_keyFocus.requestFocus();
// TODO: More error cases
setState(() {
_keyIsWrong = true;
@ -109,6 +119,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
autofocus: true,
autofillHints: const [AutofillHints.password],
controller: _keyController,
focusNode: _keyFocus,
readOnly: _defaultKeyUsed,
maxLength: !_defaultKeyUsed ? keyLen : null,
decoration: AppInputDecoration(
@ -149,7 +160,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
_keyFormatInvalid = false;
});
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -174,7 +174,7 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
_subject = value;
});
},
),
).init(),
Text(
l10n.rfc4514_examples,
style: subtitleStyle,

View File

@ -162,7 +162,7 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
});
},
onSubmitted: (_) => _examine(),
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -60,7 +60,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
ActionListItem(
key: keys.managePinAction,
feature: features.actionsPin,
title: l10n.s_pin,
title: l10n.s_change_pin,
subtitle: pinBlocked
? (pukAttempts != 0
? l10n.l_piv_pin_blocked
@ -88,7 +88,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
ActionListItem(
key: keys.managePukAction,
feature: features.actionsPuk,
title: l10n.s_puk,
title: l10n.s_change_puk,
subtitle: pukAttempts != null
? (pukAttempts == 0
? l10n.l_piv_pin_puk_blocked

View File

@ -30,6 +30,7 @@ import '../../widgets/app_text_field.dart';
import '../../widgets/app_text_form_field.dart';
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../keys.dart' as keys;
import '../models.dart';
import '../state.dart';
@ -48,6 +49,7 @@ class ManageKeyDialog extends ConsumerStatefulWidget {
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
late bool _hasMetadata;
late bool _defaultKeyUsed;
late bool _defaultPinUsed;
late bool _usesStoredKey;
late bool _storeKey;
bool _currentIsWrong = false;
@ -56,6 +58,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
int _attemptsRemaining = -1;
late ManagementKeyType _keyType;
final _currentController = TextEditingController();
final _currentFocus = FocusNode();
final _keyController = TextEditingController();
bool _isObscure = true;
@ -68,9 +71,13 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
defaultManagementKeyType;
_defaultKeyUsed =
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
_defaultPinUsed =
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
_usesStoredKey = widget.pivState.protectedKey;
if (!_usesStoredKey && _defaultKeyUsed) {
_currentController.text = defaultManagementKey;
} else if (_usesStoredKey && _defaultPinUsed) {
_currentController.text = defaultPin;
}
_storeKey = _usesStoredKey;
}
@ -79,16 +86,18 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
void dispose() {
_keyController.dispose();
_currentController.dispose();
_currentFocus.dispose();
super.dispose();
}
_submit() async {
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
final newInvalidFormat = Format.hex.isValid(_keyController.text);
if (!currentInvalidFormat || !newInvalidFormat) {
final currentValidFormat =
_usesStoredKey || Format.hex.isValid(_currentController.text);
final newValidFormat = Format.hex.isValid(_keyController.text);
if (!currentValidFormat || !newValidFormat) {
setState(() {
_currentInvalidFormat = !currentInvalidFormat;
_newInvalidFormat = !newInvalidFormat;
_currentInvalidFormat = !currentValidFormat;
_newInvalidFormat = !newValidFormat;
});
return;
}
@ -98,6 +107,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
final status = (await notifier.verifyPin(_currentController.text)).when(
success: () => true,
failure: (attemptsRemaining) {
_currentController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentController.text.length);
_currentFocus.requestFocus();
setState(() {
_attemptsRemaining = attemptsRemaining;
_currentIsWrong = true;
@ -110,6 +122,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
}
} else {
if (!await notifier.authenticate(_currentController.text)) {
_currentController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentController.text.length);
_currentFocus.requestFocus();
setState(() {
_currentIsWrong = true;
});
@ -118,15 +133,19 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
}
if (_storeKey && !_usesStoredKey) {
final withContext = ref.read(withContextProvider);
final verified = await withContext((context) async =>
await showBlurDialog(
context: context,
builder: (context) => PinDialog(widget.path))) ??
false;
if (_defaultPinUsed) {
await notifier.verifyPin(defaultPin);
} else {
final withContext = ref.read(withContextProvider);
final verified = await withContext((context) async =>
await showBlurDialog(
context: context,
builder: (context) => PinDialog(widget.path))) ??
false;
if (!verified) {
return;
if (!verified) {
return;
}
}
}
@ -147,9 +166,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
widget.pivState.metadata?.managementKeyMetadata.keyType ??
defaultManagementKeyType;
final hexLength = _keyType.keyLength * 2;
final protected = widget.pivState.protectedKey;
final currentKeyOrPin = _currentController.text;
final currentLenOk = protected
final currentLenOk = _usesStoredKey
? currentKeyOrPin.length >= 4
: currentKeyOrPin.length == currentType.keyLength * 2;
final newLenOk = _keyController.text.length == hexLength;
@ -158,7 +176,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
title: Text(l10n.l_change_management_key),
actions: [
TextButton(
onPressed: currentLenOk && newLenOk ? _submit : null,
onPressed:
!_currentIsWrong && currentLenOk && newLenOk ? _submit : null,
key: keys.saveButton,
child: Text(l10n.s_save),
)
@ -169,23 +188,25 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_change_management_key_desc),
if (protected)
if (_usesStoredKey)
AppTextField(
autofocus: true,
obscureText: _isObscure,
autofillHints: const [AutofillHints.password],
key: keys.pinPukField,
maxLength: 8,
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(_currentController.text),
controller: _currentController,
focusNode: _currentFocus,
readOnly: _defaultPinUsed,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
helperText: _defaultPinUsed ? l10n.l_default_pin_used : null,
errorText: _currentIsWrong
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: _currentInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Symbols.pin),
suffixIcon: IconButton(
@ -206,13 +227,14 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
_currentInvalidFormat = false;
});
},
),
if (!protected)
).init(),
if (!_usesStoredKey)
AppTextFormField(
key: keys.managementKeyField,
autofocus: !_defaultKeyUsed,
autofillHints: const [AutofillHints.password],
controller: _currentController,
focusNode: _currentFocus,
readOnly: _defaultKeyUsed,
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
decoration: AppInputDecoration(
@ -251,7 +273,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
_currentIsWrong = false;
});
},
),
).init(),
AppTextField(
key: keys.newPinPukField,
autofocus: _defaultKeyUsed,
@ -299,7 +321,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
_submit();
}
},
),
).init(),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.0,

View File

@ -24,6 +24,7 @@ import '../../app/models.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../keys.dart' as keys;
import '../models.dart';
import '../state.dart';
@ -44,20 +45,25 @@ class ManagePinPukDialog extends ConsumerStatefulWidget {
class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
final _currentPinController = TextEditingController();
final _currentPinFocus = FocusNode();
String _newPin = '';
String _confirmPin = '';
bool _pinIsBlocked = false;
bool _currentIsWrong = false;
int _attemptsRemaining = -1;
bool _isObscureCurrent = true;
bool _isObscureNew = true;
bool _isObscureConfirm = true;
late bool _defaultPinUsed;
late bool _defaultPukUsed;
late final bool _defaultPinUsed;
late final bool _defaultPukUsed;
late final int _minPinLen;
@override
void initState() {
super.initState();
// Old YubiKeys allowed a 4 digit PIN
_minPinLen = widget.pivState.version.isAtLeast(4, 3, 1) ? 6 : 4;
_defaultPinUsed =
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
_defaultPukUsed =
@ -73,6 +79,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
@override
void dispose() {
_currentPinController.dispose();
_currentPinFocus.dispose();
super.dispose();
}
@ -98,11 +105,16 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
_ => l10n.s_pin_set,
});
}, failure: (attemptsRemaining) {
_currentPinController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentPinController.text.length);
_currentPinFocus.requestFocus();
setState(() {
_attemptsRemaining = attemptsRemaining;
_currentIsWrong = true;
if (_attemptsRemaining == 0) {
_pinIsBlocked = true;
}
});
_currentPinController.clear();
});
}
@ -110,8 +122,12 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final currentPin = _currentPinController.text;
final isValid =
_newPin.isNotEmpty && _newPin == _confirmPin && currentPin.isNotEmpty;
final currentPinLen = byteLength(currentPin);
final newPinLen = byteLength(_newPin);
final isValid = !_currentIsWrong &&
_newPin.isNotEmpty &&
_newPin == _confirmPin &&
currentPin.isNotEmpty;
final titleText = switch (widget.target) {
ManageTarget.pin => l10n.s_change_pin,
@ -138,7 +154,6 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//TODO fix string
Text(widget.target == ManageTarget.pin
? l10n.p_enter_current_pin_or_reset
: l10n.p_enter_current_puk_or_reset),
@ -146,10 +161,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
autofocus: !(showDefaultPinUsed || showDefaultPukUsed),
obscureText: _isObscureCurrent,
maxLength: 8,
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(currentPin),
autofillHints: const [AutofillHints.password],
key: keys.pinPukField,
readOnly: showDefaultPinUsed || showDefaultPukUsed,
controller: _currentPinController,
focusNode: _currentPinFocus,
enabled: !_pinIsBlocked,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
helperText: showDefaultPinUsed
@ -160,13 +179,17 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
labelText: widget.target == ManageTarget.pin
? l10n.s_current_pin
: l10n.s_current_puk,
errorText: _currentIsWrong
errorText: _pinIsBlocked
? (widget.target == ManageTarget.pin
? l10n
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: l10n
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
: null,
? l10n.l_piv_pin_blocked
: l10n.l_piv_pin_puk_blocked)
: (_currentIsWrong
? (widget.target == ManageTarget.pin
? l10n.l_wrong_pin_attempts_remaining(
_attemptsRemaining)
: l10n.l_wrong_puk_attempts_remaining(
_attemptsRemaining))
: null),
errorMaxLines: 3,
prefixIcon: const Icon(Symbols.password),
suffixIcon: IconButton(
@ -189,7 +212,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
_currentIsWrong = false;
});
},
),
).init(),
Text(l10n.p_enter_new_piv_pin_puk(
widget.target == ManageTarget.puk ? l10n.s_puk : l10n.s_pin)),
AppTextField(
@ -197,6 +220,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
autofocus: showDefaultPinUsed || showDefaultPukUsed,
obscureText: _isObscureNew,
maxLength: 8,
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(_newPin),
autofillHints: const [AutofillHints.newPassword],
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
@ -217,8 +242,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin)
: (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk),
),
// Old YubiKeys allowed a 4 digit PIN
enabled: currentPin.length >= 4,
enabled: currentPinLen >= _minPinLen,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -231,11 +255,13 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
_submit();
}
},
),
).init(),
AppTextField(
key: keys.confirmPinPukField,
obscureText: _isObscureConfirm,
maxLength: 8,
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(_confirmPin),
autofillHints: const [AutofillHints.newPassword],
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
@ -256,7 +282,14 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin)
: (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk),
),
enabled: currentPin.length >= 4 && _newPin.length >= 6,
enabled: currentPinLen >= _minPinLen && newPinLen >= 6,
errorText:
newPinLen == _confirmPin.length && _newPin != _confirmPin
? (widget.target == ManageTarget.pin
? l10n.l_pin_mismatch
: l10n.l_puk_mismatch)
: null,
helperText: '', // Prevents resizing when errorText shown
),
textInputAction: TextInputAction.done,
onChanged: (value) {
@ -269,7 +302,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
_submit();
}
},
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -24,6 +24,7 @@ import '../../exception/cancellation_exception.dart';
import '../../widgets/app_input_decoration.dart';
import '../../widgets/app_text_field.dart';
import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../keys.dart' as keys;
import '../state.dart';
@ -37,6 +38,7 @@ class PinDialog extends ConsumerStatefulWidget {
class _PinDialogState extends ConsumerState<PinDialog> {
final _pinController = TextEditingController();
final _pinFocus = FocusNode();
bool _pinIsWrong = false;
int _attemptsRemaining = -1;
bool _isObscure = true;
@ -44,6 +46,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
@override
void dispose() {
_pinController.dispose();
_pinFocus.dispose();
super.dispose();
}
@ -58,8 +61,10 @@ class _PinDialogState extends ConsumerState<PinDialog> {
navigator.pop(true);
},
failure: (attemptsRemaining) {
_pinController.selection = TextSelection(
baseOffset: 0, extentOffset: _pinController.text.length);
_pinFocus.requestFocus();
setState(() {
_pinController.clear();
_attemptsRemaining = attemptsRemaining;
_pinIsWrong = true;
});
@ -73,12 +78,15 @@ class _PinDialogState extends ConsumerState<PinDialog> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final version = ref.watch(pivStateProvider(widget.devicePath)).valueOrNull;
final minPinLen = version?.version.isAtLeast(4, 3, 1) == true ? 6 : 4;
final currentPinLen = byteLength(_pinController.text);
return ResponsiveDialog(
title: Text(l10n.s_pin_required),
actions: [
TextButton(
key: keys.unlockButton,
onPressed: _pinController.text.length >= 4 ? _submit : null,
onPressed: currentPinLen >= minPinLen ? _submit : null,
child: Text(l10n.s_unlock),
),
],
@ -92,9 +100,12 @@ class _PinDialogState extends ConsumerState<PinDialog> {
autofocus: true,
obscureText: _isObscure,
maxLength: 8,
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(_pinController.text),
autofillHints: const [AutofillHints.password],
key: keys.managementKeyField,
controller: _pinController,
focusNode: _pinFocus,
decoration: AppInputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
@ -121,7 +132,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
});
},
onSubmitted: (_) => _submit(),
),
).init(),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'app_input_decoration.dart';
@ -86,4 +85,13 @@ class AppTextField extends TextField {
super.spellCheckConfiguration,
super.magnifierConfiguration,
}) : super(decoration: decoration);
Widget init() => Builder(
builder: (context) => DefaultSelectionStyle(
selectionColor: decoration?.errorText != null
? Theme.of(context).colorScheme.error
: null,
child: this,
),
);
}

View File

@ -20,6 +20,7 @@ import 'app_input_decoration.dart';
/// TextFormField without autocorrect and suggestions
class AppTextFormField extends TextFormField {
final AppInputDecoration? decoration;
AppTextFormField({
// default settings to turn off autocorrect
super.autocorrect = false,
@ -30,7 +31,7 @@ class AppTextFormField extends TextFormField {
super.controller,
super.initialValue,
super.focusNode,
AppInputDecoration? decoration,
this.decoration,
super.textCapitalization,
super.textInputAction,
super.style,
@ -90,4 +91,13 @@ class AppTextFormField extends TextFormField {
super.scribbleEnabled,
super.canRequestFocus,
}) : super(decoration: decoration);
Widget init() => Builder(
builder: (context) => DefaultSelectionStyle(
selectionColor: decoration?.errorText != null
? Theme.of(context).colorScheme.error
: null,
child: this,
),
);
}

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
@ -30,6 +31,8 @@ class _AppLinter extends PluginBase {
discouraged: 'TextFormField',
recommended: 'AppTextFormField',
),
const CallInitAfterCreation(className: 'AppTextField'),
const CallInitAfterCreation(className: 'AppTextFormField'),
];
}
@ -59,3 +62,35 @@ class UseRecommendedWidget extends DartLintRule {
});
}
}
class CallInitAfterCreation extends DartLintRule {
final String className;
const CallInitAfterCreation({required this.className})
: super(
code: const LintCode(
name: 'call_init_after_creation',
problemMessage: 'Call init() after creation',
));
@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addInstanceCreationExpression((node) {
if (node.constructorName.toString() == className) {
final dot = node.endToken.next;
final next = dot?.next;
if (dot?.type == TokenType.PERIOD) {
if (next?.type == TokenType.IDENTIFIER &&
next?.toString() == 'init') {
return;
}
}
reporter.reportErrorForNode(code, node.constructorName);
}
});
}
}