mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
Refactor common code between add and rename accounts.
This commit is contained in:
parent
a5f73f7d94
commit
40335e66da
@ -18,3 +18,8 @@ class Version with _$Version {
|
|||||||
return '$major.$minor.$patch';
|
return '$major.$minor.$patch';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class Pair<T1, T2> with _$Pair<T1, T2> {
|
||||||
|
factory Pair(T1 first, T2 second) = _Pair<T1, T2>;
|
||||||
|
}
|
||||||
|
@ -168,3 +168,147 @@ abstract class _Version extends Version {
|
|||||||
_$VersionCopyWith<_Version> get copyWith =>
|
_$VersionCopyWith<_Version> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PairTearOff {
|
||||||
|
const _$PairTearOff();
|
||||||
|
|
||||||
|
_Pair<T1, T2> call<T1, T2>(T1 first, T2 second) {
|
||||||
|
return _Pair<T1, T2>(
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
const $Pair = _$PairTearOff();
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$Pair<T1, T2> {
|
||||||
|
T1 get first => throw _privateConstructorUsedError;
|
||||||
|
T2 get second => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$PairCopyWith<T1, T2, Pair<T1, T2>> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $PairCopyWith<T1, T2, $Res> {
|
||||||
|
factory $PairCopyWith(Pair<T1, T2> value, $Res Function(Pair<T1, T2>) then) =
|
||||||
|
_$PairCopyWithImpl<T1, T2, $Res>;
|
||||||
|
$Res call({T1 first, T2 second});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$PairCopyWithImpl<T1, T2, $Res> implements $PairCopyWith<T1, T2, $Res> {
|
||||||
|
_$PairCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
final Pair<T1, T2> _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function(Pair<T1, T2>) _then;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? first = freezed,
|
||||||
|
Object? second = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
first: first == freezed
|
||||||
|
? _value.first
|
||||||
|
: first // ignore: cast_nullable_to_non_nullable
|
||||||
|
as T1,
|
||||||
|
second: second == freezed
|
||||||
|
? _value.second
|
||||||
|
: second // ignore: cast_nullable_to_non_nullable
|
||||||
|
as T2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$PairCopyWith<T1, T2, $Res>
|
||||||
|
implements $PairCopyWith<T1, T2, $Res> {
|
||||||
|
factory _$PairCopyWith(
|
||||||
|
_Pair<T1, T2> value, $Res Function(_Pair<T1, T2>) then) =
|
||||||
|
__$PairCopyWithImpl<T1, T2, $Res>;
|
||||||
|
@override
|
||||||
|
$Res call({T1 first, T2 second});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$PairCopyWithImpl<T1, T2, $Res> extends _$PairCopyWithImpl<T1, T2, $Res>
|
||||||
|
implements _$PairCopyWith<T1, T2, $Res> {
|
||||||
|
__$PairCopyWithImpl(_Pair<T1, T2> _value, $Res Function(_Pair<T1, T2>) _then)
|
||||||
|
: super(_value, (v) => _then(v as _Pair<T1, T2>));
|
||||||
|
|
||||||
|
@override
|
||||||
|
_Pair<T1, T2> get _value => super._value as _Pair<T1, T2>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? first = freezed,
|
||||||
|
Object? second = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_Pair<T1, T2>(
|
||||||
|
first == freezed
|
||||||
|
? _value.first
|
||||||
|
: first // ignore: cast_nullable_to_non_nullable
|
||||||
|
as T1,
|
||||||
|
second == freezed
|
||||||
|
? _value.second
|
||||||
|
: second // ignore: cast_nullable_to_non_nullable
|
||||||
|
as T2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
class _$_Pair<T1, T2> implements _Pair<T1, T2> {
|
||||||
|
_$_Pair(this.first, this.second);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final T1 first;
|
||||||
|
@override
|
||||||
|
final T2 second;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Pair<$T1, $T2>(first: $first, second: $second)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(dynamic other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _Pair<T1, T2> &&
|
||||||
|
const DeepCollectionEquality().equals(other.first, first) &&
|
||||||
|
const DeepCollectionEquality().equals(other.second, second));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
const DeepCollectionEquality().hash(first),
|
||||||
|
const DeepCollectionEquality().hash(second));
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
_$PairCopyWith<T1, T2, _Pair<T1, T2>> get copyWith =>
|
||||||
|
__$PairCopyWithImpl<T1, T2, _Pair<T1, T2>>(this, _$identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _Pair<T1, T2> implements Pair<T1, T2> {
|
||||||
|
factory _Pair(T1 first, T2 second) = _$_Pair<T1, T2>;
|
||||||
|
|
||||||
|
@override
|
||||||
|
T1 get first;
|
||||||
|
@override
|
||||||
|
T2 get second;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$PairCopyWith<T1, T2, _Pair<T1, T2>> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
@ -837,11 +837,11 @@ class _$CredentialDataTearOff {
|
|||||||
{String? issuer,
|
{String? issuer,
|
||||||
required String name,
|
required String name,
|
||||||
required String secret,
|
required String secret,
|
||||||
OathType oathType = OathType.totp,
|
OathType oathType = defaultOathType,
|
||||||
HashAlgorithm hashAlgorithm = HashAlgorithm.sha1,
|
HashAlgorithm hashAlgorithm = defaultHashAlgorithm,
|
||||||
int digits = 6,
|
int digits = defaultDigits,
|
||||||
int period = 30,
|
int period = defaultPeriod,
|
||||||
int counter = 0}) {
|
int counter = defaultCounter}) {
|
||||||
return _CredentialData(
|
return _CredentialData(
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
name: name,
|
name: name,
|
||||||
@ -1036,11 +1036,11 @@ class _$_CredentialData extends _CredentialData {
|
|||||||
{this.issuer,
|
{this.issuer,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.secret,
|
required this.secret,
|
||||||
this.oathType = OathType.totp,
|
this.oathType = defaultOathType,
|
||||||
this.hashAlgorithm = HashAlgorithm.sha1,
|
this.hashAlgorithm = defaultHashAlgorithm,
|
||||||
this.digits = 6,
|
this.digits = defaultDigits,
|
||||||
this.period = 30,
|
this.period = defaultPeriod,
|
||||||
this.counter = 0})
|
this.counter = defaultCounter})
|
||||||
: super._();
|
: super._();
|
||||||
|
|
||||||
factory _$_CredentialData.fromJson(Map<String, dynamic> json) =>
|
factory _$_CredentialData.fromJson(Map<String, dynamic> json) =>
|
||||||
|
@ -65,13 +65,13 @@ _$_CredentialData _$$_CredentialDataFromJson(Map<String, dynamic> json) =>
|
|||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
secret: json['secret'] as String,
|
secret: json['secret'] as String,
|
||||||
oathType: $enumDecodeNullable(_$OathTypeEnumMap, json['oath_type']) ??
|
oathType: $enumDecodeNullable(_$OathTypeEnumMap, json['oath_type']) ??
|
||||||
OathType.totp,
|
defaultOathType,
|
||||||
hashAlgorithm:
|
hashAlgorithm:
|
||||||
$enumDecodeNullable(_$HashAlgorithmEnumMap, json['hash_algorithm']) ??
|
$enumDecodeNullable(_$HashAlgorithmEnumMap, json['hash_algorithm']) ??
|
||||||
HashAlgorithm.sha1,
|
defaultHashAlgorithm,
|
||||||
digits: json['digits'] as int? ?? 6,
|
digits: json['digits'] as int? ?? defaultDigits,
|
||||||
period: json['period'] as int? ?? 30,
|
period: json['period'] as int? ?? defaultPeriod,
|
||||||
counter: json['counter'] as int? ?? 0,
|
counter: json['counter'] as int? ?? defaultCounter,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$_CredentialDataToJson(_$_CredentialData instance) =>
|
Map<String, dynamic> _$$_CredentialDataToJson(_$_CredentialData instance) =>
|
||||||
|
@ -8,6 +8,7 @@ import 'package:yubico_authenticator/oath/models.dart';
|
|||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
final _secretFormatterPattern =
|
final _secretFormatterPattern =
|
||||||
RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false);
|
RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false);
|
||||||
@ -33,17 +34,14 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
int remaining = 64; // 64 bytes are shared between issuer and name.
|
final remaining = getRemainingKeySpace(
|
||||||
if (_oathType == OathType.totp && _period != defaultPeriod) {
|
oathType: _oathType,
|
||||||
// Non-standard periods are stored as part of this data, as a "D/"- prefix.
|
period: _period,
|
||||||
remaining -= '$_period/'.length;
|
issuer: _issuer,
|
||||||
}
|
name: _account,
|
||||||
if (_issuer.isNotEmpty) {
|
);
|
||||||
// Issuer is separated from name with a ":", if present.
|
final issuerRemaining = remaining.first;
|
||||||
remaining -= 1;
|
final nameRemaining = remaining.second;
|
||||||
}
|
|
||||||
final issuerRemaining = remaining - max<int>(_account.length, 1);
|
|
||||||
final nameRemaining = remaining - _issuer.length;
|
|
||||||
|
|
||||||
final secretValid = _secret.length * 5 % 8 < 5;
|
final secretValid = _secret.length * 5 % 8 < 5;
|
||||||
final isValid =
|
final isValid =
|
||||||
@ -57,7 +55,7 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
|||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
enabled: issuerRemaining > 0,
|
enabled: issuerRemaining > 0,
|
||||||
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
maxLength: max(issuerRemaining, 1),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Issuer (optional)',
|
labelText: 'Issuer (optional)',
|
||||||
helperText:
|
helperText:
|
||||||
@ -65,13 +63,12 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_issuer = value;
|
_issuer = value.trim();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextField(
|
||||||
enabled: nameRemaining > 0,
|
maxLength: nameRemaining,
|
||||||
maxLength: nameRemaining > 0 ? nameRemaining : null,
|
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Account name',
|
labelText: 'Account name',
|
||||||
helperText:
|
helperText:
|
||||||
@ -79,11 +76,11 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_account = value;
|
_account = value.trim();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextField(
|
||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
@ -7,6 +5,7 @@ import '../models.dart';
|
|||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
class RenameAccountDialog extends ConsumerStatefulWidget {
|
class RenameAccountDialog extends ConsumerStatefulWidget {
|
||||||
final DeviceNode device;
|
final DeviceNode device;
|
||||||
@ -20,16 +19,15 @@ class RenameAccountDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||||
late TextEditingController _issuerController;
|
late String _issuer;
|
||||||
late TextEditingController _nameController;
|
late String _account;
|
||||||
_RenameAccountDialogState();
|
_RenameAccountDialogState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_issuerController =
|
_issuer = widget.credential.issuer ?? '';
|
||||||
TextEditingController(text: widget.credential.issuer ?? '');
|
_account = widget.credential.name;
|
||||||
_nameController = TextEditingController(text: widget.credential.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -45,20 +43,15 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
? '${credential.issuer} (${credential.name})'
|
? '${credential.issuer} (${credential.name})'
|
||||||
: credential.name;
|
: credential.name;
|
||||||
|
|
||||||
int remaining = 64; // 64 bytes are shared between issuer and name.
|
final remaining = getRemainingKeySpace(
|
||||||
if (credential.oathType == OathType.totp &&
|
oathType: credential.oathType,
|
||||||
credential.period != defaultPeriod) {
|
period: credential.period,
|
||||||
// Non-standard periods are stored as part of this data, as a "D/"- prefix.
|
issuer: _issuer,
|
||||||
remaining -= '${credential.period}/'.length;
|
name: _account,
|
||||||
}
|
);
|
||||||
if (_issuerController.text.isNotEmpty) {
|
final issuerRemaining = remaining.first;
|
||||||
// Issuer is separated from name with a ":", if present.
|
final nameRemaining = remaining.second;
|
||||||
remaining -= 1;
|
final isValid = _account.isNotEmpty;
|
||||||
}
|
|
||||||
final issuerRemaining =
|
|
||||||
remaining - max<int>(_nameController.text.length, 1);
|
|
||||||
final nameRemaining = remaining - _issuerController.text.length;
|
|
||||||
final isValid = _nameController.text.trim().isNotEmpty;
|
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Text('Rename $label?'),
|
title: Text('Rename $label?'),
|
||||||
@ -67,29 +60,32 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'This will change how the account is displayed in the list.'),
|
'This will change how the account is displayed in the list.'),
|
||||||
TextField(
|
TextFormField(
|
||||||
controller: _issuerController,
|
initialValue: _issuer,
|
||||||
enabled: issuerRemaining > 0,
|
enabled: issuerRemaining > 0,
|
||||||
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
maxLength: issuerRemaining > 0 ? issuerRemaining : null,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Issuer',
|
labelText: 'Issuer (optional)',
|
||||||
helperText: '', // Prevents dialog resizing when enabled = false
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {}); // Update maxLength
|
setState(() {
|
||||||
|
_issuer = value.trim();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextField(
|
TextFormField(
|
||||||
controller: _nameController,
|
initialValue: _account,
|
||||||
enabled: nameRemaining > 0,
|
maxLength: nameRemaining,
|
||||||
maxLength: nameRemaining > 0 ? nameRemaining : null,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Account name *',
|
labelText: 'Account name',
|
||||||
helperText: '', // Prevents dialog resizing when enabled = false
|
helperText: '', // Prevents dialog resizing when enabled = false
|
||||||
errorText: isValid ? null : 'Your account must have a name',
|
errorText: isValid ? null : 'Your account must have a name',
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {}); // Update maxLength, isValid
|
setState(() {
|
||||||
|
_account = value.trim();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -104,12 +100,10 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
|||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: isValid
|
onPressed: isValid
|
||||||
? () async {
|
? () async {
|
||||||
final issuer = _issuerController.text.trim();
|
|
||||||
final name = _nameController.text.trim();
|
|
||||||
await ref
|
await ref
|
||||||
.read(credentialListProvider(widget.device.path).notifier)
|
.read(credentialListProvider(widget.device.path).notifier)
|
||||||
.renameAccount(
|
.renameAccount(credential,
|
||||||
credential, issuer.isNotEmpty ? issuer : null, name);
|
_issuer.isNotEmpty ? _issuer : null, _account);
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
|
32
lib/oath/views/utils.dart
Executable file
32
lib/oath/views/utils.dart
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import '../models.dart';
|
||||||
|
import '../../core/models.dart';
|
||||||
|
|
||||||
|
/// Calculates the available space for issuer and account name.
|
||||||
|
///
|
||||||
|
/// Returns a [Pair] of the space available for the issuer and account name,
|
||||||
|
/// respectively, based on the current state of the credential.
|
||||||
|
Pair<int, int> getRemainingKeySpace(
|
||||||
|
{required OathType oathType,
|
||||||
|
required int period,
|
||||||
|
required String issuer,
|
||||||
|
required String name}) {
|
||||||
|
int remaining = 64; // The field is 64 bytes in total.
|
||||||
|
|
||||||
|
if (oathType == OathType.totp && period != defaultPeriod) {
|
||||||
|
// Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix.
|
||||||
|
remaining -= '$period/'.length;
|
||||||
|
}
|
||||||
|
int issuerSpace = issuer.length;
|
||||||
|
if (issuer.isNotEmpty) {
|
||||||
|
// Issuer is separated from name with a ":", if present.
|
||||||
|
issuerSpace += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(
|
||||||
|
// Always reserve at least one character for name
|
||||||
|
remaining - 1 - max(name.length, 1),
|
||||||
|
remaining - issuerSpace,
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user