yubioath-flutter/lib/app/state.dart

317 lines
9.5 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
2023-10-10 09:54:25 +03:00
* Copyright (C) 2022,2024 Yubico.
2022-10-04 13:12:54 +03:00
*
* 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 'dart:async';
import 'dart:io';
2023-02-24 16:20:17 +03:00
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
2021-11-19 17:05:57 +03:00
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
2021-11-23 16:51:36 +03:00
import 'package:shared_preferences/shared_preferences.dart';
2021-11-19 17:05:57 +03:00
import '../core/state.dart';
2023-10-10 09:54:25 +03:00
import '../theme.dart';
import 'features.dart' as features;
2024-01-11 18:56:27 +03:00
import 'key_customization.dart';
import 'logging.dart';
import 'models.dart';
2021-11-19 17:05:57 +03:00
final _log = Logger('app.state');
// Officially supported translations
const officialLocales = [
Locale('en', ''),
];
// Override this to alter the set of supported apps.
final supportedAppsProvider =
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>(
(ref) => WindowState(focused: true, visible: true, active: true),
);
2022-09-23 11:17:28 +03:00
final supportedThemesProvider = StateProvider<List<ThemeMode>>(
(ref) => throw UnimplementedError(),
);
final communityTranslationsProvider =
StateNotifierProvider<CommunityTranslationsNotifier, bool>(
(ref) => CommunityTranslationsNotifier(ref.watch(prefProvider)));
2023-02-24 16:20:17 +03:00
class CommunityTranslationsNotifier extends StateNotifier<bool> {
static const String _key = 'APP_STATE_ENABLE_COMMUNITY_TRANSLATIONS';
final SharedPreferences _prefs;
CommunityTranslationsNotifier(this._prefs)
: super(_prefs.getBool(_key) == true);
2023-02-24 16:20:17 +03:00
void setEnableCommunityTranslations(bool value) {
state = value;
_prefs.setBool(_key, value);
2023-03-01 12:57:42 +03:00
}
}
final supportedLocalesProvider = Provider<List<Locale>>((ref) {
final locales = [...officialLocales];
final localeStr = Platform.environment['_YA_LOCALE'];
if (localeStr != null) {
// Force locale
final locale = Locale(localeStr, '');
locales.add(locale);
}
return ref.watch(communityTranslationsProvider)
? AppLocalizations.supportedLocales
: locales;
});
2023-02-24 16:20:17 +03:00
final currentLocaleProvider = Provider<Locale>(
(ref) {
final localeStr = Platform.environment['_YA_LOCALE'];
if (localeStr != null) {
// Force locale
final locale = Locale(localeStr, '');
return basicLocaleListResolution(
[locale], AppLocalizations.supportedLocales);
}
// Choose from supported
return basicLocaleListResolution(PlatformDispatcher.instance.locales,
ref.watch(supportedLocalesProvider));
},
);
2023-02-24 16:20:17 +03:00
final l10nProvider = Provider<AppLocalizations>(
(ref) => lookupAppLocalizations(ref.watch(currentLocaleProvider)),
);
2023-02-24 16:20:17 +03:00
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
2024-01-11 18:56:27 +03:00
(ref) {
// initialize the keyCustomizationManager
ref.read(keyCustomizationManagerProvider);
return ThemeModeNotifier(
ref.watch(prefProvider), ref.read(supportedThemesProvider));
},
);
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
static const String _key = 'APP_STATE_THEME';
final SharedPreferences _prefs;
2022-09-22 18:48:06 +03:00
ThemeModeNotifier(this._prefs, List<ThemeMode> supportedThemes)
: super(_fromName(_prefs.getString(_key), supportedThemes));
void setThemeMode(ThemeMode mode) {
2022-05-03 12:24:25 +03:00
_log.debug('Set theme to $mode');
state = mode;
_prefs.setString(_key, mode.name);
}
2022-09-22 18:48:06 +03:00
static ThemeMode _fromName(String? name, List<ThemeMode> supportedThemes) =>
supportedThemes.firstWhere((element) => element.name == name,
orElse: () => supportedThemes.first);
}
2023-10-10 09:54:25 +03:00
final primaryColorProvider = Provider<Color?>((ref) => null);
2024-01-11 18:56:27 +03:00
final darkThemeProvider = NotifierProvider<ThemeNotifier, ThemeData>(
() => ThemeNotifier(ThemeMode.dark),
2023-10-10 09:54:25 +03:00
);
2024-01-11 18:56:27 +03:00
final lightThemeProvider = NotifierProvider<ThemeNotifier, ThemeData>(
() => ThemeNotifier(ThemeMode.light),
2023-10-10 09:54:25 +03:00
);
2024-01-11 18:56:27 +03:00
class ThemeNotifier extends Notifier<ThemeData> {
2023-10-10 09:54:25 +03:00
final ThemeMode _themeMode;
2024-01-11 18:56:27 +03:00
ThemeNotifier(this._themeMode);
@override
ThemeData build() {
return _get(
_themeMode,
yubiKeyData: ref.watch(currentDeviceDataProvider).valueOrNull,
);
}
2023-10-10 09:54:25 +03:00
static ThemeData _getDefault(ThemeMode themeMode) =>
themeMode == ThemeMode.light ? AppTheme.lightTheme : AppTheme.darkTheme;
2024-01-11 18:56:27 +03:00
ThemeData _get(ThemeMode themeMode, {YubiKeyData? yubiKeyData}) {
Color? primaryColor;
if (yubiKeyData != null) {
final manager = ref.read(keyCustomizationManagerProvider);
final customization = manager.get(yubiKeyData.info.serial?.toString());
String? displayColorCustomization =
customization?.properties['display_color'];
if (displayColorCustomization != null) {
primaryColor = Color(int.parse(displayColorCustomization, radix: 16));
}
}
primaryColor ??= ref.read(primaryColorProvider);
return (primaryColor != null)
? _getDefault(themeMode).copyWith(
colorScheme: ColorScheme.fromSeed(
brightness: themeMode == ThemeMode.dark
? Brightness.dark
: Brightness.light,
seedColor: primaryColor)
.copyWith(primary: primaryColor))
: _getDefault(themeMode);
}
2023-10-10 09:54:25 +03:00
void setPrimaryColor(Color? primaryColor) {
_log.debug('Set primary color to $primaryColor');
2024-01-11 18:56:27 +03:00
state = _get(_themeMode);
2023-10-10 09:54:25 +03:00
}
}
// Override with platform implementation
2022-03-07 11:57:29 +03:00
final attachedDevicesProvider =
NotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>(
() => throw UnimplementedError(),
);
2022-01-12 14:49:04 +03:00
abstract class AttachedDevicesNotifier extends Notifier<List<DeviceNode>> {
2022-03-07 11:57:29 +03:00
/// Force a refresh of all device data.
void refresh() {}
}
// Override with platform implementation
final currentDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) => throw UnimplementedError(),
);
2022-01-12 14:49:04 +03:00
// Override with platform implementation
2021-11-19 17:05:57 +03:00
final currentDeviceProvider =
NotifierProvider<CurrentDeviceNotifier, DeviceNode?>(
() => throw UnimplementedError());
abstract class CurrentDeviceNotifier extends Notifier<DeviceNode?> {
setCurrentDevice(DeviceNode? device);
2021-11-19 17:05:57 +03:00
}
final currentAppProvider =
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider));
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
}, fireImmediately: true);
return notifier;
});
class CurrentAppNotifier extends StateNotifier<Application> {
final List<Application> _supportedApps;
CurrentAppNotifier(this._supportedApps) : super(_supportedApps.first);
2021-11-19 17:05:57 +03:00
void setCurrentApp(Application app) {
state = app;
2021-11-19 17:05:57 +03:00
}
void _notifyDeviceChanged(YubiKeyData? data) {
if (data == null ||
state.getAvailability(data) != Availability.unsupported) {
// Keep current app
return;
}
state = _supportedApps.firstWhere(
(app) => app.getAvailability(data) == Availability.enabled,
orElse: () => _supportedApps.first,
);
}
}
2022-02-10 17:24:28 +03:00
abstract class QrScanner {
2022-06-13 17:45:26 +03:00
/// Scans (or searches the given image) for a QR code, and decodes it.
///
/// The contained data is returned as a String, or null, if no QR code is
/// found.
Future<String?> scanQr([String? imageData]);
2022-02-10 17:24:28 +03:00
}
final qrScannerProvider = Provider<QrScanner?>(
(ref) => null,
);
final contextConsumer =
StateNotifierProvider<ContextConsumer, Function(BuildContext)?>(
(ref) => ContextConsumer());
class ContextConsumer extends StateNotifier<Function(BuildContext)?> {
ContextConsumer() : super(null);
Future<T> withContext<T>(Future<T> Function(BuildContext context) action) {
final completer = Completer<T>();
if (mounted) {
state = (context) async {
completer.complete(await action(context));
};
} else {
completer.completeError('Not attached');
}
return completer.future;
}
}
abstract class AppClipboard {
const AppClipboard();
Future<void> setText(String toClipboard, {bool isSensitive = false});
bool platformGivesFeedback();
}
final clipboardProvider = Provider<AppClipboard>(
(ref) => throw UnimplementedError(),
);
/// A callback which will be invoked with a [BuildContext] that can be used to
/// open dialogs, show Snackbars, etc.
///
/// Used with the [withContextProvider] provider.
typedef WithContext = Future<T> Function<T>(
Future<T> Function(BuildContext context) action);
final withContextProvider = Provider<WithContext>(
(ref) => ref.watch(contextConsumer.notifier).withContext);