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';
|
2021-11-22 11:49:52 +03:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2021-11-19 17:05:57 +03:00
|
|
|
|
2022-02-08 15:44:35 +03:00
|
|
|
import '../app/models.dart';
|
2021-12-02 13:44:17 +03:00
|
|
|
import '../app/state.dart';
|
2021-11-19 17:05:57 +03:00
|
|
|
import '../core/state.dart';
|
|
|
|
import 'models.dart';
|
|
|
|
|
2024-03-20 19:01:26 +03:00
|
|
|
final accountsSearchProvider =
|
|
|
|
StateNotifierProvider<AccountsSearchNotifier, String>(
|
|
|
|
(ref) => AccountsSearchNotifier());
|
2022-03-25 18:24:15 +03:00
|
|
|
|
2024-03-20 19:01:26 +03:00
|
|
|
class AccountsSearchNotifier extends StateNotifier<String> {
|
|
|
|
AccountsSearchNotifier() : super('');
|
2022-03-25 18:24:15 +03:00
|
|
|
|
2023-02-10 19:37:42 +03:00
|
|
|
void setFilter(String value) {
|
2022-03-25 18:24:15 +03:00
|
|
|
state = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-01 17:21:21 +03:00
|
|
|
final oathLayoutProvider =
|
|
|
|
StateNotifierProvider.autoDispose<OathLayoutNotfier, OathLayout>((ref) {
|
|
|
|
final device = ref.watch(currentDeviceProvider);
|
|
|
|
List<OathPair> 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));
|
2024-07-04 14:05:27 +03:00
|
|
|
return OathLayoutNotfier('OATH_STATE_LAYOUT', ref.watch(prefProvider),
|
|
|
|
credentials, pinnedCreds.toList());
|
2024-07-01 17:21:21 +03:00
|
|
|
});
|
2024-06-13 10:29:30 +03:00
|
|
|
|
2024-07-01 17:21:21 +03:00
|
|
|
class OathLayoutNotfier extends StateNotifier<OathLayout> {
|
|
|
|
final String _key;
|
|
|
|
final SharedPreferences _prefs;
|
2024-07-04 14:05:27 +03:00
|
|
|
OathLayoutNotfier(this._key, this._prefs, List<OathPair> credentials,
|
|
|
|
List<OathPair> pinnedCredentials)
|
|
|
|
: super(
|
|
|
|
_fromName(_prefs.getString(_key), credentials, pinnedCredentials));
|
2024-07-01 17:21:21 +03:00
|
|
|
|
|
|
|
void setLayout(OathLayout layout) {
|
|
|
|
state = layout;
|
|
|
|
_prefs.setString(_key, layout.name);
|
|
|
|
}
|
|
|
|
|
2024-07-04 14:05:27 +03:00
|
|
|
static OathLayout _fromName(String? name, List<OathPair> credentials,
|
|
|
|
List<OathPair> pinnedCredentials) {
|
2024-07-01 17:21:21 +03:00
|
|
|
final layout = OathLayout.values.firstWhere(
|
|
|
|
(element) => element.name == name,
|
|
|
|
orElse: () => OathLayout.list,
|
|
|
|
);
|
|
|
|
// Default to list view if current key does not have
|
|
|
|
// pinned credentials
|
2024-07-04 14:05:27 +03:00
|
|
|
if (layout == OathLayout.mixed) {
|
|
|
|
if (pinnedCredentials.isEmpty) {
|
|
|
|
return OathLayout.list;
|
|
|
|
}
|
|
|
|
if (pinnedCredentials.length == credentials.length) {
|
|
|
|
return OathLayout.grid;
|
|
|
|
}
|
2024-07-01 17:21:21 +03:00
|
|
|
}
|
|
|
|
return layout;
|
|
|
|
}
|
|
|
|
}
|
2024-06-13 10:29:30 +03:00
|
|
|
|
2023-05-03 22:20:08 +03:00
|
|
|
final oathStateProvider = AsyncNotifierProvider.autoDispose
|
|
|
|
.family<OathStateNotifier, OathState, 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> {
|
2022-01-27 14:34:29 +03:00
|
|
|
Future<void> reset();
|
2022-02-22 17:22:41 +03:00
|
|
|
|
2023-05-22 12:20:30 +03:00
|
|
|
/// Unlocks the session and returns a record of `success`, `remembered`.
|
|
|
|
Future<(bool, bool)> unlock(String password, {bool remember = false});
|
2022-02-22 17:22:41 +03:00
|
|
|
|
2022-01-27 14:34:29 +03:00
|
|
|
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
|
2022-02-08 15:44:35 +03:00
|
|
|
.family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
|
2022-01-27 14:34:29 +03:00
|
|
|
(ref, arg) => throw UnimplementedError(),
|
2021-11-19 17:05:57 +03:00
|
|
|
);
|
|
|
|
|
2022-01-27 14:34:29 +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) {
|
2023-02-24 16:19:34 +03:00
|
|
|
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;
|
2021-11-19 17:05:57 +03:00
|
|
|
}
|
|
|
|
|
2022-01-27 14:34:29 +03:00
|
|
|
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);
|
2022-08-12 10:54:03 +03:00
|
|
|
}, 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
|
2022-03-02 17:25:47 +03:00
|
|
|
.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) ?? []);
|
2021-11-22 11:49:52 +03:00
|
|
|
|
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-11-22 11:49:52 +03:00
|
|
|
}
|
2021-12-03 17:15:00 +03:00
|
|
|
_prefs.setStringList(_key, state);
|
2021-11-19 17:05:57 +03:00
|
|
|
}
|
2022-09-06 09:50:42 +03:00
|
|
|
|
2023-02-10 19:37:42 +03:00
|
|
|
void renameCredential(String oldCredentialId, String newCredentialId) {
|
2022-09-06 09:50:42 +03:00
|
|
|
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) {
|
2024-03-20 19:01:26 +03:00
|
|
|
return FilteredCredentialsNotifier(full, ref.watch(accountsSearchProvider));
|
2021-11-19 17:05:57 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
|
|
|
|
final String query;
|
|
|
|
FilteredCredentialsNotifier(
|
|
|
|
List<OathPair> full,
|
|
|
|
this.query,
|
2021-12-02 13:44:17 +03:00
|
|
|
) : super(
|
|
|
|
full
|
|
|
|
.where((pair) =>
|
|
|
|
"${pair.credential.issuer ?? ''}:${pair.credential.name}"
|
|
|
|
.toLowerCase()
|
|
|
|
.contains(query.toLowerCase()))
|
2022-08-12 14:23:37 +03:00
|
|
|
.where((pair) => pair.credential.issuer != '_hidden')
|
2023-02-24 16:19:34 +03:00
|
|
|
.toList(),
|
2021-12-02 13:44:17 +03:00
|
|
|
);
|
2021-11-19 17:05:57 +03:00
|
|
|
}
|