/* * Copyright (C) 2022 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 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:yubico_authenticator/app/logging.dart'; import '../core/state.dart'; import 'models.dart'; final _log = Logger('app.state'); // Override this to alter the set of supported apps. final supportedAppsProvider = Provider>((ref) => Application.values); // Default implementation is always focused, override with platform specific version. final windowStateProvider = Provider( (ref) => WindowState(focused: true, visible: true, active: true), ); final supportedThemesProvider = StateProvider>( (ref) => throw UnimplementedError(), ); final themeModeProvider = StateNotifierProvider( (ref) => ThemeModeNotifier( ref.watch(prefProvider), ref.read(supportedThemesProvider)), ); class ThemeModeNotifier extends StateNotifier { static const String _key = 'APP_STATE_THEME'; final SharedPreferences _prefs; ThemeModeNotifier(this._prefs, List supportedThemes) : super(_fromName(_prefs.getString(_key), supportedThemes)); void setThemeMode(ThemeMode mode) { _log.debug('Set theme to $mode'); state = mode; _prefs.setString(_key, mode.name); } static ThemeMode _fromName(String? name, List supportedThemes) => supportedThemes.firstWhere((element) => element.name == name, orElse: () => supportedThemes.first); } // Override with platform implementation final attachedDevicesProvider = StateNotifierProvider>( (ref) => AttachedDevicesNotifier([]), ); class AttachedDevicesNotifier extends StateNotifier> { AttachedDevicesNotifier(super.state); /// Force a refresh of all device data. void refresh() {} } // Override with platform implementation final currentDeviceDataProvider = Provider>( (ref) => throw UnimplementedError(), ); // Override with platform implementation final currentDeviceProvider = StateNotifierProvider( (ref) => throw UnimplementedError()); abstract class CurrentDeviceNotifier extends StateNotifier { CurrentDeviceNotifier(super.state); setCurrentDevice(DeviceNode? device); } final currentAppProvider = StateNotifierProvider((ref) { final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider)); ref.listen>(currentDeviceDataProvider, (_, data) { notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data))); }, fireImmediately: true); return notifier; }); class CurrentAppNotifier extends StateNotifier { final List _supportedApps; CurrentAppNotifier(this._supportedApps) : super(_supportedApps.first); void setCurrentApp(Application app) { state = app; } 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, ); } } abstract class QrScanner { /// 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 scanQr([String? imageData]); } final qrScannerProvider = Provider( (ref) => null, ); final contextConsumer = StateNotifierProvider( (ref) => ContextConsumer()); class ContextConsumer extends StateNotifier { ContextConsumer() : super(null); Future withContext(Future Function(BuildContext context) action) { final completer = Completer(); if (mounted) { state = (context) async { completer.complete(await action(context)); }; } else { completer.completeError('Not attached'); } return completer.future; } } abstract class AppClipboard { const AppClipboard(); Future setText(String toClipboard, {bool isSensitive = false}); bool platformGivesFeedback(); } final clipboardProvider = Provider( (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 Function( Future Function(BuildContext context) action); final withContextProvider = Provider( (ref) => ref.watch(contextConsumer.notifier).withContext);