mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
Move QR scan button to add_account_page, and have it populate the form.
This commit is contained in:
parent
8340c96edf
commit
b7f1ec63f4
@ -12,30 +12,8 @@ List<MenuAction> buildOathMenuActions(AutoDisposeProviderRef ref) {
|
||||
final device = ref.watch(currentDeviceProvider);
|
||||
if (device != null) {
|
||||
final state = ref.watch(oathStateProvider(device.path));
|
||||
final qrScanner = ref.watch(qrScannerProvider);
|
||||
if (state != null) {
|
||||
return [
|
||||
if (!state.locked && qrScanner != null)
|
||||
MenuAction(
|
||||
text: 'Scan for QR code',
|
||||
icon: const Icon(Icons.qr_code),
|
||||
action: (context) async {
|
||||
var messenger = ScaffoldMessenger.of(context);
|
||||
// TODO: Go to add credential page.
|
||||
String message;
|
||||
try {
|
||||
final otpauth = await qrScanner.scanQr();
|
||||
message = 'Captured: $otpauth';
|
||||
} catch (e) {
|
||||
message = 'Unable to capture QR code';
|
||||
}
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (!state.locked)
|
||||
MenuAction(
|
||||
text: 'Add credential',
|
||||
|
@ -84,6 +84,31 @@ class CredentialData with _$CredentialData {
|
||||
factory CredentialData.fromJson(Map<String, dynamic> json) =>
|
||||
_$CredentialDataFromJson(json);
|
||||
|
||||
factory CredentialData.fromUri(Uri uri) {
|
||||
if (uri.scheme.toLowerCase() != 'otpauth') {
|
||||
throw ArgumentError('Invalid scheme, must be "otpauth://"');
|
||||
}
|
||||
final oathType = OathType.values.byName(uri.host.toLowerCase());
|
||||
final params = uri.queryParameters;
|
||||
String? issuer;
|
||||
String name = uri.pathSegments.join('/');
|
||||
final nameIndex = name.indexOf(':');
|
||||
if (nameIndex >= 0) {
|
||||
issuer = name.substring(0, nameIndex);
|
||||
name = name.substring(nameIndex + 1);
|
||||
}
|
||||
return CredentialData(
|
||||
issuer: params['issuer'] ?? issuer,
|
||||
name: name,
|
||||
oathType: oathType,
|
||||
hashAlgorithm: HashAlgorithm.values.byName(params['algorithm'] ?? 'sha1'),
|
||||
secret: params['secret']!,
|
||||
digits: int.tryParse(params['digits'] ?? '') ?? defaultDigits,
|
||||
period: int.tryParse(params['period'] ?? '') ?? defaultPeriod,
|
||||
counter: int.tryParse(params['counter'] ?? '') ?? defaultCounter,
|
||||
);
|
||||
}
|
||||
|
||||
Uri toUri() {
|
||||
final path = issuer != null ? '$issuer:$name' : name;
|
||||
var uri = 'otpauth://${oathType.name}/$path?secret=$secret';
|
||||
|
@ -13,47 +13,103 @@ import 'utils.dart';
|
||||
final _secretFormatterPattern =
|
||||
RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false);
|
||||
|
||||
class AddAccountForm extends StatefulWidget {
|
||||
enum _QrScanState { none, scanning, success, failed }
|
||||
|
||||
class AddAccountForm extends ConsumerStatefulWidget {
|
||||
final Function(CredentialData, bool) onSubmit;
|
||||
const AddAccountForm({Key? key, required this.onSubmit}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AddAccountFormState();
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AddAccountFormState();
|
||||
}
|
||||
|
||||
class _AddAccountFormState extends State<AddAccountForm> {
|
||||
String _issuer = '';
|
||||
String _account = '';
|
||||
String _secret = '';
|
||||
class _AddAccountFormState extends ConsumerState<AddAccountForm> {
|
||||
final _issuerController = TextEditingController();
|
||||
final _accountController = TextEditingController();
|
||||
final _secretController = TextEditingController();
|
||||
final _periodController = TextEditingController(text: '$defaultPeriod');
|
||||
bool _touch = false;
|
||||
bool _advanced = false;
|
||||
OathType _oathType = defaultOathType;
|
||||
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
|
||||
int _period = defaultPeriod;
|
||||
int _digits = defaultDigits;
|
||||
bool _validateSecretLength = false;
|
||||
_QrScanState _qrState = _QrScanState.none;
|
||||
|
||||
_scanQrCode(QrScanner qrScanner) async {
|
||||
try {
|
||||
setState(() {
|
||||
_qrState = _QrScanState.scanning;
|
||||
});
|
||||
final otpauth = await qrScanner.scanQr();
|
||||
final data = CredentialData.fromUri(Uri.parse(otpauth));
|
||||
setState(() {
|
||||
_issuerController.text = data.issuer ?? '';
|
||||
_accountController.text = data.name;
|
||||
_secretController.text = data.secret;
|
||||
_oathType = data.oathType;
|
||||
_hashAlgorithm = data.hashAlgorithm;
|
||||
_periodController.text = '${data.period}';
|
||||
_digits = data.digits;
|
||||
_qrState = _QrScanState.success;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_qrState = _QrScanState.failed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildQrStatus() {
|
||||
switch (_qrState) {
|
||||
case _QrScanState.success:
|
||||
return const [
|
||||
Icon(Icons.check_circle_outline_outlined),
|
||||
Text('QR code scanned!'),
|
||||
];
|
||||
case _QrScanState.scanning:
|
||||
return const [
|
||||
SizedBox.square(dimension: 16.0, child: CircularProgressIndicator()),
|
||||
];
|
||||
case _QrScanState.failed:
|
||||
return const [
|
||||
Icon(Icons.warning_amber_rounded),
|
||||
Text('No QR code found'),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final period = int.tryParse(_periodController.text) ?? -1;
|
||||
final remaining = getRemainingKeySpace(
|
||||
oathType: _oathType,
|
||||
period: _period,
|
||||
issuer: _issuer,
|
||||
name: _account,
|
||||
period: period,
|
||||
issuer: _issuerController.text,
|
||||
name: _accountController.text,
|
||||
);
|
||||
final issuerRemaining = remaining.first;
|
||||
final nameRemaining = remaining.second;
|
||||
|
||||
final secretLengthValid = _secret.length * 5 % 8 < 5;
|
||||
final isValid = _account.isNotEmpty && _secret.isNotEmpty && _period > 0;
|
||||
final secret = _secretController.text.replaceAll(' ', '');
|
||||
final secretLengthValid = secret.length * 5 % 8 < 5;
|
||||
final isValid =
|
||||
_accountController.text.isNotEmpty && secret.isNotEmpty && period > 0;
|
||||
|
||||
final qrScanner = ref.watch(qrScannerProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
enabled: issuerRemaining > 0,
|
||||
maxLength: max(issuerRemaining, 1),
|
||||
decoration: const InputDecoration(
|
||||
@ -63,11 +119,12 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_issuer = value.trim();
|
||||
// Update maxlengths
|
||||
});
|
||||
},
|
||||
),
|
||||
TextField(
|
||||
controller: _accountController,
|
||||
maxLength: nameRemaining,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Account name',
|
||||
@ -76,11 +133,12 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_account = value.trim();
|
||||
// Update maxlengths
|
||||
});
|
||||
},
|
||||
),
|
||||
TextField(
|
||||
controller: _secretController,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
||||
],
|
||||
@ -89,13 +147,30 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? 'Invalid length'
|
||||
: null),
|
||||
enabled: _qrState != _QrScanState.success,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_secret = value.replaceAll(' ', '');
|
||||
_validateSecretLength = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (qrScanner != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_scanQrCode(qrScanner);
|
||||
},
|
||||
icon: const Icon(Icons.qr_code),
|
||||
label: const Text('Scan QR code'),
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
..._buildQrStatus(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -138,11 +213,13 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
child: Text(e.name.toUpperCase()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (type) {
|
||||
setState(() {
|
||||
_oathType = type ?? OathType.totp;
|
||||
});
|
||||
},
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? null
|
||||
: (type) {
|
||||
setState(() {
|
||||
_oathType = type ?? OathType.totp;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
@ -159,11 +236,13 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
child: Text(e.name.toUpperCase()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (type) {
|
||||
setState(() {
|
||||
_hashAlgorithm = type ?? HashAlgorithm.sha1;
|
||||
});
|
||||
},
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? null
|
||||
: (type) {
|
||||
setState(() {
|
||||
_hashAlgorithm = type ?? HashAlgorithm.sha1;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -175,7 +254,8 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
if (_oathType == OathType.totp)
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _period > 0 ? _period.toString() : '',
|
||||
controller: _periodController,
|
||||
enabled: _qrState != _QrScanState.success,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
@ -185,13 +265,12 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
// Manual alignment to match digits-dropdown.
|
||||
const EdgeInsets.fromLTRB(0, 12, 0, 15),
|
||||
labelText: 'Period',
|
||||
errorText: _period > 0
|
||||
? null
|
||||
: 'Must be a positive number',
|
||||
errorText:
|
||||
period > 0 ? null : 'Must be a positive number',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_period = int.tryParse(value) ?? -1;
|
||||
// Update maxlengths
|
||||
});
|
||||
},
|
||||
),
|
||||
@ -210,11 +289,13 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
child: Text(e.toString()),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_digits = value ?? defaultDigits;
|
||||
});
|
||||
},
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
? null
|
||||
: (value) {
|
||||
setState(() {
|
||||
_digits = value ?? defaultDigits;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -229,15 +310,16 @@ class _AddAccountFormState extends State<AddAccountForm> {
|
||||
onPressed: isValid
|
||||
? () {
|
||||
if (secretLengthValid) {
|
||||
final issuer = _issuerController.text;
|
||||
widget.onSubmit(
|
||||
CredentialData(
|
||||
issuer: _issuer.isEmpty ? null : _issuer,
|
||||
name: _account,
|
||||
secret: _secret,
|
||||
issuer: issuer.isEmpty ? null : issuer,
|
||||
name: _accountController.text,
|
||||
secret: secret,
|
||||
oathType: _oathType,
|
||||
hashAlgorithm: _hashAlgorithm,
|
||||
digits: _digits,
|
||||
period: _period,
|
||||
period: period,
|
||||
),
|
||||
_touch,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user