mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-26 10:33:15 +03:00
Merge PR 1218.
This commit is contained in:
commit
8a2ab476b9
@ -165,6 +165,7 @@ class AboutPage extends ConsumerWidget {
|
||||
'os': Platform.operatingSystem,
|
||||
'os_version': Platform.operatingSystemVersion,
|
||||
});
|
||||
data.insert(data.length - 1, ref.read(featureFlagProvider));
|
||||
final text = const JsonEncoder.withIndent(' ').convert(data);
|
||||
await ref.read(clipboardProvider).setText(text);
|
||||
await ref.read(withContextProvider)(
|
||||
|
@ -53,39 +53,37 @@ Future<Widget> initialize() async {
|
||||
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
supportedAppsProvider.overrideWithValue([
|
||||
supportedAppsProvider.overrideWith(implementedApps([
|
||||
Application.oath,
|
||||
]),
|
||||
])),
|
||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
|
||||
logLevelProvider.overrideWith((ref) => AndroidLogger()),
|
||||
attachedDevicesProvider
|
||||
.overrideWith(
|
||||
() => AndroidAttachedDevicesNotifier(),
|
||||
),
|
||||
attachedDevicesProvider.overrideWith(
|
||||
() => AndroidAttachedDevicesNotifier(),
|
||||
),
|
||||
currentDeviceDataProvider.overrideWith(
|
||||
(ref) => ref.watch(androidDeviceDataProvider),
|
||||
(ref) => ref.watch(androidDeviceDataProvider),
|
||||
),
|
||||
oathStateProvider.overrideWithProvider(androidOathStateProvider),
|
||||
credentialListProvider
|
||||
.overrideWithProvider(androidCredentialListProvider),
|
||||
currentAppProvider.overrideWith(
|
||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))
|
||||
),
|
||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))),
|
||||
managementStateProvider.overrideWithProvider(androidManagementState),
|
||||
currentDeviceProvider.overrideWith(
|
||||
() => AndroidCurrentDeviceNotifier(),
|
||||
() => AndroidCurrentDeviceNotifier(),
|
||||
),
|
||||
qrScannerProvider
|
||||
.overrideWith(androidQrScannerProvider(await getHasCamera())),
|
||||
windowStateProvider.overrideWith((ref) => ref.watch(androidWindowStateProvider)),
|
||||
windowStateProvider
|
||||
.overrideWith((ref) => ref.watch(androidWindowStateProvider)),
|
||||
clipboardProvider.overrideWith(
|
||||
(ref) => ref.watch(androidClipboardProvider),
|
||||
(ref) => ref.watch(androidClipboardProvider),
|
||||
),
|
||||
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
|
||||
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
||||
supportedThemesProvider
|
||||
.overrideWith(
|
||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||
supportedThemesProvider.overrideWith(
|
||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||
)
|
||||
],
|
||||
child: DismissKeyboard(
|
||||
|
25
lib/app/features.dart
Normal file
25
lib/app/features.dart
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '../core/state.dart';
|
||||
|
||||
final oath = root.feature('oath');
|
||||
final fido = root.feature('fido');
|
||||
final piv = root.feature('piv');
|
||||
final management = root.feature('management');
|
||||
final openpgp = root.feature('openpgp');
|
||||
final hsmauth = root.feature('hsmauth');
|
||||
final otp = root.feature('otp');
|
@ -21,6 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../../management/models.dart';
|
||||
import '../core/models.dart';
|
||||
import '../core/state.dart';
|
||||
|
||||
part 'models.freezed.dart';
|
||||
|
||||
@ -129,6 +130,7 @@ class ActionItem with _$ActionItem {
|
||||
Intent? intent,
|
||||
ActionStyle? actionStyle,
|
||||
Key? key,
|
||||
Feature? feature,
|
||||
}) = _ActionItem;
|
||||
}
|
||||
|
||||
|
@ -633,6 +633,7 @@ mixin _$ActionItem {
|
||||
Intent? get intent => throw _privateConstructorUsedError;
|
||||
ActionStyle? get actionStyle => throw _privateConstructorUsedError;
|
||||
Key? get key => throw _privateConstructorUsedError;
|
||||
Feature? get feature => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$ActionItemCopyWith<ActionItem> get copyWith =>
|
||||
@ -653,7 +654,8 @@ abstract class $ActionItemCopyWith<$Res> {
|
||||
Widget? trailing,
|
||||
Intent? intent,
|
||||
ActionStyle? actionStyle,
|
||||
Key? key});
|
||||
Key? key,
|
||||
Feature? feature});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -677,6 +679,7 @@ class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
|
||||
Object? intent = freezed,
|
||||
Object? actionStyle = freezed,
|
||||
Object? key = freezed,
|
||||
Object? feature = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
icon: null == icon
|
||||
@ -711,6 +714,10 @@ class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
|
||||
? _value.key
|
||||
: key // ignore: cast_nullable_to_non_nullable
|
||||
as Key?,
|
||||
feature: freezed == feature
|
||||
? _value.feature
|
||||
: feature // ignore: cast_nullable_to_non_nullable
|
||||
as Feature?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
@ -731,7 +738,8 @@ abstract class _$$_ActionItemCopyWith<$Res>
|
||||
Widget? trailing,
|
||||
Intent? intent,
|
||||
ActionStyle? actionStyle,
|
||||
Key? key});
|
||||
Key? key,
|
||||
Feature? feature});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -753,6 +761,7 @@ class __$$_ActionItemCopyWithImpl<$Res>
|
||||
Object? intent = freezed,
|
||||
Object? actionStyle = freezed,
|
||||
Object? key = freezed,
|
||||
Object? feature = freezed,
|
||||
}) {
|
||||
return _then(_$_ActionItem(
|
||||
icon: null == icon
|
||||
@ -787,6 +796,10 @@ class __$$_ActionItemCopyWithImpl<$Res>
|
||||
? _value.key
|
||||
: key // ignore: cast_nullable_to_non_nullable
|
||||
as Key?,
|
||||
feature: freezed == feature
|
||||
? _value.feature
|
||||
: feature // ignore: cast_nullable_to_non_nullable
|
||||
as Feature?,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -802,7 +815,8 @@ class _$_ActionItem implements _ActionItem {
|
||||
this.trailing,
|
||||
this.intent,
|
||||
this.actionStyle,
|
||||
this.key});
|
||||
this.key,
|
||||
this.feature});
|
||||
|
||||
@override
|
||||
final Widget icon;
|
||||
@ -820,10 +834,12 @@ class _$_ActionItem implements _ActionItem {
|
||||
final ActionStyle? actionStyle;
|
||||
@override
|
||||
final Key? key;
|
||||
@override
|
||||
final Feature? feature;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key)';
|
||||
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key, feature: $feature)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -842,12 +858,13 @@ class _$_ActionItem implements _ActionItem {
|
||||
(identical(other.intent, intent) || other.intent == intent) &&
|
||||
(identical(other.actionStyle, actionStyle) ||
|
||||
other.actionStyle == actionStyle) &&
|
||||
(identical(other.key, key) || other.key == key));
|
||||
(identical(other.key, key) || other.key == key) &&
|
||||
(identical(other.feature, feature) || other.feature == feature));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut,
|
||||
trailing, intent, actionStyle, key);
|
||||
trailing, intent, actionStyle, key, feature);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -865,7 +882,8 @@ abstract class _ActionItem implements ActionItem {
|
||||
final Widget? trailing,
|
||||
final Intent? intent,
|
||||
final ActionStyle? actionStyle,
|
||||
final Key? key}) = _$_ActionItem;
|
||||
final Key? key,
|
||||
final Feature? feature}) = _$_ActionItem;
|
||||
|
||||
@override
|
||||
Widget get icon;
|
||||
@ -884,6 +902,8 @@ abstract class _ActionItem implements ActionItem {
|
||||
@override
|
||||
Key? get key;
|
||||
@override
|
||||
Feature? get feature;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
@ -27,6 +27,7 @@ import 'package:yubico_authenticator/app/logging.dart';
|
||||
|
||||
import '../core/state.dart';
|
||||
import 'models.dart';
|
||||
import 'features.dart' as features;
|
||||
|
||||
final _log = Logger('app.state');
|
||||
|
||||
@ -37,7 +38,25 @@ const officialLocales = [
|
||||
|
||||
// Override this to alter the set of supported apps.
|
||||
final supportedAppsProvider =
|
||||
Provider<List<Application>>((ref) => Application.values);
|
||||
Provider<List<Application>>(implementedApps(Application.values));
|
||||
|
||||
extension on Application {
|
||||
Feature get _feature => switch (this) {
|
||||
Application.oath => features.oath,
|
||||
Application.fido => features.fido,
|
||||
Application.otp => features.otp,
|
||||
Application.piv => features.piv,
|
||||
Application.management => features.management,
|
||||
Application.openpgp => features.openpgp,
|
||||
Application.hsmauth => features.oath,
|
||||
};
|
||||
}
|
||||
|
||||
List<Application> Function(Ref) implementedApps(List<Application> apps) =>
|
||||
(ref) {
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return apps.where((app) => hasFeature(app._feature)).toList();
|
||||
};
|
||||
|
||||
// Default implementation is always focused, override with platform specific version.
|
||||
final windowStateProvider = Provider<WindowState>(
|
||||
|
@ -15,7 +15,9 @@
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../models.dart';
|
||||
|
||||
@ -26,6 +28,7 @@ class ActionListItem extends StatelessWidget {
|
||||
final Widget? trailing;
|
||||
final void Function(BuildContext context)? onTap;
|
||||
final ActionStyle actionStyle;
|
||||
final Feature? feature;
|
||||
|
||||
const ActionListItem({
|
||||
super.key,
|
||||
@ -35,6 +38,7 @@ class ActionListItem extends StatelessWidget {
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.actionStyle = ActionStyle.normal,
|
||||
this.feature,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -67,7 +71,7 @@ class ActionListItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ActionListSection extends StatelessWidget {
|
||||
class ActionListSection extends ConsumerWidget {
|
||||
final String title;
|
||||
final List<ActionListItem> children;
|
||||
|
||||
@ -82,6 +86,7 @@ class ActionListSection extends StatelessWidget {
|
||||
final intent = action.intent;
|
||||
return ActionListItem(
|
||||
key: action.key,
|
||||
feature: action.feature,
|
||||
actionStyle: action.actionStyle ?? ActionStyle.normal,
|
||||
icon: action.icon,
|
||||
title: action.title,
|
||||
@ -96,14 +101,22 @@ class ActionListSection extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
width: 360,
|
||||
child: Column(children: [
|
||||
ListTitle(
|
||||
title,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
...children,
|
||||
]),
|
||||
);
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final enabledChildren = children
|
||||
.where((item) => item.feature == null || hasFeature(item.feature!));
|
||||
if (enabledChildren.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return SizedBox(
|
||||
width: 360,
|
||||
child: Column(children: [
|
||||
ListTitle(
|
||||
title,
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
...enabledChildren,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,14 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../shortcuts.dart';
|
||||
import 'action_popup_menu.dart';
|
||||
|
||||
class AppListItem extends StatefulWidget {
|
||||
class AppListItem extends ConsumerStatefulWidget {
|
||||
final Widget? leading;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
@ -41,10 +42,10 @@ class AppListItem extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _AppListItemState();
|
||||
ConsumerState<ConsumerStatefulWidget> createState() => _AppListItemState();
|
||||
}
|
||||
|
||||
class _AppListItemState extends State<AppListItem> {
|
||||
class _AppListItemState extends ConsumerState<AppListItem> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
int _lastTap = 0;
|
||||
|
||||
@ -60,6 +61,7 @@ class _AppListItemState extends State<AppListItem> {
|
||||
final buildPopupActions = widget.buildPopupActions;
|
||||
final activationIntent = widget.activationIntent;
|
||||
final trailing = widget.trailing;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
return Shortcuts(
|
||||
shortcuts: {
|
||||
@ -72,11 +74,17 @@ class _AppListItemState extends State<AppListItem> {
|
||||
onSecondaryTapDown: buildPopupActions == null
|
||||
? null
|
||||
: (details) {
|
||||
showPopupMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
buildPopupActions(context),
|
||||
);
|
||||
final menuItems = buildPopupActions(context)
|
||||
.where((action) =>
|
||||
action.feature == null || hasFeature(action.feature!))
|
||||
.toList();
|
||||
if (menuItems.isNotEmpty) {
|
||||
showPopupMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
menuItems,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: () {
|
||||
if (isDesktop) {
|
||||
|
@ -51,3 +51,65 @@ abstract class ApplicationStateNotifier<T>
|
||||
state = AsyncValue.data(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Feature flags
|
||||
sealed class BaseFeature {
|
||||
String get path;
|
||||
String _subpath(String key);
|
||||
|
||||
Feature feature(String key, {bool enabled = true}) =>
|
||||
Feature._(this, key, enabled: enabled);
|
||||
}
|
||||
|
||||
class _RootFeature extends BaseFeature {
|
||||
_RootFeature._();
|
||||
@override
|
||||
String get path => '';
|
||||
|
||||
@override
|
||||
String _subpath(String key) => key;
|
||||
}
|
||||
|
||||
class Feature extends BaseFeature {
|
||||
final BaseFeature parent;
|
||||
final String key;
|
||||
final bool _defaultState;
|
||||
|
||||
Feature._(this.parent, this.key, {bool enabled = true})
|
||||
: _defaultState = enabled;
|
||||
|
||||
@override
|
||||
String get path => parent._subpath(key);
|
||||
|
||||
@override
|
||||
String _subpath(String key) => '$path.$key';
|
||||
}
|
||||
|
||||
final BaseFeature root = _RootFeature._();
|
||||
|
||||
typedef FeatureProvider = bool Function(Feature feature);
|
||||
|
||||
final featureFlagProvider =
|
||||
StateNotifierProvider<FeatureFlagsNotifier, Map<String, bool>>(
|
||||
(_) => FeatureFlagsNotifier());
|
||||
|
||||
class FeatureFlagsNotifier extends StateNotifier<Map<String, bool>> {
|
||||
FeatureFlagsNotifier() : super({});
|
||||
|
||||
void loadConfig(Map<String, dynamic> config) {
|
||||
const falsey = [0, false, null];
|
||||
state = {for (final k in config.keys) k: !falsey.contains(config[k])};
|
||||
}
|
||||
}
|
||||
|
||||
final featureProvider = Provider<FeatureProvider>((ref) {
|
||||
final featureMap = ref.watch(featureFlagProvider);
|
||||
|
||||
bool isEnabled(BaseFeature feature) => switch (feature) {
|
||||
_RootFeature() => true,
|
||||
Feature() => isEnabled(feature.parent) &&
|
||||
(featureMap[feature.path] ?? feature._defaultState),
|
||||
};
|
||||
|
||||
return isEnabled;
|
||||
});
|
||||
|
@ -157,6 +157,11 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
.toFilePath();
|
||||
}
|
||||
|
||||
// Locate feature flags file
|
||||
final featureFile = File(Uri.file(Platform.resolvedExecutable)
|
||||
.resolve('features.json')
|
||||
.toFilePath());
|
||||
|
||||
final rpcFuture = _initHelper(exe!);
|
||||
_initLicenses();
|
||||
|
||||
@ -167,12 +172,12 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
supportedAppsProvider.overrideWithValue([
|
||||
supportedAppsProvider.overrideWith(implementedApps([
|
||||
Application.oath,
|
||||
Application.fido,
|
||||
Application.piv,
|
||||
Application.management,
|
||||
]),
|
||||
])),
|
||||
prefProvider.overrideWithValue(prefs),
|
||||
rpcProvider.overrideWith((_) => rpcFuture),
|
||||
windowStateProvider.overrideWith(
|
||||
@ -218,15 +223,33 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
ref.read(rpcProvider).valueOrNull?.setLogLevel(level);
|
||||
});
|
||||
|
||||
// Load feature flags, if they exist
|
||||
featureFile.exists().then(
|
||||
(exists) async {
|
||||
if (exists) {
|
||||
try {
|
||||
final featureConfig =
|
||||
jsonDecode(await featureFile.readAsString());
|
||||
ref
|
||||
.read(featureFlagProvider.notifier)
|
||||
.loadConfig(featureConfig);
|
||||
} catch (error) {
|
||||
_log.error('Failed to parse feature flags', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Initialize systray
|
||||
ref.watch(systrayProvider);
|
||||
|
||||
// Show a loading or error page while the Helper isn't ready
|
||||
return ref.watch(rpcProvider).when(
|
||||
data: (data) => const MainPage(),
|
||||
error: (error, stackTrace) => AppFailurePage(cause: error),
|
||||
loading: () => _HelperWaiter(),
|
||||
);
|
||||
return Consumer(
|
||||
builder: (context, ref, child) => ref.watch(rpcProvider).when(
|
||||
data: (data) => const MainPage(),
|
||||
error: (error, stackTrace) => AppFailurePage(cause: error),
|
||||
loading: () => _HelperWaiter(),
|
||||
));
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
32
lib/fido/features.dart
Normal file
32
lib/fido/features.dart
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '../app/features.dart';
|
||||
|
||||
final actions = fido.feature('actions');
|
||||
|
||||
final actionsPin = actions.feature('pin');
|
||||
final actionsAddFingerprint = actions.feature('addFingerprint');
|
||||
final actionsReset = actions.feature('reset');
|
||||
|
||||
final credentials = fido.feature('credentials');
|
||||
|
||||
final credentialsDelete = credentials.feature('delete');
|
||||
|
||||
final fingerprints = fido.feature('fingerprints');
|
||||
|
||||
final fingerprintsEdit = fingerprints.feature('edit');
|
||||
final fingerprintsDelete = fingerprints.feature('delete');
|
@ -20,11 +20,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../features.dart' as features;
|
||||
|
||||
List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
|
||||
return [
|
||||
ActionItem(
|
||||
key: keys.editFingerintAction,
|
||||
feature: features.fingerprintsEdit,
|
||||
icon: const Icon(Icons.edit),
|
||||
title: l10n.s_rename_fp,
|
||||
subtitle: l10n.l_rename_fp_desc,
|
||||
@ -32,6 +34,7 @@ List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
|
||||
),
|
||||
ActionItem(
|
||||
key: keys.deleteFingerprintAction,
|
||||
feature: features.fingerprintsDelete,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete),
|
||||
title: l10n.s_delete_fingerprint,
|
||||
@ -45,6 +48,7 @@ List<ActionItem> buildCredentialActions(AppLocalizations l10n) {
|
||||
return [
|
||||
ActionItem(
|
||||
key: keys.deleteCredentialAction,
|
||||
feature: features.credentialsDelete,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete),
|
||||
title: l10n.s_delete_passkey,
|
||||
|
@ -7,6 +7,8 @@ import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../models.dart';
|
||||
import 'actions.dart';
|
||||
import 'delete_credential_dialog.dart';
|
||||
@ -26,29 +28,32 @@ class CredentialDialog extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
return Actions(
|
||||
actions: {
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteCredentialDialog(
|
||||
node.path,
|
||||
credential,
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
if (hasFeature(features.credentialsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteCredentialDialog(
|
||||
node.path,
|
||||
credential,
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
},
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
|
@ -7,7 +7,9 @@ import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../features.dart' as features;
|
||||
import 'actions.dart';
|
||||
import 'delete_fingerprint_dialog.dart';
|
||||
import 'rename_fingerprint_dialog.dart';
|
||||
@ -27,53 +29,56 @@ class FingerprintDialog extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return Actions(
|
||||
actions: {
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final Fingerprint? renamed =
|
||||
await withContext((context) async => await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameFingerprintDialog(
|
||||
node.path,
|
||||
fingerprint,
|
||||
),
|
||||
));
|
||||
if (renamed != null) {
|
||||
// Replace the dialog with the renamed credential
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FingerprintDialog(renamed);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return renamed;
|
||||
}),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteFingerprintDialog(
|
||||
node.path,
|
||||
fingerprint,
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
if (hasFeature(features.fingerprintsEdit))
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final Fingerprint? renamed =
|
||||
await withContext((context) async => await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameFingerprintDialog(
|
||||
node.path,
|
||||
fingerprint,
|
||||
),
|
||||
));
|
||||
if (renamed != null) {
|
||||
// Replace the dialog with the renamed credential
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return FingerprintDialog(renamed);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return renamed;
|
||||
}),
|
||||
if (hasFeature(features.fingerprintsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteFingerprintDialog(
|
||||
node.path,
|
||||
fingerprint,
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
},
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
|
@ -23,6 +23,7 @@ import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../models.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../features.dart' as features;
|
||||
import 'add_fingerprint_dialog.dart';
|
||||
import 'pin_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
@ -48,6 +49,7 @@ Widget fidoBuildActions(
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.addFingerprintAction,
|
||||
feature: features.actionsAddFingerprint,
|
||||
actionStyle: ActionStyle.primary,
|
||||
icon: const Icon(Icons.fingerprint_outlined),
|
||||
title: l10n.s_add_fingerprint,
|
||||
@ -76,6 +78,7 @@ Widget fidoBuildActions(
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
icon: const Icon(Icons.pin_outlined),
|
||||
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
|
||||
subtitle: state.hasPin
|
||||
@ -96,6 +99,7 @@ Widget fidoBuildActions(
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
title: l10n.s_reset_fido,
|
||||
|
@ -22,8 +22,10 @@ import '../../app/models.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../features.dart' as features;
|
||||
import 'key_actions.dart';
|
||||
|
||||
class FidoLockedPage extends ConsumerWidget {
|
||||
@ -35,6 +37,9 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final hasActions = hasFeature(features.actions);
|
||||
|
||||
if (!state.hasPin) {
|
||||
if (state.bioEnroll != null) {
|
||||
return MessagePage(
|
||||
@ -42,7 +47,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
graphic: noFingerprints,
|
||||
header: l10n.s_no_fingerprints,
|
||||
message: l10n.l_set_pin_fingerprints,
|
||||
keyActionsBuilder: _buildActions,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
} else {
|
||||
@ -53,7 +58,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
? l10n.l_no_discoverable_accounts
|
||||
: l10n.l_ready_to_use,
|
||||
message: l10n.l_optionally_set_a_pin,
|
||||
keyActionsBuilder: _buildActions,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
}
|
||||
@ -65,7 +70,7 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
graphic: manageAccounts,
|
||||
header: l10n.l_ready_to_use,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
keyActionsBuilder: _buildActions,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
}
|
||||
@ -75,14 +80,14 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
title: Text(l10n.s_webauthn),
|
||||
header: l10n.s_pin_change_required,
|
||||
message: l10n.l_pin_change_required_desc,
|
||||
keyActionsBuilder: _buildActions,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
}
|
||||
|
||||
return AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
keyActionsBuilder: _buildActions,
|
||||
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||
child: Column(
|
||||
children: [
|
||||
_PinEntryForm(state, node),
|
||||
|
@ -25,9 +25,11 @@ import '../../app/views/app_list_item.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../features.dart' as features;
|
||||
import 'actions.dart';
|
||||
import 'credential_dialog.dart';
|
||||
import 'delete_credential_dialog.dart';
|
||||
@ -45,7 +47,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
List<Widget> children = [];
|
||||
|
||||
if (state.credMgmt) {
|
||||
final data = ref.watch(credentialProvider(node.path)).asData;
|
||||
if (data == null) {
|
||||
@ -62,15 +66,16 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => CredentialDialog(cred),
|
||||
)),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteCredentialDialog(
|
||||
node.path,
|
||||
cred,
|
||||
if (hasFeature(features.credentialsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteCredentialDialog(
|
||||
node.path,
|
||||
cred,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
child: _CredentialListItem(cred),
|
||||
)));
|
||||
@ -95,33 +100,38 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (context) => FingerprintDialog(fp),
|
||||
)),
|
||||
EditIntent: CallbackAction<EditIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameFingerprintDialog(
|
||||
node.path,
|
||||
fp,
|
||||
),
|
||||
)),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteFingerprintDialog(
|
||||
node.path,
|
||||
fp,
|
||||
),
|
||||
)),
|
||||
if (hasFeature(features.fingerprintsEdit))
|
||||
EditIntent: CallbackAction<EditIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameFingerprintDialog(
|
||||
node.path,
|
||||
fp,
|
||||
),
|
||||
)),
|
||||
if (hasFeature(features.fingerprintsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||
onInvoke: (_) => showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteFingerprintDialog(
|
||||
node.path,
|
||||
fp,
|
||||
),
|
||||
)),
|
||||
},
|
||||
child: _FingerprintListItem(fp),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||
|
||||
if (children.isNotEmpty) {
|
||||
return AppPage(
|
||||
title: Text(l10n.s_webauthn),
|
||||
keyActionsBuilder: (context) =>
|
||||
fidoBuildActions(context, node, state, nFingerprints),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => fidoBuildActions(context, node, state, nFingerprints)
|
||||
: null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
||||
@ -134,8 +144,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
graphic: noFingerprints,
|
||||
header: l10n.s_no_fingerprints,
|
||||
message: l10n.l_add_one_or_more_fps,
|
||||
keyActionsBuilder: (context) =>
|
||||
fidoBuildActions(context, node, state, 0),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => fidoBuildActions(context, node, state, 0)
|
||||
: null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
}
|
||||
@ -145,7 +156,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
||||
graphic: manageAccounts,
|
||||
header: l10n.l_no_discoverable_accounts,
|
||||
message: l10n.l_register_sk_on_websites,
|
||||
keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => fidoBuildActions(context, node, state, 0)
|
||||
: null,
|
||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||
);
|
||||
}
|
||||
|
31
lib/oath/features.dart
Normal file
31
lib/oath/features.dart
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '../app/features.dart';
|
||||
|
||||
final actions = oath.feature('actions');
|
||||
|
||||
final actionsAdd = actions.feature('add');
|
||||
final actionsIcons = actions.feature('icons');
|
||||
final actionsPassword = actions.feature('password');
|
||||
final actionsReset = actions.feature('reset');
|
||||
|
||||
final accounts = actions.feature('accounts');
|
||||
|
||||
final accountsClipboard = accounts.feature('clipboard');
|
||||
final accountsPin = accounts.feature('pin');
|
||||
final accountsRename = accounts.feature('rename');
|
||||
final accountsDelete = accounts.feature('delete');
|
@ -27,6 +27,7 @@ import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../core/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'account_helper.dart';
|
||||
@ -49,6 +50,7 @@ class AccountDialog extends ConsumerWidget {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
final helper = AccountHelper(context, ref, credential);
|
||||
final subtitle = helper.subtitle;
|
||||
|
||||
@ -56,54 +58,58 @@ class AccountDialog extends ConsumerWidget {
|
||||
credential,
|
||||
ref: ref,
|
||||
actions: {
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final credentials = ref.read(credentialsProvider);
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final renamed =
|
||||
await withContext((context) async => await showBlurDialog(
|
||||
if (hasFeature(features.accountsRename))
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final credentials = ref.read(credentialsProvider);
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final renamed =
|
||||
await withContext((context) async => await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||
ref,
|
||||
node,
|
||||
credential,
|
||||
credentials
|
||||
?.map((e) => (e.issuer, e.name))
|
||||
.toList() ??
|
||||
[],
|
||||
)));
|
||||
if (renamed != null) {
|
||||
// Replace the dialog with the renamed credential
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||
ref,
|
||||
builder: (context) {
|
||||
return AccountDialog(renamed);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return renamed;
|
||||
}),
|
||||
if (hasFeature(features.accountsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteAccountDialog(
|
||||
node,
|
||||
credential,
|
||||
credentials?.map((e) => (e.issuer, e.name)).toList() ??
|
||||
[],
|
||||
)));
|
||||
if (renamed != null) {
|
||||
// Replace the dialog with the renamed credential
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AccountDialog(renamed);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
return renamed;
|
||||
}),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
final bool? deleted =
|
||||
await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteAccountDialog(
|
||||
node,
|
||||
credential,
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
),
|
||||
) ??
|
||||
false);
|
||||
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
// Pop the account dialog if deleted
|
||||
if (deleted == true) {
|
||||
await withContext((context) async {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
}
|
||||
return deleted;
|
||||
}),
|
||||
},
|
||||
builder: (context) {
|
||||
if (helper.code == null &&
|
||||
|
@ -27,6 +27,7 @@ import '../../app/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/circle_timer.dart';
|
||||
import '../../widgets/custom_icons.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../keys.dart' as keys;
|
||||
@ -68,6 +69,7 @@ class AccountHelper {
|
||||
return [
|
||||
ActionItem(
|
||||
key: keys.copyAction,
|
||||
feature: features.accountsClipboard,
|
||||
icon: const Icon(Icons.copy),
|
||||
title: l10n.l_copy_to_clipboard,
|
||||
subtitle: l10n.l_copy_code_desc,
|
||||
@ -87,6 +89,7 @@ class AccountHelper {
|
||||
),
|
||||
ActionItem(
|
||||
key: keys.togglePinAction,
|
||||
feature: features.accountsPin,
|
||||
icon: pinned
|
||||
? pushPinStrokeIcon
|
||||
: const Icon(Icons.push_pin_outlined),
|
||||
@ -97,6 +100,7 @@ class AccountHelper {
|
||||
if (data.info.version.isAtLeast(5, 3))
|
||||
ActionItem(
|
||||
key: keys.editAction,
|
||||
feature: features.accountsRename,
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
title: l10n.s_rename_account,
|
||||
subtitle: l10n.l_rename_account_desc,
|
||||
@ -104,6 +108,7 @@ class AccountHelper {
|
||||
),
|
||||
ActionItem(
|
||||
key: keys.deleteAction,
|
||||
feature: features.accountsDelete,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
title: l10n.s_delete_account,
|
||||
|
@ -23,7 +23,9 @@ import '../../app/message.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/views/app_list_item.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../state.dart';
|
||||
import 'account_dialog.dart';
|
||||
import 'account_helper.dart';
|
||||
@ -81,6 +83,7 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final darkMode = theme.brightness == Brightness.dark;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
return registerOathActions(
|
||||
credential,
|
||||
@ -94,29 +97,31 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final node = ref.read(currentDeviceProvider)!;
|
||||
final credentials = ref.read(credentialsProvider);
|
||||
final withContext = ref.read(withContextProvider);
|
||||
return await withContext((context) async => await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||
ref,
|
||||
node,
|
||||
credential,
|
||||
credentials?.map((e) => (e.issuer, e.name)).toList() ?? [],
|
||||
),
|
||||
));
|
||||
}),
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final node = ref.read(currentDeviceProvider)!;
|
||||
return await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteAccountDialog(node, credential),
|
||||
) ??
|
||||
false);
|
||||
}),
|
||||
if (hasFeature(features.accountsRename))
|
||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||
final node = ref.read(currentDeviceProvider)!;
|
||||
final credentials = ref.read(credentialsProvider);
|
||||
final withContext = ref.read(withContextProvider);
|
||||
return await withContext((context) async => await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||
ref,
|
||||
node,
|
||||
credential,
|
||||
credentials?.map((e) => (e.issuer, e.name)).toList() ?? [],
|
||||
),
|
||||
));
|
||||
}),
|
||||
if (hasFeature(features.accountsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final node = ref.read(currentDeviceProvider)!;
|
||||
return await ref.read(withContextProvider)((context) async =>
|
||||
await showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => DeleteAccountDialog(node, credential),
|
||||
) ??
|
||||
false);
|
||||
}),
|
||||
},
|
||||
builder: (context) {
|
||||
final helper = AccountHelper(context, ref, credential);
|
||||
@ -162,7 +167,9 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
||||
onPressed:
|
||||
Actions.handler(context, const OpenIntent()),
|
||||
child: helper.buildCodeIcon()),
|
||||
activationIntent: const CopyIntent(),
|
||||
activationIntent: hasFeature(features.accountsClipboard)
|
||||
? const CopyIntent()
|
||||
: const OpenIntent(),
|
||||
buildPopupActions: (_) => helper.buildActions(),
|
||||
),
|
||||
));
|
||||
|
@ -21,8 +21,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../app/message.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../exception/cancellation_exception.dart';
|
||||
import '../models.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../state.dart';
|
||||
|
||||
class TogglePinIntent extends Intent {
|
||||
@ -46,18 +48,20 @@ Widget registerOathActions(
|
||||
required WidgetRef ref,
|
||||
required Widget Function(BuildContext context) builder,
|
||||
Map<Type, Action<Intent>> actions = const {},
|
||||
}) =>
|
||||
Actions(
|
||||
actions: {
|
||||
RefreshIntent: CallbackAction<RefreshIntent>(onInvoke: (_) {
|
||||
final code = ref.read(codeProvider(credential));
|
||||
if (!(credential.oathType == OathType.totp &&
|
||||
code != null &&
|
||||
!ref.read(expiredProvider(code.validTo)))) {
|
||||
return _calculateCode(credential, ref);
|
||||
}
|
||||
return code;
|
||||
}),
|
||||
}) {
|
||||
final hasFeature = ref.read(featureProvider);
|
||||
return Actions(
|
||||
actions: {
|
||||
RefreshIntent: CallbackAction<RefreshIntent>(onInvoke: (_) {
|
||||
final code = ref.read(codeProvider(credential));
|
||||
if (!(credential.oathType == OathType.totp &&
|
||||
code != null &&
|
||||
!ref.read(expiredProvider(code.validTo)))) {
|
||||
return _calculateCode(credential, ref);
|
||||
}
|
||||
return code;
|
||||
}),
|
||||
if (hasFeature(features.accountsClipboard))
|
||||
CopyIntent: CallbackAction<CopyIntent>(onInvoke: (_) async {
|
||||
var code = ref.read(codeProvider(credential));
|
||||
if (code == null ||
|
||||
@ -77,11 +81,13 @@ Widget registerOathActions(
|
||||
}
|
||||
return code;
|
||||
}),
|
||||
if (hasFeature(features.accountsPin))
|
||||
TogglePinIntent: CallbackAction<TogglePinIntent>(onInvoke: (_) {
|
||||
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
||||
return null;
|
||||
}),
|
||||
...actions,
|
||||
},
|
||||
child: Builder(builder: builder),
|
||||
);
|
||||
...actions,
|
||||
},
|
||||
child: Builder(builder: builder),
|
||||
);
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../state.dart';
|
||||
import 'add_account_page.dart';
|
||||
@ -49,6 +50,7 @@ Widget oathBuildActions(
|
||||
children: [
|
||||
ActionListSection(l10n.s_setup, children: [
|
||||
ActionListItem(
|
||||
feature: features.actionsAdd,
|
||||
title: l10n.s_add_account,
|
||||
subtitle: used == null
|
||||
? l10n.l_unlock_first
|
||||
@ -104,6 +106,7 @@ Widget oathBuildActions(
|
||||
ActionListSection(l10n.s_manage, children: [
|
||||
ActionListItem(
|
||||
key: keys.customIconsAction,
|
||||
feature: features.actionsIcons,
|
||||
title: l10n.s_custom_icons,
|
||||
subtitle: l10n.l_set_icons_for_accounts,
|
||||
icon: const Icon(Icons.image_outlined),
|
||||
@ -118,6 +121,7 @@ Widget oathBuildActions(
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.setOrManagePasswordAction,
|
||||
feature: features.actionsPassword,
|
||||
title: oathState.hasKey
|
||||
? l10n.s_manage_password
|
||||
: l10n.s_set_password,
|
||||
@ -133,6 +137,7 @@ Widget oathBuildActions(
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_oath,
|
||||
|
@ -25,6 +25,8 @@ import '../../app/views/app_failure_page.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/graphics.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../features.dart' as features;
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
@ -65,10 +67,12 @@ class _LockedView extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||
return AppPage(
|
||||
title: Text(AppLocalizations.of(context)!.s_authenticator),
|
||||
keyActionsBuilder: (context) =>
|
||||
oathBuildActions(context, devicePath, oathState, ref),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => oathBuildActions(context, devicePath, oathState, ref)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
child: UnlockForm(
|
||||
@ -114,15 +118,18 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
// ONLY rebuild if the number of credentials changes.
|
||||
final numCreds = ref.watch(credentialListProvider(widget.devicePath)
|
||||
.select((value) => value?.length));
|
||||
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||
if (numCreds == 0) {
|
||||
return MessagePage(
|
||||
title: Text(l10n.s_authenticator),
|
||||
key: keys.noAccountsView,
|
||||
graphic: noAccounts,
|
||||
header: l10n.s_no_accounts,
|
||||
keyActionsBuilder: (context) => oathBuildActions(
|
||||
context, widget.devicePath, widget.oathState, ref,
|
||||
used: 0),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => oathBuildActions(
|
||||
context, widget.devicePath, widget.oathState, ref,
|
||||
used: 0)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return Actions(
|
||||
@ -184,13 +191,15 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
);
|
||||
}),
|
||||
),
|
||||
keyActionsBuilder: (context) => oathBuildActions(
|
||||
context,
|
||||
widget.devicePath,
|
||||
widget.oathState,
|
||||
ref,
|
||||
used: numCreds ?? 0,
|
||||
),
|
||||
keyActionsBuilder: hasActions
|
||||
? (context) => oathBuildActions(
|
||||
context,
|
||||
widget.devicePath,
|
||||
widget.oathState,
|
||||
ref,
|
||||
used: numCreds ?? 0,
|
||||
)
|
||||
: null,
|
||||
centered: numCreds == null,
|
||||
delayedContent: numCreds == null,
|
||||
child: numCreds != null
|
||||
|
31
lib/piv/features.dart
Normal file
31
lib/piv/features.dart
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import '../app/features.dart';
|
||||
|
||||
final actions = piv.feature('actions');
|
||||
|
||||
final actionsPin = actions.feature('pin');
|
||||
final actionsPuk = actions.feature('puk');
|
||||
final actionsManagementKey = actions.feature('managementKey');
|
||||
final actionsReset = actions.feature('reset');
|
||||
|
||||
final slots = piv.feature('slots');
|
||||
|
||||
final slotsGenerate = slots.feature('generate');
|
||||
final slotsImport = slots.feature('import');
|
||||
final slotsExport = slots.feature('export');
|
||||
final slotsDelete = slots.feature('delete');
|
@ -25,9 +25,11 @@ import '../../app/message.dart';
|
||||
import '../../app/shortcuts.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../features.dart' as features;
|
||||
import 'authentication_dialog.dart';
|
||||
import 'delete_certificate_dialog.dart';
|
||||
import 'generate_key_dialog.dart';
|
||||
@ -75,9 +77,11 @@ Widget registerPivActions(
|
||||
required WidgetRef ref,
|
||||
required Widget Function(BuildContext context) builder,
|
||||
Map<Type, Action<Intent>> actions = const {},
|
||||
}) =>
|
||||
Actions(
|
||||
actions: {
|
||||
}) {
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return Actions(
|
||||
actions: {
|
||||
if (hasFeature(features.slotsGenerate))
|
||||
GenerateIntent:
|
||||
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
@ -129,6 +133,7 @@ Widget registerPivActions(
|
||||
return result != null;
|
||||
});
|
||||
}),
|
||||
if (hasFeature(features.slotsImport))
|
||||
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
|
||||
@ -164,6 +169,7 @@ Widget registerPivActions(
|
||||
) ??
|
||||
false);
|
||||
}),
|
||||
if (hasFeature(features.slotsExport))
|
||||
ExportIntent: CallbackAction<ExportIntent>(onInvoke: (intent) async {
|
||||
final (_, cert) = await ref
|
||||
.read(pivSlotsProvider(devicePath).notifier)
|
||||
@ -198,6 +204,7 @@ Widget registerPivActions(
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
if (hasFeature(features.slotsDelete))
|
||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||
final withContext = ref.read(withContextProvider);
|
||||
if (!await withContext(
|
||||
@ -216,15 +223,17 @@ Widget registerPivActions(
|
||||
false);
|
||||
return deleted;
|
||||
}),
|
||||
...actions,
|
||||
},
|
||||
child: Builder(builder: builder),
|
||||
);
|
||||
...actions,
|
||||
},
|
||||
child: Builder(builder: builder),
|
||||
);
|
||||
}
|
||||
|
||||
List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
||||
return [
|
||||
ActionItem(
|
||||
key: keys.generateAction,
|
||||
feature: features.slotsGenerate,
|
||||
icon: const Icon(Icons.add_outlined),
|
||||
actionStyle: ActionStyle.primary,
|
||||
title: l10n.s_generate_key,
|
||||
@ -233,6 +242,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
||||
),
|
||||
ActionItem(
|
||||
key: keys.importAction,
|
||||
feature: features.slotsImport,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
title: l10n.l_import_file,
|
||||
subtitle: l10n.l_import_desc,
|
||||
@ -241,6 +251,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
||||
if (hasCert) ...[
|
||||
ActionItem(
|
||||
key: keys.exportAction,
|
||||
feature: features.slotsExport,
|
||||
icon: const Icon(Icons.file_upload_outlined),
|
||||
title: l10n.l_export_certificate,
|
||||
subtitle: l10n.l_export_certificate_desc,
|
||||
@ -248,6 +259,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
||||
),
|
||||
ActionItem(
|
||||
key: keys.deleteAction,
|
||||
feature: features.slotsDelete,
|
||||
actionStyle: ActionStyle.error,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
title: l10n.l_delete_certificate,
|
||||
|
@ -24,6 +24,7 @@ import '../../app/views/fs_dialog.dart';
|
||||
import '../../app/views/action_list.dart';
|
||||
import '../models.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../features.dart' as features;
|
||||
import 'manage_key_dialog.dart';
|
||||
import 'manage_pin_puk_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
@ -49,6 +50,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
children: [
|
||||
ActionListItem(
|
||||
key: keys.managePinAction,
|
||||
feature: features.actionsPin,
|
||||
title: l10n.s_pin,
|
||||
subtitle: pinBlocked
|
||||
? (pukAttempts != 0
|
||||
@ -73,6 +75,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.managePukAction,
|
||||
feature: features.actionsPuk,
|
||||
title: l10n.s_puk,
|
||||
subtitle: pukAttempts != null
|
||||
? (pukAttempts == 0
|
||||
@ -93,6 +96,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
: null),
|
||||
ActionListItem(
|
||||
key: keys.manageManagementKeyAction,
|
||||
feature: features.actionsManagementKey,
|
||||
title: l10n.s_management_key,
|
||||
subtitle: usingDefaultMgmtKey
|
||||
? l10n.l_warning_default_key
|
||||
@ -110,6 +114,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
||||
}),
|
||||
ActionListItem(
|
||||
key: keys.resetAction,
|
||||
feature: features.actionsReset,
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
actionStyle: ActionStyle.error,
|
||||
title: l10n.s_reset_piv,
|
||||
|
@ -25,9 +25,11 @@ import '../../app/views/app_failure_page.dart';
|
||||
import '../../app/views/app_list_item.dart';
|
||||
import '../../app/views/app_page.dart';
|
||||
import '../../app/views/message_page.dart';
|
||||
import '../../core/state.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import '../features.dart' as features;
|
||||
import 'actions.dart';
|
||||
import 'key_actions.dart';
|
||||
import 'slot_dialog.dart';
|
||||
@ -40,6 +42,7 @@ class PivScreen extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
return ref.watch(pivStateProvider(devicePath)).when(
|
||||
loading: () => MessagePage(
|
||||
title: Text(l10n.s_piv),
|
||||
@ -54,8 +57,10 @@ class PivScreen extends ConsumerWidget {
|
||||
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
||||
return AppPage(
|
||||
title: Text(l10n.s_piv),
|
||||
keyActionsBuilder: (context) =>
|
||||
pivBuildActions(context, devicePath, pivState, ref),
|
||||
keyActionsBuilder: hasFeature(features.actions)
|
||||
? (context) =>
|
||||
pivBuildActions(context, devicePath, pivState, ref)
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTitle(l10n.s_certificates),
|
||||
@ -86,16 +91,17 @@ class PivScreen extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _CertificateListItem extends StatelessWidget {
|
||||
class _CertificateListItem extends ConsumerWidget {
|
||||
final PivSlot pivSlot;
|
||||
const _CertificateListItem(this.pivSlot);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final slot = pivSlot.slot;
|
||||
final certInfo = pivSlot.certInfo;
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasFeature = ref.watch(featureProvider);
|
||||
|
||||
return Semantics(
|
||||
label: slot.getDisplayName(l10n),
|
||||
@ -116,8 +122,9 @@ class _CertificateListItem extends StatelessWidget {
|
||||
onPressed: Actions.handler(context, const OpenIntent()),
|
||||
child: const Icon(Icons.more_horiz),
|
||||
),
|
||||
buildPopupActions: (context) =>
|
||||
buildSlotActions(certInfo != null, l10n),
|
||||
buildPopupActions: hasFeature(features.slots)
|
||||
? (context) => buildSlotActions(certInfo != null, l10n)
|
||||
: null,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user