/* * 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:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../app/models.dart'; import '../app/state.dart'; import '../core/state.dart'; import 'models.dart'; final accountsSearchProvider = StateNotifierProvider( (ref) => AccountsSearchNotifier()); class AccountsSearchNotifier extends StateNotifier { AccountsSearchNotifier() : super(''); void setFilter(String value) { state = value; } } final oathLayoutProvider = StateNotifierProvider.autoDispose((ref) { final device = ref.watch(currentDeviceProvider); List credentials = device != null ? ref.read(filteredCredentialsProvider( ref.read(credentialListProvider(device.path)) ?? [])) : []; final favorites = ref.watch(favoritesProvider); final pinnedCreds = credentials.where((entry) => favorites.contains(entry.credential.id)); return OathLayoutNotfier('OATH_STATE_LAYOUT', ref.watch(prefProvider), credentials, pinnedCreds.toList()); }); class OathLayoutNotfier extends StateNotifier { final String _key; final SharedPreferences _prefs; OathLayoutNotfier(this._key, this._prefs, List credentials, List pinnedCredentials) : super( _fromName(_prefs.getString(_key), credentials, pinnedCredentials)); void setLayout(OathLayout layout) { state = layout; _prefs.setString(_key, layout.name); } static OathLayout _fromName(String? name, List credentials, List pinnedCredentials) { final layout = OathLayout.values.firstWhere( (element) => element.name == name, orElse: () => OathLayout.list, ); // Default to list view if current key does not have // pinned credentials if (layout == OathLayout.mixed) { if (pinnedCredentials.isEmpty) { return OathLayout.list; } if (pinnedCredentials.length == credentials.length) { return OathLayout.grid; } } return layout; } } final oathStateProvider = AsyncNotifierProvider.autoDispose .family( () => throw UnimplementedError(), ); abstract class OathStateNotifier extends ApplicationStateNotifier { Future reset(); /// Unlocks the session and returns a record of `success`, `remembered`. Future<(bool, bool)> unlock(String password, {bool remember = false}); Future setPassword(String? current, String password); Future unsetPassword(String current); Future forgetPassword(); } final credentialListProvider = StateNotifierProvider.autoDispose .family?, DevicePath>( (ref, arg) => throw UnimplementedError(), ); abstract class OathCredentialListNotifier extends StateNotifier?> { OathCredentialListNotifier() : super(null); @override @protected set state(List? value) { super.state = value != null ? List.unmodifiable(value ..sort((a, b) { String searchKey(OathCredential c) => ((c.issuer ?? '') + c.name).toLowerCase(); return searchKey(a.credential).compareTo(searchKey(b.credential)); })) : null; } Future calculate(OathCredential credential); Future addAccount(Uri otpauth, {bool requireTouch = false}); Future renameAccount( OathCredential credential, String? issuer, String name); Future deleteAccount(OathCredential credential); } final credentialsProvider = StateNotifierProvider.autoDispose< _CredentialsProviderNotifier, List?>((ref) { final provider = _CredentialsProviderNotifier(); final node = ref.watch(currentDeviceProvider); if (node != null) { ref.listen?>(credentialListProvider(node.path), (previous, next) { provider._updatePairs(next); }, fireImmediately: true); } return provider; }); class _CredentialsProviderNotifier extends StateNotifier?> { _CredentialsProviderNotifier() : super(null); void _updatePairs(List? pairs) { if (mounted) { if (pairs == null) { if (state != null) { state = null; } } else { final creds = pairs.map((p) => p.credential).toList(); if (!const ListEquality().equals(creds, state)) { state = creds; } } } } } final codeProvider = Provider.autoDispose.family((ref, credential) { final node = ref.watch(currentDeviceProvider); if (node != null) { return ref .watch(credentialListProvider(node.path) .select((pairs) => pairs?.firstWhere( (pair) => pair.credential == credential, orElse: () => OathPair(credential, null), ))) ?.code; } return null; }); final expiredProvider = StateNotifierProvider.autoDispose.family<_ExpireNotifier, bool, int>( (ref, expiry) => _ExpireNotifier(DateTime.now().millisecondsSinceEpoch, expiry * 1000), ); class _ExpireNotifier extends StateNotifier { Timer? _timer; _ExpireNotifier(int now, int expiry) : super(expiry <= now) { if (expiry > now) { _timer = Timer(Duration(milliseconds: expiry - now), () { if (mounted) { state = true; } }); } } @override void dispose() { _timer?.cancel(); super.dispose(); } } final favoritesProvider = StateNotifierProvider>( (ref) => FavoritesNotifier(ref.watch(prefProvider))); class FavoritesNotifier extends StateNotifier> { static const String _key = 'OATH_STATE_FAVORITES'; final SharedPreferences _prefs; FavoritesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []); void toggleFavorite(String credentialId) { if (state.contains(credentialId)) { state = state.toList()..remove(credentialId); } else { state = [credentialId, ...state]; } _prefs.setStringList(_key, state); } void renameCredential(String oldCredentialId, String newCredentialId) { if (state.contains(oldCredentialId)) { state = [newCredentialId, ...state.toList()..remove(oldCredentialId)]; _prefs.setStringList(_key, state); } } } final filteredCredentialsProvider = StateNotifierProvider.autoDispose .family, List>( (ref, full) { return FilteredCredentialsNotifier(full, ref.watch(accountsSearchProvider)); }); class FilteredCredentialsNotifier extends StateNotifier> { final String query; FilteredCredentialsNotifier( List full, this.query, ) : super( full .where((pair) => "${pair.credential.issuer ?? ''}:${pair.credential.name}" .toLowerCase() .contains(query.toLowerCase())) .where((pair) => pair.credential.issuer != '_hidden') .toList(), ); }