mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge 'main' into fix/YADESK-637-refresh-creds
This commit is contained in:
commit
7c30aeb41c
2
.github/workflows/android.yaml
vendored
2
.github/workflows/android.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
channel: 'stable'
|
||||
- run: |
|
||||
flutter config
|
||||
flutter --version
|
||||
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
channel: 'stable'
|
||||
- run: flutter config --enable-linux-desktop
|
||||
- run: flutter --version
|
||||
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
channel: 'stable'
|
||||
architecture: 'x64'
|
||||
- run: flutter config --enable-macos-desktop
|
||||
- run: flutter --version
|
||||
|
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'beta'
|
||||
channel: 'stable'
|
||||
- run: flutter config --enable-windows-desktop
|
||||
- run: flutter --version
|
||||
|
||||
|
@ -25,11 +25,11 @@ String randomPadded() {
|
||||
}
|
||||
|
||||
String generateRandomIssuer() {
|
||||
return 'i' + randomPadded();
|
||||
return 'i${randomPadded()}';
|
||||
}
|
||||
|
||||
String generateRandomName() {
|
||||
return 'n' + randomPadded();
|
||||
return 'n${randomPadded()}';
|
||||
}
|
||||
|
||||
String generateRandomSecret() {
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:yubico_authenticator/app/state.dart';
|
||||
|
||||
import 'app/logging.dart';
|
||||
import 'app/message.dart';
|
||||
@ -43,7 +44,11 @@ class AboutPage extends ConsumerWidget {
|
||||
final data = response['diagnostics'];
|
||||
final text = const JsonEncoder.withIndent(' ').convert(data);
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
showMessage(context, 'Diagnostic data copied to clipboard');
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(context, 'Diagnostic data copied to clipboard');
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Run diagnostics...'),
|
||||
),
|
||||
@ -85,7 +90,11 @@ class LoggingPanel extends ConsumerWidget {
|
||||
_log.info('Copying log to clipboard...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
await Clipboard.setData(ClipboardData(text: logs.join('\n')));
|
||||
showMessage(context, 'Log copied to clipboard');
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(context, 'Log copied to clipboard');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -27,11 +27,11 @@ class OverlayClipper extends CustomClipper<Path> {
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
|
||||
}
|
||||
|
||||
class MobileScannerWrapper extends StatelessWidget {
|
||||
class _MobileScannerWrapper extends StatelessWidget {
|
||||
final Function(String) onDetect;
|
||||
final _ScanStatus status;
|
||||
|
||||
const MobileScannerWrapper({
|
||||
const _MobileScannerWrapper({
|
||||
Key? key,
|
||||
required this.onDetect,
|
||||
required this.status,
|
||||
@ -93,7 +93,7 @@ class QrScannerView extends StatefulWidget {
|
||||
const QrScannerView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_QrScannerViewState createState() => _QrScannerViewState();
|
||||
State<QrScannerView> createState() => _QrScannerViewState();
|
||||
}
|
||||
|
||||
class _QrScannerViewState extends State<QrScannerView> {
|
||||
@ -166,7 +166,7 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
),
|
||||
),
|
||||
body: Stack(children: [
|
||||
MobileScannerWrapper(
|
||||
_MobileScannerWrapper(
|
||||
status: _status,
|
||||
onDetect: (scannedData) => handleResult(scannedData),
|
||||
),
|
||||
|
@ -39,8 +39,8 @@ class FDialogApiImpl extends FDialogApi {
|
||||
size: 64,
|
||||
),
|
||||
onCancel: () {
|
||||
HDialogApi _api = HDialogApi();
|
||||
_api.dialogClosed();
|
||||
HDialogApi api = HDialogApi();
|
||||
api.dialogClosed();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ class DeviceAvatar extends StatelessWidget {
|
||||
|
||||
factory DeviceAvatar.yubiKeyData(YubiKeyData data, {bool selected = false}) =>
|
||||
DeviceAvatar(
|
||||
child: getProductImage(data.info, data.name),
|
||||
badge: data.node is NfcReaderNode ? Icons.wifi : null,
|
||||
selected: selected,
|
||||
child: getProductImage(data.info, data.name),
|
||||
);
|
||||
|
||||
factory DeviceAvatar.deviceNode(DeviceNode node, {bool selected = false}) =>
|
||||
@ -29,13 +29,13 @@ class DeviceAvatar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return DeviceAvatar(
|
||||
child: const Icon(Icons.device_unknown),
|
||||
selected: selected,
|
||||
child: const Icon(Icons.device_unknown),
|
||||
);
|
||||
},
|
||||
nfcReader: (_) => DeviceAvatar(
|
||||
child: const Icon(Icons.wifi),
|
||||
selected: selected,
|
||||
child: const Icon(Icons.wifi),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -20,6 +20,7 @@ class MessagePage extends StatelessWidget {
|
||||
Widget build(BuildContext context) => AppPage(
|
||||
title: title,
|
||||
centered: true,
|
||||
floatingActionButton: floatingActionButton,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(header, style: Theme.of(context).textTheme.headline6),
|
||||
@ -27,6 +28,5 @@ class MessagePage extends StatelessWidget {
|
||||
Text(message, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
floatingActionButton: floatingActionButton,
|
||||
);
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ class NoDeviceScreen extends ConsumerWidget {
|
||||
}),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
if (exe?.isEmpty ?? true) {
|
||||
var relativePath = 'helper/authenticator-helper';
|
||||
if (Platform.isMacOS) {
|
||||
relativePath = '../Resources/' + relativePath;
|
||||
relativePath = '../Resources/$relativePath';
|
||||
} else if (Platform.isWindows) {
|
||||
relativePath += '.exe';
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// ignore_for_file: sort_child_properties_last
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@ -110,6 +112,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
||||
await ref
|
||||
.read(fingerprintProvider(widget.devicePath).notifier)
|
||||
.renameFingerprint(_fingerprint!, _label);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Fingerprint added');
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
// ignore_for_file: sort_child_properties_last
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@ -43,8 +45,12 @@ class DeleteCredentialDialog extends ConsumerWidget {
|
||||
await ref
|
||||
.read(credentialProvider(devicePath).notifier)
|
||||
.deleteCredential(credential);
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Credential deleted');
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Credential deleted');
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
|
@ -25,6 +25,20 @@ class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Delete fingerprint'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(fingerprintProvider(devicePath).notifier)
|
||||
.deleteFingerprint(fingerprint);
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Fingerprint deleted');
|
||||
});
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -32,23 +46,11 @@ class DeleteFingerprintDialog extends ConsumerWidget {
|
||||
Text('Fingerprint: $label'),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(fingerprintProvider(devicePath).notifier)
|
||||
.deleteFingerprint(fingerprint);
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Fingerprint deleted');
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -85,9 +85,9 @@ class FidoScreen extends ConsumerWidget {
|
||||
}),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
));
|
||||
|
@ -191,9 +191,9 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
minLeadingWidth: 0,
|
||||
trailing: ElevatedButton(
|
||||
child: const Text('Unlock'),
|
||||
onPressed:
|
||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -39,6 +39,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: Text(hasPin ? 'Change PIN' : 'Set PIN'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? _submit : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -109,12 +115,6 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Save'),
|
||||
onPressed: isValid ? _submit : null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
final renamed = await ref
|
||||
.read(fingerprintProvider(widget.devicePath).notifier)
|
||||
.renameFingerprint(widget.fingerprint, _label);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(renamed);
|
||||
showMessage(context, 'Fingerprint renamed');
|
||||
}
|
||||
@ -46,6 +47,12 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Rename fingerprint'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _label.isNotEmpty ? _submit : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -71,17 +78,11 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _label.isNotEmpty ? _submit : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -55,26 +55,6 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Factory reset'),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.'),
|
||||
Text(
|
||||
'Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
Center(
|
||||
child: Text(_getMessage(),
|
||||
style: Theme.of(context).textTheme.headline6),
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
onCancel: () {
|
||||
_subscription?.cancel();
|
||||
},
|
||||
@ -103,6 +83,26 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.'),
|
||||
Text(
|
||||
'Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.',
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
Center(
|
||||
child: Text(_getMessage(),
|
||||
style: Theme.of(context).textTheme.headline6),
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -97,10 +97,10 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
if (children.isNotEmpty) {
|
||||
return AppPage(
|
||||
title: const Text('WebAuthn'),
|
||||
floatingActionButton: _buildFab(context),
|
||||
child: Column(
|
||||
children: children,
|
||||
),
|
||||
floatingActionButton: _buildFab(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,10 +31,10 @@ void main(List<String> argv) async {
|
||||
_log.warning('Platform initialization failed: $e');
|
||||
runApp(
|
||||
ProviderScope(
|
||||
child: YubicoAuthenticatorApp(page: ErrorPage(error: e.toString())),
|
||||
overrides: [
|
||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance())
|
||||
],
|
||||
child: YubicoAuthenticatorApp(page: ErrorPage(error: e.toString())),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -129,6 +129,7 @@ class ManagementScreen extends ConsumerStatefulWidget {
|
||||
class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
late Map<Transport, int> _enabled;
|
||||
late int _interfaces;
|
||||
bool _canSave = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -182,6 +183,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
.copyWith(enabledCapabilities: _enabled),
|
||||
reboot: reboot,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!reboot) Navigator.pop(context);
|
||||
showMessage(context, 'Configuration updated');
|
||||
} finally {
|
||||
@ -203,6 +205,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
await ref
|
||||
.read(managementStateProvider(widget.deviceData.node.path).notifier)
|
||||
.setMode(interfaces: _interfaces);
|
||||
if (!mounted) return;
|
||||
showMessage(
|
||||
context,
|
||||
widget.deviceData.node.maybeMap(
|
||||
@ -227,32 +230,37 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
});
|
||||
|
||||
bool canSave = false;
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Toggle applications'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _canSave ? _submitForm : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
child:
|
||||
ref.watch(managementStateProvider(widget.deviceData.node.path)).when(
|
||||
loading: () => const AppLoadingScreen(),
|
||||
error: (error, _) => AppFailureScreen('$error'),
|
||||
data: (info) {
|
||||
bool hasConfig = info.version.major > 4;
|
||||
// TODO: Check mode for < YK5 intead
|
||||
if (hasConfig) {
|
||||
canSave = !_mapEquals(
|
||||
_enabled,
|
||||
info.config.enabledCapabilities,
|
||||
);
|
||||
} else {
|
||||
canSave = _interfaces != 0 &&
|
||||
_interfaces !=
|
||||
UsbInterfaces.forCapabilites(widget
|
||||
.deviceData
|
||||
.info
|
||||
.config
|
||||
.enabledCapabilities[Transport.usb] ??
|
||||
0);
|
||||
}
|
||||
setState(() {
|
||||
if (hasConfig) {
|
||||
_canSave = !_mapEquals(
|
||||
_enabled,
|
||||
info.config.enabledCapabilities,
|
||||
);
|
||||
} else {
|
||||
_canSave = _interfaces != 0 &&
|
||||
_interfaces !=
|
||||
UsbInterfaces.forCapabilites(widget
|
||||
.deviceData
|
||||
.info
|
||||
.config
|
||||
.enabledCapabilities[Transport.usb] ??
|
||||
0);
|
||||
}
|
||||
});
|
||||
return Column(
|
||||
children: [
|
||||
hasConfig
|
||||
@ -262,12 +270,6 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: canSave ? _submitForm : null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/dialog_frame.dart';
|
||||
import '../models.dart';
|
||||
@ -19,13 +20,15 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
final renamed = await super.renameCredential(context, ref);
|
||||
if (renamed != null) {
|
||||
// Replace this dialog with a new one, for the renamed credential.
|
||||
Navigator.of(context).pop();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AccountDialog(renamed);
|
||||
},
|
||||
);
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AccountDialog(renamed);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return renamed;
|
||||
}
|
||||
@ -34,7 +37,9 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
|
||||
final deleted = await super.deleteCredential(context, ref);
|
||||
if (deleted) {
|
||||
Navigator.of(context).pop();
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ mixin AccountMixin {
|
||||
return value;
|
||||
} else {
|
||||
var i = value.length ~/ 2;
|
||||
return value.substring(0, i) + ' ' + value.substring(i);
|
||||
return '${value.substring(0, i)} ${value.substring(i)}';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/app/state.dart';
|
||||
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -43,12 +44,6 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
return buildActions(context, ref).map((e) {
|
||||
final action = e.action;
|
||||
return PopupMenuItem(
|
||||
child: ListTile(
|
||||
leading: e.icon,
|
||||
title: Text(e.text),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
enabled: action != null,
|
||||
onTap: () {
|
||||
// As soon as onTap returns, the Navigator is popped,
|
||||
@ -58,6 +53,12 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
action?.call(context);
|
||||
});
|
||||
},
|
||||
child: ListTile(
|
||||
leading: e.icon,
|
||||
title: Text(e.text),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
@ -104,7 +105,11 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
ref,
|
||||
);
|
||||
}
|
||||
copyToClipboard(context, ref);
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
copyToClipboard(context, ref);
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: darkMode ? Colors.black : Colors.white,
|
||||
|
@ -144,6 +144,45 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Add account'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () async {
|
||||
if (secretLengthValid) {
|
||||
final issuer = _issuerController.text;
|
||||
|
||||
final cred = CredentialData(
|
||||
issuer: issuer.isEmpty ? null : issuer,
|
||||
name: _accountController.text,
|
||||
secret: secret,
|
||||
oathType: _oathType,
|
||||
hashAlgorithm: _hashAlgorithm,
|
||||
digits: _digits,
|
||||
period: period,
|
||||
);
|
||||
|
||||
try {
|
||||
await ref
|
||||
.read(credentialListProvider(widget.devicePath)
|
||||
.notifier)
|
||||
.addAccount(cred.toUri(), requireTouch: _touch);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Account added');
|
||||
} catch (e) {
|
||||
_log.error('Failed to add account', e);
|
||||
showMessage(context, 'Failed adding account');
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_validateSecretLength = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save', key: Key('save_btn')),
|
||||
),
|
||||
],
|
||||
child: FileDropTarget(
|
||||
onFileDropped: (fileData) async {
|
||||
if (qrScanner != null) {
|
||||
@ -359,44 +398,6 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () async {
|
||||
if (secretLengthValid) {
|
||||
final issuer = _issuerController.text;
|
||||
|
||||
final cred = CredentialData(
|
||||
issuer: issuer.isEmpty ? null : issuer,
|
||||
name: _accountController.text,
|
||||
secret: secret,
|
||||
oathType: _oathType,
|
||||
hashAlgorithm: _hashAlgorithm,
|
||||
digits: _digits,
|
||||
period: period,
|
||||
);
|
||||
|
||||
try {
|
||||
await ref
|
||||
.read(credentialListProvider(widget.devicePath)
|
||||
.notifier)
|
||||
.addAccount(cred.toUri(), requireTouch: _touch);
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Account added');
|
||||
} catch (e) {
|
||||
_log.error('Failed to add account', e);
|
||||
showMessage(context, 'Failed adding account');
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_validateSecretLength = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save', key: Key('save_btn')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,22 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Delete account'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(credentialListProvider(device.path).notifier)
|
||||
.deleteAccount(credential);
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Account deleted');
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -39,23 +55,11 @@ class DeleteAccountDialog extends ConsumerWidget {
|
||||
Text('Account: $label'),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(credentialListProvider(device.path).notifier)
|
||||
.deleteAccount(credential);
|
||||
Navigator.of(context).pop(true);
|
||||
showMessage(context, 'Account deleted');
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.setPassword(_currentPassword, _newPassword);
|
||||
if (result) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Password set');
|
||||
} else {
|
||||
@ -52,6 +53,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Manage password'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? _submit : null,
|
||||
child: const Text('Save'),
|
||||
)
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -78,13 +85,13 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text('Remove password'),
|
||||
onPressed: _currentPassword.isNotEmpty
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.unsetPassword(_currentPassword);
|
||||
if (result) {
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Password removed');
|
||||
} else {
|
||||
@ -94,6 +101,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('Remove password'),
|
||||
),
|
||||
if (widget.state.remembered)
|
||||
OutlinedButton(
|
||||
@ -102,6 +110,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
await ref
|
||||
.read(oathStateProvider(widget.path).notifier)
|
||||
.forgetPassword();
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'Password forgotten');
|
||||
},
|
||||
@ -153,12 +162,6 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid ? _submit : null,
|
||||
child: const Text('Save'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -112,8 +112,8 @@ class _UnlockedView extends ConsumerWidget {
|
||||
);
|
||||
}),
|
||||
),
|
||||
child: AccountList(devicePath, oathState),
|
||||
floatingActionButton: _buildFab(context),
|
||||
child: AccountList(devicePath, oathState),
|
||||
);
|
||||
}
|
||||
|
||||
@ -188,6 +188,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget._devicePath).notifier)
|
||||
.unlock(_passwordController.text, remember: _remember);
|
||||
if (!mounted) return;
|
||||
if (!result.first) {
|
||||
setState(() {
|
||||
_wrong = true;
|
||||
@ -283,8 +284,8 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
child: const Text('Unlock'),
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -56,6 +56,22 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Rename account'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () async {
|
||||
final renamed = await ref
|
||||
.read(credentialListProvider(widget.device.path).notifier)
|
||||
.renameAccount(credential,
|
||||
_issuer.isNotEmpty ? _issuer : null, _account);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(renamed);
|
||||
showMessage(context, 'Account renamed');
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -94,26 +110,11 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isValid
|
||||
? () async {
|
||||
final renamed = await ref
|
||||
.read(credentialListProvider(widget.device.path).notifier)
|
||||
.renameAccount(credential,
|
||||
_issuer.isNotEmpty ? _issuer : null, _account);
|
||||
Navigator.of(context).pop(renamed);
|
||||
showMessage(context, 'Account renamed');
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,18 @@ class ResetDialog extends ConsumerWidget {
|
||||
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Factory reset'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(oathStateProvider(devicePath).notifier).reset();
|
||||
await ref.read(withContextProvider)((context) async {
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'OATH application reset');
|
||||
});
|
||||
},
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
@ -30,21 +42,11 @@ class ResetDialog extends ConsumerWidget {
|
||||
),
|
||||
]
|
||||
.map((e) => Padding(
|
||||
child: e,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: e,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ref.read(oathStateProvider(devicePath).notifier).reset();
|
||||
Navigator.of(context).pop();
|
||||
showMessage(context, 'OATH application reset');
|
||||
},
|
||||
child: const Text('Reset'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
58
pubspec.lock
58
pubspec.lock
@ -28,7 +28,7 @@ packages:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -49,7 +49,7 @@ packages:
|
||||
name: build
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.3.0"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -63,7 +63,7 @@ packages:
|
||||
name: build_daemon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.0"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -77,7 +77,7 @@ packages:
|
||||
name: build_runner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
version: "2.1.10"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -98,7 +98,7 @@ packages:
|
||||
name: built_value
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
version: "8.3.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -142,7 +142,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
@ -161,7 +161,7 @@ packages:
|
||||
name: cross_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
version: "0.3.3"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -227,7 +227,7 @@ packages:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.1"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -324,21 +324,21 @@ packages:
|
||||
name: json_annotation
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
version: "4.5.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
version: "6.2.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "2.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -373,7 +373,7 @@ packages:
|
||||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
version: "1.0.2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -394,28 +394,28 @@ packages:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.6"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.6"
|
||||
pigeon:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pigeon
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.3"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -478,35 +478,35 @@ packages:
|
||||
name: shared_preferences
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.13"
|
||||
version: "2.0.15"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.11"
|
||||
version: "2.0.12"
|
||||
shared_preferences_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_ios
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -520,14 +520,14 @@ packages:
|
||||
name: shared_preferences_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -553,14 +553,14 @@ packages:
|
||||
name: source_gen
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
source_helper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_helper
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -665,7 +665,7 @@ packages:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.2.0"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -679,14 +679,14 @@ packages:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.5.2"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
version: "0.2.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -702,5 +702,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.17.0-266.1.beta <3.0.0"
|
||||
dart: ">=2.17.0 <3.0.0"
|
||||
flutter: ">=2.8.0"
|
||||
|
7
pubspec.yaml
Normal file → Executable file
7
pubspec.yaml
Normal file → Executable file
@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.0-268.18.beta <3.0.0"
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
||||
# Dependencies specify other packages that your package needs in order to work.
|
||||
# To automatically upgrade your package dependencies to the latest versions
|
||||
@ -37,6 +37,7 @@ dependencies:
|
||||
|
||||
async: ^2.8.2
|
||||
logging: ^1.0.2
|
||||
collection: ^1.16.0
|
||||
shared_preferences: ^2.0.12
|
||||
flutter_riverpod: ^1.0.0
|
||||
json_annotation: ^4.4.0
|
||||
@ -57,12 +58,12 @@ dev_dependencies:
|
||||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
flutter_lints: ^2.0.1
|
||||
|
||||
build_runner: ^2.1.4
|
||||
freezed: ^1.0.0
|
||||
json_serializable: ^6.0.0
|
||||
pigeon: ^2.0.2
|
||||
pigeon: ^3.0.3
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
Loading…
Reference in New Issue
Block a user