yubioath-flutter/lib/oath/state.dart

204 lines
6.2 KiB
Dart
Raw Normal View History

2022-10-04 13:12:54 +03:00
/*
* 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.
*/
2021-11-19 17:05:57 +03:00
import 'dart:async';
2022-07-08 10:12:31 +03:00
import 'package:collection/collection.dart';
2021-11-19 17:05:57 +03:00
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
2021-11-19 17:05:57 +03:00
import '../app/models.dart';
import '../app/state.dart';
import '../core/models.dart';
2021-11-19 17:05:57 +03:00
import '../core/state.dart';
import 'models.dart';
final searchProvider =
StateNotifierProvider<SearchNotifier, String>((ref) => SearchNotifier());
class SearchNotifier extends StateNotifier<String> {
SearchNotifier() : super('');
2023-02-10 19:37:42 +03:00
void setFilter(String value) {
state = value;
}
}
2021-11-19 17:05:57 +03:00
final oathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
(ref, devicePath) => throw UnimplementedError(),
2021-11-19 17:05:57 +03:00
);
2022-03-09 19:47:50 +03:00
abstract class OathStateNotifier extends ApplicationStateNotifier<OathState> {
Future<void> reset();
/// Unlocks the session and returns a Pair of `success`, `remembered`.
Future<Pair<bool, bool>> unlock(String password, {bool remember = false});
Future<bool> setPassword(String? current, String password);
Future<bool> unsetPassword(String current);
2022-02-08 14:25:36 +03:00
Future<void> forgetPassword();
2021-11-19 17:05:57 +03:00
}
final credentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
(ref, arg) => throw UnimplementedError(),
2021-11-19 17:05:57 +03:00
);
abstract class OathCredentialListNotifier
extends StateNotifier<List<OathPair>?> {
OathCredentialListNotifier() : super(null);
2021-11-19 17:05:57 +03:00
@override
@protected
set state(List<OathPair>? value) {
super.state = value != null ? List.unmodifiable(value) : null;
}
Future<OathCode> calculate(OathCredential credential);
Future<OathCredential> addAccount(Uri otpauth, {bool requireTouch = false});
Future<OathCredential> renameAccount(
OathCredential credential, String? issuer, String name);
Future<void> deleteAccount(OathCredential credential);
2021-11-19 17:05:57 +03:00
}
2022-07-08 10:12:31 +03:00
final credentialsProvider = StateNotifierProvider.autoDispose<
_CredentialsProviderNotifier, List<OathCredential>?>((ref) {
final provider = _CredentialsProviderNotifier();
2022-03-02 10:23:29 +03:00
final node = ref.watch(currentDeviceProvider);
if (node != null) {
2022-07-08 10:12:31 +03:00
ref.listen<List<OathPair>?>(credentialListProvider(node.path),
(previous, next) {
provider._updatePairs(next);
}, fireImmediately: true);
2022-03-02 10:23:29 +03:00
}
2022-07-08 10:12:31 +03:00
return provider;
2022-03-02 10:23:29 +03:00
});
2022-07-08 10:12:31 +03:00
class _CredentialsProviderNotifier
extends StateNotifier<List<OathCredential>?> {
_CredentialsProviderNotifier() : super(null);
void _updatePairs(List<OathPair>? 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;
}
}
}
}
}
2022-03-02 10:23:29 +03:00
final codeProvider =
Provider.autoDispose.family<OathCode?, OathCredential>((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),
)))
2022-03-02 10:23:29 +03:00
?.code;
}
return null;
});
final expiredProvider =
StateNotifierProvider.autoDispose.family<_ExpireNotifier, bool, int>(
(ref, expiry) =>
_ExpireNotifier(DateTime.now().millisecondsSinceEpoch, expiry * 1000),
);
class _ExpireNotifier extends StateNotifier<bool> {
Timer? _timer;
_ExpireNotifier(int now, int expiry) : super(expiry <= now) {
if (expiry > now) {
_timer = Timer(Duration(milliseconds: expiry - now), () {
if (mounted) {
state = true;
}
});
}
}
@override
2023-02-10 19:37:42 +03:00
void dispose() {
2022-03-02 10:23:29 +03:00
_timer?.cancel();
super.dispose();
}
}
2021-12-03 17:15:00 +03:00
final favoritesProvider =
StateNotifierProvider<FavoritesNotifier, List<String>>(
(ref) => FavoritesNotifier(ref.watch(prefProvider)));
2021-11-19 17:05:57 +03:00
2021-12-03 17:15:00 +03:00
class FavoritesNotifier extends StateNotifier<List<String>> {
static const String _key = 'OATH_STATE_FAVORITES';
final SharedPreferences _prefs;
FavoritesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
2023-02-10 19:37:42 +03:00
void toggleFavorite(String credentialId) {
2021-12-03 17:15:00 +03:00
if (state.contains(credentialId)) {
state = state.toList()..remove(credentialId);
} else {
state = [credentialId, ...state];
}
2021-12-03 17:15:00 +03:00
_prefs.setStringList(_key, state);
2021-11-19 17:05:57 +03:00
}
2023-02-10 19:37:42 +03:00
void renameCredential(String oldCredentialId, String newCredentialId) {
if (state.contains(oldCredentialId)) {
state = [newCredentialId, ...state.toList()..remove(oldCredentialId)];
_prefs.setStringList(_key, state);
}
}
2021-11-19 17:05:57 +03:00
}
final filteredCredentialsProvider = StateNotifierProvider.autoDispose
.family<FilteredCredentialsNotifier, List<OathPair>, List<OathPair>>(
(ref, full) {
2021-12-03 17:15:00 +03:00
return FilteredCredentialsNotifier(full, ref.watch(searchProvider));
2021-11-19 17:05:57 +03:00
});
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
final String query;
FilteredCredentialsNotifier(
List<OathPair> full,
this.query,
) : super(
full
.where((pair) =>
"${pair.credential.issuer ?? ''}:${pair.credential.name}"
.toLowerCase()
.contains(query.toLowerCase()))
.where((pair) => pair.credential.issuer != '_hidden')
.toList()
..sort((a, b) {
String searchKey(OathCredential c) =>
((c.issuer ?? '') + c.name).toLowerCase();
return searchKey(a.credential).compareTo(searchKey(b.credential));
}),
);
2021-11-19 17:05:57 +03:00
}