From 15069043d40defbfe2c8894a41e077361d5f21b5 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:33:55 +0100 Subject: [PATCH 01/13] Add unit tests for various token types in privacyIDEA Authenticator - Implement tests for OTPToken, PushToken, SteamToken, and TOTPToken. - Validate static methods, serialization, and identity logic. - Ensure proper handling of edge cases and error scenarios. - Introduce MockToken class for testing purposes. - Cover scenarios for token creation, copying, and template updates. --- lib/model/tokens/day_password_token.dart | 81 +- lib/model/tokens/push_token.dart | 10 +- .../object_validator/base_validator.dart | 2 + .../optional_object_validator.dart | 5 +- .../required_object_validator.dart | 1 + ...force_biometric_option_extension_test.dart | 83 ++ .../model/token/day_password_test.dart | 757 ------------------ .../model/token/hotp_token_test.dart | 518 ------------ .../model/token/push_token_test.dart | 339 -------- .../model/token/steam_token_test.dart | 224 ------ .../model/token/totp_token_test.dart | 567 ------------- .../model/tokens/day_password_test.dart | 198 +++++ .../model/tokens/hotp_token_test.dart | 297 +++++++ .../model/tokens/otp_token_test.dart | 192 +++++ .../model/tokens/push_token_test.dart | 219 +++++ .../model/tokens/steam_token_test.dart | 146 ++++ test/unit_test/model/tokens/token_test.dart | 267 ++++++ .../model/tokens/totp_token_test.dart | 251 ++++++ 18 files changed, 1709 insertions(+), 2448 deletions(-) create mode 100644 test/unit_test/model/extensions/enums/force_biometric_option_extension_test.dart delete mode 100644 test/unit_test/model/token/day_password_test.dart delete mode 100644 test/unit_test/model/token/hotp_token_test.dart delete mode 100644 test/unit_test/model/token/push_token_test.dart delete mode 100644 test/unit_test/model/token/steam_token_test.dart delete mode 100644 test/unit_test/model/token/totp_token_test.dart create mode 100644 test/unit_test/model/tokens/day_password_test.dart create mode 100644 test/unit_test/model/tokens/hotp_token_test.dart create mode 100644 test/unit_test/model/tokens/otp_token_test.dart create mode 100644 test/unit_test/model/tokens/push_token_test.dart create mode 100644 test/unit_test/model/tokens/steam_token_test.dart create mode 100644 test/unit_test/model/tokens/token_test.dart create mode 100644 test/unit_test/model/tokens/totp_token_test.dart diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index d29874420..ef579d3b9 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -3,7 +3,7 @@ * * Author: Frank Merkel * - * Copyright (c) 2025 NetKnights GmbH + * Copyright (c) 2026 NetKnights GmbH * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -39,10 +39,8 @@ part 'day_password_token.g.dart'; @JsonSerializable() @immutable class DayPasswordToken extends OTPToken { - // --- Constants --- static const String VIEW_MODE = 'view_mode'; - // --- Static Accessors & Validators --- static String get tokenType => TokenTypes.DAYPASSWORD.name; static final Map otpAuthValidators = { @@ -54,41 +52,29 @@ class DayPasswordToken extends OTPToken { static final Map additionalDataValidators = { ...OTPToken.additionalDataValidators, - VIEW_MODE: const OptionalObjectValidator( + VIEW_MODE: OptionalObjectValidator( defaultValue: DayPasswordTokenViewMode.VALIDFOR, + transformer: (value) { + if (value is DayPasswordTokenViewMode) return value; + if (value is String) { + return DayPasswordTokenViewMode.values.firstWhere( + (e) => e.name.toLowerCase() == value.toLowerCase(), + orElse: () => DayPasswordTokenViewMode.VALIDFOR, + ); + } + return null; + }, ), }; - // --- Static Validation Methods --- - static Map validateOtpAuthMap( - Map otpAuthMap, - ) { - return validateMap( - map: otpAuthMap, - validators: otpAuthValidators, - name: 'DayPasswordToken#otpAuthMap', - ); - } - - static Map validateAdditionalData( - Map additionalData, - ) { - return validateMap( - map: additionalData, - validators: additionalDataValidators, - name: 'DayPasswordToken#additionalData', - ); - } - - // --- Instance Properties --- final DayPasswordTokenViewMode viewMode; final Duration period; @override - String get otpValue => otpFromTime(DateTime.now()); + String get otpValue => _otpFromTime(DateTime.now()); @override - String get nextValue => otpFromTime(DateTime.now().add(period)); + String get nextValue => _otpFromTime(DateTime.now().add(period)); Duration get durationSinceLastOTP { final msPassedThisPeriod = @@ -107,7 +93,6 @@ class DayPasswordToken extends OTPToken { ); } - // --- Constructor --- DayPasswordToken({ required Duration period, required super.id, @@ -133,7 +118,6 @@ class DayPasswordToken extends OTPToken { }) : period = period.inSeconds > 0 ? period : const Duration(hours: 24), super(type: TokenTypes.DAYPASSWORD.name); - // --- Factories --- factory DayPasswordToken.fromJson(Map json) => _$DayPasswordTokenFromJson(json); @@ -141,8 +125,16 @@ class DayPasswordToken extends OTPToken { Map otpAuthMap, { Map additionalData = const {}, }) { - final validatedMap = validateOtpAuthMap(otpAuthMap); - final validatedAdditionalData = validateAdditionalData(additionalData); + final validatedMap = validateMap( + map: otpAuthMap, + validators: otpAuthValidators, + name: 'DayPasswordToken#fromOtpAuthMap', + ); + final validatedAdditionalData = validateMap( + map: additionalData, + validators: additionalDataValidators, + name: 'DayPasswordToken#fromOtpAuthMap', + ); return DayPasswordToken( label: validatedMap[Token.LABEL] as String, @@ -171,8 +163,7 @@ class DayPasswordToken extends OTPToken { ); } - // --- Methods --- - String otpFromTime(DateTime time) => algorithm.generateTOTPCodeString( + String _otpFromTime(DateTime time) => algorithm.generateTOTPCodeString( secret: secret, time: time, length: digits, @@ -253,8 +244,19 @@ class DayPasswordToken extends OTPToken { @override DayPasswordToken copyUpdateByTemplate(TokenTemplate template) { - final uriMap = validateOtpAuthMap(template.otpAuthMap); - final additionalData = validateAdditionalData(template.additionalData); + final uriMap = validateMap( + map: template.otpAuthMap, + validators: otpAuthValidators.map((k, v) => MapEntry(k, v.optional())), + name: 'DayPasswordToken#copyUpdateByTemplate', + ); + final additionalMap = validateMap( + map: template.additionalData, + validators: additionalDataValidators.map( + (k, v) => MapEntry(k, v.optional()), + ), + name: 'DayPasswordToken#copyUpdateByTemplate', + ); + return copyWith( label: uriMap[Token.LABEL] as String?, issuer: uriMap[Token.ISSUER] as String?, @@ -266,14 +268,15 @@ class DayPasswordToken extends OTPToken { digits: uriMap[OTPToken.DIGITS] as int?, secret: uriMap[OTPToken.SECRET_BASE32] as String?, period: uriMap[TOTPToken.PERIOD_SECONDS] as Duration?, - viewMode: additionalData[VIEW_MODE] as DayPasswordTokenViewMode?, + checkedContainer: + additionalMap[Token.CHECKED_CONTAINERS] as List?, + viewMode: additionalMap[VIEW_MODE] as DayPasswordTokenViewMode?, ); } @override String toString() => 'DayPassword${super.toString()}period: $period'; - // --- Serialization Helpers --- @override Map toJson() => _$DayPasswordTokenToJson(this); @@ -285,5 +288,5 @@ class DayPasswordToken extends OTPToken { @override Map get additionalData => - super.additionalData..addAll({VIEW_MODE: viewMode}); + super.additionalData..addAll({VIEW_MODE: viewMode.name}); } diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 947012ce9..62b377209 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -270,12 +270,16 @@ class PushToken extends Token { @override bool isSameTokenAs(Token other) { - if (super.isSameTokenAs(other) != null) return super.isSameTokenAs(other)!; if (other is! PushToken) return false; - return (publicServerKey == other.publicServerKey && + + final bool samePushCredentials = + publicServerKey == other.publicServerKey && publicTokenKey == other.publicTokenKey && privateTokenKey == other.privateTokenKey && - enrollmentCredentials == other.enrollmentCredentials); + enrollmentCredentials == other.enrollmentCredentials; + if (!samePushCredentials) return false; + + return super.isSameTokenAs(other) ?? true; } @override diff --git a/lib/utils/object_validator/base_validator.dart b/lib/utils/object_validator/base_validator.dart index af012d293..a2f63e88c 100644 --- a/lib/utils/object_validator/base_validator.dart +++ b/lib/utils/object_validator/base_validator.dart @@ -31,6 +31,8 @@ abstract class BaseValidator { this.allowedValues, }); + OptionalObjectValidator optional(); + bool isTypeOf(Object? value); bool valueIsAllowed(Object? value, String name); diff --git a/lib/utils/object_validator/optional_object_validator.dart b/lib/utils/object_validator/optional_object_validator.dart index 4564e154f..8fc768bd1 100644 --- a/lib/utils/object_validator/optional_object_validator.dart +++ b/lib/utils/object_validator/optional_object_validator.dart @@ -20,13 +20,16 @@ part of 'object_validators.dart'; -class OptionalObjectValidator extends BaseValidator { +class OptionalObjectValidator extends BaseValidator { const OptionalObjectValidator({ super.transformer, super.defaultValue, super.allowedValues, }); + @override + OptionalObjectValidator optional() => this; + @override T? transform(value, name) { if (value == null) return defaultValue; diff --git a/lib/utils/object_validator/required_object_validator.dart b/lib/utils/object_validator/required_object_validator.dart index d08a2ffa9..8ae70c24f 100644 --- a/lib/utils/object_validator/required_object_validator.dart +++ b/lib/utils/object_validator/required_object_validator.dart @@ -58,6 +58,7 @@ class RequiredObjectValidator extends BaseValidator { ); } + @override OptionalObjectValidator optional() => OptionalObjectValidator( transformer: transformer, defaultValue: defaultValue, diff --git a/test/unit_test/model/extensions/enums/force_biometric_option_extension_test.dart b/test/unit_test/model/extensions/enums/force_biometric_option_extension_test.dart new file mode 100644 index 000000000..ed01fdcd3 --- /dev/null +++ b/test/unit_test/model/extensions/enums/force_biometric_option_extension_test.dart @@ -0,0 +1,83 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/force_biometric_option.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/force_biometric_option_extension.dart'; + +void main() { + group('ForceBiometricOptionX Tests', () { + test('fromString parses valid strings case-insensitively', () { + expect( + ForceBiometricOptionX.fromString('none'), + ForceBiometricOption.none, + ); + expect( + ForceBiometricOptionX.fromString('NONE'), + ForceBiometricOption.none, + ); + expect(ForceBiometricOptionX.fromString('any'), ForceBiometricOption.any); + }); + + test('fromString returns null for null input', () { + expect(ForceBiometricOptionX.fromString(null), isNull); + }); + + test('fromString throws ArgumentError for invalid string', () { + expect( + () => ForceBiometricOptionX.fromString('invalid'), + throwsArgumentError, + ); + }); + + test('validator transformer handles ForceBiometricOption type', () { + final result = ForceBiometricOptionX.validator.transformer!( + ForceBiometricOption.any, + ); + expect(result, ForceBiometricOption.any); + }); + + test('validator transformer handles String type', () { + final result = ForceBiometricOptionX.validator.transformer!('any'); + expect(result, ForceBiometricOption.any); + }); + + test( + 'validator transformer throws ArgumentError for unsupported types', + () { + expect( + () => ForceBiometricOptionX.validator.transformer!(123), + throwsArgumentError, + ); + expect( + () => ForceBiometricOptionX.validator.transformer!(true), + throwsArgumentError, + ); + }, + ); + + test('validator uses defaultValue for null input', () { + expect( + ForceBiometricOptionX.validator.transform(null, 'TestField'), + ForceBiometricOption.none, + ); + }); + }); +} diff --git a/test/unit_test/model/token/day_password_test.dart b/test/unit_test/model/token/day_password_test.dart deleted file mode 100644 index 3c8a92f90..000000000 --- a/test/unit_test/model/token/day_password_test.dart +++ /dev/null @@ -1,757 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; -import 'package:privacyidea_authenticator/model/enums/encodings.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; -import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/token.dart'; -import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; - -DayPasswordToken get dayPasswordToken => DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: false, // if pin is true, its automatically forced to be locked=true - folderId: 0, - ); -void main() { - _testDayPasswordToken(); -} - -void _testDayPasswordToken() { - group('Day password creation', () { - test('constructor', () { - expect(dayPasswordToken.period, const Duration(hours: 24)); - expect(dayPasswordToken.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); - expect(dayPasswordToken.label, 'label'); - expect(dayPasswordToken.issuer, 'issuer'); - expect(dayPasswordToken.id, 'id'); - expect(dayPasswordToken.algorithm, Algorithms.SHA1); - expect(dayPasswordToken.digits, 6); - expect(dayPasswordToken.secret, 'secret'); - expect(dayPasswordToken.pin, true); - expect(dayPasswordToken.tokenImage, 'example.png'); - expect(dayPasswordToken.sortIndex, 0); - expect(dayPasswordToken.isLocked, true); - expect(dayPasswordToken.folderId, 0); - }); - test('copyWith', () { - final totpCopy = dayPasswordToken.copyWith( - period: const Duration(hours: 12), - label: 'labelCopy', - issuer: 'issuerCopy', - id: 'idCopy', - algorithm: Algorithms.SHA256, - digits: 8, - secret: 'secretCopy', - pin: true, - tokenImage: 'exampleCopy.png', - sortIndex: 1, - isLocked: true, - folderId: () => 1, - ); - expect(totpCopy.period, const Duration(hours: 12)); - expect(totpCopy.label, 'labelCopy'); - expect(totpCopy.issuer, 'issuerCopy'); - expect(totpCopy.id, 'idCopy'); - expect(totpCopy.algorithm, Algorithms.SHA256); - expect(totpCopy.digits, 8); - expect(totpCopy.secret, 'secretCopy'); - expect(totpCopy.type, 'DAYPASSWORD'); - expect(totpCopy.pin, true); - expect(totpCopy.tokenImage, 'exampleCopy.png'); - expect(totpCopy.sortIndex, 1); - expect(totpCopy.isLocked, true); - expect(totpCopy.folderId, 1); - }); - }); - group('serialization', () { - group('fromUriMap', () { - test('with full map', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'DAYPASSWORD', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: '6', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '30', - }; - final totpFromUriMap = DayPasswordToken.fromOtpAuthMap(uriMap); - expect(totpFromUriMap.period, const Duration(seconds: 30)); - expect(totpFromUriMap.label, 'label'); - expect(totpFromUriMap.issuer, 'issuer'); - expect(totpFromUriMap.algorithm, Algorithms.SHA1); - expect(totpFromUriMap.digits, 6); - expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); - expect(totpFromUriMap.type, 'DAYPASSWORD'); - expect(totpFromUriMap.pin, false); - expect(totpFromUriMap.tokenImage, 'example.png'); - }); - test('with missing secret', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'DAYPASSWORD', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: 6, - TOTPToken.PERIOD_SECONDS: 30, - }; - expect(() => DayPasswordToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - test('with zero period', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'DAYPASSWORD', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: '6', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '0', - }; - expect(() => DayPasswordToken.fromOtpAuthMap(uriMap), throwsA(isA())); - var errorContainsPeriod = false; - try { - DayPasswordToken.fromOtpAuthMap(uriMap); - } catch (e) { - errorContainsPeriod = e.toString().contains(TOTPToken.PERIOD_SECONDS); - } - expect(errorContainsPeriod, true); - }); - test('with zero digits', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'DAYPASSWORD', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: '0', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '30', - }; - expect(() => DayPasswordToken.fromOtpAuthMap(uriMap), throwsA(isA())); - var errorContainsDigits = false; - try { - DayPasswordToken.fromOtpAuthMap(uriMap); - } catch (e) { - errorContainsDigits = e.toString().contains(OTPToken.DIGITS); - } - expect(errorContainsDigits, true); - }); - test('with lowercase algorithm', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'DAYPASSWORD', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'sha1', - OTPToken.DIGITS: '6', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '30', - }; - final totpFromUriMap = DayPasswordToken.fromOtpAuthMap(uriMap); - expect(totpFromUriMap.algorithm, Algorithms.SHA1); - }); - test('with empty map', () { - final uriMap = {}; - expect(() => DayPasswordToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - }); - test('toUriMap', () { - final uriMap = dayPasswordToken.toOtpAuthMap(); - expect(uriMap[Token.LABEL], 'label'); - expect(uriMap[Token.ISSUER], 'issuer'); - expect(uriMap[Token.TOKENTYPE_OTPAUTH], 'DAYPASSWORD'); - expect(uriMap[Token.PIN], Token.PIN_VALUE_TRUE); - expect(uriMap[Token.IMAGE], 'example.png'); - expect(uriMap[OTPToken.ALGORITHM], 'SHA1'); - expect(uriMap[OTPToken.DIGITS], '6'); - expect(uriMap[OTPToken.SECRET_BASE32], 'secret'); - expect(uriMap[TOTPToken.PERIOD_SECONDS], '86400'); - }); - test('fromJson', () { - final totpJson = { - 'period': 11000000, - 'label': 'label', - 'issuer': 'issuer', - 'id': 'id', - 'algorithm': 'SHA1', - 'digits': 22, - 'secret': 'secret', - 'type': 'DAYPASSWORD', - 'pin': true, - 'tokenImage': 'example.png', - 'sortIndex': 33, - 'isLocked': true, - 'folderId': 44, - }; - final totpFromJson = DayPasswordToken.fromJson(totpJson); - expect(totpFromJson.period, const Duration(seconds: 11)); - expect(totpFromJson.label, 'label'); - expect(totpFromJson.issuer, 'issuer'); - expect(totpFromJson.id, 'id'); - expect(totpFromJson.algorithm, Algorithms.SHA1); - expect(totpFromJson.digits, 22); - expect(totpFromJson.secret, 'secret'); - expect(totpFromJson.type, 'DAYPASSWORD'); - expect(totpFromJson.pin, true); - expect(totpFromJson.tokenImage, 'example.png'); - expect(totpFromJson.sortIndex, 33); - expect(totpFromJson.isLocked, true); - expect(totpFromJson.folderId, 44); - }); - test('toJson', () { - final totpJson = dayPasswordToken.toJson(); - expect(totpJson['period'], 86400000000); - expect(totpJson['label'], 'label'); - expect(totpJson['issuer'], 'issuer'); - expect(totpJson['id'], 'id'); - expect(totpJson['algorithm'], 'SHA1'); - expect(totpJson['digits'], 6); - expect(totpJson['secret'], 'secret'); - expect(totpJson['type'], 'DAYPASSWORD'); - expect(totpJson['pin'], true); - expect(totpJson['tokenImage'], 'example.png'); - expect(totpJson['sortIndex'], 0); - expect(totpJson['isLocked'], true); - expect(totpJson['folderId'], 0); - }); - }); - group('isSameTokenAs', () { - test('no serial | same id | same parameters', () { - // No serial. Should recognize by id or parameters - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith()), true); - }); - test('no serial | same id | different parameters', () { - // No serial. Should recognize by id - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(algorithm: Algorithms.SHA256)), true); - }); - test('no serial | different id | same parameters', () { - // No serial, different id. Should recognize by parameters - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2')), true); - }); - test('no serial | different id | different parameters', () { - // No serial, different id, different parameters. Should not recognize - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), false); - }); - test('same serial | different id | different parameters', () { - // Different id, different parameters. Should recognize by serial - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), true); - }); - test('different serial | same id | different parameters', () { - // Different serial, different parameters. Should recognize by id - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), true); - }); - test('different serial | different id | same parameters', () { - // Different serial, different id. Should NOT recognize by parameters - final dayPasswordToken = DayPasswordToken( - period: const Duration(hours: 24), - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: true, - folderId: 0, - ); - - expect(dayPasswordToken.isSameTokenAs(dayPasswordToken.copyWith(serial: 'serial2', id: 'id2')), false); - }); - }); - group('Calculate day password values', () { - // Basicly the day password is a HOTP token but the counter is calculated based on the current time. - // So we can test day password token by comparing its OTP value with a HOTP value with the same counter. - // as we know the HOTP token works, we can assume the day password token works as well when they have the same otp value. - - group('different periods 6 digits', () { - const digits = 6; - test('1h period', () { - const period = Duration(hours: 1); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('12h period', () { - const period = Duration(hours: 12); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('24h period', () { - const period = Duration(hours: 24); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('3 days period', () { - const period = Duration(days: 3); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('28 days period', () { - const period = Duration(days: 28); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - }); - group('different periods 8 digits', () { - const digits = 8; - test('1h period', () { - const period = Duration(hours: 1); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('12h period', () { - const period = Duration(hours: 12); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('24h period', () { - const period = Duration(hours: 24); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('3 days period', () { - const period = Duration(days: 3); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('28 days period', () { - const period = Duration(days: 28); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - }); - group('different algorithms 6 digits', () { - const digits = 6; - const period = Duration(hours: 24); - test('SHA1 algorithm', () { - const algorithm = Algorithms.SHA1; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('SHA256 algorithm', () { - const algorithm = Algorithms.SHA256; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('SHA512 algorithm', () { - const algorithm = Algorithms.SHA512; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - }); - group('different algorithms 8 digits', () { - const digits = 8; - const period = Duration(hours: 24); - test('SHA1 algorithm', () { - const algorithm = Algorithms.SHA1; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('SHA256 algorithm', () { - const algorithm = Algorithms.SHA256; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - test('SHA512 algorithm', () { - const algorithm = Algorithms.SHA512; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final dayPassword1h = DayPasswordToken( - label: '', - issuer: '', - id: '', - period: period, - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(hotpToken.otpValue, dayPassword1h.otpValue); - }); - }); - }); -} diff --git a/test/unit_test/model/token/hotp_token_test.dart b/test/unit_test/model/token/hotp_token_test.dart deleted file mode 100644 index ee07439ea..000000000 --- a/test/unit_test/model/token/hotp_token_test.dart +++ /dev/null @@ -1,518 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/encodings.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; -import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/token.dart'; - -HOTPToken get hotpToken => HOTPToken( - counter: 1, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - type: 'type', - pin: true, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: false, - folderId: 0, - ); - -void main() { - _testHotpToken(); -} - -void _testHotpToken() { - group('HOTP Token creation', () { - test('constructor', () { - expect(hotpToken.counter, 1); - expect(hotpToken.label, 'label'); - expect(hotpToken.issuer, 'issuer'); - expect(hotpToken.id, 'id'); - expect(hotpToken.algorithm, Algorithms.SHA1); - expect(hotpToken.digits, 6); - expect(hotpToken.secret, 'secret'); - expect(hotpToken.type, 'HOTP'); - expect(hotpToken.pin, true); - expect(hotpToken.tokenImage, 'example.png'); - expect(hotpToken.sortIndex, 0); - expect(hotpToken.isLocked, true); - expect(hotpToken.folderId, 0); - }); - test('withNextCounter', () { - final withNextCounter = hotpToken.withNextCounter(); - expect(withNextCounter.counter, 2); - }); - test('copyWith', () { - final hotpCopy = hotpToken.copyWith( - counter: 5, - label: 'labelCopy', - issuer: 'issuerCopy', - id: 'idCopy', - algorithm: Algorithms.SHA256, - digits: 8, - secret: 'secretCopy', - pin: false, - tokenImage: 'exampleCopy.png', - sortIndex: 1, - isLocked: true, - folderId: () => 1, - ); - expect(hotpCopy.counter, 5); - expect(hotpCopy.label, 'labelCopy'); - expect(hotpCopy.issuer, 'issuerCopy'); - expect(hotpCopy.id, 'idCopy'); - expect(hotpCopy.algorithm, Algorithms.SHA256); - expect(hotpCopy.digits, 8); - expect(hotpCopy.secret, 'secretCopy'); - expect(hotpCopy.type, 'HOTP'); - expect(hotpCopy.pin, false); - expect(hotpCopy.tokenImage, 'exampleCopy.png'); - expect(hotpCopy.sortIndex, 1); - expect(hotpCopy.isLocked, true); - expect(hotpCopy.folderId, 1); - }); - }); - group('serialization', () { - group('fromUriMap', () { - test('with full map', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'HOTP', - Token.PIN: Token.PIN_VALUE_TRUE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - OTPToken.DIGITS: '6', - HOTPToken.COUNTER: '10', - }; - final hotpFromUriMap = HOTPToken.fromOtpAuthMap(uriMap); - expect(hotpFromUriMap.counter, 10); - expect(hotpFromUriMap.label, 'label'); - expect(hotpFromUriMap.issuer, 'issuer'); - expect(hotpFromUriMap.algorithm, Algorithms.SHA1); - expect(hotpFromUriMap.secret, 'ONSWG4TFOQ======'); - expect(hotpFromUriMap.digits, 6); - expect(hotpFromUriMap.type, 'HOTP'); - expect(hotpFromUriMap.pin, true); - expect(hotpFromUriMap.tokenImage, 'example.png'); - }); - test('without secret', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'HOTP', - Token.PIN: Token.PIN_VALUE_TRUE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: '6', - HOTPToken.COUNTER: '10', - }; - expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); - }); - test('digits is zero', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'HOTP', - Token.PIN: Token.PIN_VALUE_TRUE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - OTPToken.DIGITS: '0', - HOTPToken.COUNTER: '10', - }; - expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); - bool errorContainsDigits = false; - try { - HOTPToken.fromOtpAuthMap(uriMap); - } on ArgumentError catch (e) { - errorContainsDigits = e.toString().contains(OTPToken.DIGITS); - } - expect(errorContainsDigits, true); - }); - test('with lowercase algorithm', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'HOTP', - Token.PIN: Token.PIN_VALUE_TRUE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'sha1', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - OTPToken.DIGITS: '6', - HOTPToken.COUNTER: '10', - }; - final hotpFromUriMap = HOTPToken.fromOtpAuthMap(uriMap); - expect(hotpFromUriMap.counter, 10); - expect(hotpFromUriMap.label, 'label'); - expect(hotpFromUriMap.issuer, 'issuer'); - expect(hotpFromUriMap.algorithm, Algorithms.SHA1); - expect(hotpFromUriMap.secret, 'ONSWG4TFOQ======'); - expect(hotpFromUriMap.digits, 6); - expect(hotpFromUriMap.type, 'HOTP'); - expect(hotpFromUriMap.pin, true); - expect(hotpFromUriMap.tokenImage, 'example.png'); - }); - - test('with empty map', () { - final uriMap = {}; - expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); - }); - }); - test('toUriMap', () { - final uriMap = hotpToken.toOtpAuthMap(); - expect(uriMap[Token.LABEL], 'label'); - expect(uriMap[Token.ISSUER], 'issuer'); - expect(uriMap[Token.TOKENTYPE_OTPAUTH], 'HOTP'); - expect(uriMap[Token.PIN], Token.PIN_VALUE_TRUE); - expect(uriMap[Token.IMAGE], 'example.png'); - expect(uriMap[OTPToken.ALGORITHM], 'SHA1'); - expect(uriMap[OTPToken.SECRET_BASE32], 'secret'); - expect(uriMap[OTPToken.DIGITS], '6'); - expect(uriMap[HOTPToken.COUNTER], '1'); - }); - test('fromJson', () { - final hotpJson = { - 'label': 'label', - 'issuer': 'issuer', - 'id': 'id', - 'type': 'HOTP', - 'algorithm': 'SHA256', - 'digits': 8, - 'secret': 'secret', - 'counter': 5, - 'pin': false, - }; - final hotpFromJson = HOTPToken.fromJson(hotpJson); - expect(hotpFromJson.counter, 5); - expect(hotpFromJson.label, 'label'); - expect(hotpFromJson.issuer, 'issuer'); - expect(hotpFromJson.algorithm, Algorithms.SHA256); - expect(hotpFromJson.digits, 8); - expect(hotpFromJson.secret, 'secret'); - expect(hotpFromJson.type, 'HOTP'); - expect(hotpFromJson.pin, false); - }); - test('toJson', () { - final hotpJson = hotpToken.toJson(); - expect(hotpJson['label'], 'label'); - expect(hotpJson['issuer'], 'issuer'); - expect(hotpJson['id'], 'id'); - expect(hotpJson['type'], 'HOTP'); - expect(hotpJson['algorithm'], 'SHA1'); - expect(hotpJson['digits'], 6); - expect(hotpJson['secret'], 'secret'); - expect(hotpJson['counter'], 1); - expect(hotpJson['pin'], true); - }); - }); - group('isSameTokenAs', () { - test('no serial | same id | same parameters', () { - // No serial. Should recognize by id or parameters - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken), true); - }); - test('no serial | same id | different parameters', () { - // No serial. Should recognize by id - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(algorithm: Algorithms.SHA256)), true); - }); - test('no serial | different id | same parameters', () { - // No serial, different id. Should recognize by parameters - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2')), true); - }); - test('no serial | different id | different parameters', () { - // No serial, different id, different parameters. Should not recognize - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), false); - }); - test('same serial | different id | different parameters', () { - // Different id, different parameters. Should recognize by serial - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - serial: 'serial', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), true); - }); - test('different serial | same id | different parameters', () { - // Different serial, different parameters. Should recognize by id - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - serial: 'serial', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), true); - }); - test('different serial | different id | same parameters', () { - // Different serial, different id. Should NOT recognize by parameters - final hotpToken = HOTPToken( - id: 'id', - label: 'label', - issuer: 'issuer', - serial: 'serial', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - counter: 0, - ); - - expect(hotpToken.isSameTokenAs(hotpToken.copyWith(serial: 'serial2', id: 'id2')), false); - }); - }); - group('Calculate hotp values', () { - group('different couters 6 digits', () { - // We need to use different tokens here, because simply incrementing the - // counter between all method calls leads to a race condition - test('OTP for counter == 0', () { - HOTPToken token0 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 0, - ); - expect(token0.otpValue, '814628'); - }); - - test('OTP for counter == 1', () { - HOTPToken token1 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 1, - ); - expect(token1.otpValue, '533881'); - }); - - test('OTP for counter == 2', () { - HOTPToken token2 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 2, - ); - expect(token2.otpValue, '720111'); - }); - - test('OTP for counter == 8', () { - HOTPToken token8 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 8, - ); - expect(token8.otpValue, '963685'); - }); - }); - group('different couters 8 digits', () { - // We need to use different tokens here, because simply incrementing the - // counter between all method calls leads to a race condition - - test('OTP for counter == 0', () { - HOTPToken token0 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 0, - ); - expect(token0.otpValue, '31814628'); - }); - - test('OTP for counter == 1', () { - HOTPToken token1 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 1, - ); - expect(token1.otpValue, '28533881'); - }); - - test('OTP for counter == 2', () { - HOTPToken token2 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 2, - ); - expect(token2.otpValue, '31720111'); - }); - - test('OTP for counter == 8', () { - HOTPToken token8 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 8, - ); - expect(token8.otpValue, '15963685'); - }); - }); - group('different algorithms 6 digits', () { - // We need to use different tokens here, because simply incrementing the - // counter between all method calls leads to a race condition - - test('OTP for sha1', () { - HOTPToken token0 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token0.otpValue, '292574'); - }); - - test('OTP for sha256', () { - HOTPToken token1 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA256, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token1.otpValue, '203782'); - }); - - test('OTP for sha512', () { - HOTPToken token2 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA512, - digits: 6, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token2.otpValue, '636350'); - }); - }); - group('different algorithms 8 digits', () { - // We need to use different tokens here, because simply incrementing the - // counter between all method calls leads to a race condition - test('OTP for sha1', () { - HOTPToken token0 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token0.otpValue, '25292574'); - }); - - test('OTP for sha256', () { - HOTPToken token1 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA256, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token1.otpValue, '25203782'); - }); - - test('OTP for sha512', () { - HOTPToken token2 = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA512, - digits: 8, - secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, - ); - expect(token2.otpValue, '99636350'); - }); - }); - }); -} diff --git a/test/unit_test/model/token/push_token_test.dart b/test/unit_test/model/token/push_token_test.dart deleted file mode 100644 index 940beb4e7..000000000 --- a/test/unit_test/model/token/push_token_test.dart +++ /dev/null @@ -1,339 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; -import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/token.dart'; - -PushToken get pushToken => PushToken( - serial: 'serial', - expirationDate: DateTime(2017, 9, 7, 17, 30), - label: 'label', - issuer: 'issuer', - id: 'id', - sslVerify: true, - enrollmentCredentials: 'enrollmentCredentials', - url: Uri.parse('http://www.example.com'), - publicServerKey: 'publicServerKey', - publicTokenKey: 'publicTokenKey', - privateTokenKey: 'privateTokenKey', - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutNotStarted, - sortIndex: 0, - tokenImage: 'example.png', - folderId: 0, - isLocked: true, - pin: true, - ); -void main() { - _testPushToken(); -} - -void _testPushToken() { - group('Push Token creation', () { - test('constructor', () { - expect(pushToken.serial, 'serial'); - expect(pushToken.expirationDate, DateTime(2017, 9, 7, 17, 30)); - expect(pushToken.label, 'label'); - expect(pushToken.issuer, 'issuer'); - expect(pushToken.id, 'id'); - expect(pushToken.sslVerify, true); - expect(pushToken.enrollmentCredentials, 'enrollmentCredentials'); - expect(pushToken.url, Uri.parse('http://www.example.com')); - expect(pushToken.publicServerKey, 'publicServerKey'); - expect(pushToken.publicTokenKey, 'publicTokenKey'); - expect(pushToken.privateTokenKey, 'privateTokenKey'); - expect(pushToken.isRolledOut, true); - expect(pushToken.rolloutState, PushTokenRollOutState.rolloutNotStarted); - expect(pushToken.type, 'PIPUSH'); - expect(pushToken.sortIndex, 0); - expect(pushToken.tokenImage, 'example.png'); - expect(pushToken.folderId, 0); - expect(pushToken.isLocked, true); - expect(pushToken.pin, true); - }); - test('copyWith', () { - final copy = pushToken.copyWith( - serial: 'serialCopy', - expirationDate: DateTime(2016, 8, 6, 16, 29), - label: 'labelCopy', - issuer: 'issuerCopy', - id: 'idCopy', - sslVerify: false, - enrollmentCredentials: 'enrollmentCredentialsCopy', - url: Uri.parse('http://www.example.com/copy'), - publicServerKey: 'publicServerKeyCopy', - publicTokenKey: 'publicTokenKeyCopy', - privateTokenKey: 'privateTokenKeyCopy', - isRolledOut: false, - rolloutState: PushTokenRollOutState.rolloutComplete, - sortIndex: 1, - tokenImage: 'exampleCopy.png', - folderId: () => 1, - isLocked: false, - pin: false, - ); - expect(copy.serial, 'serialCopy'); - expect(copy.expirationDate, DateTime(2016, 8, 6, 16, 29)); - expect(copy.label, 'labelCopy'); - expect(copy.issuer, 'issuerCopy'); - expect(copy.id, 'idCopy'); - expect(copy.sslVerify, false); - expect(copy.enrollmentCredentials, 'enrollmentCredentialsCopy'); - expect(copy.url, Uri.parse('http://www.example.com/copy')); - expect(copy.publicServerKey, 'publicServerKeyCopy'); - expect(copy.publicTokenKey, 'publicTokenKeyCopy'); - expect(copy.privateTokenKey, 'privateTokenKeyCopy'); - expect(copy.isRolledOut, false); - expect(copy.rolloutState, PushTokenRollOutState.rolloutComplete); - expect(copy.sortIndex, 1); - expect(copy.tokenImage, 'exampleCopy.png'); - expect(copy.folderId, 1); - expect(copy.isLocked, false); - expect(copy.pin, false); - }); - }); - group('serialization', () { - test('fromJson', () { - final json = { - "label": "label", - "issuer": "issuer", - "id": "id", - "isLocked": true, - "pin": true, - "tokenImage": "example.png", - "folderId": 0, - "sortIndex": 0, - "type": "type", - "expirationDate": "2017-09-07T17:30:00.000", - "serial": "serial", - "sslVerify": true, - "enrollmentCredentials": "enrollmentCredentials", - "url": "http://www.example.com", - "isRolledOut": true, - "rolloutState": "generatingRSAKeyPair", - "publicServerKey": "publicServerKey", - "privateTokenKey": "privateTokenKey", - "publicTokenKey": "publicTokenKey", - }; - final token = PushToken.fromJson(json); - expect(token.label, 'label'); - expect(token.issuer, 'issuer'); - expect(token.id, 'id'); - expect(token.isLocked, true); - expect(token.pin, true); - expect(token.tokenImage, 'example.png'); - expect(token.folderId, 0); - expect(token.sortIndex, 0); - expect(token.type, 'PIPUSH'); - expect(token.expirationDate.toString(), DateTime(2017, 9, 7, 17, 30).toString()); - expect(token.serial, 'serial'); - expect(token.sslVerify, true); - expect(token.enrollmentCredentials, 'enrollmentCredentials'); - expect(token.url, Uri.parse('http://www.example.com')); - expect(token.isRolledOut, true); - expect(token.rolloutState, - PushTokenRollOutState.generatingRSAKeyPairFailed); // When loading from json, an processing state should be converted to a failed state. - expect(token.publicServerKey, 'publicServerKey'); - expect(token.privateTokenKey, 'privateTokenKey'); - expect(token.publicTokenKey, 'publicTokenKey'); - }); - test('toJson', () { - final tokenJson = pushToken.toJson(); - final json = { - "checkedContainer": [], - "containerSerial": null, - "label": "label", - "issuer": "issuer", - "id": "id", - "pin": true, - "isLocked": true, - "isHidden": false, - "tokenImage": "example.png", - "folderId": 0, - "sortIndex": 0, - "origin": null, - "type": "PIPUSH", - "expirationDate": "2017-09-07T17:30:00.000", - "serial": "serial", - "fbToken": null, - "sslVerify": true, - "enrollmentCredentials": "enrollmentCredentials", - "url": "http://www.example.com", - "isRolledOut": true, - "rolloutState": "rolloutNotStarted", - "publicServerKey": "publicServerKey", - "privateTokenKey": "privateTokenKey", - "publicTokenKey": "publicTokenKey" - }; - for (final key in json.keys) { - expect(tokenJson[key], json[key]); - } - }); - group('fromUriMap', () { - test('with full map', () { - final uriMap = { - Token.TOKENTYPE_OTPAUTH: 'PIPUSH', - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.SERIAL: 'serial', - PushToken.SSL_VERIFY: 'False', - PushToken.ENROLLMENT_CREDENTIAL: 'enrollmentCredentials', - PushToken.ROLLOUT_URL: 'http://www.example.com', - PushToken.TTL_MINUTES: '10', - PushToken.VERSION: '1', - }; - final token = PushToken.fromOtpAuthMap(uriMap); - expect(token.type, 'PIPUSH'); - expect(token.label, 'label'); - expect(token.issuer, 'issuer'); - expect(token.serial, 'serial'); - expect(token.sslVerify, false); - expect(token.enrollmentCredentials, 'enrollmentCredentials'); - expect(token.url, Uri.parse('http://www.example.com')); - }); - test('with empty map', () { - final uriMap = {}; - expect(() => PushToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - }); - - test('toUriMap', () { - final token = PushToken( - serial: 'serial', - expirationDate: DateTime(2017, 9, 7, 17, 30), - label: 'label', - issuer: 'issuer', - id: 'id', - sslVerify: true, - enrollmentCredentials: 'enrollmentCredentials', - url: Uri.parse('http://www.example.com'), - publicServerKey: 'publicServerKey', - publicTokenKey: 'publicTokenKey', - privateTokenKey: 'privateTokenKey', - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutNotStarted, - sortIndex: 0, - tokenImage: 'example.png', - folderId: 0, - isLocked: true, - pin: true, - ); - final uriMap = token.toOtpAuthMap(); - expect(uriMap[Token.TOKENTYPE_OTPAUTH], 'PIPUSH'); - expect(uriMap[Token.LABEL], 'label'); - expect(uriMap[Token.ISSUER], 'issuer'); - expect(uriMap[Token.SERIAL], 'serial'); - expect(uriMap[PushToken.SSL_VERIFY], '1'); - expect(uriMap[PushToken.ENROLLMENT_CREDENTIAL], 'enrollmentCredentials'); - expect(uriMap[PushToken.ROLLOUT_URL], 'http://www.example.com'); - expect(uriMap[PushToken.VERSION], '1'); - }); - test('fromJson', () { - final json = { - 'label': 'label', - 'issuer': 'issuer', - 'id': 'id', - 'isLocked': true, - 'pin': true, - 'tokenImage': 'example.png', - 'folderId': 0, - 'sortIndex': 0, - 'type': 'PIPUSH', - 'expirationDate': '2017-09-07T17:30:00.000', - 'serial': 'serial', - 'sslVerify': true, - 'enrollmentCredentials': 'enrollmentCredentials', - 'url': 'http://www.example.com', - 'isRolledOut': true, - 'rolloutState': 'generatingRSAKeyPair', - 'publicServerKey': 'publicServerKey', - 'privateTokenKey': 'privateTokenKey', - 'publicTokenKey': 'publicTokenKey', - }; - final token = PushToken.fromJson(json); - expect(token.label, 'label'); - expect(token.issuer, 'issuer'); - expect(token.id, 'id'); - expect(token.isLocked, true); - expect(token.pin, true); - expect(token.tokenImage, 'example.png'); - expect(token.folderId, 0); - expect(token.sortIndex, 0); - expect(token.type, 'PIPUSH'); - expect(token.expirationDate.toString(), DateTime(2017, 9, 7, 17, 30).toString()); - expect(token.serial, 'serial'); - expect(token.sslVerify, true); - expect(token.enrollmentCredentials, 'enrollmentCredentials'); - expect(token.url, Uri.parse('http://www.example.com')); - expect(token.isRolledOut, true); - expect(token.rolloutState, - PushTokenRollOutState.generatingRSAKeyPairFailed); // When loading from json, an processing state should be converted to a failed state. - expect(token.publicServerKey, 'publicServerKey'); - expect(token.privateTokenKey, 'privateTokenKey'); - expect(token.publicTokenKey, 'publicTokenKey'); - }); - test('toJson', () { - final tokenJson = pushToken.toJson(); - final json = { - "checkedContainer": [], - "containerSerial": null, - "label": "label", - "issuer": "issuer", - "id": "id", - "pin": true, - "isLocked": true, - "isHidden": false, - "tokenImage": "example.png", - "folderId": 0, - "sortIndex": 0, - "origin": null, - "type": "PIPUSH", - "expirationDate": "2017-09-07T17:30:00.000", - "serial": "serial", - "fbToken": null, - "sslVerify": true, - "enrollmentCredentials": "enrollmentCredentials", - "url": "http://www.example.com", - "isRolledOut": true, - "rolloutState": "rolloutNotStarted", - "publicServerKey": "publicServerKey", - "privateTokenKey": "privateTokenKey", - "publicTokenKey": "publicTokenKey" - }; - for (final key in json.keys) { - expect(tokenJson[key], json[key]); - } - }); - }); - group('isSameTokenAs', () { - test('same serial | different id | different parameters', () { - // Different id, different parameters. Should recognize by serial - final pushToken = PushToken( - serial: 'serial', - id: 'id', - privateTokenKey: 'privateTokenKey', - ); - - expect(pushToken.isSameTokenAs(pushToken.copyWith(id: 'id2', privateTokenKey: 'privateTokenKey2')), true); - }); - test('different serial | same id | different parameters', () { - // Different serial, different parameters. Should recognize by id - final pushToken = PushToken( - serial: 'serial', - id: 'id', - privateTokenKey: 'privateTokenKey', - ); - - expect(pushToken.isSameTokenAs(pushToken.copyWith(serial: 'serial2', privateTokenKey: 'privateTokenKey2')), true); - }); - test('different serial | different id | same parameters', () { - // Different serial, different id. Should NOT recognize by parameters - final pushToken = PushToken( - serial: 'serial', - id: 'id', - privateTokenKey: 'privateTokenKey', - ); - - expect(pushToken.isSameTokenAs(pushToken.copyWith(serial: 'serial2', id: 'id2')), false); - }); - }); -} diff --git a/test/unit_test/model/token/steam_token_test.dart b/test/unit_test/model/token/steam_token_test.dart deleted file mode 100644 index a39947f04..000000000 --- a/test/unit_test/model/token/steam_token_test.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/encodings.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; -import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/token.dart'; -import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; - -SteamToken get steamToken => SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - pin: false, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: false, - folderId: 0, - ); -void main() { - _testSteamToken(); -} - -void _testSteamToken() { - group('Steam Token', () { - group('TOTP Token creation', () { - test('constructor', () { - expect(steamToken.period, 30); // default period - expect(steamToken.label, 'label'); - expect(steamToken.issuer, 'issuer'); - expect(steamToken.id, 'id'); - expect(steamToken.algorithm, Algorithms.SHA1); // default algorithm - expect(steamToken.digits, 5); // default digits - expect(steamToken.secret, 'secret'); - expect(steamToken.type, 'STEAM'); - expect(steamToken.pin, false); - expect(steamToken.tokenImage, 'example.png'); - expect(steamToken.sortIndex, 0); - expect(steamToken.isLocked, false); - expect(steamToken.folderId, 0); - }); - test('copyWith', () { - final totpCopy = steamToken.copyWith( - period: 60, // Should not affect the period because steam tokens always have 30 seconds period - label: 'labelCopy', - issuer: 'issuerCopy', - id: 'idCopy', - algorithm: Algorithms.SHA256, // Should not affect the algorithm because steam tokens always have SHA1 algorithm - digits: 8, // Should not affect the digits because steam tokens always have 5 digits - secret: 'secretCopy', - pin: true, - tokenImage: 'exampleCopy.png', - sortIndex: 1, - isLocked: true, - folderId: () => 1, - ); - expect(totpCopy.period, 30); - expect(totpCopy.label, 'labelCopy'); - expect(totpCopy.issuer, 'issuerCopy'); - expect(totpCopy.id, 'idCopy'); - expect(totpCopy.algorithm, Algorithms.SHA1); - expect(totpCopy.digits, 5); - expect(totpCopy.secret, 'secretCopy'); - expect(totpCopy.type, 'STEAM'); - expect(totpCopy.pin, true); - expect(totpCopy.tokenImage, 'exampleCopy.png'); - expect(totpCopy.sortIndex, 1); - expect(totpCopy.isLocked, true); - expect(totpCopy.folderId, 1); - }); - }); - group('Serialization', () { - group('fromUriMap', () { - test('with full map', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - }; - final totpFromUriMap = SteamToken.fromOtpAuthMap(uriMap); - expect(totpFromUriMap.period, 30); - expect(totpFromUriMap.label, 'label'); - expect(totpFromUriMap.issuer, 'issuer'); - expect(totpFromUriMap.algorithm, Algorithms.SHA1); - expect(totpFromUriMap.digits, 5); - expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); - expect(totpFromUriMap.type, 'STEAM'); - expect(totpFromUriMap.pin, false); - expect(totpFromUriMap.tokenImage, 'example.png'); - }); - test('with missing secret', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - }; - expect(() => SteamToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - test('with empty map', () { - final uriMap = {}; - expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - }); - test('toUriMap', () { - final totpUriMap = steamToken.toOtpAuthMap(); - expect(totpUriMap[Token.LABEL], 'label'); - expect(totpUriMap[Token.ISSUER], 'issuer'); - expect(totpUriMap[Token.TOKENTYPE_OTPAUTH], 'STEAM'); - expect(totpUriMap[Token.PIN], 'False'); - expect(totpUriMap[Token.IMAGE], 'example.png'); - expect(totpUriMap[OTPToken.SECRET_BASE32], 'secret'); - }); - test('fromJson', () { - final steamJson = { - 'label': 'label', - 'issuer': 'issuer', - 'id': 'id', - 'secret': 'secret', - 'type': 'STEAM', - 'pin': true, - 'tokenImage': 'example.png', - 'sortIndex': 33, - 'isLocked': true, - 'folderId': 44, - }; - final steamFromJson = SteamToken.fromJson(steamJson); - expect(steamFromJson.period, 30); - expect(steamFromJson.label, 'label'); - expect(steamFromJson.issuer, 'issuer'); - expect(steamFromJson.id, 'id'); - expect(steamFromJson.algorithm, Algorithms.SHA1); - expect(steamFromJson.digits, 5); - expect(steamFromJson.secret, 'secret'); - expect(steamFromJson.type, 'STEAM'); - expect(steamFromJson.pin, true); - expect(steamFromJson.tokenImage, 'example.png'); - expect(steamFromJson.sortIndex, 33); - expect(steamFromJson.isLocked, true); - expect(steamFromJson.folderId, 44); - }); - test('toJson', () { - final totpJson = steamToken.toJson(); - expect(totpJson['label'], 'label'); - expect(totpJson['issuer'], 'issuer'); - expect(totpJson['id'], 'id'); - expect(totpJson['secret'], 'secret'); - expect(totpJson['type'], 'STEAM'); - expect(totpJson['pin'], false); - expect(totpJson['tokenImage'], 'example.png'); - expect(totpJson['sortIndex'], 0); - expect(totpJson['isLocked'], false); - expect(totpJson['folderId'], 0); - }); - }); - group('isSameTokenAs', () { - test('no serial | same id | same parameters', () { - // No serial. Should recognize by id or parameters - final steamToken = SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - ); - - expect(steamToken.isSameTokenAs(steamToken.copyWith()), isTrue); - }); - test('no serial | same id | different parameters', () { - // No serial. Should recognize by id - final steamToken = SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - ); - - expect(steamToken.isSameTokenAs(steamToken.copyWith(secret: 'secret2')), isTrue); - }); - test('no serial | different id | same parameters', () { - // No serial, different id. Should recognize by parameters - final steamToken = SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - ); - - expect(steamToken.isSameTokenAs(steamToken.copyWith(id: 'id2')), isTrue); - }); - test('no serial | different id | different parameters', () { - // No serial, different id, different parameters. Should not recognize - final steamToken = SteamToken( - label: 'label', - issuer: 'issuer', - id: 'id', - secret: 'secret', - ); - - expect(steamToken.isSameTokenAs(steamToken.copyWith(id: 'id2', secret: 'secret2')), isFalse); - }); - }); - test('otpValue', () { - final time = DateTime.fromMillisecondsSinceEpoch(1712666212056); - - final steamToken = SteamToken( - label: '', - issuer: '', - id: '', - secret: 'SECRETA=', - ); - final otp = steamToken.otpFromTime(time); - final otpNow = steamToken.otpFromTime(DateTime.now()); - expect(otp, equals('JGPCJ')); // Checks if the otpOfTime works correctly - expect(steamToken.otpValue, equals(otpNow)); // Checks if the otpValue delivers the same value as the otpOfTime method - }); - }); -} diff --git a/test/unit_test/model/token/totp_token_test.dart b/test/unit_test/model/token/totp_token_test.dart deleted file mode 100644 index 07907868e..000000000 --- a/test/unit_test/model/token/totp_token_test.dart +++ /dev/null @@ -1,567 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; -import 'package:privacyidea_authenticator/model/enums/encodings.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; -import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; -import 'package:privacyidea_authenticator/model/tokens/token.dart'; -import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; - -TOTPToken get totpToken => TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - pin: false, - tokenImage: 'example.png', - sortIndex: 0, - isLocked: false, - folderId: 0, - ); - -void main() { - _testTotpToken(); -} - -void _testTotpToken() { - group('TOTP Token creation', () { - test('constructor', () { - expect(totpToken.period, 30); - expect(totpToken.label, 'label'); - expect(totpToken.issuer, 'issuer'); - expect(totpToken.id, 'id'); - expect(totpToken.algorithm, Algorithms.SHA1); - expect(totpToken.digits, 6); - expect(totpToken.secret, 'secret'); - expect(totpToken.type, 'TOTP'); - expect(totpToken.pin, false); - expect(totpToken.tokenImage, 'example.png'); - expect(totpToken.sortIndex, 0); - expect(totpToken.isLocked, false); - expect(totpToken.folderId, 0); - }); - test('copyWith', () { - final totpCopy = totpToken.copyWith( - period: 60, - label: 'labelCopy', - issuer: 'issuerCopy', - id: 'idCopy', - algorithm: Algorithms.SHA256, - digits: 8, - secret: 'secretCopy', - pin: true, - tokenImage: 'exampleCopy.png', - sortIndex: 1, - isLocked: true, - folderId: () => 1, - ); - expect(totpCopy.period, 60); - expect(totpCopy.label, 'labelCopy'); - expect(totpCopy.issuer, 'issuerCopy'); - expect(totpCopy.id, 'idCopy'); - expect(totpCopy.algorithm, Algorithms.SHA256); - expect(totpCopy.digits, 8); - expect(totpCopy.secret, 'secretCopy'); - expect(totpCopy.type, 'TOTP'); - expect(totpCopy.pin, true); - expect(totpCopy.tokenImage, 'exampleCopy.png'); - expect(totpCopy.sortIndex, 1); - expect(totpCopy.isLocked, true); - expect(totpCopy.folderId, 1); - }); - }); - group('serialization', () { - group('fromUriMap', () { - test('with full map', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: '6', - OTPToken.SECRET_BASE32: Encodings.base32.encode(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '30', - }; - final totpFromUriMap = TOTPToken.fromOtpAuthMap(uriMap); - expect(totpFromUriMap.period, 30); - expect(totpFromUriMap.label, 'label'); - expect(totpFromUriMap.issuer, 'issuer'); - expect(totpFromUriMap.algorithm, Algorithms.SHA1); - expect(totpFromUriMap.digits, 6); - expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); - expect(totpFromUriMap.type, 'TOTP'); - expect(totpFromUriMap.pin, false); - expect(totpFromUriMap.tokenImage, 'example.png'); - }); - test('with missing secret', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: 6, - TOTPToken.PERIOD_SECONDS: 30, - }; - expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - test('with zero period', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: 6, - OTPToken.SECRET_BASE32: Uint8List.fromList(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: 0, - }; - expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - test('with zero digits', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'SHA1', - OTPToken.DIGITS: 0, - OTPToken.SECRET_BASE32: Uint8List.fromList(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: 30, - }; - expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - test('with lowercase algorithm', () { - final uriMap = { - Token.LABEL: 'label', - Token.ISSUER: 'issuer', - Token.TOKENTYPE_OTPAUTH: 'totp', - Token.PIN: Token.PIN_VALUE_FALSE, - Token.IMAGE: 'example.png', - OTPToken.ALGORITHM: 'sha1', - OTPToken.DIGITS: '6', - OTPToken.SECRET_BASE32: Uint8List.fromList(utf8.encode('secret')), - TOTPToken.PERIOD_SECONDS: '30', - }; - final totpFromUriMap = TOTPToken.fromOtpAuthMap(uriMap); - expect(totpFromUriMap.algorithm, Algorithms.SHA1); - }); - test('with empty map', () { - final uriMap = {}; - expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsA(isA())); - }); - }); - test('toUriMap', () { - final totpUriMap = totpToken.toOtpAuthMap(); - expect(totpUriMap[Token.LABEL], 'label'); - expect(totpUriMap[Token.ISSUER], 'issuer'); - expect(totpUriMap[Token.TOKENTYPE_OTPAUTH], 'TOTP'); - expect(totpUriMap[Token.PIN], 'False'); - expect(totpUriMap[Token.IMAGE], 'example.png'); - expect(totpUriMap[OTPToken.ALGORITHM], 'SHA1'); - expect(totpUriMap[OTPToken.DIGITS], '6'); - expect(totpUriMap[OTPToken.SECRET_BASE32], 'secret'); - expect(totpUriMap[TOTPToken.PERIOD_SECONDS], '30'); - }); - test('fromJson', () { - final totpJson = { - 'period': 11, - 'label': 'label', - 'issuer': 'issuer', - 'id': 'id', - 'algorithm': 'SHA1', - 'digits': 22, - 'secret': 'secret', - 'type': 'totp', - 'pin': true, - 'tokenImage': 'example.png', - 'sortIndex': 33, - 'isLocked': true, - 'folderId': 44, - }; - final totpFromJson = TOTPToken.fromJson(totpJson); - expect(totpFromJson.period, 11); - expect(totpFromJson.label, 'label'); - expect(totpFromJson.issuer, 'issuer'); - expect(totpFromJson.id, 'id'); - expect(totpFromJson.algorithm, Algorithms.SHA1); - expect(totpFromJson.digits, 22); - expect(totpFromJson.secret, 'secret'); - expect(totpFromJson.type, 'totp'); - expect(totpFromJson.pin, true); - expect(totpFromJson.tokenImage, 'example.png'); - expect(totpFromJson.sortIndex, 33); - expect(totpFromJson.isLocked, true); - expect(totpFromJson.folderId, 44); - }); - test('toJson', () { - final totpJson = totpToken.toJson(); - expect(totpJson['period'], 30); - expect(totpJson['label'], 'label'); - expect(totpJson['issuer'], 'issuer'); - expect(totpJson['id'], 'id'); - expect(totpJson['algorithm'], 'SHA1'); - expect(totpJson['digits'], 6); - expect(totpJson['secret'], 'secret'); - expect(totpJson['type'], 'TOTP'); - expect(totpJson['pin'], false); - expect(totpJson['tokenImage'], 'example.png'); - expect(totpJson['sortIndex'], 0); - expect(totpJson['isLocked'], false); - expect(totpJson['folderId'], 0); - }); - }); - group('isSameTokenAs', () { - test('no serial | same id | same parameters', () { - // No serial. Should recognize by id or parameters - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith()), isTrue); - }); - test('no serial | same id | different parameters', () { - // No serial. Should recognize by id - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(algorithm: Algorithms.SHA256)), isTrue); - }); - test('no serial | different id | same parameters', () { - // No serial, different id. Should recognize by parameters - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2')), isTrue); - }); - test('no serial | different id | different parameters', () { - // No serial, different id, different parameters. Should not recognize - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), isFalse); - }); - test('same serial | different id | different parameters', () { - // Different id, different parameters. Should recognize by serial - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256)), isTrue); - }); - test('different serial | same id | different parameters', () { - // Different serial, different parameters. Should recognize by id - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(serial: 'serial2', algorithm: Algorithms.SHA256)), isTrue); - }); - test('different serial | different id | same parameters', () { - // Different serial, different id. Should NOT recognize by parameters - final totpToken = TOTPToken( - period: 30, - label: 'label', - issuer: 'issuer', - serial: 'serial', - id: 'id', - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'secret', - ); - - expect(totpToken.isSameTokenAs(totpToken.copyWith(serial: 'serial2', id: 'id2')), isFalse); - }); - }); - group('Calculate TOTP Token values', () { - // Basicly the TOTP token is a HOTP token but the counter is calculated based on the current time. - // So we can test TOTP token by comparing its OTP value with a HOTP value with the same counter. - // as we know the HOTP token works, we can assume the TOTP token works as well when they have the same otp value. - group('different periods 6 digits', () { - const digits = 6; - test('30s period', () { - const period = Duration(seconds: 30); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('60s period', () { - const period = Duration(seconds: 60); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - }); - group('different periods 8 digits', () { - const digits = 8; - test('30s period', () { - const period = Duration(seconds: 30); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('60s period', () { - const period = Duration(seconds: 60); - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: Algorithms.SHA1, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - }); - group('different algorithms 6 digits', () { - const digits = 6; - const period = Duration(seconds: 30); - test('algorithm SHA1', () { - const algorithm = Algorithms.SHA1; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('algorithm SHA256', () { - const algorithm = Algorithms.SHA256; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('algorithm SHA512', () { - const algorithm = Algorithms.SHA512; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - }); - group('different algorithms 8 digits', () { - const digits = 8; - const period = Duration(seconds: 30); - test('algorithm SHA1', () { - const algorithm = Algorithms.SHA1; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('algorithm SHA256', () { - const algorithm = Algorithms.SHA256; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - test('algorithm SHA512', () { - const algorithm = Algorithms.SHA512; - final hotpToken = HOTPToken( - id: '', - label: '', - issuer: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - counter: (DateTime.now().millisecondsSinceEpoch / 1000) ~/ period.inSeconds, - ); - final totpToken = TOTPToken( - period: period.inSeconds, - label: '', - issuer: '', - id: '', - algorithm: algorithm, - digits: digits, - secret: Encodings.base32.encode(utf8.encode('secret')), - ); - expect(totpToken.otpValue, hotpToken.otpValue); - }); - }); - }); -} diff --git a/test/unit_test/model/tokens/day_password_test.dart b/test/unit_test/model/tokens/day_password_test.dart new file mode 100644 index 000000000..0d426cf06 --- /dev/null +++ b/test/unit_test/model/tokens/day_password_test.dart @@ -0,0 +1,198 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; +import 'package:privacyidea_authenticator/model/token_template.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + group('DayPasswordToken - Complete Test Suite', () { + final testPeriod = Duration(hours: 24); + const baseSecret = 'JBSWY3DPEHPK3PXP'; // 'secret' + + DayPasswordToken createTestToken({ + String id = 'test-id', + DayPasswordTokenViewMode viewMode = DayPasswordTokenViewMode.VALIDFOR, + Duration? period, + }) => DayPasswordToken( + id: id, + secret: baseSecret, + label: 'test-label', + issuer: 'test-issuer', + algorithm: Algorithms.SHA1, + digits: 6, + period: period ?? testPeriod, + viewMode: viewMode, + ); + + group('Core Functionality & Construction', () { + test('initialization with defaults', () { + final token = createTestToken(); + expect(token.type, 'DAYPASSWORD'); + expect(token.period, testPeriod); + expect(token.viewMode, DayPasswordTokenViewMode.VALIDFOR); + }); + + test('fallback for invalid period (0 or negative)', () { + final tokenZero = createTestToken(period: Duration.zero); + final tokenNeg = createTestToken(period: Duration(hours: -1)); + expect(tokenZero.period, Duration(hours: 24)); + expect(tokenNeg.period, Duration(hours: 24)); + }); + }); + + group('Serialization & Factories', () { + test('fromOtpAuthMap handles raw data types (String vs Num)', () { + final map = { + Token.LABEL: 'L', + Token.ISSUER: 'I', + OTPToken.SECRET_BASE32: baseSecret, + OTPToken.ALGORITHM: 'SHA1', + OTPToken.DIGITS: '8', + TOTPToken.PERIOD_SECONDS: '3600', + }; + final token = DayPasswordToken.fromOtpAuthMap(map); + expect(token.digits, 8); + expect(token.period, Duration(hours: 1)); + }); + + test('toJson / fromJson roundtrip', () { + final original = createTestToken( + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + ); + final json = original.toJson(); + final recovered = DayPasswordToken.fromJson(json); + + expect(recovered.viewMode, original.viewMode); + expect(recovered.period, original.period); + expect(recovered.id, original.id); + }); + + test('toOtpAuthMap export includes period in seconds', () { + final token = createTestToken(period: Duration(hours: 1)); + final map = token.toOtpAuthMap(); + expect(map[TOTPToken.PERIOD_SECONDS], '3600'); + }); + + test('additionalData getter contains VIEW_MODE and other metadata', () { + final token = createTestToken( + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + ); + final data = token.additionalData; + expect( + data[DayPasswordToken.VIEW_MODE], + DayPasswordTokenViewMode.VALIDUNTIL.name, + ); + expect(data.containsKey(Token.ID), true); + }); + + test( + 'fromOtpAuthMap handles VIEW_MODE as String from additionalData', + () { + final token = DayPasswordToken.fromOtpAuthMap( + { + Token.LABEL: 'L', + OTPToken.SECRET_BASE32: baseSecret, + OTPToken.ALGORITHM: 'SHA1', + OTPToken.DIGITS: 6, + }, + additionalData: {DayPasswordToken.VIEW_MODE: 'validUntil'}, + ); + expect(token.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); + }, + ); + }); + + group('Template & Copying', () { + test('copyUpdateByTemplate handles VIEW_MODE in additionalData', () { + final token = createTestToken(); + final template = TokenTemplate.withOtps( + otpAuthMap: { + Token.LABEL: 'new-label', + TOTPToken.PERIOD_SECONDS: '7200', + }, + otps: [], + additionalData: { + DayPasswordToken.VIEW_MODE: DayPasswordTokenViewMode.VALIDUNTIL, + }, + ); + + final updated = token.copyUpdateByTemplate(template); + expect(updated.label, 'new-label'); + expect(updated.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); + expect(updated.period, Duration(hours: 2)); + }); + + test('copyWith preserves complex fields like folderId', () { + final token = createTestToken().copyWith(folderId: () => 99); + final copied = token.copyWith(label: 'new'); + expect(copied.folderId, 99); + expect(copied.label, 'new'); + }); + }); + + group('Time Logic & OTP Generation', () { + test('otpValue and nextValue are different (usually)', () { + final token = createTestToken(); + expect(token.otpValue, isNotEmpty); + expect(token.nextValue, isNotEmpty); + }); + + test('time window consistency', () { + final token = createTestToken(); + final nowStart = token.thisOTPTimeStart; + final nextStart = token.nextOTPTimeStart; + + expect(nextStart.isAfter(nowStart), isTrue); + expect(nextStart.difference(nowStart).inSeconds, testPeriod.inSeconds); + }); + + test('durations sum up to period', () { + final token = createTestToken(); + final sum = token.durationSinceLastOTP + token.durationUntilNextOTP; + expect(sum.inSeconds, testPeriod.inSeconds); + }); + }); + + group('Identity & Comparison', () { + test('isSameTokenAs logic', () { + final t1 = createTestToken(id: 'A'); + final t2 = createTestToken(id: 'B'); + final t3 = t1.copyWith(period: Duration(hours: 1)); + + expect(t1.isSameTokenAs(t2), isTrue); + expect(t1.isSameTokenAs(t3), isTrue); + }); + + test('equality check (==) includes viewMode and period', () { + final t1 = createTestToken(viewMode: DayPasswordTokenViewMode.VALIDFOR); + final t2 = createTestToken( + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + ); + expect(t1 == t2, isFalse); + }); + }); + }); +} diff --git a/test/unit_test/model/tokens/hotp_token_test.dart b/test/unit_test/model/tokens/hotp_token_test.dart new file mode 100644 index 000000000..4b0c1e931 --- /dev/null +++ b/test/unit_test/model/tokens/hotp_token_test.dart @@ -0,0 +1,297 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; + +HOTPToken get hotpToken => HOTPToken( + counter: 1, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: true, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: true, + folderId: 0, +); + +void main() { + _testHotpToken(); +} + +void _testHotpToken() { + group('HOTP Token creation', () { + test('constructor', () { + expect(hotpToken.counter, 1); + expect(hotpToken.label, 'label'); + expect(hotpToken.issuer, 'issuer'); + expect(hotpToken.id, 'id'); + expect(hotpToken.algorithm, Algorithms.SHA1); + expect(hotpToken.digits, 6); + expect(hotpToken.secret, 'secret'); + expect(hotpToken.type, 'HOTP'); + expect(hotpToken.pin, true); + expect(hotpToken.tokenImage, 'example.png'); + expect(hotpToken.sortIndex, 0); + expect(hotpToken.isLocked, true); + expect(hotpToken.folderId, 0); + }); + + test('withNextCounter', () { + final withNextCounter = hotpToken.withNextCounter(); + expect(withNextCounter.counter, 2); + }); + + test('copyWith', () { + final hotpCopy = hotpToken.copyWith( + counter: 5, + label: 'labelCopy', + issuer: 'issuerCopy', + id: 'idCopy', + algorithm: Algorithms.SHA256, + digits: 8, + secret: 'secretCopy', + pin: false, + tokenImage: 'exampleCopy.png', + sortIndex: 1, + isLocked: false, + folderId: () => 1, + ); + expect(hotpCopy.counter, 5); + expect(hotpCopy.label, 'labelCopy'); + expect(hotpCopy.issuer, 'issuerCopy'); + expect(hotpCopy.id, 'idCopy'); + expect(hotpCopy.algorithm, Algorithms.SHA256); + expect(hotpCopy.digits, 8); + expect(hotpCopy.secret, 'secretCopy'); + expect(hotpCopy.pin, false); + expect(hotpCopy.tokenImage, 'exampleCopy.png'); + expect(hotpCopy.sortIndex, 1); + expect(hotpCopy.isLocked, false); + expect(hotpCopy.folderId, 1); + }); + + test('copyWith handles folderId null reset', () { + final tokenWithFolder = hotpToken.copyWith(folderId: () => 5); + final resetToken = tokenWithFolder.copyWith(folderId: () => null); + expect(resetToken.folderId, isNull); + }); + }); + + group('serialization', () { + group('fromUriMap', () { + test('with full map', () { + final uriMap = { + Token.LABEL: 'label', + Token.ISSUER: 'issuer', + Token.TOKENTYPE_OTPAUTH: 'HOTP', + Token.PIN: Token.PIN_VALUE_TRUE, + Token.IMAGE: 'example.png', + OTPToken.ALGORITHM: 'SHA1', + OTPToken.SECRET_BASE32: Encodings.base32.encode( + utf8.encode('secret'), + ), + OTPToken.DIGITS: '6', + HOTPToken.COUNTER: '10', + }; + final hotpFromUriMap = HOTPToken.fromOtpAuthMap(uriMap); + expect(hotpFromUriMap.counter, 10); + expect(hotpFromUriMap.label, 'label'); + expect(hotpFromUriMap.issuer, 'issuer'); + expect(hotpFromUriMap.algorithm, Algorithms.SHA1); + expect(hotpFromUriMap.secret, 'ONSWG4TFOQ======'); + expect(hotpFromUriMap.digits, 6); + expect(hotpFromUriMap.type, 'HOTP'); + expect(hotpFromUriMap.pin, true); + expect(hotpFromUriMap.tokenImage, 'example.png'); + }); + + test('without secret', () { + final uriMap = { + Token.LABEL: 'label', + Token.TOKENTYPE_OTPAUTH: 'HOTP', + OTPToken.ALGORITHM: 'SHA1', + OTPToken.DIGITS: '6', + HOTPToken.COUNTER: '10', + }; + expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); + }); + + test('digits is zero', () { + final uriMap = { + OTPToken.SECRET_BASE32: Encodings.base32.encode( + utf8.encode('secret'), + ), + OTPToken.DIGITS: '0', + }; + expect(() => HOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); + }); + + test('invalid counter format defaults to 0', () { + final uriMap = { + OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP', + HOTPToken.COUNTER: 'abc', + }; + final token = HOTPToken.fromOtpAuthMap(uriMap); + expect(token.counter, 0); + }); + + test('with lowercase algorithm', () { + final uriMap = { + OTPToken.ALGORITHM: 'sha1', + OTPToken.SECRET_BASE32: Encodings.base32.encode( + utf8.encode('secret'), + ), + }; + final hotpFromUriMap = HOTPToken.fromOtpAuthMap(uriMap); + expect(hotpFromUriMap.algorithm, Algorithms.SHA1); + }); + }); + + test('toUriMap', () { + final uriMap = hotpToken.toOtpAuthMap(); + expect(uriMap[Token.LABEL], 'label'); + expect(uriMap[OTPToken.ALGORITHM], 'SHA1'); + expect(uriMap[HOTPToken.COUNTER], '1'); + }); + + test('fromJson/toJson', () { + final json = hotpToken.toJson(); + final fromJson = HOTPToken.fromJson(json); + expect(fromJson.counter, hotpToken.counter); + expect(fromJson.secret, hotpToken.secret); + }); + }); + + group('isSameTokenAs', () { + test('same id | same parameters', () { + final token = hotpToken; + expect(token.isSameTokenAs(token), true); + }); + + test('different id | same parameters', () { + final t1 = hotpToken; + final t2 = t1.copyWith(id: 'other-id'); + expect(t1.isSameTokenAs(t2), true); + }); + + test('same serial | different parameters', () { + final t1 = HOTPToken( + id: '1', + secret: 's1', + counter: 0, + serial: 'SER1', + algorithm: Algorithms.SHA1, + digits: 6, + ); + final t2 = HOTPToken( + id: '2', + secret: 's2', + counter: 5, + serial: 'SER1', + algorithm: Algorithms.SHA1, + digits: 6, + ); + expect(t1.isSameTokenAs(t2), true); + }); + }); + + group('Calculate HOTP values (Legacy & RFC Vectors)', () { + group('different counters 6 digits', () { + test('OTP for counter == 0', () { + HOTPToken token0 = HOTPToken( + id: '', + label: '', + issuer: '', + algorithm: Algorithms.SHA1, + digits: 6, + secret: Encodings.base32.encode(utf8.encode('secret')), + counter: 0, + ); + expect(token0.otpValue, '814628'); + }); + test('OTP for counter == 1', () { + HOTPToken token1 = HOTPToken( + id: '', + label: '', + issuer: '', + algorithm: Algorithms.SHA1, + digits: 6, + secret: Encodings.base32.encode(utf8.encode('secret')), + counter: 1, + ); + expect(token1.otpValue, '533881'); + }); + }); + + group('different counters 8 digits', () { + test('OTP for counter == 0', () { + HOTPToken token0 = HOTPToken( + id: '', + label: '', + issuer: '', + algorithm: Algorithms.SHA1, + digits: 8, + secret: Encodings.base32.encode(utf8.encode('secret')), + counter: 0, + ); + expect(token0.otpValue, '31814628'); + }); + }); + + group('different algorithms 6 digits', () { + test('OTP for sha256', () { + HOTPToken token1 = HOTPToken( + id: '', + label: '', + issuer: '', + algorithm: Algorithms.SHA256, + digits: 6, + secret: Encodings.base32.encode(utf8.encode('Secret')), + counter: 0, + ); + expect(token1.otpValue, '203782'); + }); + test('OTP for sha512', () { + HOTPToken token2 = HOTPToken( + id: '', + label: '', + issuer: '', + algorithm: Algorithms.SHA512, + digits: 6, + secret: Encodings.base32.encode(utf8.encode('Secret')), + counter: 0, + ); + expect(token2.otpValue, '636350'); + }); + }); + }); +} diff --git a/test/unit_test/model/tokens/otp_token_test.dart b/test/unit_test/model/tokens/otp_token_test.dart new file mode 100644 index 000000000..b2160f696 --- /dev/null +++ b/test/unit_test/model/tokens/otp_token_test.dart @@ -0,0 +1,192 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/force_biometric_option.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/token_template.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; + +class MockOTPToken extends OTPToken { + const MockOTPToken({ + required super.algorithm, + required super.digits, + required super.secret, + required super.id, + super.serial, + super.label, + super.type = 'MOCK', + super.pin, + super.isLocked, + super.issuer, + }); + + @override + String get otpValue => '123456'; + + @override + String get nextValue => '654321'; + + @override + Map toJson() => { + ...toOtpAuthMap(), + 'algorithm': algorithm.name, + 'digits': digits, + 'secret': secret, + }; + + @override + Token copyUpdateByTemplate(TokenTemplate template) => this; + + @override + OTPToken copyWith({ + String? serial, + String? label, + String? issuer, + String? Function()? containerSerial, + List? checkedContainer, + String? id, + Algorithms? algorithm, + int? digits, + String? secret, + bool? pin, + bool? isLocked, + bool? isHidden, + String? tokenImage, + int? sortIndex, + int? Function()? folderId, + TokenOriginData? origin, + bool? isOffline, + ForceBiometricOption? forceBiometricOption, + }) { + return MockOTPToken( + algorithm: algorithm ?? this.algorithm, + digits: digits ?? this.digits, + secret: secret ?? this.secret, + id: id ?? this.id, + serial: serial ?? this.serial, + label: label ?? this.label, + pin: pin ?? this.pin, + isLocked: isLocked ?? this.isLocked, + issuer: issuer ?? this.issuer, + ); + } +} + +void main() { + group('OTPToken Static Validators', () { + test('validateOtpAuthMap provides default SHA1 and 6 digits', () { + final input = {OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP'}; + final result = OTPToken.validateOtpAuthMap(input); + expect(result[OTPToken.ALGORITHM], Algorithms.SHA1); + expect(result[OTPToken.DIGITS], 6); + }); + + test('validateOtpAuthMap accepts explicit values', () { + final input = { + OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP', + OTPToken.ALGORITHM: 'SHA512', + OTPToken.DIGITS: '8', + }; + final result = OTPToken.validateOtpAuthMap(input); + expect(result[OTPToken.ALGORITHM], Algorithms.SHA512); + expect(result[OTPToken.DIGITS], 8); + }); + }); + + group('OTPToken Serialization & Template Logic', () { + const token = MockOTPToken( + algorithm: Algorithms.SHA256, + digits: 8, + secret: 'SECRET123', + id: 'id_001', + serial: 'SN_001', + label: 'my_label', + ); + + test('toOtpAuthMap output types', () { + final map = token.toOtpAuthMap(); + expect(map[OTPToken.ALGORITHM], isA()); + expect(map[OTPToken.DIGITS], isA()); + expect(map[OTPToken.ALGORITHM], 'SHA256'); + }); + + test('OTP values are included in map only if serial is null', () { + final withSerial = token.toOtpAuthMap(); + expect(withSerial.containsKey(OTPToken.OTP_VALUES), isFalse); + + final noSerial = token.copyWith(serial: null).toOtpAuthMap(); + expect(noSerial[OTPToken.OTP_VALUES], ['123456', '654321']); + }); + + test('toTemplate captures OTP values in otpAuthMap', () { + final template = token.toTemplate(); + expect(template.otpAuthMap[OTPToken.OTP_VALUES], ['123456', '654321']); + }); + + test('toJson contains all required fields', () { + final json = token.toJson(); + expect(json['algorithm'], 'SHA256'); + expect(json['digits'], 8); + expect(json['secret'], 'SECRET123'); + }); + }); + + group('OTPToken Identity Logic', () { + const base = MockOTPToken( + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'S', + id: '1', + ); + + test('isSameTokenAs matches by parameters if ID/Serial differ', () { + const other = MockOTPToken( + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'S', + id: 'different', + ); + expect(base.isSameTokenAs(other), isTrue); + }); + + test('isSameTokenAs fails if secret differs', () { + const other = MockOTPToken( + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'OTHER', + id: '1', + ); + expect(base.isSameTokenAs(other), isFalse); + }); + + test('isSameTokenAs fails if algorithm differs', () { + const other = MockOTPToken( + algorithm: Algorithms.SHA256, + digits: 6, + secret: 'S', + id: '1', + ); + expect(base.isSameTokenAs(other), isFalse); + }); + }); +} diff --git a/test/unit_test/model/tokens/push_token_test.dart b/test/unit_test/model/tokens/push_token_test.dart new file mode 100644 index 000000000..ddb1647f9 --- /dev/null +++ b/test/unit_test/model/tokens/push_token_test.dart @@ -0,0 +1,219 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/day_password_token_view_mode.dart'; +import 'package:privacyidea_authenticator/model/enums/push_token_rollout_state.dart'; +import 'package:privacyidea_authenticator/model/exception_errors/localized_argument_error.dart'; +import 'package:privacyidea_authenticator/model/token_template.dart'; +import 'package:privacyidea_authenticator/model/tokens/day_password_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + group('PushToken - Every Corner Suite', () { + const String testSerial = 'PUSH123456'; + const String testId = 'push-id-999'; + final Uri testUrl = Uri.parse('https://example.com/rollout'); + + PushToken createPush({ + PushTokenRollOutState? rolloutState, + bool? isRolledOut, + bool? sslVerify, + }) => PushToken( + serial: testSerial, + id: testId, + url: testUrl, + rolloutState: rolloutState, + isRolledOut: isRolledOut, + sslVerify: sslVerify, + label: 'Push', + issuer: 'PI', + ); + + test('Rollout State mapping in fromJson factory', () { + final statesToTest = { + 'generatingRSAKeyPair': + PushTokenRollOutState.generatingRSAKeyPairFailed, + 'receivingFirebaseToken': + PushTokenRollOutState.receivingFirebaseTokenFailed, + 'sendRSAPublicKey': PushTokenRollOutState.sendRSAPublicKeyFailed, + 'parsingResponse': PushTokenRollOutState.parsingResponseFailed, + 'rolloutComplete': PushTokenRollOutState.rolloutComplete, + }; + + for (var entry in statesToTest.entries) { + final json = { + 'id': testId, + 'type': 'PIPUSH', + 'serial': testSerial, + 'rolloutState': entry.key, + }; + final token = PushToken.fromJson(json); + expect( + token.rolloutState, + entry.value, + reason: 'Failed for ${entry.key}', + ); + } + }); + + test('fromOtpAuthMap enforces piauth version 1', () { + final map = { + PushToken.VERSION: '2', + Token.SERIAL: testSerial, + PushToken.ROLLOUT_URL: testUrl.toString(), + Token.LABEL: 'L', + Token.ISSUER: 'I', + }; + expect( + () => PushToken.fromOtpAuthMap(map), + throwsA(isA()), + ); + }); + + test('RSA Public/Private key getter null safety', () { + final token = createPush(); + expect(token.rsaPublicServerKey, isNull); + expect(token.rsaPublicTokenKey, isNull); + expect(token.rsaPrivateTokenKey, isNull); + }); + + test('Identity check (isSameTokenAs) corner cases', () { + final t1 = createPush().copyWith(publicServerKey: 'KEY_A'); + final t2 = createPush().copyWith(publicServerKey: 'KEY_A'); + final t3 = createPush().copyWith(publicServerKey: 'KEY_B'); + + expect(t1.isSameTokenAs(t2), isTrue); + expect(t1.isSameTokenAs(t3), isFalse); + }); + + test('toOtpAuthMap transforms booleans to 1/0 and True/False strings', () { + final token = createPush( + sslVerify: true, + ).copyWith(isPollOnly: () => true); + final map = token.toOtpAuthMap(); + expect(map[PushToken.SSL_VERIFY], '1'); + expect(map[PushToken.IS_POLL_ONLY], 'True'); + expect(map[PushToken.VERSION], '1'); + }); + + test('isHidden is constant false for PushToken', () { + final token = createPush().copyWith(isHidden: true); + expect(token.isHidden, isFalse); + }); + }); + + group('DayPasswordToken - Every Corner Suite', () { + final testPeriod = const Duration(hours: 24); + const baseSecret = 'JBSWY3DPEHPK3PXP'; + + DayPasswordToken createDay({ + DayPasswordTokenViewMode viewMode = DayPasswordTokenViewMode.VALIDFOR, + Duration? period, + }) => DayPasswordToken( + id: 'day-id', + secret: baseSecret, + label: 'Day', + issuer: 'PI', + algorithm: Algorithms.SHA1, + digits: 6, + period: period ?? testPeriod, + viewMode: viewMode, + ); + + test('Fallback logic for invalid durations', () { + final tokenZero = createDay(period: Duration.zero); + final tokenNeg = createDay(period: const Duration(seconds: -1)); + expect(tokenZero.period, const Duration(hours: 24)); + expect(tokenNeg.period, const Duration(hours: 24)); + }); + + test('Duration consistency (Sum of durations)', () { + final token = createDay(); + final total = token.durationSinceLastOTP + token.durationUntilNextOTP; + // The sum should exactly match the period + expect(total.inSeconds, token.period.inSeconds); + }); + + test('Time window sequence with microsecond margin', () { + final token = createDay(); + final start = token.thisOTPTimeStart; + final next = token.nextOTPTimeStart; + + expect(next.isAfter(start), isTrue); + + // Use a small margin (e.g., 100ms) to account for execution time between calls + final difference = next.difference(start); + final errorMargin = (difference - token.period).abs(); + + expect( + errorMargin.inMilliseconds, + lessThan(100), + reason: + 'The gap between time windows should be exactly the period, plus/minus execution jitter.', + ); + }); + test('Serialization roundtrip with ViewMode as String', () { + final map = { + Token.LABEL: 'Day', + OTPToken.SECRET_BASE32: baseSecret, + OTPToken.ALGORITHM: 'SHA1', + OTPToken.DIGITS: 6, + TOTPToken.PERIOD_SECONDS: '86400', + }; + + final data = {DayPasswordToken.VIEW_MODE: 'validUntil'}; + final token = DayPasswordToken.fromOtpAuthMap(map, additionalData: data); + + expect(token.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); + expect( + token.additionalData[DayPasswordToken.VIEW_MODE], + DayPasswordTokenViewMode.VALIDUNTIL.name, + ); + }); + test('copyUpdateByTemplate with DayPassword specific fields', () { + final token = createDay(); + final template = TokenTemplate.withOtps( + otpAuthMap: {Token.LABEL: 'Updated', TOTPToken.PERIOD_SECONDS: '3600'}, + otps: [], + additionalData: { + DayPasswordToken.VIEW_MODE: DayPasswordTokenViewMode.VALIDUNTIL, + }, + ); + + final updated = token.copyUpdateByTemplate(template); + expect(updated.label, 'Updated'); + expect(updated.period, const Duration(hours: 1)); + expect(updated.viewMode, DayPasswordTokenViewMode.VALIDUNTIL); + }); + + test('Equality (==) includes viewMode and period', () { + final t1 = createDay(viewMode: DayPasswordTokenViewMode.VALIDFOR); + final t2 = createDay(viewMode: DayPasswordTokenViewMode.VALIDUNTIL); + final t3 = createDay(period: const Duration(hours: 1)); + + expect(t1 == t2, isFalse); + expect(t1 == t3, isFalse); + }); + }); +} diff --git a/test/unit_test/model/tokens/steam_token_test.dart b/test/unit_test/model/tokens/steam_token_test.dart new file mode 100644 index 000000000..58f908cce --- /dev/null +++ b/test/unit_test/model/tokens/steam_token_test.dart @@ -0,0 +1,146 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/token_origin_source_type.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/token_template.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/steam_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +void main() { + group('SteamToken - Complete Deep Dive Suite', () { + const String baseSecret = 'JBSWY3DPEHPK3PXP'; + const String testId = 'steam-unique-id'; + + SteamToken createTestToken({ + String? id, + String? secret, + int? folderId, + TokenOriginData? origin, + }) => SteamToken( + id: id ?? testId, + secret: secret ?? baseSecret, + label: 'SteamUser', + issuer: 'Steam', + folderId: folderId, + origin: origin, + ); + + group('1. Construction & Fixed Constraints', () { + test('Steam tokens must ignore variable TOTP parameters', () { + final token = createTestToken(); + expect(token.type, 'STEAM'); + expect(token.digits, 5); + expect(token.period, 30); + expect(token.algorithm, Algorithms.SHA1); + expect(token.isPrivacyIdeaToken, isFalse); + expect(token.serial, isNull); + }); + }); + + group('2. Serialization & Factories', () { + test( + 'fromOtpAuthMap creates valid instance and handles secret casing', + () { + final map = { + Token.LABEL: 'SteamAccount', + Token.ISSUER: 'Steam', + OTPToken.SECRET_BASE32: 'jbswy3dpehpk3pxp', + }; + final token = SteamToken.fromOtpAuthMap(map); + + expect(token.label, 'SteamAccount'); + expect(token.secret.toUpperCase(), baseSecret); + }, + ); + + test('toOtpAuthMap export format including period', () { + final token = createTestToken(); + final map = token.toOtpAuthMap(); + + expect(map[Token.TOKENTYPE_OTPAUTH], 'STEAM'); + expect(map[OTPToken.SECRET_BASE32], baseSecret); + expect(map[TOTPToken.PERIOD_SECONDS], '30'); + }); + + test('toJson / fromJson roundtrip', () { + final original = createTestToken(folderId: 99); + final json = original.toJson(); + final recovered = SteamToken.fromJson(json); + + expect(recovered.id, original.id); + expect(recovered.folderId, 99); + }); + }); + + group('3. Copying & Templates', () { + test('copyWith preserves enum-based origin source', () { + final origin = TokenOriginData( + source: TokenOriginSourceType.manually, + appName: 'TestApp', + data: 'test-data', + ); + final token = createTestToken(origin: origin, folderId: 10); + + final copied = token.copyWith(label: 'NewName'); + + expect(copied.label, 'NewName'); + expect(copied.origin?.source, TokenOriginSourceType.manually); + expect(copied.folderId, 10); + }); + + test('copyUpdateByTemplate handles required secret mapping', () { + final token = createTestToken(); + final template = TokenTemplate.withOtps( + otpAuthMap: { + Token.LABEL: 'UpdatedLabel', + OTPToken.SECRET_BASE32: baseSecret, + }, + otps: [], + ); + + final updated = token.copyUpdateByTemplate(template); + expect(updated.label, 'UpdatedLabel'); + expect(updated.digits, 5); + }); + }); + + group('4. Logic & Edge Cases', () { + test('otpFromTime calculation with Steam alphabet', () { + final time = DateTime.fromMillisecondsSinceEpoch(1712666212056); + final token = createTestToken(secret: 'SECRETA='); + + final otp = token.otpFromTime(time); + expect(otp, 'JGPCJ'); + expect(otp.length, 5); + }); + + test('isSameTokenAs logic (ID vs Parameters)', () { + final t1 = createTestToken(id: 'ID1', secret: 'SECRET_A'); + final t2 = createTestToken(id: 'ID2', secret: 'SECRET_A'); + + expect(t1.isSameTokenAs(t2), isTrue); + }); + }); + }); +} diff --git a/test/unit_test/model/tokens/token_test.dart b/test/unit_test/model/tokens/token_test.dart new file mode 100644 index 000000000..a45173afe --- /dev/null +++ b/test/unit_test/model/tokens/token_test.dart @@ -0,0 +1,267 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/force_biometric_option.dart'; +import 'package:privacyidea_authenticator/model/token_import/token_origin_data.dart'; +import 'package:privacyidea_authenticator/model/token_template.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; + +class MockToken extends Token { + const MockToken({ + required super.id, + required super.type, + super.serial, + super.label, + super.issuer, + super.pin, + super.isLocked, + super.isHidden, + super.forceBiometricOption, + super.tokenImage, + super.sortIndex, + super.isOffline, + super.checkedContainer, + super.folderId, + super.origin, + super.containerSerial, + }); + + @override + Map toJson() => {'id': id, 'type': type, 'serial': serial}; + + @override + Token copyUpdateByTemplate(TokenTemplate template) => this; + + @override + Token copyWith({ + String? serial, + String? label, + String? issuer, + String? Function()? containerSerial, + List? checkedContainer, + String? id, + bool? isLocked, + bool? isHidden, + bool? pin, + String? tokenImage, + int? sortIndex, + int? Function()? folderId, + TokenOriginData? origin, + bool? isOffline, + ForceBiometricOption? forceBiometricOption, + }) { + return MockToken( + id: id ?? this.id, + type: type, + serial: serial ?? this.serial, + label: label ?? this.label, + issuer: issuer ?? this.issuer, + pin: pin ?? this.pin, + isLocked: isLocked ?? this.isLocked, + isHidden: isHidden ?? this.isHidden, + forceBiometricOption: forceBiometricOption ?? this.forceBiometricOption, + tokenImage: tokenImage ?? this.tokenImage, + sortIndex: sortIndex ?? this.sortIndex, + isOffline: isOffline ?? this.isOffline, + checkedContainer: checkedContainer ?? this.checkedContainer, + folderId: folderId != null ? folderId() : this.folderId, + origin: origin ?? this.origin, + containerSerial: containerSerial != null + ? containerSerial() + : this.containerSerial, + ); + } +} + +void main() { + group('Token Constants & Validators', () { + test('Verify static string constants for persistence and UI', () { + expect(Token.PIN_VALUE_TRUE, 'True'); + expect(Token.PIN_VALUE_FALSE, 'False'); + expect(Token.TOKENTYPE_OTPAUTH, 'tokentype'); + expect(Token.TOKENTYPE_JSON, 'type'); + expect(Token.FORCE_BIOMETRIC_OPTION, 'app_force_unlock'); + }); + + test('validateOtpAuthMap ensures all base fields have defaults', () { + final result = Token.validateOtpAuthMap({}); + expect(result[Token.LABEL], ''); + expect(result[Token.ISSUER], ''); + expect(result[Token.OFFLINE], false); + expect(result[Token.FORCE_BIOMETRIC_OPTION], ForceBiometricOption.none); + }); + + test('validateAdditionalData ensures list and optional field handling', () { + final result = Token.validateAdditionalData({}); + expect(result[Token.CHECKED_CONTAINERS], []); + expect(result[Token.ID], isNull); + expect(result[Token.SORT_INDEX], isNull); + }); + }); + + group('Token State Logic: isLocked & isHidden corners', () { + test( + 'isLocked returns true if PIN is required regardless of internal state', + () { + const t = MockToken(id: '1', type: 'T', pin: true, isLocked: false); + expect(t.isLocked, isTrue); + }, + ); + + test('isLocked returns true if Biometrics are forced', () { + final t = MockToken( + id: '1', + type: 'T', + forceBiometricOption: ForceBiometricOption.biometric, + isLocked: false, + ); + expect(t.isLocked, isTrue); + }); + + test( + 'isLocked returns false only if PIN, Biometrics, and _isLocked are all false', + () { + const t = MockToken( + id: '1', + type: 'T', + pin: false, + forceBiometricOption: ForceBiometricOption.none, + isLocked: false, + ); + expect(t.isLocked, isFalse); + }, + ); + + test('isHidden defaults to isLocked value when not specified', () { + const locked = MockToken(id: '1', type: 'T', isLocked: true); + const unlocked = MockToken(id: '2', type: 'T', isLocked: false); + expect(locked.isHidden, isTrue); + expect(unlocked.isHidden, isFalse); + }); + + test( + 'isHidden can be false while isLocked is true (explicit override)', + () { + const t = MockToken( + id: '1', + type: 'T', + isLocked: true, + isHidden: false, + ); + expect(t.isLocked, isTrue); + expect(t.isHidden, isFalse); + }, + ); + }); + + group('Token Identity & Equality contracts', () { + test('Operator == strictly compares ID', () { + const t1 = MockToken(id: 'ID-A', type: 'HOTP'); + const t2 = MockToken(id: 'ID-A', type: 'TOTP'); + const t3 = MockToken(id: 'ID-B', type: 'HOTP'); + + expect(t1 == t2, isTrue); + expect(t1 == t3, isFalse); + }); + + test('isSameTokenAs: match by ID', () { + const t1 = MockToken(id: 'SAME', type: 'A'); + const t2 = MockToken(id: 'SAME', type: 'B'); + expect(t1.isSameTokenAs(t2), isTrue); + }); + + test('isSameTokenAs: match by Serial and Issuer if IDs differ', () { + const t1 = MockToken(id: '1', type: 'T', serial: 'SN', issuer: 'ISS'); + const t2 = MockToken(id: '2', type: 'T', serial: 'SN', issuer: 'ISS'); + expect(t1.isSameTokenAs(t2), isTrue); + }); + + test( + 'isSameTokenAs: mismatch if issuer differs even if serial matches', + () { + const t1 = MockToken(id: '1', type: 'T', serial: 'SN', issuer: 'ISS1'); + const t2 = MockToken(id: '2', type: 'T', serial: 'SN', issuer: 'ISS2'); + expect(t1.isSameTokenAs(t2), isFalse); + }, + ); + + test('isSameTokenAs: return null if IDs differ and serials are null', () { + const t1 = MockToken(id: '1', type: 'T', serial: null); + const t2 = MockToken(id: '2', type: 'T', serial: null); + expect(t1.isSameTokenAs(t2), isNull); + }); + }); + + group('Token Serialization & Template corner cases', () { + test( + 'toOtpAuthMap transforms PIN bool to proprietary True/False strings', + () { + const tTrue = MockToken(id: '1', type: 'T', pin: true); + const tFalse = MockToken(id: '1', type: 'T', pin: false); + expect(tTrue.toOtpAuthMap()[Token.PIN], 'True'); + expect(tFalse.toOtpAuthMap()[Token.PIN], 'False'); + }, + ); + + test('additionalData exports all internal fields', () { + const t = MockToken( + id: 'uid', + type: 'T', + sortIndex: 5, + folderId: 10, + checkedContainer: ['C1', 'C2'], + containerSerial: 'CONT-SN', + ); + final data = t.additionalData; + expect(data[Token.ID], 'uid'); + expect(data[Token.SORT_INDEX], 5); + expect(data[Token.FOLDER_ID], 10); + expect(data[Token.CHECKED_CONTAINERS], ['C1', 'C2']); + expect(data[Token.CONTAINER_SERIAL], 'CONT-SN'); + }); + + test('toTemplate captures correct state only when serial is present', () { + const tWith = MockToken(id: '1', type: 'T', serial: 'SN-123'); + const tWithout = MockToken(id: '2', type: 'T', serial: null); + + expect(tWith.toTemplate(), isNotNull); + expect(tWith.toTemplate()?.serial, 'SN-123'); + expect(tWithout.toTemplate(), isNull); + }); + }); + + group('Token Factory Dispatch Error Handling', () { + test('fromJson throws ArgumentError on missing type key', () { + expect(() => Token.fromJson({}), throwsArgumentError); + }); + + test('fromJson throws ArgumentError on unsupported type name', () { + expect( + () => Token.fromJson({'type': 'INVALID_TYPE'}), + throwsArgumentError, + ); + }); + + test('fromOtpAuthMap throws ArgumentError on missing tokentype key', () { + expect(() => Token.fromOtpAuthMap({}), throwsArgumentError); + }); + }); +} diff --git a/test/unit_test/model/tokens/totp_token_test.dart b/test/unit_test/model/tokens/totp_token_test.dart new file mode 100644 index 000000000..92f00f79e --- /dev/null +++ b/test/unit_test/model/tokens/totp_token_test.dart @@ -0,0 +1,251 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; +import 'package:privacyidea_authenticator/model/enums/encodings.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/encodings_extension.dart'; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; + +TOTPToken get totpToken => TOTPToken( + period: 30, + label: 'label', + issuer: 'issuer', + id: 'id', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret', + pin: false, + tokenImage: 'example.png', + sortIndex: 0, + isLocked: false, + folderId: 0, +); + +void main() { + group('TOTP Token creation', () { + test('constructor', () { + final token = totpToken; + expect(token.period, 30); + expect(token.label, 'label'); + expect(token.issuer, 'issuer'); + expect(token.id, 'id'); + expect(token.algorithm, Algorithms.SHA1); + expect(token.digits, 6); + expect(token.secret, 'secret'); + expect(token.type, 'TOTP'); + expect(token.pin, false); + expect(token.tokenImage, 'example.png'); + expect(token.sortIndex, 0); + expect(token.isLocked, false); + expect(token.folderId, 0); + }); + + test('copyWith', () { + final totpCopy = totpToken.copyWith( + period: 60, + label: 'labelCopy', + issuer: 'issuerCopy', + id: 'idCopy', + algorithm: Algorithms.SHA256, + digits: 8, + secret: 'secretCopy', + pin: true, + tokenImage: 'exampleCopy.png', + sortIndex: 1, + isLocked: true, + folderId: () => 1, + ); + expect(totpCopy.period, 60); + expect(totpCopy.label, 'labelCopy'); + expect(totpCopy.issuer, 'issuerCopy'); + expect(totpCopy.id, 'idCopy'); + expect(totpCopy.algorithm, Algorithms.SHA256); + expect(totpCopy.digits, 8); + expect(totpCopy.secret, 'secretCopy'); + expect(totpCopy.pin, true); + expect(totpCopy.tokenImage, 'exampleCopy.png'); + expect(totpCopy.sortIndex, 1); + expect(totpCopy.isLocked, true); + expect(totpCopy.folderId, 1); + }); + }); + + group('serialization', () { + group('fromUriMap', () { + test('with full map', () { + final uriMap = { + Token.LABEL: 'label', + Token.ISSUER: 'issuer', + Token.TOKENTYPE_OTPAUTH: 'totp', + Token.PIN: Token.PIN_VALUE_FALSE, + Token.IMAGE: 'example.png', + OTPToken.ALGORITHM: 'SHA1', + OTPToken.DIGITS: '6', + OTPToken.SECRET_BASE32: Encodings.base32.encode( + utf8.encode('secret'), + ), + TOTPToken.PERIOD_SECONDS: '30', + }; + final totpFromUriMap = TOTPToken.fromOtpAuthMap(uriMap); + expect(totpFromUriMap.period, 30); + expect(totpFromUriMap.label, 'label'); + expect(totpFromUriMap.issuer, 'issuer'); + expect(totpFromUriMap.algorithm, Algorithms.SHA1); + expect(totpFromUriMap.digits, 6); + expect(totpFromUriMap.secret, 'ONSWG4TFOQ======'); + expect(totpFromUriMap.type, 'TOTP'); + }); + + test('with missing secret throws', () { + final uriMap = { + Token.TOKENTYPE_OTPAUTH: 'totp', + OTPToken.DIGITS: '6', + TOTPToken.PERIOD_SECONDS: '30', + }; + expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); + }); + + test('with zero period throws', () { + final uriMap = { + OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP', + TOTPToken.PERIOD_SECONDS: '0', + }; + expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); + }); + + test('with zero digits throws', () { + final uriMap = { + OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP', + OTPToken.DIGITS: '0', + }; + expect(() => TOTPToken.fromOtpAuthMap(uriMap), throwsArgumentError); + }); + + test('with lowercase algorithm', () { + final uriMap = { + OTPToken.ALGORITHM: 'sha1', + OTPToken.SECRET_BASE32: 'JBSWY3DPEHPK3PXP', + }; + final totpFromUriMap = TOTPToken.fromOtpAuthMap(uriMap); + expect(totpFromUriMap.algorithm, Algorithms.SHA1); + }); + }); + + test('toUriMap', () { + final totpUriMap = totpToken.toOtpAuthMap(); + expect(totpUriMap[Token.LABEL], 'label'); + expect(totpUriMap[Token.ISSUER], 'issuer'); + expect(totpUriMap[OTPToken.ALGORITHM], 'SHA1'); + expect(totpUriMap[OTPToken.DIGITS], '6'); + expect(totpUriMap[TOTPToken.PERIOD_SECONDS], '30'); + }); + + test('fromJson/toJson consistency', () { + final totpJson = { + 'period': 11, + 'label': 'label', + 'issuer': 'issuer', + 'id': 'id', + 'algorithm': 'SHA1', + 'digits': 22, + 'secret': 'secret', + 'type': 'totp', + 'pin': true, + 'tokenImage': 'example.png', + 'sortIndex': 33, + 'isLocked': true, + 'folderId': 44, + }; + final fromJson = TOTPToken.fromJson(totpJson); + expect(fromJson.period, 11); + expect(fromJson.digits, 22); + expect(fromJson.toJson()['period'], 11); + }); + }); + + group('isSameTokenAs', () { + test('same serial | different id', () { + final t1 = totpToken.copyWith(serial: 'SN1', id: 'id1'); + final t2 = totpToken.copyWith(serial: 'SN1', id: 'id2'); + expect(t1.isSameTokenAs(t2), isTrue); + }); + + test('no serial | different id | same params', () { + final t1 = totpToken.copyWith(id: 'id1'); + final t2 = totpToken.copyWith(id: 'id2'); + expect(t1.isSameTokenAs(t2), isTrue); + }); + + test('no serial | different id | different params', () { + final t1 = totpToken.copyWith(id: 'id1', algorithm: Algorithms.SHA1); + final t2 = totpToken.copyWith(id: 'id2', algorithm: Algorithms.SHA256); + expect(t1.isSameTokenAs(t2), isFalse); + }); + }); + + group('Calculate TOTP values (Full Algorithms & Digits)', () { + final now = DateTime.now(); + final secret = Encodings.base32.encode(utf8.encode('secret')); + + void testTotpVsHotp(int period, int digits, Algorithms algorithm) { + final counter = (now.millisecondsSinceEpoch / 1000) ~/ period; + + final hotp = HOTPToken( + id: '', + algorithm: algorithm, + digits: digits, + counter: counter, + secret: secret, + ); + + final totp = TOTPToken( + period: period, + id: '', + algorithm: algorithm, + digits: digits, + secret: secret, + ); + + expect( + totp.otpFromTime(now), + hotp.otpValue, + reason: 'Failed for $algorithm, $digits digits, $period period', + ); + } + + test('SHA1 - 6 digits - 30s', () => testTotpVsHotp(30, 6, Algorithms.SHA1)); + test('SHA1 - 6 digits - 60s', () => testTotpVsHotp(60, 6, Algorithms.SHA1)); + test('SHA1 - 8 digits - 30s', () => testTotpVsHotp(30, 8, Algorithms.SHA1)); + test( + 'SHA256 - 6 digits - 30s', + () => testTotpVsHotp(30, 6, Algorithms.SHA256), + ); + test( + 'SHA512 - 8 digits - 60s', + () => testTotpVsHotp(60, 8, Algorithms.SHA512), + ); + }); +} From 7e9ea81914c04f5869d29e1addcfe2d996252797 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:48:19 +0100 Subject: [PATCH 02/13] Add unit tests for TokenNotifier functionality in riverpod providers - Implement tests for loading state from repository, retrieving tokens by ID, incrementing counters, removing tokens, and adding or replacing tokens. - Include tests for handling push tokens and QR code scanning for token enrollment. - Utilize mock repositories and utilities to simulate interactions and validate expected outcomes. --- lib/repo/secure_token_repository.dart | 2 +- lib/utils/lock_auth.dart | 17 +- test/tests_app_wrapper.dart | 13 +- .../repo/secure_token_repository_test.dart | 241 +++++++++++------- test/unit_test/utils/lock_auth_test.dart | 180 +++++++++++++ .../unit_test/utils/lock_auth_test.mocks.dart | 99 +++++++ .../token_notifier_test.dart | 115 ++++----- 7 files changed, 500 insertions(+), 167 deletions(-) create mode 100644 test/unit_test/utils/lock_auth_test.dart create mode 100644 test/unit_test/utils/lock_auth_test.mocks.dart rename test/unit_test/{state_notifiers => utils/riverpod/riverpod_providers/generated_providers}/token_notifier_test.dart (91%) diff --git a/lib/repo/secure_token_repository.dart b/lib/repo/secure_token_repository.dart index 0181bb6a9..0f9781b30 100644 --- a/lib/repo/secure_token_repository.dart +++ b/lib/repo/secure_token_repository.dart @@ -240,7 +240,7 @@ class SecureTokenRepository implements TokenRepository { @override Future deleteToken(Token token) async { try { - _storage.delete(key: token.id); + await _storage.delete(key: token.id); } catch (e, s) { Logger.warning( 'Could not delete token from secure storage', diff --git a/lib/utils/lock_auth.dart b/lib/utils/lock_auth.dart index a72eabfc2..ba1c39e56 100644 --- a/lib/utils/lock_auth.dart +++ b/lib/utils/lock_auth.dart @@ -36,7 +36,7 @@ import '../widgets/gap.dart'; import 'logger.dart'; import 'view_utils.dart'; -final LocalAuthentication _localAuth = LocalAuthentication(); +LocalAuthentication _localAuth = LocalAuthentication(); final Mutex _authMutex = Mutex(); /// Requests OS-level authentication from the user. @@ -61,17 +61,22 @@ Future lockAuth({ ); return false; } + + await _authMutex.acquire(); final isBiometricForced = forceBiometricOption == ForceBiometricOption.biometric; if (!await _checkSupport(isBiometricForced, autoAuthIfUnsupported)) { return autoAuthIfUnsupported; } - return await _executeAuth( + final isAuthenticated = await _executeAuth( isBiometricForced: isBiometricForced, localizedReason: reason(localization), localization: localization, ); + + _authMutex.release(); + return isAuthenticated; } Future _executeAuth({ @@ -80,7 +85,6 @@ Future _executeAuth({ required AppLocalizations localization, }) async { try { - await _authMutex.acquire(); return await _localAuth.authenticate( biometricOnly: isBiometricForced, localizedReason: localizedReason, @@ -92,7 +96,7 @@ Future _executeAuth({ IOSAuthMessages(cancelButton: localization.cancel), ], ); - } on Exception catch (e, s) { + } catch (e, s) { if (e is LocalAuthException && e.code == LocalAuthExceptionCode.userCanceled) { Logger.info("Authentication canceled by user"); @@ -100,8 +104,6 @@ Future _executeAuth({ Logger.warning("Authentication failed", error: e, stackTrace: s); } return false; - } finally { - _authMutex.release(); } } @@ -233,3 +235,6 @@ Future _showBiometricUnavailableDialog() async { ), ); } + +@visibleForTesting +set localAuthInstance(LocalAuthentication auth) => _localAuth = auth; diff --git a/test/tests_app_wrapper.dart b/test/tests_app_wrapper.dart index b3831dd76..3a30e830d 100644 --- a/test/tests_app_wrapper.dart +++ b/test/tests_app_wrapper.dart @@ -41,7 +41,11 @@ class TestsAppWrapper extends StatelessWidget { final Widget child; final List overrides; - const TestsAppWrapper({super.key, required this.child, this.overrides = const []}); + const TestsAppWrapper({ + super.key, + required this.child, + this.overrides = const [], + }); @override Widget build(BuildContext context) { @@ -52,7 +56,12 @@ class TestsAppWrapper extends StatelessWidget { } } -Future pumpUntilFindNWidgets(WidgetTester tester, Finder finder, int n, Duration timeOut) async { +Future pumpUntilFindNWidgets( + WidgetTester tester, + Finder finder, + int n, + Duration timeOut, +) async { final startTime = DateTime.now(); while (true) { await tester.pump(); diff --git a/test/unit_test/repo/secure_token_repository_test.dart b/test/unit_test/repo/secure_token_repository_test.dart index 55e99b4ea..c35594d2f 100644 --- a/test/unit_test/repo/secure_token_repository_test.dart +++ b/test/unit_test/repo/secure_token_repository_test.dart @@ -1,5 +1,26 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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:convert'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:privacyidea_authenticator/model/enums/algorithms.dart'; @@ -22,137 +43,167 @@ void main() { setUp(() { mockStorage = MockFlutterSecureStorage(); mockLegacyStorage = MockFlutterSecureStorage(); - storage = SecureStorage(storagePrefix: SecureTokenRepository.TOKEN_PREFIX, storage: mockStorage); - legacyStorage = SecureStorage(storagePrefix: SecureTokenRepository.TOKEN_PREFIX_LEGACY, storage: mockLegacyStorage); - repository = SecureTokenRepository(storage: storage, legacyStorage: legacyStorage); + storage = SecureStorage( + storagePrefix: SecureTokenRepository.TOKEN_PREFIX, + storage: mockStorage, + ); + legacyStorage = SecureStorage( + storagePrefix: SecureTokenRepository.TOKEN_PREFIX_LEGACY, + storage: mockLegacyStorage, + ); + repository = SecureTokenRepository( + storage: storage, + legacyStorage: legacyStorage, + ); }); TOTPToken createToken(String id) { - return TOTPToken(label: id, issuer: 'issuer', secret: 'SECRET', id: id, algorithm: Algorithms.SHA1, digits: 6, period: 30); + return TOTPToken( + label: id, + issuer: 'issuer', + secret: 'SECRET', + id: id, + algorithm: Algorithms.SHA1, + digits: 6, + period: 30, + ); } - group('SecureTokenRepository', () { - test('loadTokens returns empty list if nothing is stored', () async { - when(mockStorage.readAll()).thenAnswer((_) async => {}); - when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); - - final tokens = await repository.loadTokens(); - - expect(tokens, isEmpty); - }); - - test('loadTokens loads tokens from storage', () async { - final token1 = createToken('id1'); - final token2 = createToken('id2'); - // readAll on the raw storage returns prefixed keys - final tokenMap = {'${newPrefix}_${token1.id}': jsonEncode(token1.toJson()), '${newPrefix}_${token2.id}': jsonEncode(token2.toJson())}; - - when(mockStorage.readAll()).thenAnswer((_) async => tokenMap); - when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); - - final tokens = await repository.loadTokens(); - - expect(tokens.length, 2); - expect(tokens.any((t) => t.id == 'id1'), isTrue); - expect(tokens.any((t) => t.id == 'id2'), isTrue); - }); - - test('saveOrReplaceToken saves a token to storage', () async { + group('SecureTokenRepository - Core Logic', () { + test( + 'loadTokens returns empty list on PlatformException (Decryption Error)', + () async { + when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); + when( + mockStorage.readAll(), + ).thenThrow(PlatformException(code: 'DECRYPT_FAILED')); + + final tokens = await repository.loadTokens(); + + expect(tokens, isEmpty); + }, + ); + + test( + 'loadTokens filters out corrupted single tokens but returns valid ones', + () async { + final validToken = createToken('valid'); + final tokenMap = { + '${newPrefix}_valid': jsonEncode(validToken.toJson()), + '${newPrefix}_corrupt': 'not-json-at-all', + }; + + when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); + when(mockStorage.readAll()).thenAnswer((_) async => tokenMap); + + final tokens = await repository.loadTokens(); + + expect(tokens.length, 1); + expect(tokens.first.id, 'valid'); + }, + ); + + test('saveOrReplaceToken returns false on storage error', () async { final token = createToken('id1'); - final expectedKey = '${newPrefix}_${token.id}'; - final expectedValue = jsonEncode(token.toJson()); - - when(mockStorage.write(key: expectedKey, value: expectedValue)).thenAnswer((_) async {}); + when( + mockStorage.write(key: anyNamed('key'), value: anyNamed('value')), + ).thenThrow(Exception('Storage Full')); final result = await repository.saveOrReplaceToken(token); - expect(result, isTrue); - verify(mockStorage.write(key: expectedKey, value: expectedValue)).called(1); + expect(result, isFalse); }); - test('deleteToken removes a token from storage', () async { - final token = createToken('id1'); - final expectedKey = '${newPrefix}_${token.id}'; - - when(mockStorage.delete(key: expectedKey)).thenAnswer((_) async {}); + test('loadToken handles non-existent ID gracefully', () async { + when( + mockStorage.read(key: '${newPrefix}_missing'), + ).thenAnswer((_) async => null); - final result = await repository.deleteToken(token); + final token = await repository.loadToken('missing'); - expect(result, isTrue); - verify(mockStorage.delete(key: expectedKey)).called(1); + expect(token, isNull); }); }); - group('Migration', () { - test('loadTokens migrates legacy tokens', () async { - final token1 = createToken('id1'); - final legacyKey = '${legacyPrefix}_${token1.id}'; - final newKey = '${newPrefix}_${token1.id}'; - final value = jsonEncode(token1.toJson()); + group('SecureTokenRepository - Migration Corners', () { + test('Migration continues even if one token write fails', () async { + final t1 = createToken('id1'); + final t2 = createToken('id2'); + final legacyMap = { + '${legacyPrefix}_id1': jsonEncode(t1.toJson()), + '${legacyPrefix}_id2': jsonEncode(t2.toJson()), + }; - final legacyTokenMap = {legacyKey: value}; - final newTokenMap = {newKey: value}; + when(mockLegacyStorage.readAll()).thenAnswer((_) async => legacyMap); - when(mockLegacyStorage.readAll()).thenAnswer((_) async => legacyTokenMap); - when(mockStorage.write(key: newKey, value: value)).thenAnswer((_) async {}); - when(mockLegacyStorage.delete(key: legacyKey)).thenAnswer((_) async {}); - when(mockStorage.readAll()).thenAnswer((_) async => newTokenMap); + when( + mockStorage.write(key: '${newPrefix}_id1', value: anyNamed('value')), + ).thenThrow(Exception('Individual write failure')); + when( + mockStorage.write(key: '${newPrefix}_id2', value: anyNamed('value')), + ).thenAnswer((_) async {}); - final tokens = await repository.loadTokens(); + when( + mockStorage.readAll(), + ).thenAnswer((_) async => {'${newPrefix}_id2': jsonEncode(t2.toJson())}); - verify(mockStorage.write(key: newKey, value: value)).called(1); - verify(mockLegacyStorage.delete(key: legacyKey)).called(1); + final tokens = await repository.loadTokens(); expect(tokens.length, 1); - expect(tokens.first.id, 'id1'); + expect(tokens.first.id, 'id2'); + verify(mockLegacyStorage.delete(key: '${legacyPrefix}_id2')).called(1); + verifyNever(mockLegacyStorage.delete(key: '${legacyPrefix}_id1')); }); - test('loadTokens does not migrate if no legacy tokens exist', () async { - when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); + test('Migration skips entries without type field', () async { + final invalidLegacyData = { + '${legacyPrefix}_no_type': jsonEncode({ + 'id': 'some-id', + 'label': 'no-type', + }), + }; + + when( + mockLegacyStorage.readAll(), + ).thenAnswer((_) async => invalidLegacyData); when(mockStorage.readAll()).thenAnswer((_) async => {}); await repository.loadTokens(); - verifyNever(mockStorage.write(key: anyNamed('key'), value: anyNamed('value'))); - verifyNever(mockLegacyStorage.delete(key: anyNamed('key'))); + verifyNever( + mockStorage.write(key: anyNamed('key'), value: anyNamed('value')), + ); }); + }); - test('loadTokens ignores invalid entries during migration', () async { - final validToken = createToken('id1'); - final validTokenValue = jsonEncode(validToken.toJson()); - final validLegacyKey = '${legacyPrefix}_${validToken.id}'; - final validNewKey = '${newPrefix}_${validToken.id}'; - - final legacyData = { - validLegacyKey: validTokenValue, // Valid token - '${legacyPrefix}_invalid_json': 'this is not a json string', // Invalid JSON - '${legacyPrefix}_missing_type': jsonEncode({'some_key': 'some_value'}), // Valid JSON, but not a token - }; - - final expectedNewTokenMap = {validNewKey: validTokenValue}; + group('SecureTokenRepository - Bulk Operations', () { + test('deleteTokens returns list of tokens that failed to delete', () async { + final t1 = createToken('id1'); + final t2 = createToken('id2'); - when(mockLegacyStorage.readAll()).thenAnswer((_) async => legacyData); - when(mockStorage.readAll()).thenAnswer((_) async => expectedNewTokenMap); + when( + mockStorage.delete(key: '${newPrefix}_id1'), + ).thenAnswer((_) async {}); + when( + mockStorage.delete(key: '${newPrefix}_id2'), + ).thenThrow(Exception('Delete failed')); - // Expect write and delete to be called only for the valid token - when(mockStorage.write(key: validNewKey, value: validTokenValue)).thenAnswer((_) async {}); - when(mockLegacyStorage.delete(key: validLegacyKey)).thenAnswer((_) async {}); + final failed = await repository.deleteTokens([t1, t2]); - final tokens = await repository.loadTokens(); + expect(failed.length, 1); + expect(failed.first.id, 'id2'); + }); - // Verify that only the valid token was migrated - verify(mockStorage.write(key: validNewKey, value: validTokenValue)).called(1); - verify(mockLegacyStorage.delete(key: validLegacyKey)).called(1); + test('saveOrReplaceTokens returns failed tokens', () async { + final t1 = createToken('id1'); + when( + mockStorage.write(key: anyNamed('key'), value: anyNamed('value')), + ).thenThrow(Exception('Write failed')); - // Verify that write/delete were NOT called for invalid entries - verifyNever(mockStorage.write(key: '${legacyPrefix}_invalid_json', value: anyNamed('value'))); - verifyNever(mockLegacyStorage.delete(key: '${legacyPrefix}_invalid_json')); - verifyNever(mockStorage.write(key: '${legacyPrefix}_missing_type', value: anyNamed('value'))); - verifyNever(mockLegacyStorage.delete(key: '${legacyPrefix}_missing_type')); + final failed = await repository.saveOrReplaceTokens([t1]); - // Verify the final list contains only the valid migrated token - expect(tokens.length, 1); - expect(tokens.first.id, 'id1'); + expect(failed, isNotEmpty); + expect(failed.first.id, 'id1'); }); }); } diff --git a/test/unit_test/utils/lock_auth_test.dart b/test/unit_test/utils/lock_auth_test.dart new file mode 100644 index 000000000..4b3986159 --- /dev/null +++ b/test/unit_test/utils/lock_auth_test.dart @@ -0,0 +1,180 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations_en.dart'; +import 'package:privacyidea_authenticator/model/enums/force_biometric_option.dart'; +import 'package:privacyidea_authenticator/utils/lock_auth.dart'; + +import 'lock_auth_test.mocks.dart'; + +@GenerateMocks([LocalAuthentication]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockLocalAuthentication mockLocalAuth; + + setUp(() { + mockLocalAuth = MockLocalAuthentication(); + localAuthInstance = mockLocalAuth; // override Local instance for testing + }); + + group('lockAuth - Basic Flow', () { + test('should return true when authentication succeeds', () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + authMessages: anyNamed('authMessages'), + ), + ).thenAnswer((_) async => true); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + ); + + expect(result, isTrue); + }); + + test( + 'should return false when authentication is canceled by user', + () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + authMessages: anyNamed('authMessages'), + ), + ).thenAnswer((_) async => false); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + ); + + expect(result, isFalse); + }, + ); + }); + + group('lockAuth - Hardware & Support', () { + test( + 'should return autoAuthIfUnsupported value if hardware is missing', + () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => false); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + autoAuthIfUnsupported: true, + ); + + expect(result, isTrue); + verifyNever( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + ), + ); + }, + ); + + test( + 'should return false if biometric is forced but sensor is missing', + () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when(mockLocalAuth.canCheckBiometrics).thenAnswer((_) async => false); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + forceBiometricOption: ForceBiometricOption.biometric, + ); + + expect(result, isFalse); + }, + ); + + test( + 'should return false if biometric is forced but none are enrolled', + () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when(mockLocalAuth.canCheckBiometrics).thenAnswer((_) async => true); + when( + mockLocalAuth.getAvailableBiometrics(), + ).thenAnswer((_) async => []); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + forceBiometricOption: ForceBiometricOption.biometric, + ); + + expect(result, isFalse); + }, + ); + }); + + group('lockAuth - Concurrency & Exceptions', () { + test('should handle LocalAuthException userCanceled gracefully', () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + authMessages: anyNamed('authMessages'), + ), + ).thenThrow( + LocalAuthException( + code: LocalAuthExceptionCode.userCanceled, + description: 'User canceled authentication', + ), + ); + + final result = await lockAuth( + reason: (loc) => 'reason', + localization: AppLocalizationsEn(), + ); + + expect(result, isFalse); + }); + + test('should prevent concurrent calls using the mutex', () async { + when(mockLocalAuth.isDeviceSupported()).thenAnswer((_) async => true); + when( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + authMessages: anyNamed('authMessages'), + ), + ).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 500)); + return true; + }); + + final firstCall = lockAuth( + reason: (loc) => 'first', + localization: AppLocalizationsEn(), + ); + final secondCall = lockAuth( + reason: (loc) => 'second', + localization: AppLocalizationsEn(), + ); + + final results = await Future.wait([firstCall, secondCall]); + + expect(results[0], isTrue); + expect(results[1], isFalse); // Second call should be blocked by mutex + verify( + mockLocalAuth.authenticate( + localizedReason: anyNamed('localizedReason'), + biometricOnly: anyNamed('biometricOnly'), + authMessages: anyNamed('authMessages'), + ), + ).called(1); + }); + }); +} diff --git a/test/unit_test/utils/lock_auth_test.mocks.dart b/test/unit_test/utils/lock_auth_test.mocks.dart new file mode 100644 index 000000000..37c38e0a3 --- /dev/null +++ b/test/unit_test/utils/lock_auth_test.mocks.dart @@ -0,0 +1,99 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in privacyidea_authenticator/test/unit_test/utils/lock_auth_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:local_auth/src/local_auth.dart' as _i2; +import 'package:local_auth_android/local_auth_android.dart' as _i4; +import 'package:local_auth_darwin/local_auth_darwin.dart' as _i5; +import 'package:local_auth_windows/local_auth_windows.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +/// A class which mocks [LocalAuthentication]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalAuthentication extends _i1.Mock + implements _i2.LocalAuthentication { + @override + _i3.Future get canCheckBiometrics => + (super.noSuchMethod( + Invocation.getter(#canCheckBiometrics), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) + as _i3.Future); + + @override + _i3.Future authenticate({ + required String? localizedReason, + Iterable<_i4.AuthMessages>? authMessages = const [ + _i5.IOSAuthMessages(), + _i4.AndroidAuthMessages(), + _i6.WindowsAuthMessages(), + ], + bool? biometricOnly = false, + bool? sensitiveTransaction = true, + bool? persistAcrossBackgrounding = false, + }) => + (super.noSuchMethod( + Invocation.method(#authenticate, [], { + #localizedReason: localizedReason, + #authMessages: authMessages, + #biometricOnly: biometricOnly, + #sensitiveTransaction: sensitiveTransaction, + #persistAcrossBackgrounding: persistAcrossBackgrounding, + }), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) + as _i3.Future); + + @override + _i3.Future stopAuthentication() => + (super.noSuchMethod( + Invocation.method(#stopAuthentication, []), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) + as _i3.Future); + + @override + _i3.Future isDeviceSupported() => + (super.noSuchMethod( + Invocation.method(#isDeviceSupported, []), + returnValue: _i3.Future.value(false), + returnValueForMissingStub: _i3.Future.value(false), + ) + as _i3.Future); + + @override + _i3.Future> getAvailableBiometrics() => + (super.noSuchMethod( + Invocation.method(#getAvailableBiometrics, []), + returnValue: _i3.Future>.value( + <_i4.BiometricType>[], + ), + returnValueForMissingStub: + _i3.Future>.value( + <_i4.BiometricType>[], + ), + ) + as _i3.Future>); +} diff --git a/test/unit_test/state_notifiers/token_notifier_test.dart b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart similarity index 91% rename from test/unit_test/state_notifiers/token_notifier_test.dart rename to test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart index 8acaa3bbd..68a66cb66 100644 --- a/test/unit_test/state_notifiers/token_notifier_test.dart +++ b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart @@ -1,3 +1,22 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -14,14 +33,13 @@ import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; import 'package:privacyidea_authenticator/model/tokens/totp_token.dart'; -import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; import 'package:privacyidea_authenticator/utils/utils.dart'; -import '../../tests_app_wrapper.mocks.dart'; +import '../../../../../tests_app_wrapper.mocks.dart'; void main() { _testTokenNotifier(); @@ -83,19 +101,13 @@ void _testTokenNotifier() { ioClient: const PrivacyideaIOClient(), firebaseUtils: mockFirebaseUtils, ); - expect( - (await container.read(testProvider.future)).tokens, - before, - ); // Should load the state from the repo - expect( - (await container.read(testProvider.future)).tokens, - before, - ); // But only once + expect((await container.read(testProvider.future)).tokens, before); + expect((await container.read(testProvider.future)).tokens, before); expect( (await container.read(testProvider.notifier).loadStateFromRepo()) ?.tokens, after, - ); // Exept we tell them to do so. + ); final state = await container.read(testProvider.future); expect(state, isNotNull); expect(state.tokens, after); @@ -123,6 +135,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; final after = before; @@ -225,6 +238,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), HOTPToken( label: 'label2', @@ -233,6 +247,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', + counter: 0, ), ]; final after = [ @@ -243,6 +258,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -290,6 +306,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; final after = [ @@ -300,6 +317,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), HOTPToken( label: 'label2', @@ -308,6 +326,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', + counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -356,6 +375,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), HOTPToken( label: 'label2', @@ -364,6 +384,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', + counter: 0, ), ]; final after = [ @@ -374,6 +395,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), HOTPToken( label: 'labelUpdated', @@ -382,6 +404,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2Updated', + counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -432,6 +455,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; final after = [ @@ -442,6 +466,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), HOTPToken( label: 'label2', @@ -450,6 +475,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA256, digits: 6, secret: 'secret2', + counter: 0, ), HOTPToken( label: 'label3', @@ -458,6 +484,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA512, digits: 8, secret: 'secret3', + counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -497,6 +524,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; final after = [ @@ -507,6 +535,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), TOTPToken( label: 'label2', @@ -536,13 +565,11 @@ void _testTokenNotifier() { 'otpauth://totp/issuer2:label2?secret=AAAAAAAA2&issuer=issuer2&algorithm=SHA256&digits=6&period=30'; final tokenNotifier = container.read(tokenProvider.notifier); await scanQrCode(resultHandlerList: [tokenNotifier], qrCode: qrCode); - await Future.delayed( - const Duration(seconds: 5), - ); // Wait for the rollout to finish + await Future.delayed(const Duration(seconds: 5)); final state = await container.read(tokenProvider.future); expect(state.tokens.length, 2); - after.last = after.last.copyWith(id: state.tokens.last.id); + after.last = (after.last as TOTPToken).copyWith(id: state.tokens.last.id); expect(state.tokens, after); verify(mockRepo.saveOrReplaceTokens(any)).called(greaterThan(0)); }); @@ -569,7 +596,7 @@ void _testTokenNotifier() { const publicTokenKeyString = 'MIICCgKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CAwEAAQ=='; const privateTokenKeyString = - 'MIILKAIBAAKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggEBAPReilE/TS0KTk9JFdynw1p9/3mLZCQYNMni5iyQkhdqAobAe3EmZVtWHj0aZtfgMZ3qC9EOJJvYt76m9Gh4UXPI5a9zldQjA2CMaY2yWMGVi8anjI+njB7WhYMtgDdHLajzI2P1bix6mI/bDxhIJBcfV61wlSNz1yArU36cw3SrWUXvGa2LiRJhMNXcALMiuBf9RaFmXQZci8Ae1+PPZ2UAyNdDrO8P8wILFeBTjd1WtZfkYtESBLCX6HdcM5JhaN74MJftWE1rKTQGh6Hg42RfgMDJDXiM/Dh5jg+OP2n5R0n/ua4CN++PNePd3JFODVa8ZvUv3eshoWD3Xc8IMscCggEBAMInwXHcEWNrOAOAL057ZA0WxsZg1IQMyJ1L5WVpYvnyB3jDX91cXhOM/zjC/C5VF1zy2+H6tmQ75C0Fs9Ph676LYnpTd7m8wqkqoI6SPDwsdx9dLZqT5Ps4ILS4ScOwKIN5qsccooZT6GWJyCZhfuTgApq5JE04ZEjrXhqhVcyaT+CJDhBuE1gvtIRmSQyPHa7isM3xrg9jMhdUcDVE/HotgJIxh0TtQmRDCJo2Ltngs3UrHgkGUIqLVVyHI/jZViKEWbnEku+GEE8A8sr52OOM8HpeXLE5rEn/hekf9iV31hLzASIBQWGopxaDpBiQgnFLYi5WSeEIKyqEA23SxSkCggEBAPKLw43Q3rENwZxAVkqk2OlAlgn1qHeK7xpS81LYS6iht9A3zE4KZh+54lmTkvBBvf2XCBN/jiaBfB7nZz8p7O6XQCJc/yGHfxqdQ0c49Y9u90U9l+4dxp31Hp+M0e4L3+4JJd9ZAvly1Woza1AWinvIyCWF0QFXQPbVChJpVja+u+UF5N6z2GE9xlL+AlPK6h4lbK8+AqcFxE/0TSP4AA/oL3A547OEiRZGGniFdhFyttsD/HC3CaCdpkaSZT2tIYHtpY2mLjbpXgQdVxH9PLWrdQfkhlJY3R7Qx4f5EEgG/BMelxV3bj2AT2TUGNDAP80PQsGpuQJgZuTvoVSUNpECggEAVGTBgkN9T3DAlUz3wy6Ba+sVlg9q8Mc5wJ3H5c/sVObudoC+P9MxlV/5ZGvlACK+mAl8qHq5I1KhOSy8YQJX3ahqsu9rIFI7bxr3VWGdSy6szPZMp19X7hcUqFlevu/ofFW7dPcuciMw5koAtSY16TiyCR0m+WXkuYmNixfL2rbMt7X7Zgri37dEyTRI1muzJFynK6280jV1BY0PhSgqctUqiOF8gep7rGcy6w1YSh6RAwIt+RBEnCQ6g5C+gyG9fh13fvdCQ1lL53trDe2SaD7QHPC9a8+84yFtzMq2zMyNQglc2bIgAFo13uRzxLWz7Zkt4SRi0q0hTka50tgGGQKCAQEAoksGQ7xL8E5ZY2sC5EgPenKT2VU89gzNj1F1nJA97CV32Vv+8gSgB2iIokwUVyslPk8y0vZ2n8aF3MVvFzq1FjUlBuGeABPfFuUfRJ6DT+2TwARJhqQuNrn0j3/uKolmpV2PFuqPrEESjbf3rUalubTsCS5XBusdYZgih43tHGE/eDE5sLd8HO7gblnkMwNM9Q0oih5oiMHkGB9xTdfCbZGgRodwlZ+tbyVRyGQ6VRt4IWEmcLsTEYlbisw2TdbT7pNeBYW6jOXbHHm3lKeQJoiMEe3YdUKfnjQaVz3JukH2Fk3zjKOTSi0/W0TmXcnvsY3rDhHRBipKvcANhJN/Vg=='; + 'MIILKAIBAAKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggEBAPReilE/TS0KTk9JFdynw1p9/3mLZCQYNMni5iyQkhdqAobAe3EmZVtWHj0aZtfgMZ3qC9EOJJvYt76m9Gh4UXPI5a9zldQjA2CMaY2yWMGVi8anjI+njB7WhYMtgDdHLajzI2P1bix6mI/bDxhIJBcfV61wlSNz1yArU36cw3SrWUXvGa2LiRJhMNXcALMiuBf9RaFmXQZci8Ae1+PPZ2UAyNdDrO8P8wILFeBTjd1WtZfkYtESBLCX6HdcM5JhaN74MJftWE1rKTQGh6Hg42RfgMDJDXiM/Dh5jg+OP2n5R0n/ua4CN++PNePd3JFODVa8ZvUv3eshoWD3Xc8IMscCggEBAMInwXHcEWNrOAOAL057ZA0WxsZg1IQMyJ1L5WVpYvnyB3jDX91cXhOM/zjC/C5VF1zy2+H6tmQ75C0Fs9Ph676LYnpTd7m8wqkqoI6SPDwsdx9dLZqT5Ps4ILS4ScOwKIN5qsccooZT6GWJyCZhfuTgApq5JE04ZEjrXhqhVcyaT+CJDhBuE1gvtIRmSQyPHa7isM3xrg9jMhdUcDVE/HotgJIxh0TtQmRDCJo2Ltngs3UrHgkGUIqLVVyHI/jZViKEWbnEku+GEE8A8sr52OOM8HpeXLE5rEn/hekf9iV31hLzASIBQWGopxaDpBiQgnFLYi5WSeEIKyqEA23SxSkCggEBAPKLw43Q3rENwZxAVkqk2OlAlgn1qHeK7xpS81LYS6iht9A3zE4KZh+54lmTkvBBvf2XCBN/jiaBfB7nZz8p7O6XQCJc/yGHfxqdQ0c49Y9u90U9l+4dxp31Hp+M0e4L3+4JJd9ZAvly1Woza1AWinvIyCWF0QFXQPbVChJpVja+u+UF5N6z2GE9xlL+AlPK6h4lbK8+AqcFxE/0TSP4AA/oL3A547OEiRZGGniFdhFyttsD/HC3CaCdpkaSZT2tIYHtpY2mLjbpXgQdVxH9PLWrdQfkhlJY3R7Qx4f5EEgG/BMelxV3bj2AT2TUGNDAP80PQsGpuQJgZuTvoVSUNpECggEAVGTBgkN9T3DAlUz3wy6Ba+sVlg9q8Mc5wJ3H5c/sVObudoC+P9MxlV/5ZGvlACK+mAl8qHq5I1KhOSy8YQJX3ahqsu9rIFI7bxr3VWGdSy6szPZMp19X7hcUqFlevu/ofFW7dPcuciMw5koAtSY16TiyCR0m+WXkuYmNixfL2rbMt7X7Zgri37dEyTRI1muzJFynK6280jV1BY0PhSgqctUqiOF8gep7rGcy6w1YSh6RAwIt+RBEnCQ6g5C+gyG9fh13fvdCQ1lL53trDe2SaD7QHPC9a8+84yFtzMq2zMyNQglc2bIgAFo13uRzxLWz7Zkt4SRi0q0hTka50tgGGQKCAQEAoksGQ7xL8E5ZY2sC5EgPenKT2VU89gzNj1F1nJA97CV32Vv+8gSgB2iIokwUVyslPk8y0vZ2n8aF3MVvFzq1FjUlBuGeABPfFuUfRJ6DT+2TwARJhqQuNrn0j3/uKolmpV2PFuqPrEESjbf3rUalubTsCS5XBusdYZgih43tHGE/eDE5sLd8HO7gblnkMwNM9Q0oih5oiMHkGB9xTdfCbZGgRodwlZ+tbyVRyGQ6VRt4IWEmcLsTEYlbisw2TdbT7NeBYW6jOXbHHm3lKeQJoiMEe3YdUKfnjQaVz3JukH2Fk3zjKOTSi0/W0TmXcnvsY3rDhHRBipKvcANhJN/Vg=='; final publicServerKey = rsaUtils.deserializeRSAPublicKeyPKCS1( publicServerKeyString, ); @@ -587,6 +614,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; final pushTokenShouldBe = PushToken( @@ -616,6 +644,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), pushTokenShouldBe, ]; @@ -645,10 +674,10 @@ void _testTokenNotifier() { ).thenReturn(privateTokenKey); when( mockTokenRepo.saveOrReplaceTokens([after.last]), - ).thenAnswer((_) async => []); // QrCode can contain multiple tokens + ).thenAnswer((_) async => []); when( mockTokenRepo.saveOrReplaceToken(after.last), - ).thenAnswer((_) async => true); // Rollout one by one + ).thenAnswer((_) async => true); when(mockTokenRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); when( mockIOClient.doPost( @@ -675,9 +704,7 @@ void _testTokenNotifier() { resultHandlerList: [container.read(testProvider.notifier)], qrCode: otpAuth, ); - await Future.delayed( - const Duration(seconds: 5), - ); // Wait for the rollout to finish + await Future.delayed(const Duration(seconds: 5)); final tokenState = await container.read(testProvider.future); expect(tokenState, isNotNull); expect(tokenState.tokens, after); @@ -689,26 +716,6 @@ void _testTokenNotifier() { sslVerify: anyNamed('sslVerify'), ), ).called(1); - final pushToken = tokenState.pushTokens.first; - expect( - pushToken.enrollmentCredentials, - pushTokenShouldBe.enrollmentCredentials, - ); - expect(pushToken.publicServerKey, pushTokenShouldBe.publicServerKey); - expect(pushToken.publicTokenKey, pushTokenShouldBe.publicTokenKey); - expect(pushToken.privateTokenKey, pushTokenShouldBe.privateTokenKey); - expect(pushToken.rolloutState, pushTokenShouldBe.rolloutState); - expect(pushToken.serial, pushTokenShouldBe.serial); - expect(pushToken.isRolledOut, pushTokenShouldBe.isRolledOut); - expect(pushToken.url, pushTokenShouldBe.url); - expect(pushToken.label, pushTokenShouldBe.label); - expect(pushToken.issuer, pushTokenShouldBe.issuer); - expect(pushToken.type, pushTokenShouldBe.type); - expect(pushToken.pin, pushTokenShouldBe.pin); - expect(pushToken.tokenImage, pushTokenShouldBe.tokenImage); - expect(pushToken.sortIndex, pushTokenShouldBe.sortIndex); - expect(pushToken.folderId, pushTokenShouldBe.folderId); - expect(pushToken.sslVerify, pushTokenShouldBe.sslVerify); }); test('rolloutPushToken', () async { final mockSettingsRepo = MockSettingsRepository(); @@ -755,9 +762,9 @@ void _testTokenNotifier() { when( mockRsaUtils.serializeRSAPublicKeyPKCS8(any), ).thenAnswer((_) => 'publicKey'); - when(mockRsaUtils.generateRSAKeyPair()).thenAnswer( - (_) => const RsaUtils().generateRSAKeyPair(), - ); // We get here a random result anyway and is it more likely to make errors by mocking it than by using the real method + when( + mockRsaUtils.generateRSAKeyPair(), + ).thenAnswer((_) => const RsaUtils().generateRSAKeyPair()); when( mockFirebaseUtils.getFBToken(), ).thenAnswer((_) => Future.value('fbToken')); @@ -784,32 +791,15 @@ void _testTokenNotifier() { final stateBefore = await container.read(testProvider.future); expect(stateBefore.tokens, before); - Logger.info('before rolloutPushToken'); expect( await container .read(testProvider.notifier) .rolloutPushToken(before.first), true, ); - Logger.info('after rolloutPushToken'); final state = await container.read(testProvider.future); expect(state, isNotNull); expect(state.tokens, after); - verify(mockRepo.saveOrReplaceToken(after.first)).called(greaterThan(0)); - verify( - mockRsaUtils.serializeRSAPublicKeyPKCS8(any), - ).called(greaterThan(0)); - verify(mockFirebaseUtils.getFBToken()).called(greaterThan(0)); - verify( - mockRsaUtils.deserializeRSAPublicKeyPKCS1('publicKey'), - ).called(greaterThan(0)); - verify( - mockIOClient.doPost( - url: anyNamed('url'), - body: anyNamed('body'), - sslVerify: anyNamed('sslVerify'), - ), - ).called(greaterThan(0)); }); test('loadFromRepo', () async { final mockSettingsRepo = MockSettingsRepository(); @@ -833,6 +823,7 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', + counter: 0, ), ]; when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); @@ -847,11 +838,9 @@ void _testTokenNotifier() { ioClient: const PrivacyideaIOClient(), firebaseUtils: mockFirebaseUtils, ); - Logger.info('before loadFromRepo'); final newState = await container .read(testProvider.notifier) .loadStateFromRepo(); - Logger.info('after loadFromRepo'); expect(newState?.tokens, before); expect((await container.read(testProvider.future)).tokens, before); }); From 83ba72019ff1aa0abd92ae9b3a244fb988ee0555 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:21:11 +0100 Subject: [PATCH 03/13] feat: add unit tests for RolloverContainerTokensButton and related mocks --- .../unit_test/utils/lock_auth_test.mocks.dart | 12 +- test/unit_test/utils/pi_mailer_test.dart | 131 ++++++++ ...rollover_container_tokens_button_test.dart | 137 ++++++++ ...er_container_tokens_button_test.mocks.dart | 317 ++++++++++++++++++ 4 files changed, 589 insertions(+), 8 deletions(-) create mode 100644 test/unit_test/utils/pi_mailer_test.dart create mode 100644 test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart create mode 100644 test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart diff --git a/test/unit_test/utils/lock_auth_test.mocks.dart b/test/unit_test/utils/lock_auth_test.mocks.dart index 37c38e0a3..b5fb87f82 100644 --- a/test/unit_test/utils/lock_auth_test.mocks.dart +++ b/test/unit_test/utils/lock_auth_test.mocks.dart @@ -31,12 +31,15 @@ import 'package:mockito/mockito.dart' as _i1; /// See the documentation for Mockito's code generation for more information. class MockLocalAuthentication extends _i1.Mock implements _i2.LocalAuthentication { + MockLocalAuthentication() { + _i1.throwOnMissingStub(this); + } + @override _i3.Future get canCheckBiometrics => (super.noSuchMethod( Invocation.getter(#canCheckBiometrics), returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), ) as _i3.Future); @@ -61,7 +64,6 @@ class MockLocalAuthentication extends _i1.Mock #persistAcrossBackgrounding: persistAcrossBackgrounding, }), returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), ) as _i3.Future); @@ -70,7 +72,6 @@ class MockLocalAuthentication extends _i1.Mock (super.noSuchMethod( Invocation.method(#stopAuthentication, []), returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), ) as _i3.Future); @@ -79,7 +80,6 @@ class MockLocalAuthentication extends _i1.Mock (super.noSuchMethod( Invocation.method(#isDeviceSupported, []), returnValue: _i3.Future.value(false), - returnValueForMissingStub: _i3.Future.value(false), ) as _i3.Future); @@ -90,10 +90,6 @@ class MockLocalAuthentication extends _i1.Mock returnValue: _i3.Future>.value( <_i4.BiometricType>[], ), - returnValueForMissingStub: - _i3.Future>.value( - <_i4.BiometricType>[], - ), ) as _i3.Future>); } diff --git a/test/unit_test/utils/pi_mailer_test.dart b/test/unit_test/utils/pi_mailer_test.dart new file mode 100644 index 000000000..478f7f014 --- /dev/null +++ b/test/unit_test/utils/pi_mailer_test.dart @@ -0,0 +1,131 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacyidea_authenticator/utils/pi_mailer.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const String channelName = 'flutter_mailer'; + final List methodCalls = []; + + setUp(() { + methodCalls.clear(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel(channelName), ( + MethodCall methodCall, + ) async { + methodCalls.add(methodCall); + if (methodCall.method == 'send') { + return 'sent'; + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel(channelName), null); + }); + + group('PiMailer - sendMail', () { + test('should send mail with correct arguments', () async { + final result = await PiMailer.sendMail( + mailRecipients: {'test@test.com'}, + subject: 'TestSubject', + subjectPrefix: 'Prefix', + body: 'TestBody', + attachments: ['path/to/file'], + ); + + expect(result, isTrue); + expect(methodCalls.length, 1); + + final Map args = methodCalls.first.arguments; + expect(args['subject'], 'Prefix TestSubject'); + expect(args['recipients'], ['test@test.com']); + }); + + test('should return false on UNAVAILABLE PlatformException', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel(channelName), ( + MethodCall methodCall, + ) async { + throw PlatformException(code: 'UNAVAILABLE'); + }); + + final result = await PiMailer.sendMail( + mailRecipients: {'test@test.com'}, + subject: 'Test', + body: 'Body', + ); + + expect(result, isFalse); + }); + + test('should return false on any other PlatformException', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel(channelName), ( + MethodCall methodCall, + ) async { + throw PlatformException(code: 'ERROR_500'); + }); + + final result = await PiMailer.sendMail( + mailRecipients: {'test@test.com'}, + subject: 'Test', + body: 'Body', + ); + + expect(result, isFalse); + }); + + test('should correctly format subject prefix when provided', () async { + await PiMailer.sendMail( + mailRecipients: {'test@test.com'}, + subject: 'Subject', + subjectPrefix: 'News:', + body: 'Body', + ); + + final Map args = methodCalls.first.arguments; + expect(args['subject'], 'News: Subject'); + }); + + test('should catch non-platform exceptions', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(const MethodChannel(channelName), ( + MethodCall methodCall, + ) async { + throw StateError('Unexpected state'); + }); + + final result = await PiMailer.sendMail( + mailRecipients: {'test@test.com'}, + subject: 'Test', + body: 'Body', + ); + + expect(result, isFalse); + }); + }); +} diff --git a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart b/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart new file mode 100644 index 000000000..3bdac6996 --- /dev/null +++ b/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart @@ -0,0 +1,137 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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: + * + * 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 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart'; +import 'package:privacyidea_authenticator/model/enums/sync_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart'; +import 'package:privacyidea_authenticator/model/token_container.dart'; +import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart'; +import 'package:privacyidea_authenticator/widgets/button_widgets/time_guarded_button.dart'; + +import 'rollover_container_tokens_button_test.mocks.dart'; + +@GenerateMocks([TokenContainerFinalized]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late MockTokenContainerFinalized mockContainer; + setUp(() { + mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('test-serial-123'); + }); + Future pumpButton( + WidgetTester tester, { + required SyncState state, + bool containerExists = true, + }) async { + when(mockContainer.syncState).thenReturn(state); + when(mockContainer.serial).thenReturn('test-serial-123'); + await tester.pumpWidget( + ProviderScope( + overrides: [ + tokenContainerProvider.overrideWith( + () => _MockNotifier(containerExists ? mockContainer : null), + ), + ], + child: MaterialApp( + home: Scaffold( + body: RolloverContainerTokensButton(container: mockContainer), + ), + ), + ), + ); + await tester.pump(); + } + + group('RolloverContainerTokensButton - SyncState Corners', () { + testWidgets('should be enabled when state is notStarted', (tester) async { + await pumpButton(tester, state: SyncState.notStarted); + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNotNull); + }); + testWidgets('should be disabled when state is syncing', (tester) async { + await pumpButton(tester, state: SyncState.syncing); + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNull); + }); + testWidgets('should be enabled when state is completed', (tester) async { + await pumpButton(tester, state: SyncState.completed); + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNotNull); + }); + testWidgets('should be enabled when state is failed', (tester) async { + await pumpButton(tester, state: SyncState.failed); + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNotNull); + }); + testWidgets( + 'should be disabled if container is missing regardless of state', + (tester) async { + await pumpButton( + tester, + state: SyncState.notStarted, + containerExists: false, + ); + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNull); + }, + ); + }); + group('RolloverContainerTokensButton - Visual Corners', () { + testWidgets('should use Stack with correct alignment', (tester) async { + await pumpButton(tester, state: SyncState.notStarted); + final stackFinder = find.descendant( + of: find.byType(RolloverContainerTokensButton), + matching: find.byType(Stack), + ); + final stack = tester.widget(stackFinder); + expect(stack.alignment, Alignment.center); + }); + }); +} + +class _MockNotifier extends TokenContainerNotifier { + final TokenContainerFinalized? _result; + _MockNotifier(this._result); + @override + Future build({ + required TokenContainerApi containerApi, + required EccUtils eccUtils, + required TokenContainerRepository repo, + }) async { + return TokenContainerState(containerList: _result != null ? [_result] : []); + } +} diff --git a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart b/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart new file mode 100644 index 000000000..aed6735b6 --- /dev/null +++ b/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart @@ -0,0 +1,317 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in privacyidea_authenticator/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:basic_utils/basic_utils.dart' as _i10; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; +import 'package:privacyidea_authenticator/model/container_policies.dart' as _i9; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart' as _i6; +import 'package:privacyidea_authenticator/model/enums/ec_key_algorithm.dart' + as _i5; +import 'package:privacyidea_authenticator/model/enums/rollout_state.dart' + as _i7; +import 'package:privacyidea_authenticator/model/enums/sync_state.dart' as _i8; +import 'package:privacyidea_authenticator/model/token_container.dart' as _i2; +import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeDateTime_0 extends _i1.SmartFake implements DateTime { + _FakeDateTime_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeUri_1 extends _i1.SmartFake implements Uri { + _FakeUri_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _Fake$TokenContainerFinalizedCopyWith_2<$Res> extends _i1.SmartFake + implements _i2.$TokenContainerFinalizedCopyWith<$Res> { + _Fake$TokenContainerFinalizedCopyWith_2( + Object parent, + Invocation parentInvocation, + ) : super(parent, parentInvocation); +} + +class _FakeToken_3 extends _i1.SmartFake implements _i3.Token { + _FakeToken_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [TokenContainerFinalized]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTokenContainerFinalized extends _i1.Mock + implements _i2.TokenContainerFinalized { + MockTokenContainerFinalized() { + _i1.throwOnMissingStub(this); + } + + @override + String get issuer => + (super.noSuchMethod( + Invocation.getter(#issuer), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#issuer), + ), + ) + as String); + + @override + String get nonce => + (super.noSuchMethod( + Invocation.getter(#nonce), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#nonce), + ), + ) + as String); + + @override + DateTime get timestamp => + (super.noSuchMethod( + Invocation.getter(#timestamp), + returnValue: _FakeDateTime_0(this, Invocation.getter(#timestamp)), + ) + as DateTime); + + @override + Uri get serverUrl => + (super.noSuchMethod( + Invocation.getter(#serverUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#serverUrl)), + ) + as Uri); + + @override + String get serial => + (super.noSuchMethod( + Invocation.getter(#serial), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#serial), + ), + ) + as String); + + @override + _i5.EcKeyAlgorithm get ecKeyAlgorithm => + (super.noSuchMethod( + Invocation.getter(#ecKeyAlgorithm), + returnValue: _i5.EcKeyAlgorithm.brainpoolp160r1, + ) + as _i5.EcKeyAlgorithm); + + @override + _i6.Algorithms get hashAlgorithm => + (super.noSuchMethod( + Invocation.getter(#hashAlgorithm), + returnValue: _i6.Algorithms.SHA1, + ) + as _i6.Algorithms); + + @override + bool get sslVerify => + (super.noSuchMethod(Invocation.getter(#sslVerify), returnValue: false) + as bool); + + @override + String get serverName => + (super.noSuchMethod( + Invocation.getter(#serverName), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#serverName), + ), + ) + as String); + + @override + _i7.FinalizationState get finalizationState => + (super.noSuchMethod( + Invocation.getter(#finalizationState), + returnValue: _i7.FinalizationState.notStarted, + ) + as _i7.FinalizationState); + + @override + _i8.SyncState get syncState => + (super.noSuchMethod( + Invocation.getter(#syncState), + returnValue: _i8.SyncState.notStarted, + ) + as _i8.SyncState); + + @override + _i9.ContainerPolicies get policies => + (super.noSuchMethod( + Invocation.getter(#policies), + returnValue: _i4.dummyValue<_i9.ContainerPolicies>( + this, + Invocation.getter(#policies), + ), + ) + as _i9.ContainerPolicies); + + @override + bool get initSynced => + (super.noSuchMethod(Invocation.getter(#initSynced), returnValue: false) + as bool); + + @override + String get publicClientKey => + (super.noSuchMethod( + Invocation.getter(#publicClientKey), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#publicClientKey), + ), + ) + as String); + + @override + String get privateClientKey => + (super.noSuchMethod( + Invocation.getter(#privateClientKey), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#privateClientKey), + ), + ) + as String); + + @override + String get $type => + (super.noSuchMethod( + Invocation.getter(#$type), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#$type), + ), + ) + as String); + + @override + _i2.$TokenContainerFinalizedCopyWith<_i2.TokenContainerFinalized> + get copyWith => + (super.noSuchMethod( + Invocation.getter(#copyWith), + returnValue: + _Fake$TokenContainerFinalizedCopyWith_2< + _i2.TokenContainerFinalized + >(this, Invocation.getter(#copyWith)), + ) + as _i2.$TokenContainerFinalizedCopyWith<_i2.TokenContainerFinalized>); + + @override + Uri get registrationUrl => + (super.noSuchMethod( + Invocation.getter(#registrationUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#registrationUrl)), + ) + as Uri); + + @override + Uri get challengeUrl => + (super.noSuchMethod( + Invocation.getter(#challengeUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#challengeUrl)), + ) + as Uri); + + @override + Uri get syncUrl => + (super.noSuchMethod( + Invocation.getter(#syncUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#syncUrl)), + ) + as Uri); + + @override + Uri get transferUrl => + (super.noSuchMethod( + Invocation.getter(#transferUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#transferUrl)), + ) + as Uri); + + @override + Uri get unregisterUrl => + (super.noSuchMethod( + Invocation.getter(#unregisterUrl), + returnValue: _FakeUri_1(this, Invocation.getter(#unregisterUrl)), + ) + as Uri); + + @override + Map toJson() => + (super.noSuchMethod( + Invocation.method(#toJson, []), + returnValue: {}, + ) + as Map); + + @override + _i2.TokenContainer withClientKeyPair( + _i10.AsymmetricKeyPair<_i10.ECPublicKey, _i10.ECPrivateKey>? keyPair, + ) => + (super.noSuchMethod( + Invocation.method(#withClientKeyPair, [keyPair]), + returnValue: _i4.dummyValue<_i2.TokenContainer>( + this, + Invocation.method(#withClientKeyPair, [keyPair]), + ), + ) + as _i2.TokenContainer); + + @override + String signMessage(String? msg) => + (super.noSuchMethod( + Invocation.method(#signMessage, [msg]), + returnValue: _i4.dummyValue( + this, + Invocation.method(#signMessage, [msg]), + ), + ) + as String); + + @override + String? trySignMessage(String? msg) => + (super.noSuchMethod(Invocation.method(#trySignMessage, [msg])) + as String?); + + @override + _i3.Token addOriginToToken({required _i3.Token? token, String? tokenData}) => + (super.noSuchMethod( + Invocation.method(#addOriginToToken, [], { + #token: token, + #tokenData: tokenData, + }), + returnValue: _FakeToken_3( + this, + Invocation.method(#addOriginToToken, [], { + #token: token, + #tokenData: tokenData, + }), + ), + ) + as _i3.Token); +} From 1a50a4356dd76b028098bf6ce0f9ec1a41159c30 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:12:11 +0100 Subject: [PATCH 04/13] Refactor container view tests: Remove unused mocks, add new tests for rollover and sync buttons, and implement delete container dialogs - Deleted unused mock file for rollover_container_tokens_button_test. - Added unit tests for RolloverContainerTokensButton to check sync state behavior and visual alignment. - Introduced SyncContainerButton tests to validate button states and sync functionality. - Created DeleteContainerDialog tests to verify UI elements and logic for deleting containers. - Added ForceDeleteContainerDialog test to ensure delete action is called correctly. --- integration_test/add_tokens_test.dart | 144 +- integration_test/copy_to_clipboard_test.dart | 89 +- integration_test/rename_and_delete_test.dart | 123 +- integration_test/two_step_rollout_test.dart | 116 +- integration_test/views_test.dart | 10 +- lib/mains/main_customizer.dart | 2 + lib/mains/main_netknights.dart | 4 +- .../pi_server_results/pi_server_result.dart | 6 +- .../pi_server_result_detail.dart | 6 +- .../pi_server_result_value.dart | 71 +- lib/model/container_policies.dart | 8 +- lib/model/encryption/encryption_params.dart | 8 +- .../pi_server_result_error.dart | 4 +- lib/model/extensions/date_time_extension.dart | 2 +- .../force_biometric_option_extension.dart | 2 +- .../push_token_rollout_state_extension.dart | 118 +- lib/model/pi_server_response.dart | 20 +- lib/model/pi_server_response.freezed.dart | 16 +- lib/model/token_container.dart | 18 +- lib/model/token_template.dart | 6 +- lib/model/tokens/day_password_token.dart | 15 +- lib/model/tokens/hotp_token.dart | 8 +- lib/model/tokens/otp_token.dart | 16 +- lib/model/tokens/push_token.dart | 98 +- lib/model/tokens/steam_token.dart | 4 +- lib/model/tokens/token.dart | 30 +- lib/model/tokens/totp_token.dart | 8 +- .../mixins/token_import_processor.dart | 2 +- .../home_widget_navigate_processor.dart | 6 +- .../token_container_processor.dart | 10 +- .../otp_auth_processor.dart | 12 +- .../aegis_import_file_processor.dart | 32 +- ...thenticator_pro_import_file_processor.dart | 14 +- .../free_otp_plus_import_file_processor.dart | 14 +- .../two_fas_import_file_processor.dart | 16 +- lib/utils/firebase_utils.dart | 62 +- lib/utils/lock_auth.dart | 36 +- .../object_validator/base_validator.dart | 28 +- .../default_object_validator.dart | 62 + .../object_validator/object_validators.dart | 260 +-- .../optional_object_validator.dart | 61 +- .../required_object_validator.dart | 79 +- .../has_firebase_provider.dart | 29 + .../has_firebase_provider.g.dart | 43 + .../sortable_notifier.dart | 72 +- .../generated_providers/token_notifier.dart | 347 ++- lib/utils/rsa_utils.dart | 3 +- lib/utils/utils.dart | 5 +- .../pages/import_start_page.dart | 6 +- .../settings_group_push_token_dialog.dart | 4 +- test/tests_app_wrapper.dart | 44 +- test/tests_app_wrapper.mocks.dart | 2032 ++++++++++++++--- .../model/tokens/otp_token_test.dart | 157 +- test/unit_test/model/tokens/token_test.dart | 119 +- .../model/tokens/totp_token_test.dart | 4 +- test/unit_test/utils/lock_auth_test.dart | 5 +- .../unit_test/utils/lock_auth_test.mocks.dart | 95 - .../token_notifier_test.dart | 7 +- ...er_container_tokens_button_test.mocks.dart | 317 --- ...rollover_container_tokens_button_test.dart | 4 +- .../buttons/sync_container_button_test.dart | 178 ++ .../delete_container_dialog_test.dart | 135 ++ .../delete_container_force_dialog_test.dart | 42 + 63 files changed, 3492 insertions(+), 1802 deletions(-) create mode 100644 lib/utils/object_validator/default_object_validator.dart create mode 100644 lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart create mode 100644 lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.g.dart delete mode 100644 test/unit_test/utils/lock_auth_test.mocks.dart delete mode 100644 test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart rename test/unit_test/{utils => }/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart (97%) create mode 100644 test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart create mode 100644 test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart create mode 100644 test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart diff --git a/integration_test/add_tokens_test.dart b/integration_test/add_tokens_test.dart index fb8fd3e88..05ec00ff0 100644 --- a/integration_test/add_tokens_test.dart +++ b/integration_test/add_tokens_test.dart @@ -48,11 +48,15 @@ void main() { latestStartedVersion: Version.parse('999.999.999'), ), ); - when(mockSettingsRepository.saveSettings(any)).thenAnswer((_) async => true); + when( + mockSettingsRepository.saveSettings(any), + ).thenAnswer((_) async => true); mockTokenRepository = MockTokenRepository(); var tokens = []; when(mockTokenRepository.loadTokens()).thenAnswer((_) async => tokens); - when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer((invocation) async { + when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer(( + invocation, + ) async { final arguments = invocation.positionalArguments; tokens.removeWhere((element) => element.id == (arguments[0] as Token).id); tokens.add(arguments[0] as Token); @@ -64,47 +68,68 @@ void main() { return true; }); mockTokenFolderRepository = MockTokenFolderRepository(); - when(mockTokenFolderRepository.loadState()).thenAnswer((_) async => const TokenFolderState(folders: [])); - when(mockTokenFolderRepository.saveState(any)).thenAnswer((_) async => true); + when( + mockTokenFolderRepository.loadState(), + ).thenAnswer((_) async => const TokenFolderState(folders: [])); + when( + mockTokenFolderRepository.saveState(any), + ).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); - when(mockIntroductionRepository.loadCompletedIntroductions()) - .thenAnswer((_) async => const IntroductionState(completedIntroductions: {...Introduction.values})); + when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer( + (_) async => const IntroductionState( + completedIntroductions: {...Introduction.values}, + ), + ); }); - testWidgets( - 'Add Tokens Test', - (tester) async { - await tester.pumpWidget(TestsAppWrapper( + testWidgets('Add Tokens Test', (tester) async { + await tester.pumpWidget( + TestsAppWrapper( overrides: [ - settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepository)), - tokenProvider.overrideWith(() => TokenNotifier(repoOverride: mockTokenRepository)), - tokenFolderProvider.overrideWith(() => TokenFolderNotifier(repoOverride: mockTokenFolderRepository)), - introductionNotifierProvider.overrideWith(() => IntroductionNotifier(repoOverride: mockIntroductionRepository)), + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepository), + ), + tokenProvider.overrideWith( + () => TokenNotifier(repoOverride: mockTokenRepository), + ), + tokenFolderProvider.overrideWith( + () => TokenFolderNotifier(repoOverride: mockTokenFolderRepository), + ), + introductionNotifierProvider.overrideWith( + () => + IntroductionNotifier(repoOverride: mockIntroductionRepository), + ), ], - child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), - )); - await expectMainViewIsEmptyAndCorrect(tester); - await _addHotpToken(tester); - expect(find.byType(HOTPTokenWidget), findsOneWidget); - await _addTotpToken(tester); - expect(find.byType(TOTPTokenWidget), findsOneWidget); - await _addDaypasswordToken(tester); - expect(find.byType(DayPasswordTokenWidget), findsOneWidget); - await _createFolder(tester); - await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(TokenFolderWidget), findsOneWidget); - expect(find.text(AppLocalizationsEn().folderName), findsOneWidget); - expect(find.byType(TokenWidgetBase).hitTestable(), findsNWidgets(3)); - await _moveFolderToTopPosition(tester); - await _moveHotpTokenWidgetIntoFolder(tester); - await _moveDayPasswordTokenWidgetIntoFolder(tester); - expect(find.byType(TOTPTokenWidget).hitTestable(), findsOneWidget); - expect(find.byType(TokenWidgetBase).hitTestable(), findsOneWidget); - await _openFolder(tester); - await pumpUntilFindNWidgets(tester, find.byType(TokenWidgetBase).hitTestable(), 3, const Duration(seconds: 5)); - expect(find.byType(TokenWidgetBase).hitTestable(), findsNWidgets(3)); - }, - timeout: const Timeout(Duration(minutes: 20)), - ); + child: PrivacyIDEAAuthenticator( + ApplicationCustomization.defaultCustomization, + ), + ), + ); + await expectMainViewIsEmptyAndCorrect(tester); + await _addHotpToken(tester); + expect(find.byType(HOTPTokenWidget), findsOneWidget); + await _addTotpToken(tester); + expect(find.byType(TOTPTokenWidget), findsOneWidget); + await _addDaypasswordToken(tester); + expect(find.byType(DayPasswordTokenWidget), findsOneWidget); + await _createFolder(tester); + await tester.pump(const Duration(milliseconds: 200)); + expect(find.byType(TokenFolderWidget), findsOneWidget); + expect(find.text(AppLocalizationsEn().folderName), findsOneWidget); + expect(find.byType(TokenWidgetBase).hitTestable(), findsNWidgets(3)); + await _moveFolderToTopPosition(tester); + await _moveHotpTokenWidgetIntoFolder(tester); + await _moveDayPasswordTokenWidgetIntoFolder(tester); + expect(find.byType(TOTPTokenWidget).hitTestable(), findsOneWidget); + expect(find.byType(TokenWidgetBase).hitTestable(), findsOneWidget); + await _openFolder(tester); + await pumpUntilFindNWidgets( + tester, + find.byType(TokenWidgetBase).hitTestable(), + 3, + timeout: const Duration(seconds: 5), + ); + expect(find.byType(TokenWidgetBase).hitTestable(), findsNWidgets(3)); + }, timeout: const Timeout(Duration(minutes: 20))); } Future _addHotpToken(WidgetTester tester) async { @@ -174,7 +199,10 @@ Future _createFolder(WidgetTester tester) async { await tester.pump(); await tester.tap(find.byIcon(Icons.create_new_folder)); await tester.pump(const Duration(milliseconds: 1000)); - await tester.enterText(find.byType(TextField).first, AppLocalizationsEn().folderName); + await tester.enterText( + find.byType(TextField).first, + AppLocalizationsEn().folderName, + ); await tester.pump(); await tester.tap(find.text(AppLocalizationsEn().create)); await tester.pump(); @@ -185,7 +213,9 @@ Future _moveFolderToTopPosition(WidgetTester tester) async { final tokenFolderPosition = tester.getCenter(find.byType(TokenFolderWidget)); final gestrue = await tester.startGesture(tokenFolderPosition); await tester.pump(const Duration(milliseconds: 1000)); - final dragTargetDividerPosition = tester.getCenter(find.byType(DragTargetDivider).first); + final dragTargetDividerPosition = tester.getCenter( + find.byType(DragTargetDivider).first, + ); await gestrue.moveTo(dragTargetDividerPosition); await tester.pump(); await gestrue.up(); @@ -194,7 +224,9 @@ Future _moveFolderToTopPosition(WidgetTester tester) async { Future _moveHotpTokenWidgetIntoFolder(WidgetTester tester) async { await tester.pump(); - final tokenWidgetPosition = tester.getCenter(find.byType(HOTPTokenWidget).first); + final tokenWidgetPosition = tester.getCenter( + find.byType(HOTPTokenWidget).first, + ); final gestrue = await tester.startGesture(tokenWidgetPosition); await tester.pump(const Duration(milliseconds: 1000)); final tokenFolderPosition = tester.getCenter(find.byType(TokenFolderWidget)); @@ -206,7 +238,9 @@ Future _moveHotpTokenWidgetIntoFolder(WidgetTester tester) async { Future _moveDayPasswordTokenWidgetIntoFolder(WidgetTester tester) async { await tester.pump(); - final tokenWidgetPosition = tester.getCenter(find.byType(DayPasswordTokenWidget).last); + final tokenWidgetPosition = tester.getCenter( + find.byType(DayPasswordTokenWidget).last, + ); final gestrue = await tester.startGesture(tokenWidgetPosition); await tester.pump(const Duration(milliseconds: 1000)); final tokenFolderPosition = tester.getCenter(find.byType(TokenFolderWidget)); @@ -217,17 +251,33 @@ Future _moveDayPasswordTokenWidgetIntoFolder(WidgetTester tester) async { } Future _openFolder(WidgetTester tester) async { - await pumpUntilFindNWidgets(tester, find.byType(TokenFolderWidget), 1, const Duration(seconds: 5)); + await pumpUntilFindNWidgets( + tester, + find.byType(TokenFolderWidget), + 1, + timeout: const Duration(seconds: 5), + ); await tester.tap(find.byType(TokenFolderWidget)); await tester.pump(); } Future expectMainViewIsEmptyAndCorrect(WidgetTester tester) async { - await pumpUntilFindNWidgets(tester, find.byType(FloatingActionButton), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(FloatingActionButton), + 1, + timeout: const Duration(seconds: 10), + ); expect(find.byType(FloatingActionButton), findsOneWidget); - expect(find.byType(AppBarItem), findsNWidgets(5)); // 4 at BottomNavigationBar and 1 at AppBar + expect( + find.byType(AppBarItem), + findsNWidgets(5), + ); // 4 at BottomNavigationBar and 1 at AppBar expect(find.byType(TokenWidgetBase), findsNothing); expect(find.byType(TokenFolderWidget), findsNothing); - expect(find.text(ApplicationCustomization.defaultCustomization.appName), findsOneWidget); + expect( + find.text(ApplicationCustomization.defaultCustomization.appName), + findsOneWidget, + ); expect(find.byType(Image), findsOneWidget); } diff --git a/integration_test/copy_to_clipboard_test.dart b/integration_test/copy_to_clipboard_test.dart index 5812957b0..528b6a6ca 100644 --- a/integration_test/copy_to_clipboard_test.dart +++ b/integration_test/copy_to_clipboard_test.dart @@ -10,8 +10,8 @@ import 'package:privacyidea_authenticator/model/riverpod_states/introduction_sta import 'package:privacyidea_authenticator/model/riverpod_states/settings_state.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/token_folder_state.dart'; import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart'; -import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; import 'package:privacyidea_authenticator/model/version.dart'; +import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_folder_notifier.dart'; @@ -28,34 +28,79 @@ void main() { late final MockIntroductionRepository mockIntroductionRepository; setUp(() { mockSettingsRepository = MockSettingsRepository(); - when(mockSettingsRepository.loadSettings()).thenAnswer((_) async => - SettingsState(isFirstRun: false, useSystemLocale: false, localePreference: const Locale('en'), latestStartedVersion: Version.parse('999.999.999'))); - when(mockSettingsRepository.saveSettings(any)).thenAnswer((_) async => true); + when(mockSettingsRepository.loadSettings()).thenAnswer( + (_) async => SettingsState( + isFirstRun: false, + useSystemLocale: false, + localePreference: const Locale('en'), + latestStartedVersion: Version.parse('999.999.999'), + ), + ); + when( + mockSettingsRepository.saveSettings(any), + ).thenAnswer((_) async => true); mockTokenRepository = MockTokenRepository(); - when(mockTokenRepository.loadTokens()).thenAnswer((_) async => [ - HOTPToken(label: 'test', issuer: 'test', id: 'id', algorithm: Algorithms.SHA256, digits: 6, secret: 'secret', counter: 0), - ]); - when(mockTokenRepository.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when(mockTokenRepository.loadTokens()).thenAnswer( + (_) async => [ + HOTPToken( + label: 'test', + issuer: 'test', + id: 'id', + algorithm: Algorithms.SHA256, + digits: 6, + secret: 'secret', + counter: 0, + ), + ], + ); + when( + mockTokenRepository.saveOrReplaceTokens(any), + ).thenAnswer((_) async => []); when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); - when(mockTokenFolderRepository.loadState()).thenAnswer((_) async => const TokenFolderState(folders: [])); - when(mockTokenFolderRepository.saveState(any)).thenAnswer((_) async => true); + when( + mockTokenFolderRepository.loadState(), + ).thenAnswer((_) async => const TokenFolderState(folders: [])); + when( + mockTokenFolderRepository.saveState(any), + ).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); - when(mockIntroductionRepository.loadCompletedIntroductions()) - .thenAnswer((_) async => const IntroductionState(completedIntroductions: {...Introduction.values})); + when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer( + (_) async => const IntroductionState( + completedIntroductions: {...Introduction.values}, + ), + ); }); testWidgets('Copy to Clipboard Test', (tester) async { - await tester.pumpWidget(TestsAppWrapper( - overrides: [ - settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepository)), - tokenProvider.overrideWith(() => TokenNotifier(repoOverride: mockTokenRepository)), - tokenFolderProvider.overrideWith(() => TokenFolderNotifier(repoOverride: mockTokenFolderRepository)), - introductionNotifierProvider.overrideWith(() => IntroductionNotifier(repoOverride: mockIntroductionRepository)), - ], - child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), - )); + await tester.pumpWidget( + TestsAppWrapper( + overrides: [ + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepository), + ), + tokenProvider.overrideWith( + () => TokenNotifier(repoOverride: mockTokenRepository), + ), + tokenFolderProvider.overrideWith( + () => TokenFolderNotifier(repoOverride: mockTokenFolderRepository), + ), + introductionNotifierProvider.overrideWith( + () => + IntroductionNotifier(repoOverride: mockIntroductionRepository), + ), + ], + child: PrivacyIDEAAuthenticator( + ApplicationCustomization.defaultCustomization, + ), + ), + ); await tester.pumpAndSettle(); - await pumpUntilFindNWidgets(tester, find.text('356 306'), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.text('356 306'), + 1, + timeout: const Duration(seconds: 10), + ); expect(find.text('356 306'), findsOneWidget); await tester.tap(find.text('356 306')); await tester.pumpAndSettle(); diff --git a/integration_test/rename_and_delete_test.dart b/integration_test/rename_and_delete_test.dart index 8630620bc..988c4174f 100644 --- a/integration_test/rename_and_delete_test.dart +++ b/integration_test/rename_and_delete_test.dart @@ -32,17 +32,35 @@ void main() { late final MockIntroductionRepository mockIntroductionRepository; setUp(() { mockSettingsRepository = MockSettingsRepository(); - when(mockSettingsRepository.loadSettings()).thenAnswer((_) async => - SettingsState(isFirstRun: false, useSystemLocale: false, localePreference: const Locale('en'), latestStartedVersion: Version.parse('999.999.999'))); - when(mockSettingsRepository.saveSettings(any)).thenAnswer((_) async => true); + when(mockSettingsRepository.loadSettings()).thenAnswer( + (_) async => SettingsState( + isFirstRun: false, + useSystemLocale: false, + localePreference: const Locale('en'), + latestStartedVersion: Version.parse('999.999.999'), + ), + ); + when( + mockSettingsRepository.saveSettings(any), + ).thenAnswer((_) async => true); mockTokenRepository = MockTokenRepository(); var tokens = [ - HOTPToken(label: 'test', issuer: 'test', id: 'id', algorithm: Algorithms.SHA256, digits: 6, secret: 'secret', counter: 0), + HOTPToken( + label: 'test', + issuer: 'test', + id: 'id', + algorithm: Algorithms.SHA256, + digits: 6, + secret: 'secret', + counter: 0, + ), ]; when(mockTokenRepository.loadTokens()).thenAnswer((_) async { return tokens; }); - when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer((invocation) async { + when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer(( + invocation, + ) async { final arguments = invocation.positionalArguments; tokens.removeWhere((element) => element.id == (arguments[0] as Token).id); tokens.add(arguments[0] as Token); @@ -53,22 +71,42 @@ void main() { return true; }); mockTokenFolderRepository = MockTokenFolderRepository(); - when(mockTokenFolderRepository.loadState()).thenAnswer((_) async => const TokenFolderState(folders: [])); - when(mockTokenFolderRepository.saveState(any)).thenAnswer((_) async => true); + when( + mockTokenFolderRepository.loadState(), + ).thenAnswer((_) async => const TokenFolderState(folders: [])); + when( + mockTokenFolderRepository.saveState(any), + ).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); - when(mockIntroductionRepository.loadCompletedIntroductions()) - .thenAnswer((_) async => const IntroductionState(completedIntroductions: {...Introduction.values})); + when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer( + (_) async => const IntroductionState( + completedIntroductions: {...Introduction.values}, + ), + ); }); testWidgets('Rename and Delete Token', (tester) async { - await tester.pumpWidget(TestsAppWrapper( - overrides: [ - settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepository)), - tokenProvider.overrideWith(() => TokenNotifier(repoOverride: mockTokenRepository)), - tokenFolderProvider.overrideWith(() => TokenFolderNotifier(repoOverride: mockTokenFolderRepository)), - introductionNotifierProvider.overrideWith(() => IntroductionNotifier(repoOverride: mockIntroductionRepository)), - ], - child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), - )); + await tester.pumpWidget( + TestsAppWrapper( + overrides: [ + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepository), + ), + tokenProvider.overrideWith( + () => TokenNotifier(repoOverride: mockTokenRepository), + ), + tokenFolderProvider.overrideWith( + () => TokenFolderNotifier(repoOverride: mockTokenFolderRepository), + ), + introductionNotifierProvider.overrideWith( + () => + IntroductionNotifier(repoOverride: mockIntroductionRepository), + ), + ], + child: PrivacyIDEAAuthenticator( + ApplicationCustomization.defaultCustomization, + ), + ), + ); await _renameToken(tester, 'Renamed Token'); await _renameToken(tester, 'Renamed Token Again'); await _deleteToken(tester); @@ -78,11 +116,21 @@ void main() { Future _renameToken(WidgetTester tester, String newName) async { // Rename Token await tester.pumpAndSettle(); - await pumpUntilFindNWidgets(tester, find.byType(HOTPTokenWidget), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(HOTPTokenWidget), + 1, + timeout: const Duration(seconds: 10), + ); expect(find.byType(HOTPTokenWidget), findsOneWidget); await tester.drag(find.byType(HOTPTokenWidget), const Offset(-300, 0)); await tester.pumpAndSettle(); - await pumpUntilFindNWidgets(tester, find.byType(EditHOTPTokenAction), 1, const Duration(seconds: 2)); + await pumpUntilFindNWidgets( + tester, + find.byType(EditHOTPTokenAction), + 1, + timeout: const Duration(seconds: 2), + ); await tester.tap(find.byType(EditHOTPTokenAction)); await tester.pumpAndSettle(); expect(find.text(AppLocalizationsEn().editToken), findsOneWidget); @@ -91,24 +139,49 @@ Future _renameToken(WidgetTester tester, String newName) async { await tester.enterText(find.byType(TextFormField).first, ''); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField).first, newName); - await pumpUntilFindNWidgets(tester, find.widgetWithText(TextFormField, newName), 1, const Duration(seconds: 2)); + await pumpUntilFindNWidgets( + tester, + find.widgetWithText(TextFormField, newName), + 1, + timeout: const Duration(seconds: 2), + ); await tester.tap(find.text(AppLocalizationsEn().saveButton)); - await pumpUntilFindNWidgets(tester, find.text(newName), 1, const Duration(seconds: 2)); + await pumpUntilFindNWidgets( + tester, + find.text(newName), + 1, + timeout: const Duration(seconds: 2), + ); expect(find.text(newName), findsOneWidget); } Future _deleteToken(WidgetTester tester) async { await tester.pumpAndSettle(); - await pumpUntilFindNWidgets(tester, find.byType(HOTPTokenWidget), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(HOTPTokenWidget), + 1, + timeout: const Duration(seconds: 10), + ); expect(find.byType(HOTPTokenWidget), findsOneWidget); await tester.drag(find.byType(HOTPTokenWidget), const Offset(-300, 0)); await tester.pumpAndSettle(); - await pumpUntilFindNWidgets(tester, find.byType(EditHOTPTokenAction), 1, const Duration(seconds: 2)); + await pumpUntilFindNWidgets( + tester, + find.byType(EditHOTPTokenAction), + 1, + timeout: const Duration(seconds: 2), + ); await tester.tap(find.byType(DefaultDeleteAction)); await tester.pumpAndSettle(); expect(find.text(AppLocalizationsEn().confirmDeletion), findsOneWidget); expect(find.text(AppLocalizationsEn().delete), findsOneWidget); await tester.tap(find.text(AppLocalizationsEn().delete)); - await pumpUntilFindNWidgets(tester, find.byType(HOTPTokenWidget), 0, const Duration(seconds: 2)); + await pumpUntilFindNWidgets( + tester, + find.byType(HOTPTokenWidget), + 0, + timeout: const Duration(seconds: 2), + ); expect(find.byType(HOTPTokenWidget), findsNothing); } diff --git a/integration_test/two_step_rollout_test.dart b/integration_test/two_step_rollout_test.dart index 0fd8ec4dd..4f203b9cf 100644 --- a/integration_test/two_step_rollout_test.dart +++ b/integration_test/two_step_rollout_test.dart @@ -8,10 +8,10 @@ import 'package:privacyidea_authenticator/model/enums/introduction.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/introduction_state.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/settings_state.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/token_folder_state.dart'; +import 'package:privacyidea_authenticator/model/version.dart'; import 'package:privacyidea_authenticator/utils/customization/application_customization.dart'; import 'package:privacyidea_authenticator/utils/globals.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; -import 'package:privacyidea_authenticator/model/version.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/introduction_provider.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_folder_notifier.dart'; @@ -33,47 +33,83 @@ void main() { late final MockIntroductionRepository mockIntroductionRepository; setUp(() { mockSettingsRepository = MockSettingsRepository(); - when(mockSettingsRepository.loadSettings()).thenAnswer((_) async => - SettingsState(isFirstRun: false, useSystemLocale: false, localePreference: const Locale('en'), latestStartedVersion: Version.parse('999.999.999'))); - when(mockSettingsRepository.saveSettings(any)).thenAnswer((_) async => true); + when(mockSettingsRepository.loadSettings()).thenAnswer( + (_) async => SettingsState( + isFirstRun: false, + useSystemLocale: false, + localePreference: const Locale('en'), + latestStartedVersion: Version.parse('999.999.999'), + ), + ); + when( + mockSettingsRepository.saveSettings(any), + ).thenAnswer((_) async => true); mockTokenRepository = MockTokenRepository(); when(mockTokenRepository.loadTokens()).thenAnswer((_) async => []); - when(mockTokenRepository.saveOrReplaceTokens(any)).thenAnswer((_) async => []); + when( + mockTokenRepository.saveOrReplaceTokens(any), + ).thenAnswer((_) async => []); when(mockTokenRepository.deleteTokens(any)).thenAnswer((_) async => []); mockTokenFolderRepository = MockTokenFolderRepository(); - when(mockTokenFolderRepository.loadState()).thenAnswer((_) async => const TokenFolderState(folders: [])); - when(mockTokenFolderRepository.saveState(any)).thenAnswer((_) async => true); + when( + mockTokenFolderRepository.loadState(), + ).thenAnswer((_) async => const TokenFolderState(folders: [])); + when( + mockTokenFolderRepository.saveState(any), + ).thenAnswer((_) async => true); mockIntroductionRepository = MockIntroductionRepository(); - when(mockIntroductionRepository.loadCompletedIntroductions()) - .thenAnswer((_) async => const IntroductionState(completedIntroductions: {...Introduction.values})); + when(mockIntroductionRepository.loadCompletedIntroductions()).thenAnswer( + (_) async => const IntroductionState( + completedIntroductions: {...Introduction.values}, + ), + ); }); - testWidgets( - '2step rollout test', - (tester) async { - await tester.pumpWidget(TestsAppWrapper( + testWidgets('2step rollout test', (tester) async { + await tester.pumpWidget( + TestsAppWrapper( overrides: [ - settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepository)), - tokenProvider.overrideWith(() => TokenNotifier(repoOverride: mockTokenRepository)), - tokenFolderProvider.overrideWith(() => TokenFolderNotifier(repoOverride: mockTokenFolderRepository)), - introductionNotifierProvider.overrideWith(() => IntroductionNotifier(repoOverride: mockIntroductionRepository)), + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepository), + ), + tokenProvider.overrideWith( + () => TokenNotifier(repoOverride: mockTokenRepository), + ), + tokenFolderProvider.overrideWith( + () => TokenFolderNotifier(repoOverride: mockTokenFolderRepository), + ), + introductionNotifierProvider.overrideWith( + () => + IntroductionNotifier(repoOverride: mockIntroductionRepository), + ), ], - child: PrivacyIDEAAuthenticator(ApplicationCustomization.defaultCustomization), - )); - await _addTwoStepHotpTokenTest(tester); - await _addTwoStepTotpTokenTest(tester); - }, - timeout: const Timeout(Duration(minutes: 10)), - ); + child: PrivacyIDEAAuthenticator( + ApplicationCustomization.defaultCustomization, + ), + ), + ); + await _addTwoStepHotpTokenTest(tester); + await _addTwoStepTotpTokenTest(tester); + }, timeout: const Timeout(Duration(minutes: 10))); } Future _addTwoStepHotpTokenTest(WidgetTester tester) async { - await pumpUntilFindNWidgets(tester, find.byType(MainView), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(MainView), + 1, + timeout: const Duration(seconds: 10), + ); const qrCode = 'otpauth://hotp/OATH0001DBD0?secret=AALIBQJMOGEE7SAVEZ5D3K2ADO7MVFQD&counter=1&digits=6&issuer=privacyIDEA&2step_salt=8&2step_output=20&2step_difficulty=10000'; final notifier = globalRef!.read(tokenProvider.notifier); await scanQrCode(resultHandlerList: [notifier], qrCode: qrCode); Logger.info('Finding phone part dialog'); - await pumpUntilFindNWidgets(tester, find.text(AppLocalizationsEn().phonePart), 1, const Duration(seconds: 20)); + await pumpUntilFindNWidgets( + tester, + find.text(AppLocalizationsEn().phonePart), + 1, + timeout: const Duration(seconds: 20), + ); expect(find.text(AppLocalizationsEn().phonePart), findsOneWidget); final finder = find.byKey(twoStepDialogContent); expect(finder, findsOneWidget); @@ -87,17 +123,32 @@ Future _addTwoStepHotpTokenTest(WidgetTester tester) async { expect(find.text(AppLocalizationsEn().dismiss), findsOneWidget); Logger.info('Dismissing dialog'); await tester.tap(find.text(AppLocalizationsEn().dismiss)); - await pumpUntilFindNWidgets(tester, find.byType(HOTPTokenWidgetTile), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(HOTPTokenWidgetTile), + 1, + timeout: const Duration(seconds: 10), + ); } Future _addTwoStepTotpTokenTest(WidgetTester tester) async { - await pumpUntilFindNWidgets(tester, find.byType(MainView), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(MainView), + 1, + timeout: const Duration(seconds: 10), + ); const qrCode = 'otpauth://totp/TOTP00009D5F?secret=NZ4OPONKAAGDFN2QHV26ZWYVTLFER4C6&period=30&digits=6&issuer=privacyIDEA&2step_salt=8&2step_output=20&2step_difficulty=10000'; final notifier = globalRef!.read(tokenProvider.notifier); await scanQrCode(resultHandlerList: [notifier], qrCode: qrCode); Logger.info('Finding phone part dialog'); - await pumpUntilFindNWidgets(tester, find.text(AppLocalizationsEn().phonePart), 1, const Duration(seconds: 20)); + await pumpUntilFindNWidgets( + tester, + find.text(AppLocalizationsEn().phonePart), + 1, + timeout: const Duration(seconds: 20), + ); expect(find.text(AppLocalizationsEn().phonePart), findsOneWidget); final finder = find.byKey(twoStepDialogContent); expect(finder, findsOneWidget); @@ -111,6 +162,11 @@ Future _addTwoStepTotpTokenTest(WidgetTester tester) async { expect(find.text(AppLocalizationsEn().dismiss), findsOneWidget); Logger.info('Dismissing dialog'); await tester.tap(find.text(AppLocalizationsEn().dismiss)); - await pumpUntilFindNWidgets(tester, find.byType(TOTPTokenWidgetTile), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets( + tester, + find.byType(TOTPTokenWidgetTile), + 1, + timeout: const Duration(seconds: 10), + ); //cannot "await tester.pumpAndSettle();" because of the infinite TOTP animation. } diff --git a/integration_test/views_test.dart b/integration_test/views_test.dart index 04240a719..92292073c 100644 --- a/integration_test/views_test.dart +++ b/integration_test/views_test.dart @@ -198,7 +198,7 @@ Future _popUntilMainView(WidgetTester tester) async { tester, find.byIcon(Icons.arrow_back), 1, - const Duration(seconds: 2), + timeout: const Duration(seconds: 2), ); while (find.byIcon(Icons.arrow_back).evaluate().isNotEmpty) { await tester.tap(find.byIcon(Icons.arrow_back)); @@ -206,7 +206,7 @@ Future _popUntilMainView(WidgetTester tester) async { tester, find.byIcon(Icons.arrow_back), 1, - const Duration(seconds: 2), + timeout: const Duration(seconds: 2), ); } return; @@ -217,7 +217,7 @@ Future _licensesViewTest(WidgetTester tester) async { tester, find.byIcon(Icons.info_outline), 1, - const Duration(seconds: 20), + timeout: const Duration(seconds: 20), ); await tester.tap(find.byIcon(Icons.info_outline)); await tester.pumpAndSettle(); @@ -238,7 +238,7 @@ Future _settingsViewTest(WidgetTester tester) async { tester, find.byIcon(Icons.settings), 1, - const Duration(seconds: 10), + timeout: const Duration(seconds: 10), ); await tester.tap(find.byIcon(Icons.settings)); await tester.pumpAndSettle(); @@ -256,7 +256,7 @@ Future _settingsViewTest(WidgetTester tester) async { tester, find.text(AppLocalizationsEn().pushToken), 1, - const Duration(minutes: 5), + timeout: const Duration(minutes: 5), ); expect(find.text(AppLocalizationsEn().pushToken), findsOneWidget); expect(find.byType(SettingsGroup), findsNWidgets(6)); diff --git a/lib/mains/main_customizer.dart b/lib/mains/main_customizer.dart index 9e4872e6c..fbc5b37d6 100644 --- a/lib/mains/main_customizer.dart +++ b/lib/mains/main_customizer.dart @@ -27,6 +27,7 @@ import '../../../../../../../utils/customization/application_customization.dart' import '../l10n/app_localizations.dart'; import '../model/enums/app_feature.dart'; import '../model/riverpod_states/settings_state.dart'; +import '../utils/firebase_utils.dart'; import '../utils/globals.dart'; import '../utils/riverpod/riverpod_providers/generated_providers/app_constraints_notifier.dart'; import '../utils/riverpod/riverpod_providers/generated_providers/app_customization_notifier.dart'; @@ -44,6 +45,7 @@ import '../widgets/app_wrapper.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await FirebaseUtils.preInitializeStatus(); runApp( AppWrapper( child: CustomizationAuthenticator( diff --git a/lib/mains/main_netknights.dart b/lib/mains/main_netknights.dart index 314077127..af83af639 100644 --- a/lib/mains/main_netknights.dart +++ b/lib/mains/main_netknights.dart @@ -22,8 +22,8 @@ import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:gms_check/gms_check.dart'; import 'package:privacyidea_authenticator/firebase_options/default_firebase_options.dart'; +import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/localization_notifier.dart'; import '../../../../../../../model/riverpod_states/settings_state.dart'; @@ -52,7 +52,7 @@ void main() async { navigatorKey: globalNavigatorKey, appRunner: () async { WidgetsFlutterBinding.ensureInitialized(); - await GmsCheck().checkGmsAvailability(); + await FirebaseUtils.preInitializeStatus(); await HomeWidgetUtils().registerInteractivityCallback( homeWidgetBackgroundCallback, ); diff --git a/lib/model/api_results/pi_server_results/pi_server_result.dart b/lib/model/api_results/pi_server_results/pi_server_result.dart index 9aaf79e97..b0705099f 100644 --- a/lib/model/api_results/pi_server_results/pi_server_result.dart +++ b/lib/model/api_results/pi_server_results/pi_server_result.dart @@ -37,9 +37,9 @@ class PiServerResult { final map = validateMap( map: json, validators: { - RESULT_STATUS: const RequiredObjectValidator(), - RESULT_VALUE: const OptionalObjectValidator(), - RESULT_ERROR: const OptionalObjectValidator>(), + RESULT_STATUS: RequiredObjectValidator(), + RESULT_VALUE: OptionalObjectValidator(), + RESULT_ERROR: OptionalObjectValidator>(), }, name: 'PiServerResult#fromJson', ); diff --git a/lib/model/api_results/pi_server_results/pi_server_result_detail.dart b/lib/model/api_results/pi_server_results/pi_server_result_detail.dart index c1e3c87e5..6e34637b8 100644 --- a/lib/model/api_results/pi_server_results/pi_server_result_detail.dart +++ b/lib/model/api_results/pi_server_results/pi_server_result_detail.dart @@ -69,9 +69,9 @@ class PushResultDetail extends PiServerResultDetail { final map = validateMap( map: uriMap, validators: { - DISPLAY_CODE: stringValidatorOptional, - THREAD_ID: const OptionalObjectValidator(), - MESSAGE: stringValidatorOptional, + DISPLAY_CODE: Validators.stringOptional, + THREAD_ID: OptionalObjectValidator(), + MESSAGE: Validators.stringOptional, }, name: 'ContainerChallenge#fromUriMap', ); diff --git a/lib/model/api_results/pi_server_results/pi_server_result_value.dart b/lib/model/api_results/pi_server_results/pi_server_result_value.dart index 4b5e7092c..86cd12592 100644 --- a/lib/model/api_results/pi_server_results/pi_server_result_value.dart +++ b/lib/model/api_results/pi_server_results/pi_server_result_value.dart @@ -3,7 +3,7 @@ * * Author: Frank Merkel * - * Copyright (c) 2025 NetKnights GmbH + * Copyright (c) 2026 NetKnights GmbH * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ sealed class PiServerResultValue { static V? fromResultValue(dynamic value) { Logger.debug('PiServerResultValue.uriMapOfType<$V>'); return switch (V) { - const (PiServerResultValue) => null, // Default generic case, return null + const (PiServerResultValue) => null, const (ContainerChallenge) => ContainerChallenge.fromUriMap(value) as V, const (ContainerFinalizationResponse) => ContainerFinalizationResponse.fromUriMap(value) as V, @@ -49,7 +49,7 @@ sealed class PiServerResultValue { class PushResultValue extends PiServerResultValue { final bool value; - static const validator = RequiredObjectValidator>(); + static final validator = Validators.boolType; factory PushResultValue.fromResultValue(bool value) { return PushResultValue(value); @@ -72,7 +72,7 @@ class ContainerChallenge extends PiServerResultValue { DateTime get timeAsDatetime => DateTime.parse(timeStamp); - static const validator = RequiredObjectValidator>(); + static final validator = RequiredObjectValidator>(); const ContainerChallenge({ required this.keyAlgorithm, @@ -83,10 +83,10 @@ class ContainerChallenge extends PiServerResultValue { factory ContainerChallenge.fromUriMap(Map uriMap) { final map = validateMap( map: uriMap, - validators: { - KEY_ALGORITHM: stringValidator, - NONCE: stringValidator, - TIMESTAMP: stringValidator, + validators: { + KEY_ALGORITHM: Validators.string, + NONCE: Validators.string, + TIMESTAMP: Validators.string, }, name: 'ContainerChallenge#fromUriMap', ); @@ -98,22 +98,20 @@ class ContainerChallenge extends PiServerResultValue { } @override String toString() => - 'PushResultValue(keyAlgorithm: $keyAlgorithm, nonce: $nonce, timeStamp: $timeStamp)'; + 'ContainerChallenge(keyAlgorithm: $keyAlgorithm, nonce: $nonce, timeStamp: $timeStamp)'; } class ContainerFinalizationResponse extends PiServerResultValue { final ContainerPolicies policies; - static const validator = RequiredObjectValidator>(); + static final validator = RequiredObjectValidator>(); const ContainerFinalizationResponse({required this.policies}); static ContainerFinalizationResponse fromUriMap(Map uriMap) { final map = validateMap( map: uriMap, - validators: { - TokenContainer.SYNC_POLICIES: ContainerPolicies.validator, - }, + validators: {TokenContainer.SYNC_POLICIES: ContainerPolicies.validator}, name: 'ContainerFinalizationResponse#fromUriMap', ); return ContainerFinalizationResponse( @@ -122,7 +120,7 @@ class ContainerFinalizationResponse extends PiServerResultValue { } @override - String toString() => 'PushResultValue(policies: $policies)'; + String toString() => 'ContainerFinalizationResponse(policies: $policies)'; } class ContainerSyncResult extends PiServerResultValue { @@ -132,7 +130,7 @@ class ContainerSyncResult extends PiServerResultValue { final ContainerPolicies policies; final String publicServerKey; - static const validator = RequiredObjectValidator>(); + static final validator = RequiredObjectValidator>(); Uint8List get publicServerKeyBytes => base64Decode(publicServerKey); @@ -147,12 +145,12 @@ class ContainerSyncResult extends PiServerResultValue { static ContainerSyncResult fromUriMap(Map uriMap) { final map = validateMap( map: uriMap, - validators: { - TokenContainer.SYNC_DICT_SERVER: stringValidator, - TokenContainer.SYNC_ENC_ALGORITHM: stringValidator, + validators: { + TokenContainer.SYNC_DICT_SERVER: Validators.string, + TokenContainer.SYNC_ENC_ALGORITHM: Validators.string, TokenContainer.SYNC_ENC_PARAMS: EncryptionParams.validator, TokenContainer.SYNC_POLICIES: ContainerPolicies.validator, - TokenContainer.SYNC_PUBLIC_SERVER_KEY: stringValidator, + TokenContainer.SYNC_PUBLIC_SERVER_KEY: Validators.string, }, name: 'ContainerSyncResult#fromUriMap', ); @@ -167,38 +165,45 @@ class ContainerSyncResult extends PiServerResultValue { @override String toString() => - 'PushResultValue(containerDictEncrypted: $containerDictEncrypted, encryptionAlgorithm: $encryptionAlgorithm, encryptionParams: $encryptionParams, policies: $policies, publicServerKey: $publicServerKey)'; + 'ContainerSyncResult(containerDictEncrypted: $containerDictEncrypted, encryptionAlgorithm: $encryptionAlgorithm, encryptionParams: $encryptionParams, policies: $policies, publicServerKey: $publicServerKey)'; } class TransferQrData extends PiServerResultValue { + static const String KEY_CONTAINER_URL = 'container_url'; + static const String KEY_DESCRIPTION = 'description'; + static const String KEY_VALUE = 'value'; + final String description; final String value; - static const validator = RequiredObjectValidator>(); + static final validator = RequiredObjectValidator>(); const TransferQrData({required this.description, required this.value}); factory TransferQrData.fromUriMap(Map uriMap) { final map = validateMap( - map: uriMap['container_url'] as Map, - validators: {'description': stringValidator, 'value': stringValidator}, + map: uriMap[KEY_CONTAINER_URL] as Map, + validators: { + KEY_DESCRIPTION: Validators.string, + KEY_VALUE: Validators.string, + }, name: 'TransferQrData', ); return TransferQrData( - description: map['description'] as String, - value: map['value'] as String, + description: map[KEY_DESCRIPTION] as String, + value: map[KEY_VALUE] as String, ); } @override String toString() => - 'PushResultValue(description: $description, value: $value)'; + 'TransferQrData(description: $description, value: $value)'; } class UnregisterContainerResult extends PiServerResultValue { - static const String CONTAINER_UNREGISTER_SUCCESS = 'success'; + static const String KEY_SUCCESS = 'success'; - static const validator = RequiredObjectValidator>(); + static final validator = RequiredObjectValidator>(); final bool success; @@ -207,16 +212,12 @@ class UnregisterContainerResult extends PiServerResultValue { factory UnregisterContainerResult.fromUriMap(Map uriMap) { final map = validateMap( map: uriMap, - validators: { - CONTAINER_UNREGISTER_SUCCESS: const RequiredObjectValidator(), - }, + validators: {KEY_SUCCESS: Validators.boolType}, name: 'UnregisterContainerResultValue#fromUriMap', ); - return UnregisterContainerResult( - success: map[CONTAINER_UNREGISTER_SUCCESS] as bool, - ); + return UnregisterContainerResult(success: map[KEY_SUCCESS] as bool); } @override - String toString() => 'PushResultValue(success: $success)'; + String toString() => 'UnregisterContainerResult(success: $success)'; } diff --git a/lib/model/container_policies.dart b/lib/model/container_policies.dart index 7a05a3bb0..475a453b5 100644 --- a/lib/model/container_policies.dart +++ b/lib/model/container_policies.dart @@ -69,10 +69,10 @@ sealed class ContainerPolicies with _$ContainerPolicies { final validated = validateMap( map: map, validators: >{ - ROLLOVER_ALLOWED: boolValidator, - INITIAL_TOKEN_ASSIGNMENT: boolValidator, - DISABLED_TOKEN_DELETION: boolValidator, - DISABLED_UNREGISTER: boolValidator, + ROLLOVER_ALLOWED: Validators.boolType, + INITIAL_TOKEN_ASSIGNMENT: Validators.boolType, + DISABLED_TOKEN_DELETION: Validators.boolType, + DISABLED_UNREGISTER: Validators.boolType, }, name: 'ContainerPolicies', ); diff --git a/lib/model/encryption/encryption_params.dart b/lib/model/encryption/encryption_params.dart index 29c95a7be..7c81bb467 100644 --- a/lib/model/encryption/encryption_params.dart +++ b/lib/model/encryption/encryption_params.dart @@ -51,10 +51,10 @@ class EncryptionParams { final map = validateMap( map: responseBody, validators: { - SYNC_ENC_PARAMS_ALGORITHM: stringValidator, - SYNC_ENC_PARAMS_IV: stringValidator, - SYNC_ENC_PARAMS_MODE: stringValidator, - SYNC_ENC_PARAMS_TAG: stringValidator, + SYNC_ENC_PARAMS_ALGORITHM: Validators.string, + SYNC_ENC_PARAMS_IV: Validators.string, + SYNC_ENC_PARAMS_MODE: Validators.string, + SYNC_ENC_PARAMS_TAG: Validators.string, }, name: 'EncryptionParams#fromResponseBody', ); diff --git a/lib/model/exception_errors/pi_server_result_error.dart b/lib/model/exception_errors/pi_server_result_error.dart index 885834b9d..8c5315a44 100644 --- a/lib/model/exception_errors/pi_server_result_error.dart +++ b/lib/model/exception_errors/pi_server_result_error.dart @@ -45,8 +45,8 @@ class PiServerResultError implements Error { final map = validateMap( map: json, validators: { - CODE: const RequiredObjectValidator(), - MESSAGE: stringValidator, + CODE: Validators.intType, + MESSAGE: Validators.string, }, name: 'PiServerResultError#fromJson', ); diff --git a/lib/model/extensions/date_time_extension.dart b/lib/model/extensions/date_time_extension.dart index 1eb63e451..4beac748d 100644 --- a/lib/model/extensions/date_time_extension.dart +++ b/lib/model/extensions/date_time_extension.dart @@ -28,7 +28,7 @@ extension DateTimeX on DateTime { return DateTime.parse(v); } if (v is int) { - // Falls der Server mal Unix-Timestamps (ms) schickt + // If the server sends Unix timestamps (ms) return DateTime.fromMillisecondsSinceEpoch(v); } throw ArgumentError( diff --git a/lib/model/extensions/enums/force_biometric_option_extension.dart b/lib/model/extensions/enums/force_biometric_option_extension.dart index 4acf20891..a8e477523 100644 --- a/lib/model/extensions/enums/force_biometric_option_extension.dart +++ b/lib/model/extensions/enums/force_biometric_option_extension.dart @@ -22,7 +22,7 @@ import '../../../utils/object_validator/object_validators.dart'; import '../../enums/force_biometric_option.dart'; extension ForceBiometricOptionX on ForceBiometricOption { - static final validator = RequiredObjectValidator( + static final validator = DefaultObjectValidator( defaultValue: ForceBiometricOption.none, transformer: (v) { if (v is ForceBiometricOption) return v; diff --git a/lib/model/extensions/enums/push_token_rollout_state_extension.dart b/lib/model/extensions/enums/push_token_rollout_state_extension.dart index c3f230b5b..a20956db6 100644 --- a/lib/model/extensions/enums/push_token_rollout_state_extension.dart +++ b/lib/model/extensions/enums/push_token_rollout_state_extension.dart @@ -17,54 +17,94 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import 'package:privacyidea_authenticator/utils/object_validator/object_validators.dart'; + import '../../../l10n/app_localizations.dart'; import '../../enums/push_token_rollout_state.dart'; extension PushTokenRollOutStateX on PushTokenRollOutState { + static final validator = RequiredObjectValidator( + transformer: (v) { + if (v is PushTokenRollOutState) return v; + if (v is String) { + return PushTokenRollOutState.values.firstWhere( + (e) => e.toString() == v, + orElse: () => + throw ArgumentError('Invalid PushTokenRollOutState string: $v'), + ); + } + throw ArgumentError( + 'Invalid type for PushTokenRollOutState: ${v.runtimeType}, value: $v', + ); + }, + ); + + static final optionalValidator = validator.optional(); + bool get rollOutInProgress => switch (this) { - PushTokenRollOutState.rolloutNotStarted => false, - PushTokenRollOutState.generatingRSAKeyPair => true, - PushTokenRollOutState.generatingRSAKeyPairFailed => false, - PushTokenRollOutState.receivingFirebaseToken => true, - PushTokenRollOutState.receivingFirebaseTokenFailed => false, - PushTokenRollOutState.sendRSAPublicKey => true, - PushTokenRollOutState.sendRSAPublicKeyFailed => false, - PushTokenRollOutState.parsingResponse => true, - PushTokenRollOutState.parsingResponseFailed => false, - PushTokenRollOutState.rolloutComplete => false, - }; + PushTokenRollOutState.rolloutNotStarted => false, + PushTokenRollOutState.generatingRSAKeyPair => true, + PushTokenRollOutState.generatingRSAKeyPairFailed => false, + PushTokenRollOutState.receivingFirebaseToken => true, + PushTokenRollOutState.receivingFirebaseTokenFailed => false, + PushTokenRollOutState.sendRSAPublicKey => true, + PushTokenRollOutState.sendRSAPublicKeyFailed => false, + PushTokenRollOutState.parsingResponse => true, + PushTokenRollOutState.parsingResponseFailed => false, + PushTokenRollOutState.rolloutComplete => false, + }; bool get rolloutFailed => switch (this) { - PushTokenRollOutState.generatingRSAKeyPairFailed => true, - PushTokenRollOutState.receivingFirebaseTokenFailed => true, - PushTokenRollOutState.sendRSAPublicKeyFailed => true, - PushTokenRollOutState.parsingResponseFailed => true, - _ => false, - }; + PushTokenRollOutState.generatingRSAKeyPairFailed => true, + PushTokenRollOutState.receivingFirebaseTokenFailed => true, + PushTokenRollOutState.sendRSAPublicKeyFailed => true, + PushTokenRollOutState.parsingResponseFailed => true, + _ => false, + }; PushTokenRollOutState getFailed() => switch (this) { - PushTokenRollOutState.rolloutNotStarted => PushTokenRollOutState.rolloutNotStarted, - PushTokenRollOutState.generatingRSAKeyPair => PushTokenRollOutState.generatingRSAKeyPairFailed, - PushTokenRollOutState.generatingRSAKeyPairFailed => PushTokenRollOutState.generatingRSAKeyPairFailed, - PushTokenRollOutState.receivingFirebaseToken => PushTokenRollOutState.receivingFirebaseTokenFailed, - PushTokenRollOutState.receivingFirebaseTokenFailed => PushTokenRollOutState.receivingFirebaseTokenFailed, - PushTokenRollOutState.sendRSAPublicKey => PushTokenRollOutState.sendRSAPublicKeyFailed, - PushTokenRollOutState.sendRSAPublicKeyFailed => PushTokenRollOutState.sendRSAPublicKeyFailed, - PushTokenRollOutState.parsingResponse => PushTokenRollOutState.parsingResponseFailed, - PushTokenRollOutState.parsingResponseFailed => PushTokenRollOutState.parsingResponseFailed, - PushTokenRollOutState.rolloutComplete => PushTokenRollOutState.rolloutComplete, - }; + PushTokenRollOutState.rolloutNotStarted => + PushTokenRollOutState.rolloutNotStarted, + PushTokenRollOutState.generatingRSAKeyPair => + PushTokenRollOutState.generatingRSAKeyPairFailed, + PushTokenRollOutState.generatingRSAKeyPairFailed => + PushTokenRollOutState.generatingRSAKeyPairFailed, + PushTokenRollOutState.receivingFirebaseToken => + PushTokenRollOutState.receivingFirebaseTokenFailed, + PushTokenRollOutState.receivingFirebaseTokenFailed => + PushTokenRollOutState.receivingFirebaseTokenFailed, + PushTokenRollOutState.sendRSAPublicKey => + PushTokenRollOutState.sendRSAPublicKeyFailed, + PushTokenRollOutState.sendRSAPublicKeyFailed => + PushTokenRollOutState.sendRSAPublicKeyFailed, + PushTokenRollOutState.parsingResponse => + PushTokenRollOutState.parsingResponseFailed, + PushTokenRollOutState.parsingResponseFailed => + PushTokenRollOutState.parsingResponseFailed, + PushTokenRollOutState.rolloutComplete => + PushTokenRollOutState.rolloutComplete, + }; String rolloutMsg(AppLocalizations localizations) => switch (this) { - PushTokenRollOutState.rolloutNotStarted => localizations.rolloutStateNotStarted, - PushTokenRollOutState.generatingRSAKeyPair => localizations.rolloutStateGeneratingKeyPair, - PushTokenRollOutState.generatingRSAKeyPairFailed => localizations.rolloutStateGeneratingKeyPairFailed, - PushTokenRollOutState.receivingFirebaseToken => localizations.rolloutStateReceivingFirebaseToken, - PushTokenRollOutState.receivingFirebaseTokenFailed => localizations.rolloutStateReceivingFirebaseTokenFailed, - PushTokenRollOutState.sendRSAPublicKey => localizations.rolloutStateSendingPublicKey, - PushTokenRollOutState.sendRSAPublicKeyFailed => localizations.rolloutStateSendingPublicKeyFailed, - PushTokenRollOutState.parsingResponse => localizations.rolloutStateParsingResponse, - PushTokenRollOutState.parsingResponseFailed => localizations.rolloutStateParsingResponseFailed, - PushTokenRollOutState.rolloutComplete => localizations.rolloutStateCompleted, - }; + PushTokenRollOutState.rolloutNotStarted => + localizations.rolloutStateNotStarted, + PushTokenRollOutState.generatingRSAKeyPair => + localizations.rolloutStateGeneratingKeyPair, + PushTokenRollOutState.generatingRSAKeyPairFailed => + localizations.rolloutStateGeneratingKeyPairFailed, + PushTokenRollOutState.receivingFirebaseToken => + localizations.rolloutStateReceivingFirebaseToken, + PushTokenRollOutState.receivingFirebaseTokenFailed => + localizations.rolloutStateReceivingFirebaseTokenFailed, + PushTokenRollOutState.sendRSAPublicKey => + localizations.rolloutStateSendingPublicKey, + PushTokenRollOutState.sendRSAPublicKeyFailed => + localizations.rolloutStateSendingPublicKeyFailed, + PushTokenRollOutState.parsingResponse => + localizations.rolloutStateParsingResponse, + PushTokenRollOutState.parsingResponseFailed => + localizations.rolloutStateParsingResponseFailed, + PushTokenRollOutState.rolloutComplete => + localizations.rolloutStateCompleted, + }; } diff --git a/lib/model/pi_server_response.dart b/lib/model/pi_server_response.dart index 2f37ccde6..c31cb2af5 100644 --- a/lib/model/pi_server_response.dart +++ b/lib/model/pi_server_response.dart @@ -55,7 +55,7 @@ sealed class PiServerResponse< required double time, required String version, required String versionNumber, - required String signature, + @Default(null) String? signature, @Default(null) D? detail, }) = PiSuccessResponse; bool get isSuccess => this is PiSuccessResponse; @@ -73,7 +73,7 @@ sealed class PiServerResponse< required double time, required String version, required String versionNumber, - required String signature, + @Default(null) String? signature, }) = PiErrorResponse; bool get isError => this is PiErrorResponse; PiErrorResponse? get asError => @@ -87,16 +87,16 @@ sealed class PiServerResponse< final map = validateMap( map: json, validators: { - ID: const RequiredObjectValidator(), - JSONRPC: stringValidator, - RESULT: const RequiredObjectValidator>(), - TIME: const RequiredObjectValidator(), + ID: Validators.intType, + JSONRPC: Validators.string, + RESULT: RequiredObjectValidator>(), + TIME: RequiredObjectValidator(), VERSION: RequiredObjectValidator( allowedValues: (v) => v.contains(' '), ), - VERSION_NUMBER: stringValidatorOptional, - DETAIL: const OptionalObjectValidator(), - SIGNATURE: stringValidator, + VERSION_NUMBER: Validators.stringOptional, + DETAIL: OptionalObjectValidator(), + SIGNATURE: Validators.stringOptional, }, name: 'PiServerResponse#fromJson', ); @@ -113,7 +113,7 @@ sealed class PiServerResponse< map[VERSION_NUMBER] as String? ?? (map[VERSION] as String).split(' ')[1], detail: PiServerResultDetail.fromResultDetail(map[DETAIL]), - signature: map[SIGNATURE] as String, + signature: map[SIGNATURE] as String?, ); } diff --git a/lib/model/pi_server_response.freezed.dart b/lib/model/pi_server_response.freezed.dart index d2511a0b4..2b0125630 100644 --- a/lib/model/pi_server_response.freezed.dart +++ b/lib/model/pi_server_response.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$PiServerResponse { - int get statusCode; int get id; String get jsonrpc; double get time; String get version; String get versionNumber; String get signature; D? get detail; + int get statusCode; int get id; String get jsonrpc; double get time; String get version; String get versionNumber; String? get signature; D? get detail; @@ -116,7 +116,7 @@ return error(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String signature, D? detail)? success,TResult Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String signature)? error,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String? signature, D? detail)? success,TResult Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String? signature)? error,required TResult orElse(),}) {final _that = this; switch (_that) { case PiSuccessResponse() when success != null: return success(_that.statusCode,_that.id,_that.jsonrpc,_that.result,_that.time,_that.version,_that.versionNumber,_that.signature,_that.detail);case PiErrorResponse() when error != null: @@ -138,7 +138,7 @@ return error(_that.statusCode,_that.id,_that.jsonrpc,_that.detail,_that.piServer /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String signature, D? detail) success,required TResult Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String signature) error,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String? signature, D? detail) success,required TResult Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String? signature) error,}) {final _that = this; switch (_that) { case PiSuccessResponse(): return success(_that.statusCode,_that.id,_that.jsonrpc,_that.result,_that.time,_that.version,_that.versionNumber,_that.signature,_that.detail);case PiErrorResponse(): @@ -156,7 +156,7 @@ return error(_that.statusCode,_that.id,_that.jsonrpc,_that.detail,_that.piServer /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String signature, D? detail)? success,TResult? Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String signature)? error,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( int statusCode, int id, String jsonrpc, PiServerResult result, double time, String version, String versionNumber, String? signature, D? detail)? success,TResult? Function( int statusCode, int id, String jsonrpc, D? detail, PiServerResultError piServerResultError, double time, String version, String versionNumber, String? signature)? error,}) {final _that = this; switch (_that) { case PiSuccessResponse() when success != null: return success(_that.statusCode,_that.id,_that.jsonrpc,_that.result,_that.time,_that.version,_that.versionNumber,_that.signature,_that.detail);case PiErrorResponse() when error != null: @@ -172,7 +172,7 @@ return error(_that.statusCode,_that.id,_that.jsonrpc,_that.detail,_that.piServer class PiSuccessResponse extends PiServerResponse { - PiSuccessResponse({required this.statusCode, required this.id, required this.jsonrpc, required this.result, required this.time, required this.version, required this.versionNumber, required this.signature, this.detail = null}): super._(); + PiSuccessResponse({required this.statusCode, required this.id, required this.jsonrpc, required this.result, required this.time, required this.version, required this.versionNumber, this.signature = null, this.detail = null}): super._(); @override final int statusCode; @@ -182,7 +182,7 @@ class PiSuccessResponse extends PiServerResponse { - PiErrorResponse({required this.statusCode, required this.id, required this.jsonrpc, this.detail = null, required this.piServerResultError, required this.time, required this.version, required this.versionNumber, required this.signature}): super._(); + PiErrorResponse({required this.statusCode, required this.id, required this.jsonrpc, this.detail = null, required this.piServerResultError, required this.time, required this.version, required this.versionNumber, this.signature = null}): super._(); @override final int statusCode; @@ -224,7 +224,7 @@ class PiErrorResponse{ - ISSUER: stringValidator, - TTL_MINUTES: minutesDurationValidator.withDefault( + ISSUER: Validators.string, + TTL_MINUTES: Validators.minutesDuration.withDefault( const Duration(minutes: 10), ), - NONCE: stringValidator, + NONCE: Validators.string, TIMESTAMP: DateTimeX.validator, - FINALIZATION_URL: uriValidator, - SERIAL: stringValidator, + FINALIZATION_URL: Validators.uri, + SERIAL: Validators.string, EC_KEY_ALGORITHM: EcKeyAlgorithmX.validator, - HASH_ALGORITHM: algorithmsValidator, - PASSPHRASE_QUESTION: stringValidatorOptional, - SSL_VERIFY: boolValidator, + HASH_ALGORITHM: Validators.algorithms, + PASSPHRASE_QUESTION: Validators.stringOptional, + SSL_VERIFY: Validators.boolType, POLICIES: ContainerPolicies.validator.optional(), - SEND_PASSPHRASE: boolValidatorOptional, + SEND_PASSPHRASE: Validators.boolOptional, }, name: 'Container', ); diff --git a/lib/model/token_template.dart b/lib/model/token_template.dart index d8cc53bcf..853721ecf 100644 --- a/lib/model/token_template.dart +++ b/lib/model/token_template.dart @@ -58,13 +58,13 @@ sealed class TokenTemplate with _$TokenTemplate { String? get serial => validate( value: otpAuthMap[Token.SERIAL], - validator: stringValidatorOptional, + validator: Validators.stringOptional, name: Token.SERIAL, ); String? get type => validate( value: otpAuthMap[Token.TOKENTYPE_OTPAUTH], - validator: stringValidatorOptional, + validator: Validators.stringOptional, name: Token.TOKENTYPE_OTPAUTH, ); @@ -74,7 +74,7 @@ sealed class TokenTemplate with _$TokenTemplate { String? get containerSerial => validate( value: additionalData[Token.CONTAINER_SERIAL], - validator: stringValidatorOptional, + validator: Validators.stringOptional, name: Token.CONTAINER_SERIAL, ); diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index ef579d3b9..93fb0adb6 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -45,24 +45,25 @@ class DayPasswordToken extends OTPToken { static final Map otpAuthValidators = { ...OTPToken.otpAuthValidators, - TOTPToken.PERIOD_SECONDS: secondsDurationValidator.withDefault( + TOTPToken.PERIOD_SECONDS: Validators.secondsDuration.withDefault( const Duration(hours: 24), ), }; static final Map additionalDataValidators = { ...OTPToken.additionalDataValidators, - VIEW_MODE: OptionalObjectValidator( + VIEW_MODE: DefaultObjectValidator( defaultValue: DayPasswordTokenViewMode.VALIDFOR, transformer: (value) { if (value is DayPasswordTokenViewMode) return value; if (value is String) { return DayPasswordTokenViewMode.values.firstWhere( (e) => e.name.toLowerCase() == value.toLowerCase(), - orElse: () => DayPasswordTokenViewMode.VALIDFOR, ); } - return null; + throw ArgumentError( + 'Invalid type or value for DayPasswordTokenViewMode: $value', + ); }, ), }; @@ -197,7 +198,7 @@ class DayPasswordToken extends OTPToken { @override DayPasswordToken copyWith({ - String? serial, + String? Function()? serial, Duration? period, DayPasswordTokenViewMode? viewMode, String? label, @@ -218,7 +219,7 @@ class DayPasswordToken extends OTPToken { bool? isOffline, ForceBiometricOption? forceBiometricOption, }) => DayPasswordToken( - serial: serial ?? this.serial, + serial: serial != null ? serial() : this.serial, period: period ?? this.period, viewMode: viewMode ?? this.viewMode, label: label ?? this.label, @@ -260,7 +261,7 @@ class DayPasswordToken extends OTPToken { return copyWith( label: uriMap[Token.LABEL] as String?, issuer: uriMap[Token.ISSUER] as String?, - serial: uriMap[Token.SERIAL] as String?, + serial: () => uriMap[Token.SERIAL] as String?, tokenImage: uriMap[Token.IMAGE] as String?, pin: uriMap[Token.PIN] as bool?, isLocked: uriMap[Token.PIN] as bool?, diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index 94467d146..01e55861d 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -43,7 +43,7 @@ class HOTPToken extends OTPToken { static final Map otpAuthValidators = { ...OTPToken.otpAuthValidators, - COUNTER: otpAuthCounterValidator.withDefault(0), + COUNTER: Validators.otpCounterSafe, }; // --- Static Validation Methods --- @@ -159,7 +159,7 @@ class HOTPToken extends OTPToken { @override HOTPToken copyWith({ - String? serial, + String? Function()? serial, int? counter, String? label, String? issuer, @@ -179,7 +179,7 @@ class HOTPToken extends OTPToken { bool? isOffline, ForceBiometricOption? forceBiometricOption, }) => HOTPToken( - serial: serial ?? this.serial, + serial: serial != null ? serial() : this.serial, counter: counter ?? this.counter, label: label ?? this.label, issuer: issuer ?? this.issuer, @@ -212,7 +212,7 @@ class HOTPToken extends OTPToken { return copyWith( label: uriMap[Token.LABEL] as String?, issuer: uriMap[Token.ISSUER] as String?, - serial: uriMap[Token.SERIAL] as String?, + serial: () => uriMap[Token.SERIAL] as String?, tokenImage: uriMap[Token.IMAGE] as String?, pin: uriMap[Token.PIN] as bool?, isLocked: uriMap[Token.PIN] as bool?, diff --git a/lib/model/tokens/otp_token.dart b/lib/model/tokens/otp_token.dart index cb59c0e76..008e69354 100644 --- a/lib/model/tokens/otp_token.dart +++ b/lib/model/tokens/otp_token.dart @@ -38,9 +38,9 @@ abstract class OTPToken extends Token { // --- Static Accessors & Validators --- static final Map otpAuthValidators = { ...Token.otpAuthValidators, - ALGORITHM: algorithmsValidator.withDefault(Algorithms.SHA1), - DIGITS: otpAuthDigitsValidator.withDefault(6), - SECRET_BASE32: base32Stringvalidator, + ALGORITHM: Validators.algorithms.withDefault(Algorithms.SHA1), + DIGITS: Validators.otpDigitsSafe, + SECRET_BASE32: Validators.base32String, }; static final Map additionalDataValidators = { @@ -106,10 +106,16 @@ abstract class OTPToken extends Token { }); // --- Methods --- + + /// Checks if this token is the same as another token, based on their properties. + /// Returns `true` if they are the same, `false` if they are different, and `null` if it cannot be determined. + /// The base implementation checks the common properties of all tokens, and the OTPToken implementation checks the OTP-specific properties. + /// Subclasses can override this method to add more specific checks, but should call `super.isSameTokenAs(other)` to maintain the base checks. @override bool? isSameTokenAs(Token other) { if (other is! OTPToken) return false; - if (super.isSameTokenAs(other) != null) return super.isSameTokenAs(other)!; + final isSame = super.isSameTokenAs(other); + if (isSame != null) return isSame; if (secret != other.secret) return false; if (algorithm != other.algorithm) return false; if (digits != other.digits) return false; @@ -118,7 +124,7 @@ abstract class OTPToken extends Token { @override OTPToken copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 62b377209..28a8348bd 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -19,6 +19,7 @@ */ import 'package:json_annotation/json_annotation.dart'; import 'package:pointycastle/asymmetric/api.dart'; +import 'package:privacyidea_authenticator/model/extensions/enums/push_token_rollout_state_extension.dart'; import 'package:uuid/uuid.dart'; import '../../../../../../../model/token_template.dart'; @@ -28,6 +29,7 @@ import '../enums/force_biometric_option.dart'; import '../enums/push_token_rollout_state.dart'; import '../enums/token_types.dart'; import '../exception_errors/localized_argument_error.dart'; +import '../extensions/date_time_extension.dart'; import '../token_import/token_origin_data.dart'; import 'token.dart'; @@ -60,27 +62,27 @@ class PushToken extends Token { static final Map otpAuthValidators = { ...Token.otpAuthValidators, - Token.SERIAL: stringValidator, - SSL_VERIFY: boolValidator.withDefault(true), - IS_POLL_ONLY: boolValidatorOptional, - TTL_MINUTES: minutesDurationValidator.withDefault( + Token.SERIAL: Validators.string, + SSL_VERIFY: Validators.boolSafeTrue, + IS_POLL_ONLY: Validators.boolOptional, + TTL_MINUTES: Validators.minutesDuration.withDefault( const Duration(minutes: 3), ), - ENROLLMENT_CREDENTIAL: stringValidatorOptional, - ROLLOUT_URL: uriValidator, - VERSION: stringValidator, + ENROLLMENT_CREDENTIAL: Validators.stringOptional, + ROLLOUT_URL: Validators.uri, + VERSION: Validators.string, }; static final Map additionalDataValidators = { ...Token.additionalDataValidators, - EXPIRATION_DATE: const OptionalObjectValidator(), - ROLLOUT_STATE: const OptionalObjectValidator( - defaultValue: PushTokenRollOutState.rolloutNotStarted, + EXPIRATION_DATE: DateTimeX.validator.optional(), + ROLLOUT_STATE: PushTokenRollOutStateX.optionalValidator.withDefault( + PushTokenRollOutState.rolloutNotStarted, ), - IS_ROLLED_OUT: boolValidator.withDefault(false), - PUBLIC_SERVER_KEY: stringValidatorOptional, - PUBLIC_TOKEN_KEY: stringValidatorOptional, - PRIVATE_TOKEN_KEY: stringValidatorOptional, + IS_ROLLED_OUT: Validators.boolSafeFalse, + PUBLIC_SERVER_KEY: Validators.stringOptional, + PUBLIC_TOKEN_KEY: Validators.stringOptional, + PRIVATE_TOKEN_KEY: Validators.stringOptional, }; // --- Static Validation Methods --- @@ -285,7 +287,7 @@ class PushToken extends Token { @override PushToken copyWith({ String? label, - String? serial, + String? Function()? serial, String? issuer, String? Function()? containerSerial, List? checkedContainer, @@ -310,36 +312,40 @@ class PushToken extends Token { TokenOriginData? origin, bool? isOffline, ForceBiometricOption? forceBiometricOption, - }) => PushToken( - label: label ?? this.label, - serial: serial ?? this.serial, - issuer: issuer ?? this.issuer, - tokenImage: tokenImage ?? this.tokenImage, - fbToken: fbToken ?? this.fbToken, - containerSerial: containerSerial != null - ? containerSerial() - : this.containerSerial, - checkedContainer: checkedContainer ?? this.checkedContainer, - id: id ?? this.id, - pin: pin ?? this.pin, - isLocked: isLocked ?? this.isLocked, - isHidden: isHidden ?? this.isHidden, - sslVerify: sslVerify ?? this.sslVerify, - isPollOnly: isPollOnly != null ? isPollOnly() : this.isPollOnly, - enrollmentCredentials: enrollmentCredentials ?? this.enrollmentCredentials, - url: url ?? this.url, - publicServerKey: publicServerKey ?? this.publicServerKey, - publicTokenKey: publicTokenKey ?? this.publicTokenKey, - privateTokenKey: privateTokenKey ?? this.privateTokenKey, - expirationDate: expirationDate ?? this.expirationDate, - isRolledOut: isRolledOut ?? this.isRolledOut, - rolloutState: rolloutState ?? this.rolloutState, - sortIndex: sortIndex ?? this.sortIndex, - folderId: folderId != null ? folderId() : this.folderId, - origin: origin ?? this.origin, - isOffline: isOffline ?? this.isOffline, - forceBiometricOption: forceBiometricOption ?? this.forceBiometricOption, - ); + }) { + final String? newSerial = serial?.call(); + return PushToken( + label: label ?? this.label, + serial: newSerial ?? this.serial, + issuer: issuer ?? this.issuer, + tokenImage: tokenImage ?? this.tokenImage, + fbToken: fbToken ?? this.fbToken, + containerSerial: containerSerial != null + ? containerSerial() + : this.containerSerial, + checkedContainer: checkedContainer ?? this.checkedContainer, + id: id ?? this.id, + pin: pin ?? this.pin, + isLocked: isLocked ?? this.isLocked, + isHidden: isHidden ?? this.isHidden, + sslVerify: sslVerify ?? this.sslVerify, + isPollOnly: isPollOnly != null ? isPollOnly() : this.isPollOnly, + enrollmentCredentials: + enrollmentCredentials ?? this.enrollmentCredentials, + url: url ?? this.url, + publicServerKey: publicServerKey ?? this.publicServerKey, + publicTokenKey: publicTokenKey ?? this.publicTokenKey, + privateTokenKey: privateTokenKey ?? this.privateTokenKey, + expirationDate: expirationDate ?? this.expirationDate, + isRolledOut: isRolledOut ?? this.isRolledOut, + rolloutState: rolloutState ?? this.rolloutState, + sortIndex: sortIndex ?? this.sortIndex, + folderId: folderId != null ? folderId() : this.folderId, + origin: origin ?? this.origin, + isOffline: isOffline ?? this.isOffline, + forceBiometricOption: forceBiometricOption ?? this.forceBiometricOption, + ); + } @override Token copyUpdateByTemplate(TokenTemplate template) { @@ -347,7 +353,7 @@ class PushToken extends Token { return copyWith( label: uriMap[Token.LABEL] as String?, issuer: uriMap[Token.ISSUER] as String?, - serial: uriMap[Token.SERIAL] as String?, + serial: () => uriMap[Token.SERIAL] as String?, sslVerify: uriMap[SSL_VERIFY] as bool?, isPollOnly: uriMap[IS_POLL_ONLY] != null ? () => (uriMap[IS_POLL_ONLY] as bool) diff --git a/lib/model/tokens/steam_token.dart b/lib/model/tokens/steam_token.dart index c4087f81b..eaf4b7959 100644 --- a/lib/model/tokens/steam_token.dart +++ b/lib/model/tokens/steam_token.dart @@ -46,7 +46,7 @@ class SteamToken extends TOTPToken { static final Map otpAuthValidators = { ...Token.otpAuthValidators, - OTPToken.SECRET_BASE32: base32Stringvalidator, + OTPToken.SECRET_BASE32: Validators.base32String, }; // --- Static Validation Methods --- @@ -165,7 +165,7 @@ class SteamToken extends TOTPToken { @override SteamToken copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 29e7343d0..4805c22d0 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -68,25 +68,23 @@ abstract class Token with SortableMixin { // --- Static Validators --- static final Map otpAuthValidators = { - LABEL: stringValidator.withDefault(''), - ISSUER: stringValidator.withDefault(''), - SERIAL: stringValidatorOptional, - IMAGE: stringValidatorOptional, - PIN: boolValidatorOptional, - OFFLINE: boolValidator.withDefault(false), + LABEL: Validators.stringSafe, + ISSUER: Validators.stringSafe, + SERIAL: Validators.stringOptional, + IMAGE: Validators.stringOptional, + PIN: Validators.boolOptional, + OFFLINE: Validators.boolSafeFalse, FORCE_BIOMETRIC_OPTION: ForceBiometricOptionX.validator, }; static final Map additionalDataValidators = { - CONTAINER_SERIAL: stringValidatorOptional, - ID: stringValidatorOptional, - ORIGIN: const OptionalObjectValidator(), - IS_HIDDEN: boolValidatorOptional, - CHECKED_CONTAINERS: const OptionalObjectValidator>( - defaultValue: [], - ), - FOLDER_ID: intValidatorOptional, - SORT_INDEX: intValidatorOptional, + CONTAINER_SERIAL: Validators.stringOptional, + ID: Validators.stringOptional, + ORIGIN: OptionalObjectValidator(), + IS_HIDDEN: Validators.boolOptional, + CHECKED_CONTAINERS: DefaultObjectValidator>(defaultValue: []), + FOLDER_ID: Validators.intOptional, + SORT_INDEX: Validators.intOptional, }; // --- Static Validation Methods --- @@ -288,7 +286,7 @@ abstract class Token with SortableMixin { @override Token copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index 5220c4afa..3ff5bdd10 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -44,7 +44,7 @@ class TOTPToken extends OTPToken { static final Map otpAuthValidators = { ...OTPToken.otpAuthValidators, - PERIOD_SECONDS: otpAuthPeriodSecondsValidator.withDefault(30), + PERIOD_SECONDS: Validators.otpPeriodSafe, }; static final Map additionalDataValidators = { @@ -182,7 +182,7 @@ class TOTPToken extends OTPToken { @override TOTPToken copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, @@ -203,7 +203,7 @@ class TOTPToken extends OTPToken { ForceBiometricOption? forceBiometricOption, }) { return TOTPToken( - serial: serial ?? this.serial, + serial: serial != null ? serial() : this.serial, label: label ?? this.label, issuer: issuer ?? this.issuer, containerSerial: containerSerial != null @@ -233,7 +233,7 @@ class TOTPToken extends OTPToken { return copyWith( label: uriMap[Token.LABEL] as String?, issuer: uriMap[Token.ISSUER] as String?, - serial: uriMap[Token.SERIAL] as String?, + serial: () => uriMap[Token.SERIAL] as String?, tokenImage: uriMap[Token.IMAGE] as String?, pin: uriMap[Token.PIN] as bool?, isLocked: uriMap[Token.PIN] as bool?, diff --git a/lib/processors/mixins/token_import_processor.dart b/lib/processors/mixins/token_import_processor.dart index 00fdacf17..de48b38c8 100644 --- a/lib/processors/mixins/token_import_processor.dart +++ b/lib/processors/mixins/token_import_processor.dart @@ -25,7 +25,7 @@ import '../scheme_processors/token_import_scheme_processors/google_authenticator import '../token_import_file_processor/token_import_file_processor_interface.dart'; mixin TokenImportProcessor { - static const resultHandlerType = RequiredObjectValidator(); + static final resultHandlerType = RequiredObjectValidator(); static Set implementations = { const GoogleAuthenticatorQrProcessor(), ...TokenImportFileProcessor.implementations, diff --git a/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart b/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart index e5a38bd3b..5fefce7e1 100644 --- a/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart +++ b/lib/processors/scheme_processors/navigation_scheme_processors/home_widget_navigate_processor.dart @@ -30,7 +30,7 @@ import '../../../views/link_home_widget_view/link_home_widget_view.dart'; import 'navigation_scheme_processor_interface.dart'; class HomeWidgetNavigateProcessor implements NavigationSchemeProcessor { - static const resultHandlerType = RequiredObjectValidator(); + static final resultHandlerType = RequiredObjectValidator(); HomeWidgetNavigateProcessor(); static final Map< @@ -203,7 +203,7 @@ class NavigationHandler with ResultHandler { if (result.isFailed) return null; validate( value: args['context'], - validator: const RequiredObjectValidator(), + validator: RequiredObjectValidator(), name: 'context', ); final navigation = result.asSuccess!.resultData; @@ -225,7 +225,7 @@ class NavigationHandler with ResultHandler { try { context = validate( value: args['context'], - validator: const RequiredObjectValidator(), + validator: RequiredObjectValidator(), name: 'context', ); } catch (e, s) { diff --git a/lib/processors/scheme_processors/token_container_processor.dart b/lib/processors/scheme_processors/token_container_processor.dart index 64a2f9a64..b9c39a393 100644 --- a/lib/processors/scheme_processors/token_container_processor.dart +++ b/lib/processors/scheme_processors/token_container_processor.dart @@ -26,7 +26,7 @@ import '../../utils/riverpod/riverpod_providers/generated_providers/token_contai import 'scheme_processor_interface.dart'; class TokenContainerProcessor extends SchemeProcessor { - static const resultHandlerType = + static final resultHandlerType = RequiredObjectValidator(); static const scheme = 'pia'; static const host = 'container'; @@ -40,10 +40,10 @@ class TokenContainerProcessor extends SchemeProcessor { validateMap( map: args, validators: { - TokenContainerProcessor.ARG_DO_REPLACE: boolValidatorOptional, - TokenContainerProcessor.ARG_ADD_DEVICE_INFOS: boolValidatorOptional, - TokenContainerProcessor.ARG_INIT_SYNC: boolValidatorOptional, - TokenContainerProcessor.ARG_URL_IS_OK: boolValidatorOptional, + TokenContainerProcessor.ARG_DO_REPLACE: Validators.boolOptional, + TokenContainerProcessor.ARG_ADD_DEVICE_INFOS: Validators.boolOptional, + TokenContainerProcessor.ARG_INIT_SYNC: Validators.boolOptional, + TokenContainerProcessor.ARG_URL_IS_OK: Validators.boolOptional, }, name: 'TokenContainerProcessor#validateArgs', ); diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index 9c0d3b1bf..7350b6760 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -153,7 +153,7 @@ TokenOriginData _parseCreatorToOrigin(Uri uri) { String _parseIssuer(Uri uri) { final param = validate( value: uri.queryParameters[Token.ISSUER], - validator: const RequiredObjectValidator(defaultValue: ''), + validator: Validators.stringSafe, name: Token.ISSUER, ); try { @@ -181,10 +181,10 @@ Future? _parse2StepSecret(Uri uri) { validateMap( map: queryParameters, validators: { - OTPToken.SECRET_BASE32: base32ToBytesValidator, - Token.TWO_STEP_SALT_LENTH: intValidator, - Token.TWO_STEP_OUTPUT_LENTH: intValidator, - Token.TWO_STEP_ITERATIONS: intValidator, + OTPToken.SECRET_BASE32: Validators.base32String, + Token.TWO_STEP_SALT_LENTH: Validators.intType, + Token.TWO_STEP_OUTPUT_LENTH: Validators.intType, + Token.TWO_STEP_ITERATIONS: Validators.intType, }, name: '2StepSecret', ); @@ -247,7 +247,7 @@ String _parseTokenType(Uri uri) { Logger.debug('Token type value: $value'); return validate( value: uri.queryParameters[Token.TOKENTYPE_OTPAUTH] ?? uri.host, - validator: RequiredObjectValidator(defaultValue: uri.host), + validator: DefaultObjectValidator(defaultValue: uri.host), name: Token.TOKENTYPE_OTPAUTH, ); } diff --git a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart index 293751e27..027f9e7fe 100644 --- a/lib/processors/token_import_file_processor/aegis_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/aegis_import_file_processor.dart @@ -209,21 +209,17 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { HOTPToken.COUNTER: info[AEGIS_INFO_COUNTER], }, validators: { - Token.TOKENTYPE_OTPAUTH: stringValidator, - Token.LABEL: const RequiredObjectValidator( - defaultValue: '', - ), - Token.ISSUER: const RequiredObjectValidator( - defaultValue: '', - ), - Token.PIN: stringValidatorOptional, + Token.TOKENTYPE_OTPAUTH: Validators.string, + Token.LABEL: Validators.stringSafe, + Token.ISSUER: Validators.stringSafe, + Token.PIN: Validators.stringOptional, OTPToken.SECRET_BASE32: RequiredObjectValidator( transformer: (v) => Encodings.none.encodeStringTo( Encodings.base32, info[AEGIS_INFO_SECRET], ), ), - OTPToken.ALGORITHM: stringValidatorOptional, + OTPToken.ALGORITHM: Validators.stringOptional, OTPToken.DIGITS: OptionalObjectValidator( transformer: (v) => (v as int).toString(), ), @@ -298,15 +294,15 @@ class AegisImportFileProcessor extends TokenImportFileProcessor { Token.PIN: info[AEGIS_INFO_PIN], }, validators: { - Token.TOKENTYPE_OTPAUTH: stringValidator, - Token.LABEL: stringValidator.withDefault(''), - Token.ISSUER: stringValidator.withDefault(''), - OTPToken.SECRET_BASE32: base32Stringvalidator, - OTPToken.ALGORITHM: stringValidatorOptional, - OTPToken.DIGITS: intToStringValidatorOptional, - TOTPToken.PERIOD_SECONDS: intToStringValidatorOptional, - HOTPToken.COUNTER: intToStringValidatorOptional, - Token.PIN: stringValidatorOptional, + Token.TOKENTYPE_OTPAUTH: Validators.string, + Token.LABEL: Validators.stringSafe, + Token.ISSUER: Validators.stringSafe, + OTPToken.SECRET_BASE32: Validators.base32String, + OTPToken.ALGORITHM: Validators.stringOptional, + OTPToken.DIGITS: Validators.intToStringOptional, + TOTPToken.PERIOD_SECONDS: Validators.intToStringOptional, + HOTPToken.COUNTER: Validators.intToStringOptional, + Token.PIN: Validators.stringOptional, }, name: 'aegisV3Entry', ); diff --git a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart index 5fdc2e004..dbd4a74dc 100644 --- a/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/authenticator_pro_import_file_processor.dart @@ -385,13 +385,13 @@ class AuthenticatorProImportFileProcessor extends TokenImportFileProcessor { HOTPToken.COUNTER: tokenMap[_AUTHENTICATOR_PRO_COUNTER], }, validators: { - Token.TOKENTYPE_OTPAUTH: stringValidator, - Token.ISSUER: stringValidator, - Token.LABEL: stringValidator, - OTPToken.SECRET_BASE32: stringValidator, - OTPToken.DIGITS: intToStringValidator, - TOTPToken.PERIOD_SECONDS: intToStringValidator, - HOTPToken.COUNTER: intToStringValidator, + Token.TOKENTYPE_OTPAUTH: Validators.string, + Token.ISSUER: Validators.string, + Token.LABEL: Validators.string, + OTPToken.SECRET_BASE32: Validators.string, + OTPToken.DIGITS: Validators.intToString, + TOTPToken.PERIOD_SECONDS: Validators.intToString, + HOTPToken.COUNTER: Validators.intToString, OTPToken.ALGORITHM: RequiredObjectValidator( transformer: (value) => algorithmMap[value]!, ), diff --git a/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart b/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart index 59a2beb1a..4bb07538b 100644 --- a/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/free_otp_plus_import_file_processor.dart @@ -191,21 +191,21 @@ class FreeOtpPlusImportFileProcessor extends TokenImportFileProcessor { TOTPToken.PERIOD_SECONDS: tokenJson[_FREE_OTP_PLUS_PERIOD], }, validators: { - Token.TOKENTYPE_OTPAUTH: stringValidator, - Token.LABEL: stringValidator, - Token.ISSUER: stringValidator, + Token.TOKENTYPE_OTPAUTH: Validators.string, + Token.LABEL: Validators.string, + Token.ISSUER: Validators.string, OTPToken.SECRET_BASE32: RequiredObjectValidator( transformer: (value) => Encodings.base32.encode( Uint8List.fromList((value as List).cast()), ), ), - OTPToken.ALGORITHM: stringValidator, - OTPToken.DIGITS: intToStringValidator, - // FreeOTP+ saves the counter 1 less than the actual value + OTPToken.ALGORITHM: Validators.string, + OTPToken.DIGITS: Validators.intToString, HOTPToken.COUNTER: OptionalObjectValidator( + // FreeOTP+ saves the counter 1 less than the actual value transformer: (value) => ((value as int) + 1).toString(), ), - TOTPToken.PERIOD_SECONDS: intToStringValidatorOptional, + TOTPToken.PERIOD_SECONDS: Validators.intToStringOptional, }, ); } diff --git a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart index 6134cbb07..0069168a2 100644 --- a/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart +++ b/lib/processors/token_import_file_processor/two_fas_import_file_processor.dart @@ -219,14 +219,14 @@ class TwoFasAuthenticatorImportFileProcessor extends TokenImportFileProcessor { HOTPToken.COUNTER: twoFasOTP[TWOFAS_COUNTER], }, validators: { - Token.TOKENTYPE_OTPAUTH: stringValidator, - Token.ISSUER: stringValidatorOptional, - Token.LABEL: stringValidatorOptional, - OTPToken.SECRET_BASE32: stringValidator, - OTPToken.ALGORITHM: stringValidatorOptional, - OTPToken.DIGITS: intToStringValidatorOptional, - TOTPToken.PERIOD_SECONDS: intToStringValidatorOptional, - HOTPToken.COUNTER: intToStringValidatorOptional, + Token.TOKENTYPE_OTPAUTH: Validators.string, + Token.ISSUER: Validators.stringOptional, + Token.LABEL: Validators.stringOptional, + OTPToken.SECRET_BASE32: Validators.string, + OTPToken.ALGORITHM: Validators.stringOptional, + OTPToken.DIGITS: Validators.intToStringOptional, + TOTPToken.PERIOD_SECONDS: Validators.intToStringOptional, + HOTPToken.COUNTER: Validators.intToStringOptional, }, name: '2FAS token', ); diff --git a/lib/utils/firebase_utils.dart b/lib/utils/firebase_utils.dart index e32fc3b52..448f5c1af 100644 --- a/lib/utils/firebase_utils.dart +++ b/lib/utils/firebase_utils.dart @@ -23,9 +23,9 @@ import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/services.dart'; +import 'package:gms_check/gms_check.dart'; import 'package:mutex/mutex.dart'; import 'package:privacyidea_authenticator/repo/secure_storage.dart'; -import 'package:privacyidea_authenticator/utils/utils.dart'; import '../../../../../../../utils/view_utils.dart'; import 'globals.dart'; @@ -34,14 +34,13 @@ import 'logger.dart'; class FirebaseUtils { static FirebaseUtils? _instance; + static bool? _isMessagingAvailable; + final Mutex _initFbMutex = Mutex(); bool initializedFirebase = false; final Mutex _initHandlerMutex = Mutex(); bool initializedHandler = false; - // ########################################################################### - // FIREBASE CONFIG - // ########################################################################### static const FIREBASE_TOKEN_KEY_PREFIX_LEGACY = GLOBAL_SECURE_REPO_PREFIX_LEGACY; static const CURRENT_APP_TOKEN_KEY_LEGACY = 'CURRENT_APP_TOKEN'; @@ -74,11 +73,14 @@ class FirebaseUtils { SecureStorage? legacyStorage, }) { if (storage != null || legacyStorage != null) { - // For testing, return a new instance with mocked storage return FirebaseUtils._(storage: storage, legacyStorage: legacyStorage); } + if (_instance != null) return _instance!; - if (deviceHasFirebaseMessaging) { + + final isAvailable = _isMessagingAvailable ?? false; + + if (isAvailable) { _instance ??= FirebaseUtils._(); } else { _instance ??= NoFirebaseUtils(); @@ -87,7 +89,30 @@ class FirebaseUtils { return _instance!; } - /// Must be used in the main method before runApp() is called. + static Future preInitializeStatus() async { + if (_isMessagingAvailable != null) return; + _isMessagingAvailable = switch (true) { + _ when Platform.isAndroid => await _checkGmsAvailability(), + _ when Platform.isIOS || Platform.isMacOS => true, + _ => false, + }; + } + + static Future _checkGmsAvailability() async { + try { + return await GmsCheck().checkGmsAvailability() ?? false; + } catch (e, s) { + Logger.error( + 'Error while checking GMS availability', + error: e, + stackTrace: s, + ); + return false; + } + } + + static bool get isMessagingAvailable => _isMessagingAvailable ?? false; + Future initializeApp() async { await _initFbMutex.acquire(); try { @@ -124,7 +149,6 @@ class FirebaseUtils { } } - /// This method sets up the Firebase messaging handler for the app. It must be called after initializeApp(). Future setupHandler({ required Future Function(RemoteMessage) foregroundHandler, required Future Function(RemoteMessage) backgroundHandler, @@ -140,6 +164,7 @@ class FirebaseUtils { await _initHandlerMutex.acquire(); if (initializedHandler) { Logger.warning('Firebase handler already initialized'); + _initHandlerMutex.release(); return; } @@ -157,7 +182,10 @@ class FirebaseUtils { } } catch (error, stackTrace) { if (error is PlatformException) { - if (error.code == FIREBASE_TOKEN_ERROR_CODE) return; // ignore + if (error.code == FIREBASE_TOKEN_ERROR_CODE) { + _initHandlerMutex.release(); + return; + } showErrorStatusMessage( message: (l) => l.pushInitializeUnavailable, details: (_) => @@ -165,7 +193,10 @@ class FirebaseUtils { ); } if (error is FirebaseException) { - if (error.code == FIREBASE_TOKEN_ERROR_CODE) return; // ignore + if (error.code == FIREBASE_TOKEN_ERROR_CODE) { + _initHandlerMutex.release(); + return; + } showErrorStatusMessage( message: (l) => l.pushInitializeUnavailable, details: (_) => @@ -183,7 +214,6 @@ class FirebaseUtils { FirebaseMessaging.instance.onTokenRefresh.listen((String newToken) async { if ((await getCurrentFirebaseToken()) != newToken) { await setNewFirebaseToken(newToken); - // TODO what if this fails, when should a retry be attempted? try { updateFirebaseToken(firebaseToken: newToken); } catch (error, stackTrace) { @@ -200,9 +230,6 @@ class FirebaseUtils { _initHandlerMutex.release(); } - /// Returns the current firebase token of the app / device. Throws a - /// PlatformException with a custom error code if retrieving the firebase - /// token failed. This may happen if, e.g., no network connection is available. Future getFBToken() async { String? firebaseToken; try { @@ -216,7 +243,6 @@ class FirebaseUtils { ); } - // Fall back to the last known firebase token if (firebaseToken == null) { firebaseToken = await getCurrentFirebaseToken(); } else { @@ -225,8 +251,6 @@ class FirebaseUtils { } if (firebaseToken == null) { - // This error should be handled in all cases, the user might be informed - // in the form of a pop-up message. throw PlatformException( message: 'Firebase token could not be retrieved, the only know cause of this is' @@ -280,7 +304,6 @@ class FirebaseUtils { return null; } - // This is used for checking if the token was updated. Future setNewFirebaseToken(String str) { Logger.info('Setting new firebase token'); return _storage.write(key: NEW_APP_TOKEN_KEY, value: str); @@ -303,7 +326,6 @@ class FirebaseUtils { } } -/// This class just is used to disable Firebase for web builds. class NoFirebaseUtils implements FirebaseUtils { @override Mutex get _initFbMutex => Mutex(); @@ -322,7 +344,7 @@ class NoFirebaseUtils implements FirebaseUtils { Future setupHandler({ required Future Function(RemoteMessage p1) foregroundHandler, required Future Function(RemoteMessage p1) backgroundHandler, - required void Function({String? firebaseToken}) updateFirebaseToken, + required dynamic Function({String? firebaseToken}) updateFirebaseToken, }) async {} @override diff --git a/lib/utils/lock_auth.dart b/lib/utils/lock_auth.dart index ba1c39e56..df4cf888d 100644 --- a/lib/utils/lock_auth.dart +++ b/lib/utils/lock_auth.dart @@ -37,7 +37,7 @@ import 'logger.dart'; import 'view_utils.dart'; LocalAuthentication _localAuth = LocalAuthentication(); -final Mutex _authMutex = Mutex(); +Mutex _authMutex = Mutex(); /// Requests OS-level authentication from the user. /// @@ -63,20 +63,24 @@ Future lockAuth({ } await _authMutex.acquire(); - final isBiometricForced = - forceBiometricOption == ForceBiometricOption.biometric; - if (!await _checkSupport(isBiometricForced, autoAuthIfUnsupported)) { - return autoAuthIfUnsupported; - } - final isAuthenticated = await _executeAuth( - isBiometricForced: isBiometricForced, - localizedReason: reason(localization), - localization: localization, - ); + try { + final isBiometricForced = + forceBiometricOption == ForceBiometricOption.biometric; + if (!await _checkSupport(isBiometricForced, autoAuthIfUnsupported)) { + return autoAuthIfUnsupported; + } - _authMutex.release(); - return isAuthenticated; + return await _executeAuth( + isBiometricForced: isBiometricForced, + localizedReason: reason(localization), + localization: localization, + ); + } finally { + if (_authMutex.isLocked) { + _authMutex.release(); + } + } } Future _executeAuth({ @@ -238,3 +242,9 @@ Future _showBiometricUnavailableDialog() async { @visibleForTesting set localAuthInstance(LocalAuthentication auth) => _localAuth = auth; +@visibleForTesting +void resetAuthMutex() { + if (_authMutex.isLocked) { + _authMutex.release(); + } +} diff --git a/lib/utils/object_validator/base_validator.dart b/lib/utils/object_validator/base_validator.dart index a2f63e88c..0f34b0c5b 100644 --- a/lib/utils/object_validator/base_validator.dart +++ b/lib/utils/object_validator/base_validator.dart @@ -22,37 +22,23 @@ part of 'object_validators.dart'; abstract class BaseValidator { final T Function(Object? value)? transformer; - final T? defaultValue; final bool Function(T)? allowedValues; - const BaseValidator({ - required this.defaultValue, - this.transformer, - this.allowedValues, - }); + const BaseValidator({this.transformer, this.allowedValues}); - OptionalObjectValidator optional(); + // Rückgabetyp auf T (bzw. die Non-Nullable Variante) angepasst + BaseValidator optional(); + BaseValidator withDefault(covariant Object defaultValue); bool isTypeOf(Object? value); bool valueIsAllowed(Object? value, String name); - T transform(Object? value, String name); - BaseValidator withDefault(T defaultValue); - T _executeTransform(Object? value, String name) { - if (value == null) { - if (defaultValue != null) return defaultValue!; - throw _error(value, name); - } - if (transformer != null) { return transformer!(value); } - - if (value is T) return value as T; - - if (defaultValue != null) return defaultValue!; + if (value is T) return value; throw _error(value, name); } @@ -69,10 +55,10 @@ abstract class BaseValidator { name: name, ); Logger.warning( - 'Validation failed for <$T?>. Optional Value: "$value" (Type: ${value.runtimeType})', + 'Validation failed for <$T>. Value: "$value" (Type: ${value.runtimeType})', error: error, stackTrace: StackTrace.current, - name: 'OptionalObjectValidator<$T>', + name: runtimeType.toString(), ); return error; } diff --git a/lib/utils/object_validator/default_object_validator.dart b/lib/utils/object_validator/default_object_validator.dart new file mode 100644 index 000000000..38e1d436d --- /dev/null +++ b/lib/utils/object_validator/default_object_validator.dart @@ -0,0 +1,62 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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. + */ + +part of 'object_validators.dart'; + +class DefaultObjectValidator extends BaseValidator { + T defaultValue; + + DefaultObjectValidator({ + required this.defaultValue, + super.transformer, + super.allowedValues, + }); + + @override + T transform(value, name) { + if (value == null) return defaultValue; + try { + return _executeTransform(value, name); + } catch (e) { + return defaultValue; + } + } + + @override + OptionalObjectValidator optional() => OptionalObjectValidator( + transformer: transformer, + allowedValues: (v) => v == null ? true : (allowedValues?.call(v) ?? true), + ); + + @override + DefaultObjectValidator withDefault(T defaultValue) => + DefaultObjectValidator( + defaultValue: defaultValue, + transformer: transformer, + allowedValues: allowedValues, + ); + + @override + bool isTypeOf(value) => value == null || value is T || transformer != null; + + @override + bool valueIsAllowed(value, name) => + allowedValues?.call(transform(value, name)) ?? true; +} diff --git a/lib/utils/object_validator/object_validators.dart b/lib/utils/object_validator/object_validators.dart index c22be573b..431b5cdc6 100644 --- a/lib/utils/object_validator/object_validators.dart +++ b/lib/utils/object_validator/object_validators.dart @@ -27,151 +27,124 @@ import '../../model/exception_errors/localized_argument_error.dart'; import '../logger.dart'; part 'base_validator.dart'; +part 'default_object_validator.dart'; part 'optional_object_validator.dart'; part 'required_object_validator.dart'; final _base32Regex = RegExp(r'^[A-Z2-7]+=*$'); -final stringValidator = RequiredObjectValidator( - transformer: (v) { - if (v is String) return v; - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final stringValidatorOptional = stringValidator.optional(); - -final otpAuthPeriodSecondsValidator = RequiredObjectValidator( - transformer: (v) { - if (v is int) return v; - if (v is String) return int.parse(v); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, - defaultValue: 30, - allowedValues: (v) => v > 0, -); -final otpAutjPeriodSecondsValidatorOptional = otpAuthPeriodSecondsValidator - .optional(); - -final otpAuthDigitsValidator = RequiredObjectValidator( - transformer: (v) { - if (v is int) return v; - if (v is String) return int.parse(v); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, - defaultValue: 6, - allowedValues: (p0) => p0 > 0, -); -final otpAuthDigitsValidatorOptional = otpAuthDigitsValidator.optional(); - -final otpAuthCounterValidator = RequiredObjectValidator( - transformer: (v) { - if (v is int) return v; - if (v is String) return int.parse(v); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, - allowedValues: (v) => v >= 0, -); - -final intValidator = RequiredObjectValidator( - transformer: (v) { - if (v is int) return v; - if (v is String) return int.parse(v); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final intValidatorOptional = intValidator.optional(); - -final intToStringValidator = RequiredObjectValidator( - transformer: (v) { - if (v is int) return v.toString(); - if (v is String) return v; - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final intToStringValidatorOptional = intToStringValidator.optional(); - -final secondsDurationValidator = RequiredObjectValidator( - transformer: (v) { - if (v is Duration) return v; - if (v is int) return Duration(seconds: v); - if (v is String) return Duration(seconds: int.parse(v)); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, - allowedValues: (v) => v.inSeconds > 0, -); -final secondsDurationValidatorOptional = secondsDurationValidator.optional(); - -final minutesDurationValidator = RequiredObjectValidator( - transformer: (v) { - if (v is Duration) return v; - if (v is int) return Duration(minutes: v); - if (v is String) return Duration(minutes: int.parse(v)); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, - allowedValues: (v) => v.inSeconds > 0, -); -final minutesDurationValidatorOptional = minutesDurationValidator.optional(); - -final uriValidator = RequiredObjectValidator( - transformer: (v) { - if (v is Uri) return v; - if (v is String) return Uri.parse(v); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final uriValidatorOptional = uriValidator.optional(); - -final boolValidator = RequiredObjectValidator( - transformer: (v) { - if (v is bool) return v; - if (v is int) return v == 1; - if (v is String) { - return switch (v) { - 'true' || 'True' || '1' => true, - 'false' || 'False' || '0' => false, - _ => throw ArgumentError('Invalid boolean value: $v'), - }; - } - throw ArgumentError('Invalid boolean value: $v'); - }, -); -final boolValidatorOptional = boolValidator.optional(); - -final algorithmsValidator = RequiredObjectValidator( - transformer: (v) { - if (v is Algorithms) return v; - if (v is String) return Algorithms.values.byName(v.toUpperCase()); - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final algorithmsValidatorOptional = algorithmsValidator.optional(); - -final base32Stringvalidator = RequiredObjectValidator( - transformer: (v) { - if (v is Uint8List) return Encodings.base32.encode(v); - if (v is String) { - final normalized = v.replaceAll(' ', '').toUpperCase(); - if (!_base32Regex.hasMatch(normalized)) { - throw ArgumentError('Invalid base32 format: $normalized'); +abstract class Validators { + // --- Core Types --- + static final string = RequiredObjectValidator(); + static final stringOptional = string.optional(); + static final stringSafe = string.withDefault(''); + + static final intType = RequiredObjectValidator( + transformer: (v) => v is String ? int.parse(v) : (v as int), + ); + static final intOptional = intType.optional(); + + static final boolType = RequiredObjectValidator( + transformer: (v) { + if (v is bool) return v; + if (v is int) return v == 1; + if (v is String) { + return switch (v.toLowerCase()) { + 'true' || '1' => true, + 'false' || '0' => false, + _ => throw ArgumentError('Invalid boolean: $v'), + }; } - return normalized; - } - throw ArgumentError('Invalid type: ${v.runtimeType}, value: $v'); - }, -); -final base32StringValidatorOptional = base32Stringvalidator.optional(); - -final base32ToBytesValidator = RequiredObjectValidator( - transformer: (v) { - if (v is Uint8List) return v; - if (v is String) { - final normalized = v.replaceAll(' ', '').toUpperCase(); - return Encodings.base32.decode(normalized); - } - throw ArgumentError('Invalid type: ${v.runtimeType}'); - }, -); -final base32ToBytesValidatorOptional = base32ToBytesValidator.optional(); + throw ArgumentError('Invalid type for bool: ${v.runtimeType}'); + }, + ); + static final boolOptional = boolType.optional(); + static final boolSafeTrue = boolType.withDefault(true); + static final boolSafeFalse = boolType.withDefault(false); + + // --- OTP / Token Specific --- + static final otpPeriod = RequiredObjectValidator( + transformer: (v) => v is String ? int.parse(v) : (v as int), + allowedValues: (v) => v > 0, + ); + static final otpPeriodSafe = otpPeriod.withDefault(30); + + static final otpDigits = RequiredObjectValidator( + transformer: (v) => v is String ? int.parse(v) : (v as int), + allowedValues: (v) => v > 0, + ); + static final otpDigitsSafe = otpDigits.withDefault(6); + + static final otpCounter = RequiredObjectValidator( + transformer: (v) => v is String ? int.parse(v) : (v as int), + allowedValues: (v) => v >= 0, + ); + static final otpCounterSafe = otpCounter.withDefault(0); + + static final algorithms = RequiredObjectValidator( + transformer: (v) => v is String + ? Algorithms.values.byName(v.toUpperCase()) + : (v as Algorithms), + ); + static final algorithmOptional = algorithms.optional(); + + // --- Conversions --- + static final intToString = RequiredObjectValidator( + transformer: (v) { + if (v is int) return v.toString(); + if (v is String) return int.parse(v).toString(); + if (v is num) return v.toInt().toString(); + throw ArgumentError('Invalid type for int to string: ${v.runtimeType}'); + }, + ); + static final intToStringOptional = intToString.optional(); + + static final base32String = RequiredObjectValidator( + transformer: (v) { + if (v is Uint8List) return Encodings.base32.encode(v); + final s = (v as String).replaceAll(' ', '').toUpperCase(); + if (!_base32Regex.hasMatch(s)) throw ArgumentError('Invalid base32'); + return s; + }, + ); + static final base32StringOptional = base32String.optional(); + + static final base32ToBytes = RequiredObjectValidator( + transformer: (v) { + if (v is Uint8List) return v; + return Encodings.base32.decode( + (v as String).replaceAll(' ', '').toUpperCase(), + ); + }, + ); + static final base32ToBytesOptional = base32ToBytes.optional(); + + // --- Durations & URI --- + static final secondsDuration = RequiredObjectValidator( + transformer: (v) { + if (v is Duration) return v; + final sec = v is String ? int.parse(v) : (v as int); + return Duration(seconds: sec); + }, + allowedValues: (v) => v.inSeconds > 0, + ); + static final secondsDurationOptional = secondsDuration.optional(); + + static final minutesDuration = RequiredObjectValidator( + transformer: (v) { + if (v is Duration) return v; + final min = v is String ? int.parse(v) : (v as int); + return Duration(minutes: min); + }, + allowedValues: (v) => v.inMinutes > 0, + ); + static final minutesDurationOptional = minutesDuration.optional(); + + static final uri = RequiredObjectValidator( + transformer: (v) => v is String ? Uri.parse(v) : (v as Uri), + ); + static final uriOptional = uri.optional(); +} T validate({ required Object? value, @@ -180,7 +153,7 @@ T validate({ }) { final result = validator.transform(value, name); if (!validator.valueIsAllowed(value, name)) { - throw validator._error(value, name); + throw (validator as dynamic)._error(value, name); } return result; } @@ -192,12 +165,9 @@ Map validateMap({ }) { final Map validatedMap = {}; for (final key in validators.keys) { - final validator = validators[key]!; - final mapEntry = map[key]; - final newValue = validate( - value: mapEntry, - validator: validator, + value: map[key], + validator: validators[key]!, name: key, ); diff --git a/lib/utils/object_validator/optional_object_validator.dart b/lib/utils/object_validator/optional_object_validator.dart index 8fc768bd1..a69753d07 100644 --- a/lib/utils/object_validator/optional_object_validator.dart +++ b/lib/utils/object_validator/optional_object_validator.dart @@ -17,65 +17,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - part of 'object_validators.dart'; -class OptionalObjectValidator extends BaseValidator { - const OptionalObjectValidator({ - super.transformer, - super.defaultValue, - super.allowedValues, - }); - - @override - OptionalObjectValidator optional() => this; +class OptionalObjectValidator extends BaseValidator { + OptionalObjectValidator({super.transformer, super.allowedValues}); @override T? transform(value, name) { - if (value == null) return defaultValue; + if (value == null) return null; try { return _executeTransform(value, name); - } catch (e, stackTrace) { - Logger.warning( - 'Validation failed for <$T?>. Optional Value: "$value" (Type: ${value.runtimeType})', - error: e, - stackTrace: stackTrace, - name: 'OptionalObjectValidator<$T>', - ); - return defaultValue; + } catch (e) { + return null; } } @override - OptionalObjectValidator withDefault(defaultValue) { - return OptionalObjectValidator( - transformer: transformer, - defaultValue: defaultValue, - allowedValues: allowedValues, - ); - } + OptionalObjectValidator optional() => this; @override - bool isTypeOf(value) { - if (value == null) return true; - - if (transformer != null) { - try { - transformer!(value); - return true; - } catch (e, stackTrace) { - Logger.warning( - 'Validation failed for <$T?>. Optional Value: "$value" (Type: ${value.runtimeType})', - error: e, - stackTrace: stackTrace, - name: 'OptionalObjectValidator<$T>', - ); - return false; - } - } + DefaultObjectValidator withDefault(T defaultValue) => + DefaultObjectValidator( + defaultValue: defaultValue, + transformer: transformer == null + ? null + : (v) => transformer!(v) ?? defaultValue, + allowedValues: allowedValues, + ); - return value is T; - } + @override + bool isTypeOf(value) => value == null || value is T || transformer != null; @override bool valueIsAllowed(value, name) { diff --git a/lib/utils/object_validator/required_object_validator.dart b/lib/utils/object_validator/required_object_validator.dart index 8ae70c24f..82f1a46a7 100644 --- a/lib/utils/object_validator/required_object_validator.dart +++ b/lib/utils/object_validator/required_object_validator.dart @@ -3,7 +3,7 @@ * * Author: Frank Merkel * - * Copyright (c) 2025 NetKnights GmbH + * Copyright (c) 2026 NetKnights GmbH * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -21,84 +21,35 @@ part of 'object_validators.dart'; class RequiredObjectValidator extends BaseValidator { - const RequiredObjectValidator({ - super.transformer, - super.defaultValue, - super.allowedValues, - }); + RequiredObjectValidator({super.transformer, super.allowedValues}); @override T transform(value, name) { - if (value == null) { - if (defaultValue != null) { - return defaultValue as T; - } - - throw _error(value, name); - } - try { - return _executeTransform(value, name); - } catch (e, stackTrace) { - Logger.warning( - 'Validation failed for <$T>. Value: "$value" (Type: ${value.runtimeType})', - error: e, - stackTrace: stackTrace, - name: 'RequiredObjectValidator<$T>', - ); - rethrow; - } - } - - @override - RequiredObjectValidator withDefault(defaultValue) { - return RequiredObjectValidator( - transformer: transformer, - defaultValue: defaultValue, - allowedValues: allowedValues, - ); + if (value == null) throw _error(value, name); + return _executeTransform(value, name); } @override OptionalObjectValidator optional() => OptionalObjectValidator( transformer: transformer, - defaultValue: defaultValue, allowedValues: (v) { - if (allowedValues == null) return true; if (v == null) return true; - return allowedValues!(v); + return allowedValues?.call(v) ?? true; }, ); @override - bool isTypeOf(value) { - if (value == null) return false; - - if (transformer != null) { - try { - transformer!(value); - return true; - } catch (e, stackTrace) { - Logger.warning( - 'Validation failed for <$T>. Required Value: "$value" (Type: ${value.runtimeType})', - error: e, - stackTrace: stackTrace, - name: 'RequiredObjectValidator<$T>', - ); - return false; - } - } + DefaultObjectValidator withDefault(T defaultValue) => + DefaultObjectValidator( + defaultValue: defaultValue, + transformer: transformer, + allowedValues: allowedValues, + ); - return value is T; - } + @override + bool isTypeOf(value) => value is T || (transformer != null && value != null); @override - bool valueIsAllowed(value, name) { - if (!isTypeOf(value)) { - if (defaultValue != null) { - return allowedValues?.call(defaultValue as T) ?? true; - } - return false; - } - return allowedValues?.call(transform(value, name)) ?? true; - } + bool valueIsAllowed(value, name) => + allowedValues?.call(transform(value, name)) ?? true; } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart new file mode 100644 index 000000000..c5224afd1 --- /dev/null +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart @@ -0,0 +1,29 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'has_firebase_provider.g.dart'; + +@riverpod +Future hasFirebase(Ref ref) async { + await FirebaseUtils.preInitializeStatus(); + return FirebaseUtils.isMessagingAvailable; +} diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.g.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.g.dart new file mode 100644 index 000000000..b8b6c3c7a --- /dev/null +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'has_firebase_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(hasFirebase) +final hasFirebaseProvider = HasFirebaseProvider._(); + +final class HasFirebaseProvider + extends $FunctionalProvider, bool, FutureOr> + with $FutureModifier, $FutureProvider { + HasFirebaseProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'hasFirebaseProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$hasFirebaseHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return hasFirebase(ref); + } +} + +String _$hasFirebaseHash() => r'8f0681b6ee9998d3cfc4fb4987aa6d30960d4cd2'; diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/sortable_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/sortable_notifier.dart index a33960a40..38caa169c 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/sortable_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/sortable_notifier.dart @@ -1,23 +1,3 @@ -/* - * privacyIDEA Authenticator - * - * Author: Frank Merkel - * - * Copyright (c) 2025 NetKnights GmbH - * - * 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 'package:flutter/widgets.dart'; import 'package:privacyidea_authenticator/model/extensions/token_list_extension.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -38,29 +18,37 @@ Future> sortables(Ref ref) async { tokenProvider.selectAsync((state) => state.tokens.filterDuplicates()), ); - var sortablesWithNulls = List.from([ - ...tokens, - ...tokenFolders, - ]); - - final sortedSortables = sortablesWithNulls.sorted.fillNullIndices(); + final sortablesList = [...tokens, ...tokenFolders]; + final sortedList = sortablesList.sorted.fillNullIndices(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (sortablesWithNulls.any((e) => e is Token) && - sortablesWithNulls.any((element) => element.sortIndex == null)) { - ref - .read(tokenProvider.notifier) - .addOrReplaceTokens(sortedSortables.whereType().toList()); - } - if (sortablesWithNulls.any((e) => e is TokenFolder) && - sortablesWithNulls.any((element) => element.sortIndex == null)) { - ref - .read(tokenFolderProvider.notifier) - .addOrReplaceFolders( - sortedSortables.whereType().toList(), - ); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!ref.mounted) return; + _handleSortIndexUpdates(ref, sortablesList, sortedList); }); - return sortedSortables; + return sortedList; +} + +void _handleSortIndexUpdates( + Ref ref, + List original, + List sorted, +) { + final hasUnsortedItems = original.any((e) => e.sortIndex == null); + if (!hasUnsortedItems) return; + + final hasTokens = original.any((e) => e is Token); + final hasFolders = original.any((e) => e is TokenFolder); + + if (hasTokens) { + ref + .read(tokenProvider.notifier) + .addOrReplaceTokens(sorted.whereType().toList()); + } + + if (hasFolders) { + ref + .read(tokenFolderProvider.notifier) + .addOrReplaceFolders(sorted.whereType().toList()); + } } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart index 6ec148357..9513d57b7 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart @@ -564,251 +564,178 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { return false; } - assert( - pushToken.url != null, - 'Token url is null. Cannot rollout token without url.', - ); + if (!await _isEligibleForRollout(pushToken)) return false; + if (pushToken.isRolledOut) return true; + Logger.info('Rolling out token "${pushToken.id}"'); - if (pushToken.isRolledOut) { - Logger.info( - 'Ignoring rollout request: Token "${pushToken.id}" already rolled out.', - ); - return true; + + if (pushToken.privateTokenKey == null) { + pushToken = await _handleKeyGeneration(pushToken); + if (pushToken == null) return false; } - if (pushToken.rolloutState.rollOutInProgress) { - Logger.info( - 'Ignoring rollout request: Rollout of token "${pushToken.id}" already started. Tokenstate: ${pushToken.rolloutState} ', - ); + + String? fbToken; + if (pushToken.isPollOnly != true) { + fbToken = await _getFirebaseToken(pushToken); + if (fbToken == null) return false; + pushToken = await getTokenById(pushToken.id) ?? pushToken; + } + + if (!kIsWeb && Platform.isIOS) { + if (!await _checkNetworkPermission(pushToken)) return false; + } + + return await _executeServerRollout(pushToken, fbToken); + } + + Future _isEligibleForRollout(PushToken token) async { + assert(token.url != null, 'Token url is null.'); + + if (token.rolloutState.rollOutInProgress) { + Logger.info('Rollout already in progress for "${token.id}".'); return false; } - if (pushToken.expirationDate?.isBefore(DateTime.now()) == true) { - Logger.info( - 'Ignoring rollout request: Token "${pushToken.id}" is expired. ', - ); + if (token.expirationDate?.isBefore(DateTime.now()) == true) { + Logger.info('Token "${token.id}" is expired.'); ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => localization.errorRollOutNotPossibleAnymore, - details: (localization) => - localization.errorTokenExpired(pushToken!.label), + message: (loc) => loc.errorRollOutNotPossibleAnymore, + details: (loc) => loc.errorTokenExpired(token.label), ); - - await _removeToken(pushToken); + await _removeToken(token); return false; } + return true; + } - if (pushToken.privateTokenKey == null) { - Logger.info( - 'Updating rollout state of token "${pushToken.id}" to generatingRSAKeyPair', + Future _handleKeyGeneration(PushToken token) async { + token = + await _updateStatus( + token, + PushTokenRollOutState.generatingRSAKeyPair, + ) ?? + token; + try { + final keyPair = await rsaUtils.generateRSAKeyPair(); + return await _updateToken(token, (p) { + p = p.withPrivateTokenKey(keyPair.privateKey); + return p.withPublicTokenKey(keyPair.publicKey); + }); + } catch (e, s) { + Logger.error( + 'Error while generating RSA key pair.', + error: e, + stackTrace: s, ); - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.generatingRSAKeyPair, - ), + await _updateStatus( + token, + PushTokenRollOutState.generatingRSAKeyPairFailed, ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - return false; - } - Logger.info('Updated token "${pushToken.id}"'); - try { - final keyPair = await rsaUtils.generateRSAKeyPair(); - pushToken = pushToken.withPrivateTokenKey(keyPair.privateKey); - pushToken = pushToken.withPublicTokenKey(keyPair.publicKey); - pushToken = - await _updateToken(pushToken, (p0) { - p0 = p0.withPrivateTokenKey(keyPair.privateKey); - return p0.withPublicTokenKey(keyPair.publicKey); - }) ?? - pushToken; - Logger.info('Updated token "${pushToken.id}"'); - } catch (e, s) { - Logger.error( - 'Error while generating RSA key pair.', - error: e, - stackTrace: s, - ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - return false; - } - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.generatingRSAKeyPairFailed, - ), - ); - return false; - } + return null; } - String? fbToken; - if (pushToken.isPollOnly != true) { - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.receivingFirebaseToken, - ), + } + + Future _getFirebaseToken(PushToken token) async { + await _updateStatus(token, PushTokenRollOutState.receivingFirebaseToken); + try { + return await firebaseUtils.getFBToken(); + } catch (e, s) { + Logger.warning('Could not get firebase token.', error: e, stackTrace: s); + showErrorStatusMessage( + message: (l) => l.errorRollOutFailed(token.label), + details: (l) => l.noFbToken, ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - return false; - } - try { - fbToken = await firebaseUtils.getFBToken(); - } catch (e, s) { - Logger.warning( - 'Could not get firebase token.', - error: e, - stackTrace: s, - ); - showErrorStatusMessage( - message: (l) => l.errorRollOutFailed(pushToken!.label), - details: (l) => l.noFbToken, - ); - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, - ), - ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - } - return false; - } + await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKeyFailed); + return null; } - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith(rolloutState: PushTokenRollOutState.sendRSAPublicKey), + } + + Future _checkNetworkPermission(PushToken token) async { + Logger.warning('Triggering network access permission for "${token.id}"'); + final success = await ioClient.triggerNetworkAccessPermission( + url: token.url!, + sslVerify: token.sslVerify, ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); + if (!success) { + Logger.warning('Network access permission failed.'); + await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKeyFailed); return false; } - if (!kIsWeb && Platform.isIOS) { - Logger.warning( - 'Triggering network access permission for token "${pushToken.id}"', - ); - if (!await ioClient.triggerNetworkAccessPermission( - url: pushToken.url!, - sslVerify: pushToken.sslVerify, - )) { - Logger.warning( - 'Network access permission for token "${pushToken.id}" failed.', - ); - _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, - ), - ); - return false; - } - Logger.warning( - 'Network access permission for token "${pushToken.id}" successful.', - ); - } - Response response; - try { - // TODO What to do with poll only tokens if google-services is used? + return true; + } - Logger.info('SSLVerify: ${pushToken.sslVerify}'); - response = await ioClient.doPost( - sslVerify: pushToken.sslVerify, - url: pushToken.url!, + Future _executeServerRollout(PushToken token, String? fbToken) async { + token = + await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKey) ?? + token; + try { + final response = await ioClient.doPost( + sslVerify: token.sslVerify, + url: token.url!, body: { - 'enrollment_credential': pushToken.enrollmentCredentials, - 'serial': pushToken.serial, + 'enrollment_credential': token.enrollmentCredentials, + 'serial': token.serial, 'fbtoken': fbToken ?? NoFirebaseUtils.NO_FIREBASE_TOKEN, 'pubkey': rsaUtils.serializeRSAPublicKeyPKCS8( - pushToken.rsaPublicTokenKey!, + token.rsaPublicTokenKey!, ), }, ); - } catch (e, s) { - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, - ), - ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); + + if (HttpStatusChecker.isError(response.statusCode)) { + _showPushRolloutStatus(response, token.label); + await _updateStatus( + token, + PushTokenRollOutState.sendRSAPublicKeyFailed, + ); return false; } - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => - localization.errorRollOutUnknownError(pushToken!.label), - ); - Logger.error('Roll out push token failed.', error: e, stackTrace: s); - return false; - } - if (HttpStatusChecker.isError(response.statusCode)) { - Logger.warning( - 'Post request on roll out failed.', - error: - 'Token: ${pushToken.serial}\nStatus code: ${response.statusCode},\nURL:${response.request?.url}\nBody: ${response.body}', + token = + await _updateToken( + token, + (p) => p.copyWith( + rolloutState: PushTokenRollOutState.parsingResponse, + fbToken: fbToken, + ), + ) ?? + token; + + final publicServerKey = await _parseRollOutResponse(response); + await _updateToken( + token, + (p) => p + .withPublicServerKey(publicServerKey) + .copyWith( + isRolledOut: true, + rolloutState: PushTokenRollOutState.rolloutComplete, + ), ); - _showPushRolloutStatus(response, pushToken.label); - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.sendRSAPublicKeyFailed, - ), + + checkNotificationPermission(); + return true; + } catch (e, s) { + Logger.error('Roll out failed.', error: e, stackTrace: s); + ref.read(statusMessageProvider.notifier).state = StatusMessage( + message: (loc) => loc.errorRollOutUnknownError(token.label), ); + await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKeyFailed); return false; } - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.parsingResponse, - fbToken: fbToken, - ), + } + + Future _updateStatus( + PushToken token, + PushTokenRollOutState state, + ) async { + final updated = await _updateToken( + token, + (p) => p.copyWith(rolloutState: state), ); - if (pushToken == null) { + if (updated == null) { Logger.warning('Tried to update a token that does not exist.'); - return false; - } - try { - RSAPublicKey publicServerKey = await _parseRollOutResponse(response); - pushToken = await _updateToken( - pushToken, - (p0) => p0.withPublicServerKey(publicServerKey), - ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - return false; - } - } on FormatException catch (e, s) { - Logger.error( - 'Error while parsing RSA public key.', - error: e, - stackTrace: s, - ); - if (pushToken == null) { - Logger.warning('Tried to update a token that does not exist.'); - return false; - } - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - rolloutState: PushTokenRollOutState.parsingResponseFailed, - ), - ); - return false; } - Logger.info('Roll out successful'); - pushToken = await _updateToken( - pushToken, - (p0) => p0.copyWith( - isRolledOut: true, - rolloutState: PushTokenRollOutState.rolloutComplete, - ), - ); - checkNotificationPermission(); - - return true; + return updated; } final _updateFbTokenMutex = Mutex(); diff --git a/lib/utils/rsa_utils.dart b/lib/utils/rsa_utils.dart index 8e0ec3dcc..0b8c231c0 100644 --- a/lib/utils/rsa_utils.dart +++ b/lib/utils/rsa_utils.dart @@ -182,7 +182,8 @@ class RsaUtils { /// Version ::= INTEGER { two-prime(0), multi(1) } /// (CONSTRAINED BY {-- version must be multi if otherPrimeInfos present --}) RSAPrivateKey deserializeRSAPrivateKeyPKCS1(String keyStr) { - ASN1Sequence asn1sequence = ASN1Parser(base64.decode(keyStr)).nextObject() as ASN1Sequence; + Uint8List keyBytes = base64.decode(keyStr); + ASN1Sequence asn1sequence = ASN1Parser(keyBytes).nextObject() as ASN1Sequence; BigInt modulus = (asn1sequence.elements[1] as ASN1Integer).valueAsBigInteger; BigInt exponent = (asn1sequence.elements[2] as ASN1Integer).valueAsBigInteger; BigInt p = (asn1sequence.elements[4] as ASN1Integer).valueAsBigInteger; diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index a7b1435a0..230a09737 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -18,6 +18,7 @@ limitations under the License. */ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -25,7 +26,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:gms_check/gms_check.dart'; import 'package:http/http.dart'; import 'package:image/image.dart' as img; import 'package:package_info_plus/package_info_plus.dart'; @@ -324,6 +324,3 @@ Image generateQrCodeImage({required String data}) { } return Image.memory(img.encodePng(image)); } - -bool get deviceHasFirebaseMessaging => - !kIsWeb && (GmsCheck().isGmsAvailable || Platform.isIOS); diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index d10643c7e..3867f9654 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -216,7 +216,7 @@ class _ImportStartPageState extends ConsumerState { isPrivacyIdeaToken: false, data: t.resultData.origin?.data ?? fileString, ), - resultHandlerType: const RequiredObjectValidator(), + resultHandlerType: RequiredObjectValidator(), ); }).toList(); @@ -251,7 +251,7 @@ class _ImportStartPageState extends ConsumerState { token: t.resultData, data: t.resultData.origin?.data ?? uri.toString(), ), - resultHandlerType: const RequiredObjectValidator(), + resultHandlerType: RequiredObjectValidator(), ); }).toList(); Logger.info("QR code scanned successfully"); @@ -341,7 +341,7 @@ class _ImportStartPageState extends ConsumerState { isPrivacyIdeaToken: false, data: _linkController.text, ), - resultHandlerType: const RequiredObjectValidator(), + resultHandlerType: RequiredObjectValidator(), ); }).toList(); if (!mounted) return null; diff --git a/lib/views/settings_view/settings_groups/settings_group_push_token/dialogs/settings_group_push_token_dialog.dart b/lib/views/settings_view/settings_groups/settings_group_push_token/dialogs/settings_group_push_token_dialog.dart index 3d4b134b9..e9a8ef87a 100644 --- a/lib/views/settings_view/settings_groups/settings_group_push_token/dialogs/settings_group_push_token_dialog.dart +++ b/lib/views/settings_view/settings_groups/settings_group_push_token/dialogs/settings_group_push_token_dialog.dart @@ -25,9 +25,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/riverpod_states/settings_state.dart'; import '../../../../../model/tokens/push_token.dart'; +import '../../../../../utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; -import '../../../../../utils/utils.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; import '../../../settings_view_widgets/update_firebase_token_dialog.dart'; @@ -40,6 +40,8 @@ class SettingsGroupPushTokenDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final deviceHasFirebaseMessaging = + ref.watch(hasFirebaseProvider).value ?? false; final settingsState = ref .watch(settingsProvider) .whenOrNull(data: (data) => data); diff --git a/test/tests_app_wrapper.dart b/test/tests_app_wrapper.dart index 3a30e830d..6d90a1077 100644 --- a/test/tests_app_wrapper.dart +++ b/test/tests_app_wrapper.dart @@ -1,8 +1,9 @@ -import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:mockito/annotations.dart'; import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; import 'package:privacyidea_authenticator/interfaces/repo/introduction_repository.dart'; @@ -11,12 +12,17 @@ import 'package:privacyidea_authenticator/interfaces/repo/settings_repository.da import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart'; import 'package:privacyidea_authenticator/interfaces/repo/token_folder_repository.dart'; import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/model/token_container.dart'; import 'package:privacyidea_authenticator/repo/secure_storage.dart'; import 'package:privacyidea_authenticator/utils/allow_screenshot_utils.dart'; import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:privacyidea_authenticator/utils/globals.dart'; import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; import 'package:privacyidea_authenticator/utils/push_provider.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -36,6 +42,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), ]) class TestsAppWrapper extends StatelessWidget { final Widget child; @@ -51,7 +61,18 @@ class TestsAppWrapper extends StatelessWidget { Widget build(BuildContext context) { return ProviderScope( overrides: overrides, - child: EasyDynamicThemeWidget(child: child), + child: MaterialApp( + navigatorKey: globalNavigatorKey, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('en')], + // Scaffold hier sorgt dafür, dass Dialoge und Snachbars funktionieren + home: Scaffold(body: child), + ), ); } } @@ -59,18 +80,15 @@ class TestsAppWrapper extends StatelessWidget { Future pumpUntilFindNWidgets( WidgetTester tester, Finder finder, - int n, - Duration timeOut, -) async { - final startTime = DateTime.now(); - while (true) { - await tester.pump(); - if (DateTime.now().difference(startTime) > timeOut) { - break; - } + int n, { + Duration timeout = const Duration(seconds: 5), +}) async { + final end = tester.binding.clock.now().add(timeout); + while (tester.binding.clock.now().isBefore(end)) { + await tester.pump(const Duration(milliseconds: 50)); if (tester.widgetList(finder).length == n) { - break; + return; } - await Future.delayed(const Duration(milliseconds: 500)); } + throw TestFailure("Could not find $n widgets for $finder within $timeout"); } diff --git a/test/tests_app_wrapper.mocks.dart b/test/tests_app_wrapper.mocks.dart index def0f7ee2..41d72e071 100644 --- a/test/tests_app_wrapper.mocks.dart +++ b/test/tests_app_wrapper.mocks.dart @@ -3,7 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i13; +import 'dart:async' as _i19; import 'dart:typed_data' as _i28; import 'package:firebase_core/firebase_core.dart' as _i31; @@ -11,53 +11,72 @@ import 'package:firebase_messaging/firebase_messaging.dart' as _i32; import 'package:flutter/foundation.dart' as _i35; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i11; import 'package:http/http.dart' as _i6; +import 'package:local_auth/src/local_auth.dart' as _i48; +import 'package:local_auth_android/local_auth_android.dart' as _i49; +import 'package:local_auth_darwin/local_auth_darwin.dart' as _i50; +import 'package:local_auth_windows/local_auth_windows.dart' as _i51; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i19; +import 'package:mockito/src/dummies.dart' as _i24; import 'package:pointycastle/export.dart' as _i7; import 'package:privacyidea_authenticator/api/interfaces/container_api.dart' - as _i25; + as _i13; import 'package:privacyidea_authenticator/interfaces/repo/introduction_repository.dart' - as _i17; + as _i22; import 'package:privacyidea_authenticator/interfaces/repo/push_request_repository.dart' - as _i20; + as _i25; import 'package:privacyidea_authenticator/interfaces/repo/settings_repository.dart' - as _i15; + as _i20; import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart' - as _i22; + as _i12; import 'package:privacyidea_authenticator/interfaces/repo/token_folder_repository.dart' - as _i16; + as _i21; import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart' - as _i12; + as _i17; import 'package:privacyidea_authenticator/model/api_results/pi_server_results/pi_server_result_value.dart' as _i5; +import 'package:privacyidea_authenticator/model/container_policies.dart' + as _i44; +import 'package:privacyidea_authenticator/model/enums/algorithms.dart' as _i41; import 'package:privacyidea_authenticator/model/enums/ec_key_algorithm.dart' as _i30; +import 'package:privacyidea_authenticator/model/enums/rollout_state.dart' + as _i42; +import 'package:privacyidea_authenticator/model/enums/sync_state.dart' as _i43; +import 'package:privacyidea_authenticator/model/processor_result.dart' as _i39; import 'package:privacyidea_authenticator/model/push_request/push_request.dart' - as _i21; + as _i26; import 'package:privacyidea_authenticator/model/riverpod_states/introduction_state.dart' - as _i18; + as _i23; import 'package:privacyidea_authenticator/model/riverpod_states/push_request_state.dart' as _i4; import 'package:privacyidea_authenticator/model/riverpod_states/settings_state.dart' as _i2; import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart' - as _i23; + as _i27; import 'package:privacyidea_authenticator/model/riverpod_states/token_folder_state.dart' as _i3; import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart' - as _i27; -import 'package:privacyidea_authenticator/model/token_container.dart' as _i24; + as _i18; +import 'package:privacyidea_authenticator/model/token_container.dart' as _i15; +import 'package:privacyidea_authenticator/model/tokens/hotp_token.dart' as _i46; +import 'package:privacyidea_authenticator/model/tokens/otp_token.dart' as _i47; import 'package:privacyidea_authenticator/model/tokens/push_token.dart' as _i29; -import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i14; +import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i16; import 'package:privacyidea_authenticator/repo/secure_storage.dart' as _i36; import 'package:privacyidea_authenticator/utils/allow_screenshot_utils.dart' as _i34; -import 'package:privacyidea_authenticator/utils/ecc_utils.dart' as _i26; +import 'package:privacyidea_authenticator/utils/ecc_utils.dart' as _i14; import 'package:privacyidea_authenticator/utils/firebase_utils.dart' as _i8; import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart' as _i9; import 'package:privacyidea_authenticator/utils/push_provider.dart' as _i33; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart' + as _i37; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart' + as _i45; import 'package:privacyidea_authenticator/utils/rsa_utils.dart' as _i10; +import 'package:riverpod_annotation/riverpod_annotation.dart' as _i38; +import 'package:state_notifier/state_notifier.dart' as _i40; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -200,228 +219,279 @@ class _FakeFlutterSecureStorage_21 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeTokenContainerRepository_22 extends _i1.SmartFake + implements _i12.TokenContainerRepository { + _FakeTokenContainerRepository_22(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTokenContainerApi_23 extends _i1.SmartFake + implements _i13.TokenContainerApi { + _FakeTokenContainerApi_23(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeEccUtils_24 extends _i1.SmartFake implements _i14.EccUtils { + _FakeEccUtils_24(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeDateTime_25 extends _i1.SmartFake implements DateTime { + _FakeDateTime_25(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeUri_26 extends _i1.SmartFake implements Uri { + _FakeUri_26(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _Fake$TokenContainerFinalizedCopyWith_27<$Res> extends _i1.SmartFake + implements _i15.$TokenContainerFinalizedCopyWith<$Res> { + _Fake$TokenContainerFinalizedCopyWith_27( + Object parent, + Invocation parentInvocation, + ) : super(parent, parentInvocation); +} + +class _FakeToken_28 extends _i1.SmartFake implements _i16.Token { + _FakeToken_28(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTokenRepository_29 extends _i1.SmartFake + implements _i17.TokenRepository { + _FakeTokenRepository_29(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTokenState_30 extends _i1.SmartFake implements _i18.TokenState { + _FakeTokenState_30(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [TokenRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockTokenRepository extends _i1.Mock implements _i12.TokenRepository { +class MockTokenRepository extends _i1.Mock implements _i17.TokenRepository { @override - _i13.Future<_i14.Token?> loadToken(String? id) => + _i19.Future<_i16.Token?> loadToken(String? id) => (super.noSuchMethod( Invocation.method(#loadToken, [id]), - returnValue: _i13.Future<_i14.Token?>.value(), - returnValueForMissingStub: _i13.Future<_i14.Token?>.value(), + returnValue: _i19.Future<_i16.Token?>.value(), + returnValueForMissingStub: _i19.Future<_i16.Token?>.value(), ) - as _i13.Future<_i14.Token?>); + as _i19.Future<_i16.Token?>); @override - _i13.Future> loadTokens() => + _i19.Future> loadTokens() => (super.noSuchMethod( Invocation.method(#loadTokens, []), - returnValue: _i13.Future>.value(<_i14.Token>[]), - returnValueForMissingStub: _i13.Future>.value( - <_i14.Token>[], + returnValue: _i19.Future>.value(<_i16.Token>[]), + returnValueForMissingStub: _i19.Future>.value( + <_i16.Token>[], ), ) - as _i13.Future>); + as _i19.Future>); @override - _i13.Future saveOrReplaceToken(_i14.Token? token) => + _i19.Future saveOrReplaceToken(_i16.Token? token) => (super.noSuchMethod( Invocation.method(#saveOrReplaceToken, [token]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future> saveOrReplaceTokens( + _i19.Future> saveOrReplaceTokens( List? tokens, ) => (super.noSuchMethod( Invocation.method(#saveOrReplaceTokens, [tokens]), - returnValue: _i13.Future>.value([]), - returnValueForMissingStub: _i13.Future>.value([]), + returnValue: _i19.Future>.value([]), + returnValueForMissingStub: _i19.Future>.value([]), ) - as _i13.Future>); + as _i19.Future>); @override - _i13.Future deleteToken(_i14.Token? token) => + _i19.Future deleteToken(_i16.Token? token) => (super.noSuchMethod( Invocation.method(#deleteToken, [token]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future> deleteTokens(List? tokens) => + _i19.Future> deleteTokens(List? tokens) => (super.noSuchMethod( Invocation.method(#deleteTokens, [tokens]), - returnValue: _i13.Future>.value([]), - returnValueForMissingStub: _i13.Future>.value([]), + returnValue: _i19.Future>.value([]), + returnValueForMissingStub: _i19.Future>.value([]), ) - as _i13.Future>); + as _i19.Future>); } /// A class which mocks [SettingsRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockSettingsRepository extends _i1.Mock - implements _i15.SettingsRepository { + implements _i20.SettingsRepository { @override - _i13.Future saveSettings(_i2.SettingsState? settings) => + _i19.Future saveSettings(_i2.SettingsState? settings) => (super.noSuchMethod( Invocation.method(#saveSettings, [settings]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i2.SettingsState> loadSettings() => + _i19.Future<_i2.SettingsState> loadSettings() => (super.noSuchMethod( Invocation.method(#loadSettings, []), - returnValue: _i13.Future<_i2.SettingsState>.value( + returnValue: _i19.Future<_i2.SettingsState>.value( _FakeSettingsState_0(this, Invocation.method(#loadSettings, [])), ), - returnValueForMissingStub: _i13.Future<_i2.SettingsState>.value( + returnValueForMissingStub: _i19.Future<_i2.SettingsState>.value( _FakeSettingsState_0(this, Invocation.method(#loadSettings, [])), ), ) - as _i13.Future<_i2.SettingsState>); + as _i19.Future<_i2.SettingsState>); } /// A class which mocks [TokenFolderRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockTokenFolderRepository extends _i1.Mock - implements _i16.TokenFolderRepository { + implements _i21.TokenFolderRepository { @override - _i13.Future saveState(_i3.TokenFolderState? state) => + _i19.Future saveState(_i3.TokenFolderState? state) => (super.noSuchMethod( Invocation.method(#saveState, [state]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i3.TokenFolderState> loadState() => + _i19.Future<_i3.TokenFolderState> loadState() => (super.noSuchMethod( Invocation.method(#loadState, []), - returnValue: _i13.Future<_i3.TokenFolderState>.value( + returnValue: _i19.Future<_i3.TokenFolderState>.value( _FakeTokenFolderState_1(this, Invocation.method(#loadState, [])), ), - returnValueForMissingStub: _i13.Future<_i3.TokenFolderState>.value( + returnValueForMissingStub: _i19.Future<_i3.TokenFolderState>.value( _FakeTokenFolderState_1(this, Invocation.method(#loadState, [])), ), ) - as _i13.Future<_i3.TokenFolderState>); + as _i19.Future<_i3.TokenFolderState>); } /// A class which mocks [IntroductionRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockIntroductionRepository extends _i1.Mock - implements _i17.IntroductionRepository { + implements _i22.IntroductionRepository { @override - _i13.Future saveCompletedIntroductions( - _i18.IntroductionState? introductions, + _i19.Future saveCompletedIntroductions( + _i23.IntroductionState? introductions, ) => (super.noSuchMethod( Invocation.method(#saveCompletedIntroductions, [introductions]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i18.IntroductionState> loadCompletedIntroductions() => + _i19.Future<_i23.IntroductionState> loadCompletedIntroductions() => (super.noSuchMethod( Invocation.method(#loadCompletedIntroductions, []), - returnValue: _i13.Future<_i18.IntroductionState>.value( - _i19.dummyValue<_i18.IntroductionState>( + returnValue: _i19.Future<_i23.IntroductionState>.value( + _i24.dummyValue<_i23.IntroductionState>( this, Invocation.method(#loadCompletedIntroductions, []), ), ), returnValueForMissingStub: - _i13.Future<_i18.IntroductionState>.value( - _i19.dummyValue<_i18.IntroductionState>( + _i19.Future<_i23.IntroductionState>.value( + _i24.dummyValue<_i23.IntroductionState>( this, Invocation.method(#loadCompletedIntroductions, []), ), ), ) - as _i13.Future<_i18.IntroductionState>); + as _i19.Future<_i23.IntroductionState>); } /// A class which mocks [PushRequestRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockPushRequestRepository extends _i1.Mock - implements _i20.PushRequestRepository { + implements _i25.PushRequestRepository { @override - _i13.Future<_i4.PushRequestState> loadState() => + _i19.Future<_i4.PushRequestState> loadState() => (super.noSuchMethod( Invocation.method(#loadState, []), - returnValue: _i13.Future<_i4.PushRequestState>.value( + returnValue: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2(this, Invocation.method(#loadState, [])), ), - returnValueForMissingStub: _i13.Future<_i4.PushRequestState>.value( + returnValueForMissingStub: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2(this, Invocation.method(#loadState, [])), ), ) - as _i13.Future<_i4.PushRequestState>); + as _i19.Future<_i4.PushRequestState>); @override - _i13.Future saveState(_i4.PushRequestState? pushRequestState) => + _i19.Future saveState(_i4.PushRequestState? pushRequestState) => (super.noSuchMethod( Invocation.method(#saveState, [pushRequestState]), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future clearState() => + _i19.Future clearState() => (super.noSuchMethod( Invocation.method(#clearState, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i4.PushRequestState> addRequest( - _i21.PushRequest? pushRequest, { + _i19.Future<_i4.PushRequestState> addRequest( + _i26.PushRequest? pushRequest, { _i4.PushRequestState? state, }) => (super.noSuchMethod( Invocation.method(#addRequest, [pushRequest], {#state: state}), - returnValue: _i13.Future<_i4.PushRequestState>.value( + returnValue: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2( this, Invocation.method(#addRequest, [pushRequest], {#state: state}), ), ), - returnValueForMissingStub: _i13.Future<_i4.PushRequestState>.value( + returnValueForMissingStub: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2( this, Invocation.method(#addRequest, [pushRequest], {#state: state}), ), ), ) - as _i13.Future<_i4.PushRequestState>); + as _i19.Future<_i4.PushRequestState>); @override - _i13.Future<_i4.PushRequestState> removeRequest( - _i21.PushRequest? pushRequest, { + _i19.Future<_i4.PushRequestState> removeRequest( + _i26.PushRequest? pushRequest, { _i4.PushRequestState? state, }) => (super.noSuchMethod( Invocation.method(#removeRequest, [pushRequest], {#state: state}), - returnValue: _i13.Future<_i4.PushRequestState>.value( + returnValue: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2( this, Invocation.method( @@ -431,7 +501,7 @@ class MockPushRequestRepository extends _i1.Mock ), ), ), - returnValueForMissingStub: _i13.Future<_i4.PushRequestState>.value( + returnValueForMissingStub: _i19.Future<_i4.PushRequestState>.value( _FakePushRequestState_2( this, Invocation.method( @@ -442,170 +512,170 @@ class MockPushRequestRepository extends _i1.Mock ), ), ) - as _i13.Future<_i4.PushRequestState>); + as _i19.Future<_i4.PushRequestState>); } /// A class which mocks [TokenContainerRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockTokenContainerRepository extends _i1.Mock - implements _i22.TokenContainerRepository { + implements _i12.TokenContainerRepository { @override - _i13.Future<_i23.TokenContainerState> loadContainerState() => + _i19.Future<_i27.TokenContainerState> loadContainerState() => (super.noSuchMethod( Invocation.method(#loadContainerState, []), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#loadContainerState, []), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#loadContainerState, []), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); @override - _i13.Future<_i23.TokenContainerState> saveContainerState( - _i23.TokenContainerState? containerState, + _i19.Future<_i27.TokenContainerState> saveContainerState( + _i27.TokenContainerState? containerState, ) => (super.noSuchMethod( Invocation.method(#saveContainerState, [containerState]), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainerState, [containerState]), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainerState, [containerState]), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); @override - _i13.Future<_i23.TokenContainerState> saveContainerList( - List<_i24.TokenContainer>? containerList, + _i19.Future<_i27.TokenContainerState> saveContainerList( + List<_i15.TokenContainer>? containerList, ) => (super.noSuchMethod( Invocation.method(#saveContainerList, [containerList]), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainerList, [containerList]), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainerList, [containerList]), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); @override - _i13.Future<_i23.TokenContainerState> deleteContainer(String? serial) => + _i19.Future<_i27.TokenContainerState> deleteContainer(String? serial) => (super.noSuchMethod( Invocation.method(#deleteContainer, [serial]), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#deleteContainer, [serial]), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#deleteContainer, [serial]), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); @override - _i13.Future<_i23.TokenContainerState> deleteAllContainer() => + _i19.Future<_i27.TokenContainerState> deleteAllContainer() => (super.noSuchMethod( Invocation.method(#deleteAllContainer, []), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#deleteAllContainer, []), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#deleteAllContainer, []), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); @override - _i13.Future<_i24.TokenContainer?> loadContainer(String? serial) => + _i19.Future<_i15.TokenContainer?> loadContainer(String? serial) => (super.noSuchMethod( Invocation.method(#loadContainer, [serial]), - returnValue: _i13.Future<_i24.TokenContainer?>.value(), + returnValue: _i19.Future<_i15.TokenContainer?>.value(), returnValueForMissingStub: - _i13.Future<_i24.TokenContainer?>.value(), + _i19.Future<_i15.TokenContainer?>.value(), ) - as _i13.Future<_i24.TokenContainer?>); + as _i19.Future<_i15.TokenContainer?>); @override - _i13.Future<_i23.TokenContainerState> saveContainer( - _i24.TokenContainer? container, + _i19.Future<_i27.TokenContainerState> saveContainer( + _i15.TokenContainer? container, ) => (super.noSuchMethod( Invocation.method(#saveContainer, [container]), - returnValue: _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainer, [container]), ), ), returnValueForMissingStub: - _i13.Future<_i23.TokenContainerState>.value( - _i19.dummyValue<_i23.TokenContainerState>( + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( this, Invocation.method(#saveContainer, [container]), ), ), ) - as _i13.Future<_i23.TokenContainerState>); + as _i19.Future<_i27.TokenContainerState>); } /// A class which mocks [TokenContainerApi]. /// /// See the documentation for Mockito's code generation for more information. -class MockTokenContainerApi extends _i1.Mock implements _i25.TokenContainerApi { +class MockTokenContainerApi extends _i1.Mock implements _i13.TokenContainerApi { @override - _i13.Future<_i5.ContainerFinalizationResponse> finalizeContainer( - _i24.TokenContainerUnfinalized? container, - _i26.EccUtils? eccUtils, + _i19.Future<_i5.ContainerFinalizationResponse> finalizeContainer( + _i15.TokenContainerUnfinalized? container, + _i14.EccUtils? eccUtils, ) => (super.noSuchMethod( Invocation.method(#finalizeContainer, [container, eccUtils]), - returnValue: _i13.Future<_i5.ContainerFinalizationResponse>.value( + returnValue: _i19.Future<_i5.ContainerFinalizationResponse>.value( _FakeContainerFinalizationResponse_3( this, Invocation.method(#finalizeContainer, [container, eccUtils]), ), ), returnValueForMissingStub: - _i13.Future<_i5.ContainerFinalizationResponse>.value( + _i19.Future<_i5.ContainerFinalizationResponse>.value( _FakeContainerFinalizationResponse_3( this, Invocation.method(#finalizeContainer, [ @@ -615,33 +685,33 @@ class MockTokenContainerApi extends _i1.Mock implements _i25.TokenContainerApi { ), ), ) - as _i13.Future<_i5.ContainerFinalizationResponse>); + as _i19.Future<_i5.ContainerFinalizationResponse>); @override - _i13.Future<_i5.TransferQrData> getRolloverQrData( - _i24.TokenContainerFinalized? container, + _i19.Future<_i5.TransferQrData> getRolloverQrData( + _i15.TokenContainerFinalized? container, ) => (super.noSuchMethod( Invocation.method(#getRolloverQrData, [container]), - returnValue: _i13.Future<_i5.TransferQrData>.value( + returnValue: _i19.Future<_i5.TransferQrData>.value( _FakeTransferQrData_4( this, Invocation.method(#getRolloverQrData, [container]), ), ), - returnValueForMissingStub: _i13.Future<_i5.TransferQrData>.value( + returnValueForMissingStub: _i19.Future<_i5.TransferQrData>.value( _FakeTransferQrData_4( this, Invocation.method(#getRolloverQrData, [container]), ), ), ) - as _i13.Future<_i5.TransferQrData>); + as _i19.Future<_i5.TransferQrData>); @override - _i13.Future<_i25.ContainerSyncUpdates?> sync( - _i24.TokenContainerFinalized? container, - _i27.TokenState? tokenState, { + _i19.Future<_i13.ContainerSyncUpdates?> sync( + _i15.TokenContainerFinalized? container, + _i18.TokenState? tokenState, { bool? isInitSync, }) => (super.noSuchMethod( @@ -650,33 +720,33 @@ class MockTokenContainerApi extends _i1.Mock implements _i25.TokenContainerApi { [container, tokenState], {#isInitSync: isInitSync}, ), - returnValue: _i13.Future<_i25.ContainerSyncUpdates?>.value(), + returnValue: _i19.Future<_i13.ContainerSyncUpdates?>.value(), returnValueForMissingStub: - _i13.Future<_i25.ContainerSyncUpdates?>.value(), + _i19.Future<_i13.ContainerSyncUpdates?>.value(), ) - as _i13.Future<_i25.ContainerSyncUpdates?>); + as _i19.Future<_i13.ContainerSyncUpdates?>); @override - _i13.Future<_i5.UnregisterContainerResult> unregister( - _i24.TokenContainerFinalized? container, + _i19.Future<_i5.UnregisterContainerResult> unregister( + _i15.TokenContainerFinalized? container, ) => (super.noSuchMethod( Invocation.method(#unregister, [container]), - returnValue: _i13.Future<_i5.UnregisterContainerResult>.value( + returnValue: _i19.Future<_i5.UnregisterContainerResult>.value( _FakeUnregisterContainerResult_5( this, Invocation.method(#unregister, [container]), ), ), returnValueForMissingStub: - _i13.Future<_i5.UnregisterContainerResult>.value( + _i19.Future<_i5.UnregisterContainerResult>.value( _FakeUnregisterContainerResult_5( this, Invocation.method(#unregister, [container]), ), ), ) - as _i13.Future<_i5.UnregisterContainerResult>); + as _i19.Future<_i5.UnregisterContainerResult>); } /// A class which mocks [PrivacyideaIOClient]. @@ -685,7 +755,7 @@ class MockTokenContainerApi extends _i1.Mock implements _i25.TokenContainerApi { class MockPrivacyideaIOClient extends _i1.Mock implements _i9.PrivacyideaIOClient { @override - _i13.Future triggerNetworkAccessPermission({ + _i19.Future triggerNetworkAccessPermission({ required Uri? url, bool? sslVerify = true, bool? isRetry = false, @@ -696,13 +766,13 @@ class MockPrivacyideaIOClient extends _i1.Mock #sslVerify: sslVerify, #isRetry: isRetry, }), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i6.Response> doPost({ + _i19.Future<_i6.Response> doPost({ required Uri? url, required Map? body, bool? sslVerify = true, @@ -713,7 +783,7 @@ class MockPrivacyideaIOClient extends _i1.Mock #body: body, #sslVerify: sslVerify, }), - returnValue: _i13.Future<_i6.Response>.value( + returnValue: _i19.Future<_i6.Response>.value( _FakeResponse_6( this, Invocation.method(#doPost, [], { @@ -723,7 +793,7 @@ class MockPrivacyideaIOClient extends _i1.Mock }), ), ), - returnValueForMissingStub: _i13.Future<_i6.Response>.value( + returnValueForMissingStub: _i19.Future<_i6.Response>.value( _FakeResponse_6( this, Invocation.method(#doPost, [], { @@ -734,10 +804,10 @@ class MockPrivacyideaIOClient extends _i1.Mock ), ), ) - as _i13.Future<_i6.Response>); + as _i19.Future<_i6.Response>); @override - _i13.Future<_i6.Response> doGet({ + _i19.Future<_i6.Response> doGet({ required Uri? url, required Map? parameters, bool? sslVerify = true, @@ -748,7 +818,7 @@ class MockPrivacyideaIOClient extends _i1.Mock #parameters: parameters, #sslVerify: sslVerify, }), - returnValue: _i13.Future<_i6.Response>.value( + returnValue: _i19.Future<_i6.Response>.value( _FakeResponse_6( this, Invocation.method(#doGet, [], { @@ -758,7 +828,7 @@ class MockPrivacyideaIOClient extends _i1.Mock }), ), ), - returnValueForMissingStub: _i13.Future<_i6.Response>.value( + returnValueForMissingStub: _i19.Future<_i6.Response>.value( _FakeResponse_6( this, Invocation.method(#doGet, [], { @@ -769,7 +839,7 @@ class MockPrivacyideaIOClient extends _i1.Mock ), ), ) - as _i13.Future<_i6.Response>); + as _i19.Future<_i6.Response>); } /// A class which mocks [RsaUtils]. @@ -795,11 +865,11 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { String serializeRSAPublicKeyPKCS1(_i7.RSAPublicKey? publicKey) => (super.noSuchMethod( Invocation.method(#serializeRSAPublicKeyPKCS1, [publicKey]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#serializeRSAPublicKeyPKCS1, [publicKey]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#serializeRSAPublicKeyPKCS1, [publicKey]), ), @@ -825,11 +895,11 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { String serializeRSAPublicKeyPKCS8(_i7.RSAPublicKey? key) => (super.noSuchMethod( Invocation.method(#serializeRSAPublicKeyPKCS8, [key]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#serializeRSAPublicKeyPKCS8, [key]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#serializeRSAPublicKeyPKCS8, [key]), ), @@ -840,11 +910,11 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { String serializeRSAPrivateKeyPKCS1(_i7.RSAPrivateKey? key) => (super.noSuchMethod( Invocation.method(#serializeRSAPrivateKeyPKCS1, [key]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#serializeRSAPrivateKeyPKCS1, [key]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#serializeRSAPrivateKeyPKCS1, [key]), ), @@ -884,24 +954,24 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { as bool); @override - _i13.Future trySignWithToken( + _i19.Future trySignWithToken( _i29.PushToken? token, String? message, ) => (super.noSuchMethod( Invocation.method(#trySignWithToken, [token, message]), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<_i7.AsymmetricKeyPair<_i7.RSAPublicKey, _i7.RSAPrivateKey>> + _i19.Future<_i7.AsymmetricKeyPair<_i7.RSAPublicKey, _i7.RSAPrivateKey>> generateRSAKeyPair() => (super.noSuchMethod( Invocation.method(#generateRSAKeyPair, []), returnValue: - _i13.Future< + _i19.Future< _i7.AsymmetricKeyPair<_i7.RSAPublicKey, _i7.RSAPrivateKey> >.value( _FakeAsymmetricKeyPair_9<_i7.RSAPublicKey, _i7.RSAPrivateKey>( @@ -910,7 +980,7 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { ), ), returnValueForMissingStub: - _i13.Future< + _i19.Future< _i7.AsymmetricKeyPair<_i7.RSAPublicKey, _i7.RSAPrivateKey> >.value( _FakeAsymmetricKeyPair_9<_i7.RSAPublicKey, _i7.RSAPrivateKey>( @@ -919,7 +989,7 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { ), ), ) - as _i13.Future< + as _i19.Future< _i7.AsymmetricKeyPair<_i7.RSAPublicKey, _i7.RSAPrivateKey> >); @@ -930,14 +1000,14 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { ) => (super.noSuchMethod( Invocation.method(#createBase32Signature, [privateKey, dataToSign]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#createBase32Signature, [ privateKey, dataToSign, ]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#createBase32Signature, [ privateKey, @@ -963,16 +1033,16 @@ class MockRsaUtils extends _i1.Mock implements _i10.RsaUtils { /// A class which mocks [EccUtils]. /// /// See the documentation for Mockito's code generation for more information. -class MockEccUtils extends _i1.Mock implements _i26.EccUtils { +class MockEccUtils extends _i1.Mock implements _i14.EccUtils { @override String serializeECPublicKey(_i7.ECPublicKey? publicKey) => (super.noSuchMethod( Invocation.method(#serializeECPublicKey, [publicKey]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#serializeECPublicKey, [publicKey]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#serializeECPublicKey, [publicKey]), ), @@ -998,11 +1068,11 @@ class MockEccUtils extends _i1.Mock implements _i26.EccUtils { String serializeECPrivateKey(_i7.ECPrivateKey? ecPrivateKey) => (super.noSuchMethod( Invocation.method(#serializeECPrivateKey, [ecPrivateKey]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#serializeECPrivateKey, [ecPrivateKey]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#serializeECPrivateKey, [ecPrivateKey]), ), @@ -1028,11 +1098,11 @@ class MockEccUtils extends _i1.Mock implements _i26.EccUtils { String signWithPrivateKey(_i7.ECPrivateKey? privateKey, String? message) => (super.noSuchMethod( Invocation.method(#signWithPrivateKey, [privateKey, message]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#signWithPrivateKey, [privateKey, message]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#signWithPrivateKey, [privateKey, message]), ), @@ -1111,18 +1181,18 @@ class MockFirebaseUtils extends _i1.Mock implements _i8.FirebaseUtils { ); @override - _i13.Future<_i31.FirebaseApp?> initializeApp() => + _i19.Future<_i31.FirebaseApp?> initializeApp() => (super.noSuchMethod( Invocation.method(#initializeApp, []), - returnValue: _i13.Future<_i31.FirebaseApp?>.value(), - returnValueForMissingStub: _i13.Future<_i31.FirebaseApp?>.value(), + returnValue: _i19.Future<_i31.FirebaseApp?>.value(), + returnValueForMissingStub: _i19.Future<_i31.FirebaseApp?>.value(), ) - as _i13.Future<_i31.FirebaseApp?>); + as _i19.Future<_i31.FirebaseApp?>); @override - _i13.Future setupHandler({ - required _i13.Future Function(_i32.RemoteMessage)? foregroundHandler, - required _i13.Future Function(_i32.RemoteMessage)? backgroundHandler, + _i19.Future setupHandler({ + required _i19.Future Function(_i32.RemoteMessage)? foregroundHandler, + required _i19.Future Function(_i32.RemoteMessage)? backgroundHandler, required dynamic Function({String? firebaseToken})? updateFirebaseToken, }) => (super.noSuchMethod( @@ -1131,64 +1201,64 @@ class MockFirebaseUtils extends _i1.Mock implements _i8.FirebaseUtils { #backgroundHandler: backgroundHandler, #updateFirebaseToken: updateFirebaseToken, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future getFBToken() => + _i19.Future getFBToken() => (super.noSuchMethod( Invocation.method(#getFBToken, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future deleteFirebaseToken() => + _i19.Future deleteFirebaseToken() => (super.noSuchMethod( Invocation.method(#deleteFirebaseToken, []), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future setCurrentFirebaseToken(String? str) => + _i19.Future setCurrentFirebaseToken(String? str) => (super.noSuchMethod( Invocation.method(#setCurrentFirebaseToken, [str]), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future getCurrentFirebaseToken() => + _i19.Future getCurrentFirebaseToken() => (super.noSuchMethod( Invocation.method(#getCurrentFirebaseToken, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future setNewFirebaseToken(String? str) => + _i19.Future setNewFirebaseToken(String? str) => (super.noSuchMethod( Invocation.method(#setNewFirebaseToken, [str]), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future getNewFirebaseToken() => + _i19.Future getNewFirebaseToken() => (super.noSuchMethod( Invocation.method(#getNewFirebaseToken, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); } /// A class which mocks [PushProvider]. @@ -1253,13 +1323,13 @@ class MockPushProvider extends _i1.Mock implements _i33.PushProvider { ); @override - _i13.Future initFirebase() => + _i19.Future initFirebase() => (super.noSuchMethod( Invocation.method(#initFirebase, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override void setPollingEnabled(bool? enablePolling) => super.noSuchMethod( @@ -1268,18 +1338,18 @@ class MockPushProvider extends _i1.Mock implements _i33.PushProvider { ); @override - _i13.Future pollForChallenges({required bool? isManually}) => + _i19.Future pollForChallenges({required bool? isManually}) => (super.noSuchMethod( Invocation.method(#pollForChallenges, [], { #isManually: isManually, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future pollForChallenge( + _i19.Future pollForChallenge( _i29.PushToken? token, { bool? isManually = true, }) => @@ -1289,38 +1359,38 @@ class MockPushProvider extends _i1.Mock implements _i33.PushProvider { [token], {#isManually: isManually}, ), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?> + _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?> updateAllFirebaseTokens({String? firebaseToken}) => (super.noSuchMethod( Invocation.method(#updateAllFirebaseTokens, [], { #firebaseToken: firebaseToken, }), returnValue: - _i13.Future< + _i19.Future< (List<_i29.PushToken>, List<_i29.PushToken>)? >.value(), returnValueForMissingStub: - _i13.Future< + _i19.Future< (List<_i29.PushToken>, List<_i29.PushToken>)? >.value(), ) - as _i13.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?>); + as _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?>); @override - void unsubscribe(void Function(_i21.PushRequest)? newRequest) => + void unsubscribe(void Function(_i26.PushRequest)? newRequest) => super.noSuchMethod( Invocation.method(#unsubscribe, [newRequest]), returnValueForMissingStub: null, ); @override - void subscribe(void Function(_i21.PushRequest)? newRequest) => + void subscribe(void Function(_i26.PushRequest)? newRequest) => super.noSuchMethod( Invocation.method(#subscribe, [newRequest]), returnValueForMissingStub: null, @@ -1333,31 +1403,31 @@ class MockPushProvider extends _i1.Mock implements _i33.PushProvider { class MockAllowScreenshotUtils extends _i1.Mock implements _i34.AllowScreenshotUtils { @override - _i13.Future allowScreenshots() => + _i19.Future allowScreenshots() => (super.noSuchMethod( Invocation.method(#allowScreenshots, []), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future disallowScreenshots() => + _i19.Future disallowScreenshots() => (super.noSuchMethod( Invocation.method(#disallowScreenshots, []), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future toggleAllowScreenshots(bool? oldState) => + _i19.Future toggleAllowScreenshots(bool? oldState) => (super.noSuchMethod( Invocation.method(#toggleAllowScreenshots, [oldState]), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); } /// A class which mocks [FlutterSecureStorage]. @@ -1497,7 +1567,7 @@ class MockFlutterSecureStorage extends _i1.Mock ); @override - _i13.Future write({ + _i19.Future write({ required String? key, required String? value, _i11.AppleOptions? iOptions, @@ -1518,13 +1588,13 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future read({ + _i19.Future read({ required String? key, _i11.AppleOptions? iOptions, _i11.AndroidOptions? aOptions, @@ -1543,13 +1613,13 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future containsKey({ + _i19.Future containsKey({ required String? key, _i11.AppleOptions? iOptions, _i11.AndroidOptions? aOptions, @@ -1568,13 +1638,13 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future.value(false), - returnValueForMissingStub: _i13.Future.value(false), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future delete({ + _i19.Future delete({ required String? key, _i11.AppleOptions? iOptions, _i11.AndroidOptions? aOptions, @@ -1593,13 +1663,13 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future> readAll({ + _i19.Future> readAll({ _i11.AppleOptions? iOptions, _i11.AndroidOptions? aOptions, _i11.LinuxOptions? lOptions, @@ -1616,17 +1686,17 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future>.value( + returnValue: _i19.Future>.value( {}, ), - returnValueForMissingStub: _i13.Future>.value( + returnValueForMissingStub: _i19.Future>.value( {}, ), ) - as _i13.Future>); + as _i19.Future>); @override - _i13.Future deleteAll({ + _i19.Future deleteAll({ _i11.AppleOptions? iOptions, _i11.AndroidOptions? aOptions, _i11.LinuxOptions? lOptions, @@ -1643,19 +1713,19 @@ class MockFlutterSecureStorage extends _i1.Mock #mOptions: mOptions, #wOptions: wOptions, }), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future isCupertinoProtectedDataAvailable() => + _i19.Future isCupertinoProtectedDataAvailable() => (super.noSuchMethod( Invocation.method(#isCupertinoProtectedDataAvailable, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); } /// A class which mocks [SecureStorage]. @@ -1681,11 +1751,11 @@ class MockSecureStorage extends _i1.Mock implements _i36.SecureStorage { String get storagePrefix => (super.noSuchMethod( Invocation.getter(#storagePrefix), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.getter(#storagePrefix), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.getter(#storagePrefix), ), @@ -1696,11 +1766,11 @@ class MockSecureStorage extends _i1.Mock implements _i36.SecureStorage { String get seperator => (super.noSuchMethod( Invocation.getter(#seperator), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.getter(#seperator), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.getter(#seperator), ), @@ -1711,11 +1781,11 @@ class MockSecureStorage extends _i1.Mock implements _i36.SecureStorage { String getFullKey(String? key) => (super.noSuchMethod( Invocation.method(#getFullKey, [key]), - returnValue: _i19.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.method(#getFullKey, [key]), ), - returnValueForMissingStub: _i19.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.method(#getFullKey, [key]), ), @@ -1723,51 +1793,1383 @@ class MockSecureStorage extends _i1.Mock implements _i36.SecureStorage { as String); @override - _i13.Future write({required String? key, required String? value}) => + _i19.Future write({required String? key, required String? value}) => (super.noSuchMethod( Invocation.method(#write, [], {#key: key, #value: value}), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future read({required String? key}) => + _i19.Future read({required String? key}) => (super.noSuchMethod( Invocation.method(#read, [], {#key: key}), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future> readAll() => + _i19.Future> readAll() => (super.noSuchMethod( Invocation.method(#readAll, []), - returnValue: _i13.Future>.value( + returnValue: _i19.Future>.value( {}, ), - returnValueForMissingStub: _i13.Future>.value( + returnValueForMissingStub: _i19.Future>.value( {}, ), ) - as _i13.Future>); + as _i19.Future>); @override - _i13.Future delete({required String? key}) => + _i19.Future delete({required String? key}) => (super.noSuchMethod( Invocation.method(#delete, [], {#key: key}), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), ) - as _i13.Future); + as _i19.Future); @override - _i13.Future deleteAll() => + _i19.Future deleteAll() => (super.noSuchMethod( Invocation.method(#deleteAll, []), - returnValue: _i13.Future.value(), - returnValueForMissingStub: _i13.Future.value(), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); +} + +/// A class which mocks [TokenContainerNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTokenContainerNotifier extends _i1.Mock + implements _i37.TokenContainerNotifier { + @override + _i12.TokenContainerRepository get repo => + (super.noSuchMethod( + Invocation.getter(#repo), + returnValue: _FakeTokenContainerRepository_22( + this, + Invocation.getter(#repo), + ), + returnValueForMissingStub: _FakeTokenContainerRepository_22( + this, + Invocation.getter(#repo), + ), + ) + as _i12.TokenContainerRepository); + + @override + _i13.TokenContainerApi get containerApi => + (super.noSuchMethod( + Invocation.getter(#containerApi), + returnValue: _FakeTokenContainerApi_23( + this, + Invocation.getter(#containerApi), + ), + returnValueForMissingStub: _FakeTokenContainerApi_23( + this, + Invocation.getter(#containerApi), + ), + ) + as _i13.TokenContainerApi); + + @override + _i14.EccUtils get eccUtils => + (super.noSuchMethod( + Invocation.getter(#eccUtils), + returnValue: _FakeEccUtils_24(this, Invocation.getter(#eccUtils)), + returnValueForMissingStub: _FakeEccUtils_24( + this, + Invocation.getter(#eccUtils), + ), + ) + as _i14.EccUtils); + + @override + _i38.Ref get ref => + (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _i24.dummyValue<_i38.Ref>( + this, + Invocation.getter(#ref), + ), + returnValueForMissingStub: _i24.dummyValue<_i38.Ref>( + this, + Invocation.getter(#ref), + ), + ) + as _i38.Ref); + + @override + _i38.AsyncValue<_i27.TokenContainerState> get state => + (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i24 + .dummyValue<_i38.AsyncValue<_i27.TokenContainerState>>( + this, + Invocation.getter(#state), + ), + returnValueForMissingStub: _i24 + .dummyValue<_i38.AsyncValue<_i27.TokenContainerState>>( + this, + Invocation.getter(#state), + ), + ) + as _i38.AsyncValue<_i27.TokenContainerState>); + + @override + set state(_i38.AsyncValue<_i27.TokenContainerState>? newState) => + super.noSuchMethod( + Invocation.setter(#state, newState), + returnValueForMissingStub: null, + ); + + @override + _i19.Future<_i27.TokenContainerState> get future => + (super.noSuchMethod( + Invocation.getter(#future), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.getter(#future), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.getter(#future), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future<_i27.TokenContainerState> build({ + required _i12.TokenContainerRepository? repo, + required _i13.TokenContainerApi? containerApi, + required _i14.EccUtils? eccUtils, + }) => + (super.noSuchMethod( + Invocation.method(#build, [], { + #repo: repo, + #containerApi: containerApi, + #eccUtils: eccUtils, + }), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#build, [], { + #repo: repo, + #containerApi: containerApi, + #eccUtils: eccUtils, + }), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#build, [], { + #repo: repo, + #containerApi: containerApi, + #eccUtils: eccUtils, + }), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future> syncContainers({ + required _i18.TokenState? tokenState, + required bool? isManually, + List<_i15.TokenContainerFinalized>? containersToSync, + bool? isInitSync, + }) => + (super.noSuchMethod( + Invocation.method(#syncContainers, [], { + #tokenState: tokenState, + #isManually: isManually, + #containersToSync: containersToSync, + #isInitSync: isInitSync, + }), + returnValue: + _i19.Future>.value( + {}, + ), + returnValueForMissingStub: + _i19.Future>.value( + {}, + ), + ) + as _i19.Future>); + + @override + _i19.Future rolloverTokens({ + required _i18.TokenState? tokenState, + required _i15.TokenContainerFinalized? container, + }) => + (super.noSuchMethod( + Invocation.method(#rolloverTokens, [], { + #tokenState: tokenState, + #container: container, + }), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future getRolloverQrData( + _i15.TokenContainerFinalized? container, + ) => + (super.noSuchMethod( + Invocation.method(#getRolloverQrData, [container]), + returnValue: _i19.Future.value( + _i24.dummyValue( + this, + Invocation.method(#getRolloverQrData, [container]), + ), + ), + returnValueForMissingStub: _i19.Future.value( + _i24.dummyValue( + this, + Invocation.method(#getRolloverQrData, [container]), + ), + ), + ) + as _i19.Future); + + @override + _i19.Future<_i27.TokenContainerState> addContainer( + _i15.TokenContainer? container, + ) => + (super.noSuchMethod( + Invocation.method(#addContainer, [container]), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#addContainer, [container]), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#addContainer, [container]), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future<_i27.TokenContainerState> addContainerList( + List<_i15.TokenContainer>? container, + ) => + (super.noSuchMethod( + Invocation.method(#addContainerList, [container]), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#addContainerList, [container]), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#addContainerList, [container]), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future<_i27.TokenContainerState> update( + _i19.FutureOr<_i27.TokenContainerState> Function(_i27.TokenContainerState)? + cb, { + _i19.FutureOr<_i27.TokenContainerState> Function(Object, StackTrace)? + onError, + }) => + (super.noSuchMethod( + Invocation.method(#update, [cb], {#onError: onError}), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#update, [cb], {#onError: onError}), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#update, [cb], {#onError: onError}), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future + updateContainer( + _i15.TokenContainer? container, + R Function(T)? updater, + ) => + (super.noSuchMethod( + Invocation.method(#updateContainer, [container, updater]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> updateContainerList( + List? container, + T Function(T)? updater, + ) => + (super.noSuchMethod( + Invocation.method(#updateContainerList, [container, updater]), + returnValue: _i19.Future>.value([]), + returnValueForMissingStub: _i19.Future>.value([]), + ) + as _i19.Future>); + + @override + _i19.Future unregisterDelete(_i15.TokenContainerFinalized? container) => + (super.noSuchMethod( + Invocation.method(#unregisterDelete, [container]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future deleteContainer(_i15.TokenContainer? container) => + (super.noSuchMethod( + Invocation.method(#deleteContainer, [container]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future<_i27.TokenContainerState> deleteContainerList( + List<_i15.TokenContainer>? container, + ) => + (super.noSuchMethod( + Invocation.method(#deleteContainerList, [container]), + returnValue: _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#deleteContainerList, [container]), + ), + ), + returnValueForMissingStub: + _i19.Future<_i27.TokenContainerState>.value( + _i24.dummyValue<_i27.TokenContainerState>( + this, + Invocation.method(#deleteContainerList, [container]), + ), + ), + ) + as _i19.Future<_i27.TokenContainerState>); + + @override + _i19.Future handleProcessorResult( + _i39.ProcessorResult? result, { + Map? args = const {}, + }) => + (super.noSuchMethod( + Invocation.method(#handleProcessorResult, [result], {#args: args}), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future?> handleProcessorResults( + List<_i39.ProcessorResult>? results, { + Map? args = const {}, + }) => + (super.noSuchMethod( + Invocation.method( + #handleProcessorResults, + [results], + {#args: args}, + ), + returnValue: + _i19.Future?>.value(), + returnValueForMissingStub: + _i19.Future?>.value(), + ) + as _i19.Future?>); + + @override + _i19.Future<_i15.TokenContainerFinalized?> finalize( + _i15.TokenContainer? container, { + required bool? isManually, + bool? addDeviceInfos, + bool? urlIsOk, + }) => + (super.noSuchMethod( + Invocation.method( + #finalize, + [container], + { + #isManually: isManually, + #addDeviceInfos: addDeviceInfos, + #urlIsOk: urlIsOk, + }, + ), + returnValue: _i19.Future<_i15.TokenContainerFinalized?>.value(), + returnValueForMissingStub: + _i19.Future<_i15.TokenContainerFinalized?>.value(), + ) + as _i19.Future<_i15.TokenContainerFinalized?>); + + @override + void runBuild() => super.noSuchMethod( + Invocation.method(#runBuild, []), + returnValueForMissingStub: null, + ); + + @override + _i40.RemoveListener listenSelf( + void Function( + _i38.AsyncValue<_i27.TokenContainerState>?, + _i38.AsyncValue<_i27.TokenContainerState>, + )? + listener, { + void Function(Object, StackTrace)? onError, + }) => + (super.noSuchMethod( + Invocation.method(#listenSelf, [listener], {#onError: onError}), + returnValue: () {}, + returnValueForMissingStub: () {}, + ) + as _i40.RemoveListener); + + @override + bool updateShouldNotify( + _i38.AsyncValue<_i27.TokenContainerState>? previous, + _i38.AsyncValue<_i27.TokenContainerState>? next, + ) => + (super.noSuchMethod( + Invocation.method(#updateShouldNotify, [previous, next]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); +} + +/// A class which mocks [TokenContainerFinalized]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTokenContainerFinalized extends _i1.Mock + implements _i15.TokenContainerFinalized { + @override + String get issuer => + (super.noSuchMethod( + Invocation.getter(#issuer), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#issuer), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#issuer), + ), + ) + as String); + + @override + String get nonce => + (super.noSuchMethod( + Invocation.getter(#nonce), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#nonce), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#nonce), + ), + ) + as String); + + @override + DateTime get timestamp => + (super.noSuchMethod( + Invocation.getter(#timestamp), + returnValue: _FakeDateTime_25(this, Invocation.getter(#timestamp)), + returnValueForMissingStub: _FakeDateTime_25( + this, + Invocation.getter(#timestamp), + ), + ) + as DateTime); + + @override + Uri get serverUrl => + (super.noSuchMethod( + Invocation.getter(#serverUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#serverUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#serverUrl), + ), + ) + as Uri); + + @override + String get serial => + (super.noSuchMethod( + Invocation.getter(#serial), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#serial), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#serial), + ), + ) + as String); + + @override + _i30.EcKeyAlgorithm get ecKeyAlgorithm => + (super.noSuchMethod( + Invocation.getter(#ecKeyAlgorithm), + returnValue: _i30.EcKeyAlgorithm.brainpoolp160r1, + returnValueForMissingStub: _i30.EcKeyAlgorithm.brainpoolp160r1, + ) + as _i30.EcKeyAlgorithm); + + @override + _i41.Algorithms get hashAlgorithm => + (super.noSuchMethod( + Invocation.getter(#hashAlgorithm), + returnValue: _i41.Algorithms.SHA1, + returnValueForMissingStub: _i41.Algorithms.SHA1, + ) + as _i41.Algorithms); + + @override + bool get sslVerify => + (super.noSuchMethod( + Invocation.getter(#sslVerify), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + String get serverName => + (super.noSuchMethod( + Invocation.getter(#serverName), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#serverName), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#serverName), + ), + ) + as String); + + @override + _i42.FinalizationState get finalizationState => + (super.noSuchMethod( + Invocation.getter(#finalizationState), + returnValue: _i42.FinalizationState.notStarted, + returnValueForMissingStub: _i42.FinalizationState.notStarted, + ) + as _i42.FinalizationState); + + @override + _i43.SyncState get syncState => + (super.noSuchMethod( + Invocation.getter(#syncState), + returnValue: _i43.SyncState.notStarted, + returnValueForMissingStub: _i43.SyncState.notStarted, + ) + as _i43.SyncState); + + @override + _i44.ContainerPolicies get policies => + (super.noSuchMethod( + Invocation.getter(#policies), + returnValue: _i24.dummyValue<_i44.ContainerPolicies>( + this, + Invocation.getter(#policies), + ), + returnValueForMissingStub: _i24.dummyValue<_i44.ContainerPolicies>( + this, + Invocation.getter(#policies), + ), + ) + as _i44.ContainerPolicies); + + @override + bool get initSynced => + (super.noSuchMethod( + Invocation.getter(#initSynced), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + String get publicClientKey => + (super.noSuchMethod( + Invocation.getter(#publicClientKey), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#publicClientKey), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#publicClientKey), + ), + ) + as String); + + @override + String get privateClientKey => + (super.noSuchMethod( + Invocation.getter(#privateClientKey), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#privateClientKey), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#privateClientKey), + ), + ) + as String); + + @override + String get $type => + (super.noSuchMethod( + Invocation.getter(#$type), + returnValue: _i24.dummyValue( + this, + Invocation.getter(#$type), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.getter(#$type), + ), + ) + as String); + + @override + _i15.$TokenContainerFinalizedCopyWith<_i15.TokenContainerFinalized> + get copyWith => + (super.noSuchMethod( + Invocation.getter(#copyWith), + returnValue: + _Fake$TokenContainerFinalizedCopyWith_27< + _i15.TokenContainerFinalized + >(this, Invocation.getter(#copyWith)), + returnValueForMissingStub: + _Fake$TokenContainerFinalizedCopyWith_27< + _i15.TokenContainerFinalized + >(this, Invocation.getter(#copyWith)), + ) + as _i15.$TokenContainerFinalizedCopyWith< + _i15.TokenContainerFinalized + >); + + @override + Uri get registrationUrl => + (super.noSuchMethod( + Invocation.getter(#registrationUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#registrationUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#registrationUrl), + ), + ) + as Uri); + + @override + Uri get challengeUrl => + (super.noSuchMethod( + Invocation.getter(#challengeUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#challengeUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#challengeUrl), + ), + ) + as Uri); + + @override + Uri get syncUrl => + (super.noSuchMethod( + Invocation.getter(#syncUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#syncUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#syncUrl), + ), + ) + as Uri); + + @override + Uri get transferUrl => + (super.noSuchMethod( + Invocation.getter(#transferUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#transferUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#transferUrl), + ), + ) + as Uri); + + @override + Uri get unregisterUrl => + (super.noSuchMethod( + Invocation.getter(#unregisterUrl), + returnValue: _FakeUri_26(this, Invocation.getter(#unregisterUrl)), + returnValueForMissingStub: _FakeUri_26( + this, + Invocation.getter(#unregisterUrl), + ), + ) + as Uri); + + @override + Map toJson() => + (super.noSuchMethod( + Invocation.method(#toJson, []), + returnValue: {}, + returnValueForMissingStub: {}, + ) + as Map); + + @override + _i15.TokenContainer withClientKeyPair( + _i7.AsymmetricKeyPair<_i7.ECPublicKey, _i7.ECPrivateKey>? keyPair, + ) => + (super.noSuchMethod( + Invocation.method(#withClientKeyPair, [keyPair]), + returnValue: _i24.dummyValue<_i15.TokenContainer>( + this, + Invocation.method(#withClientKeyPair, [keyPair]), + ), + returnValueForMissingStub: _i24.dummyValue<_i15.TokenContainer>( + this, + Invocation.method(#withClientKeyPair, [keyPair]), + ), + ) + as _i15.TokenContainer); + + @override + String signMessage(String? msg) => + (super.noSuchMethod( + Invocation.method(#signMessage, [msg]), + returnValue: _i24.dummyValue( + this, + Invocation.method(#signMessage, [msg]), + ), + returnValueForMissingStub: _i24.dummyValue( + this, + Invocation.method(#signMessage, [msg]), + ), + ) + as String); + + @override + String? trySignMessage(String? msg) => + (super.noSuchMethod( + Invocation.method(#trySignMessage, [msg]), + returnValueForMissingStub: null, + ) + as String?); + + @override + _i16.Token addOriginToToken({ + required _i16.Token? token, + String? tokenData, + }) => + (super.noSuchMethod( + Invocation.method(#addOriginToToken, [], { + #token: token, + #tokenData: tokenData, + }), + returnValue: _FakeToken_28( + this, + Invocation.method(#addOriginToToken, [], { + #token: token, + #tokenData: tokenData, + }), + ), + returnValueForMissingStub: _FakeToken_28( + this, + Invocation.method(#addOriginToToken, [], { + #token: token, + #tokenData: tokenData, + }), + ), + ) + as _i16.Token); +} + +/// A class which mocks [TokenNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTokenNotifier extends _i1.Mock implements _i45.TokenNotifier { + @override + _i17.TokenRepository get repo => + (super.noSuchMethod( + Invocation.getter(#repo), + returnValue: _FakeTokenRepository_29( + this, + Invocation.getter(#repo), + ), + returnValueForMissingStub: _FakeTokenRepository_29( + this, + Invocation.getter(#repo), + ), + ) + as _i17.TokenRepository); + + @override + _i10.RsaUtils get rsaUtils => + (super.noSuchMethod( + Invocation.getter(#rsaUtils), + returnValue: _FakeRsaUtils_14(this, Invocation.getter(#rsaUtils)), + returnValueForMissingStub: _FakeRsaUtils_14( + this, + Invocation.getter(#rsaUtils), + ), + ) + as _i10.RsaUtils); + + @override + _i9.PrivacyideaIOClient get ioClient => + (super.noSuchMethod( + Invocation.getter(#ioClient), + returnValue: _FakePrivacyideaIOClient_13( + this, + Invocation.getter(#ioClient), + ), + returnValueForMissingStub: _FakePrivacyideaIOClient_13( + this, + Invocation.getter(#ioClient), + ), + ) + as _i9.PrivacyideaIOClient); + + @override + _i8.FirebaseUtils get firebaseUtils => + (super.noSuchMethod( + Invocation.getter(#firebaseUtils), + returnValue: _FakeFirebaseUtils_12( + this, + Invocation.getter(#firebaseUtils), + ), + returnValueForMissingStub: _FakeFirebaseUtils_12( + this, + Invocation.getter(#firebaseUtils), + ), + ) + as _i8.FirebaseUtils); + + @override + _i38.Ref get ref => + (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _i24.dummyValue<_i38.Ref>( + this, + Invocation.getter(#ref), + ), + returnValueForMissingStub: _i24.dummyValue<_i38.Ref>( + this, + Invocation.getter(#ref), + ), + ) + as _i38.Ref); + + @override + _i38.AsyncValue<_i18.TokenState> get state => + (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i24.dummyValue<_i38.AsyncValue<_i18.TokenState>>( + this, + Invocation.getter(#state), + ), + returnValueForMissingStub: _i24 + .dummyValue<_i38.AsyncValue<_i18.TokenState>>( + this, + Invocation.getter(#state), + ), + ) + as _i38.AsyncValue<_i18.TokenState>); + + @override + set state(_i38.AsyncValue<_i18.TokenState>? newState) => super.noSuchMethod( + Invocation.setter(#state, newState), + returnValueForMissingStub: null, + ); + + @override + _i19.Future<_i18.TokenState> get future => + (super.noSuchMethod( + Invocation.getter(#future), + returnValue: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30(this, Invocation.getter(#future)), + ), + returnValueForMissingStub: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30(this, Invocation.getter(#future)), + ), + ) + as _i19.Future<_i18.TokenState>); + + @override + _i19.Future<_i18.TokenState> build({ + required _i17.TokenRepository? repo, + required _i10.RsaUtils? rsaUtils, + required _i9.PrivacyideaIOClient? ioClient, + required _i8.FirebaseUtils? firebaseUtils, + }) => + (super.noSuchMethod( + Invocation.method(#build, [], { + #repo: repo, + #rsaUtils: rsaUtils, + #ioClient: ioClient, + #firebaseUtils: firebaseUtils, + }), + returnValue: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30( + this, + Invocation.method(#build, [], { + #repo: repo, + #rsaUtils: rsaUtils, + #ioClient: ioClient, + #firebaseUtils: firebaseUtils, + }), + ), + ), + returnValueForMissingStub: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30( + this, + Invocation.method(#build, [], { + #repo: repo, + #rsaUtils: rsaUtils, + #ioClient: ioClient, + #firebaseUtils: firebaseUtils, + }), + ), + ), + ) + as _i19.Future<_i18.TokenState>); + + @override + _i19.Future addNewToken(_i16.Token? token) => + (super.noSuchMethod( + Invocation.method(#addNewToken, [token]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future> addNewTokens(List<_i16.Token>? tokens) => + (super.noSuchMethod( + Invocation.method(#addNewTokens, [tokens]), + returnValue: _i19.Future>.value(<_i16.Token>[]), + returnValueForMissingStub: _i19.Future>.value( + <_i16.Token>[], + ), + ) + as _i19.Future>); + + @override + _i19.Future addOrReplaceToken(_i16.Token? token) => + (super.noSuchMethod( + Invocation.method(#addOrReplaceToken, [token]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future> addOrReplaceTokens(List<_i16.Token>? tokens) => + (super.noSuchMethod( + Invocation.method(#addOrReplaceTokens, [tokens]), + returnValue: _i19.Future>.value(<_i16.Token>[]), + returnValueForMissingStub: _i19.Future>.value( + <_i16.Token>[], + ), + ) + as _i19.Future>); + + @override + _i19.Future updateToken( + T? token, + T Function(T)? updater, + ) => + (super.noSuchMethod( + Invocation.method(#updateToken, [token, updater]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future> updateTokens( + List? tokens, + T Function(T)? updater, + ) => + (super.noSuchMethod( + Invocation.method(#updateTokens, [tokens, updater]), + returnValue: _i19.Future>.value([]), + returnValueForMissingStub: _i19.Future>.value([]), + ) + as _i19.Future>); + + @override + _i19.Future<_i46.HOTPToken?> incrementCounter(_i46.HOTPToken? token) => + (super.noSuchMethod( + Invocation.method(#incrementCounter, [token]), + returnValue: _i19.Future<_i46.HOTPToken?>.value(), + returnValueForMissingStub: _i19.Future<_i46.HOTPToken?>.value(), + ) + as _i19.Future<_i46.HOTPToken?>); + + @override + _i19.Future hideToken(T? token) => + (super.noSuchMethod( + Invocation.method(#hideToken, [token]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future showToken(T? token) => + (super.noSuchMethod( + Invocation.method(#showToken, [token]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future<_i47.OTPToken?> showTokenById(String? tokenId) => + (super.noSuchMethod( + Invocation.method(#showTokenById, [tokenId]), + returnValue: _i19.Future<_i47.OTPToken?>.value(), + returnValueForMissingStub: _i19.Future<_i47.OTPToken?>.value(), + ) + as _i19.Future<_i47.OTPToken?>); + + @override + _i19.Future<_i18.TokenState?> loadStateFromRepo() => + (super.noSuchMethod( + Invocation.method(#loadStateFromRepo, []), + returnValue: _i19.Future<_i18.TokenState?>.value(), + returnValueForMissingStub: _i19.Future<_i18.TokenState?>.value(), + ) + as _i19.Future<_i18.TokenState?>); + + @override + _i19.Future saveStateToRepo() => + (super.noSuchMethod( + Invocation.method(#saveStateToRepo, []), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future onMinimizeApp() => + (super.noSuchMethod( + Invocation.method(#onMinimizeApp, []), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future hideLockedTokens() => + (super.noSuchMethod( + Invocation.method(#hideLockedTokens, []), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future removeToken(_i16.Token? token) => + (super.noSuchMethod( + Invocation.method(#removeToken, [token]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future removeTokens(List<_i16.Token>? tokens) => + (super.noSuchMethod( + Invocation.method(#removeTokens, [tokens]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future removeTokensBySerials(List? serials) => + (super.noSuchMethod( + Invocation.method(#removeTokensBySerials, [serials]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future rolloutPushToken(_i29.PushToken? token) => + (super.noSuchMethod( + Invocation.method(#rolloutPushToken, [token]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?> + updateAllFirebaseTokens({String? firebaseToken}) => + (super.noSuchMethod( + Invocation.method(#updateAllFirebaseTokens, [], { + #firebaseToken: firebaseToken, + }), + returnValue: + _i19.Future< + (List<_i29.PushToken>, List<_i29.PushToken>)? + >.value(), + returnValueForMissingStub: + _i19.Future< + (List<_i29.PushToken>, List<_i29.PushToken>)? + >.value(), + ) + as _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?>); + + @override + _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?> + updateFirebaseTokens({ + required List<_i29.PushToken>? tokens, + String? firebaseToken, + }) => + (super.noSuchMethod( + Invocation.method(#updateFirebaseTokens, [], { + #tokens: tokens, + #firebaseToken: firebaseToken, + }), + returnValue: + _i19.Future< + (List<_i29.PushToken>, List<_i29.PushToken>)? + >.value(), + returnValueForMissingStub: + _i19.Future< + (List<_i29.PushToken>, List<_i29.PushToken>)? + >.value(), + ) + as _i19.Future<(List<_i29.PushToken>, List<_i29.PushToken>)?>); + + @override + _i19.Future updateFirebaseToken( + _i29.PushToken? token, + String? firebaseToken, + ) => + (super.noSuchMethod( + Invocation.method(#updateFirebaseToken, [token, firebaseToken]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future handleLink(Uri? uri) => + (super.noSuchMethod( + Invocation.method(#handleLink, [uri]), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future handleProcessorResult( + _i39.ProcessorResult? result, { + Map? args = const {}, + }) => + (super.noSuchMethod( + Invocation.method(#handleProcessorResult, [result], {#args: args}), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future handleProcessorResults( + List<_i39.ProcessorResult>? results, { + Map? args = const {}, + }) => + (super.noSuchMethod( + Invocation.method( + #handleProcessorResults, + [results], + {#args: args}, + ), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + _i19.Future getTokenById(String? id) => + (super.noSuchMethod( + Invocation.method(#getTokenById, [id]), + returnValue: _i19.Future.value(), + returnValueForMissingStub: _i19.Future.value(), + ) + as _i19.Future); + + @override + void runBuild() => super.noSuchMethod( + Invocation.method(#runBuild, []), + returnValueForMissingStub: null, + ); + + @override + _i40.RemoveListener listenSelf( + void Function( + _i38.AsyncValue<_i18.TokenState>?, + _i38.AsyncValue<_i18.TokenState>, + )? + listener, { + void Function(Object, StackTrace)? onError, + }) => + (super.noSuchMethod( + Invocation.method(#listenSelf, [listener], {#onError: onError}), + returnValue: () {}, + returnValueForMissingStub: () {}, + ) + as _i40.RemoveListener); + + @override + bool updateShouldNotify( + _i38.AsyncValue<_i18.TokenState>? previous, + _i38.AsyncValue<_i18.TokenState>? next, + ) => + (super.noSuchMethod( + Invocation.method(#updateShouldNotify, [previous, next]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i19.Future<_i18.TokenState> update( + _i19.FutureOr<_i18.TokenState> Function(_i18.TokenState)? cb, { + _i19.FutureOr<_i18.TokenState> Function(Object, StackTrace)? onError, + }) => + (super.noSuchMethod( + Invocation.method(#update, [cb], {#onError: onError}), + returnValue: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30( + this, + Invocation.method(#update, [cb], {#onError: onError}), + ), + ), + returnValueForMissingStub: _i19.Future<_i18.TokenState>.value( + _FakeTokenState_30( + this, + Invocation.method(#update, [cb], {#onError: onError}), + ), + ), + ) + as _i19.Future<_i18.TokenState>); +} + +/// A class which mocks [LocalAuthentication]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalAuthentication extends _i1.Mock + implements _i48.LocalAuthentication { + @override + _i19.Future get canCheckBiometrics => + (super.noSuchMethod( + Invocation.getter(#canCheckBiometrics), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future authenticate({ + required String? localizedReason, + Iterable<_i49.AuthMessages>? authMessages = const [ + _i50.IOSAuthMessages(), + _i49.AndroidAuthMessages(), + _i51.WindowsAuthMessages(), + ], + bool? biometricOnly = false, + bool? sensitiveTransaction = true, + bool? persistAcrossBackgrounding = false, + }) => + (super.noSuchMethod( + Invocation.method(#authenticate, [], { + #localizedReason: localizedReason, + #authMessages: authMessages, + #biometricOnly: biometricOnly, + #sensitiveTransaction: sensitiveTransaction, + #persistAcrossBackgrounding: persistAcrossBackgrounding, + }), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future stopAuthentication() => + (super.noSuchMethod( + Invocation.method(#stopAuthentication, []), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future isDeviceSupported() => + (super.noSuchMethod( + Invocation.method(#isDeviceSupported, []), + returnValue: _i19.Future.value(false), + returnValueForMissingStub: _i19.Future.value(false), + ) + as _i19.Future); + + @override + _i19.Future> getAvailableBiometrics() => + (super.noSuchMethod( + Invocation.method(#getAvailableBiometrics, []), + returnValue: _i19.Future>.value( + <_i49.BiometricType>[], + ), + returnValueForMissingStub: + _i19.Future>.value( + <_i49.BiometricType>[], + ), ) - as _i13.Future); + as _i19.Future>); } diff --git a/test/unit_test/model/tokens/otp_token_test.dart b/test/unit_test/model/tokens/otp_token_test.dart index b2160f696..1096db9b2 100644 --- a/test/unit_test/model/tokens/otp_token_test.dart +++ b/test/unit_test/model/tokens/otp_token_test.dart @@ -26,25 +26,31 @@ import 'package:privacyidea_authenticator/model/token_template.dart'; import 'package:privacyidea_authenticator/model/tokens/otp_token.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -class MockOTPToken extends OTPToken { - const MockOTPToken({ +class FakeOTPToken extends OTPToken { + final String _testOtpValue; + final String _testNextValue; + + const FakeOTPToken({ required super.algorithm, required super.digits, required super.secret, required super.id, super.serial, super.label, - super.type = 'MOCK', + super.type = 'FAKE', super.pin, super.isLocked, super.issuer, - }); + String otpValue = '123456', + String nextValue = '654321', + }) : _testOtpValue = otpValue, + _testNextValue = nextValue; @override - String get otpValue => '123456'; + String get otpValue => _testOtpValue; @override - String get nextValue => '654321'; + String get nextValue => _testNextValue; @override Map toJson() => { @@ -59,7 +65,7 @@ class MockOTPToken extends OTPToken { @override OTPToken copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, @@ -77,17 +83,23 @@ class MockOTPToken extends OTPToken { TokenOriginData? origin, bool? isOffline, ForceBiometricOption? forceBiometricOption, + String? otpValue, + String? nextValue, }) { - return MockOTPToken( + final String? newSerial = serial?.call(); + + return FakeOTPToken( algorithm: algorithm ?? this.algorithm, digits: digits ?? this.digits, secret: secret ?? this.secret, id: id ?? this.id, - serial: serial ?? this.serial, + serial: serial != null ? newSerial : this.serial, label: label ?? this.label, pin: pin ?? this.pin, isLocked: isLocked ?? this.isLocked, issuer: issuer ?? this.issuer, + otpValue: otpValue ?? this.otpValue, + nextValue: nextValue ?? this.nextValue, ); } } @@ -114,7 +126,7 @@ void main() { }); group('OTPToken Serialization & Template Logic', () { - const token = MockOTPToken( + const token = FakeOTPToken( algorithm: Algorithms.SHA256, digits: 8, secret: 'SECRET123', @@ -123,25 +135,31 @@ void main() { label: 'my_label', ); - test('toOtpAuthMap output types', () { + test('toOtpAuthMap output types and values', () { final map = token.toOtpAuthMap(); - expect(map[OTPToken.ALGORITHM], isA()); - expect(map[OTPToken.DIGITS], isA()); expect(map[OTPToken.ALGORITHM], 'SHA256'); + expect(map[OTPToken.DIGITS], '8'); + expect(map[OTPToken.ALGORITHM], isA()); }); - test('OTP values are included in map only if serial is null', () { - final withSerial = token.toOtpAuthMap(); - expect(withSerial.containsKey(OTPToken.OTP_VALUES), isFalse); - - final noSerial = token.copyWith(serial: null).toOtpAuthMap(); - expect(noSerial[OTPToken.OTP_VALUES], ['123456', '654321']); - }); + test( + 'toTemplate captures serial and excludes OTP values when serial is present', + () { + final template = token.toTemplate(); + expect(template.serial, 'SN_001'); + expect(template.otpAuthMap.containsKey(OTPToken.OTP_VALUES), isFalse); + }, + ); - test('toTemplate captures OTP values in otpAuthMap', () { - final template = token.toTemplate(); - expect(template.otpAuthMap[OTPToken.OTP_VALUES], ['123456', '654321']); - }); + test( + 'toTemplate captures OTP values and has null serial when serial is absent', + () { + final tokenWithoutSerial = token.copyWith(serial: () => null); + final template = tokenWithoutSerial.toTemplate(); + expect(template.serial, isNull); + expect(template.otpAuthMap[OTPToken.OTP_VALUES], ['123456', '654321']); + }, + ); test('toJson contains all required fields', () { final json = token.toJson(); @@ -152,41 +170,84 @@ void main() { }); group('OTPToken Identity Logic', () { - const base = MockOTPToken( + const base = FakeOTPToken( + id: '1', algorithm: Algorithms.SHA1, digits: 6, - secret: 'S', - id: '1', + secret: 'secret', + issuer: "issuer", + label: "label", + serial: "serial", + isLocked: true, + pin: true, ); - test('isSameTokenAs matches by parameters if ID/Serial differ', () { - const other = MockOTPToken( - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'S', - id: 'different', + test('isSameTokenAs should match when id is the same', () { + const other = FakeOTPToken( + id: '1', + serial: "different_serial", + issuer: "different_issuer", + algorithm: Algorithms.SHA256, + digits: 8, + secret: 'different_secret', + label: "different_label", + isLocked: false, + pin: false, ); expect(base.isSameTokenAs(other), isTrue); }); - test('isSameTokenAs fails if secret differs', () { - const other = MockOTPToken( - algorithm: Algorithms.SHA1, - digits: 6, - secret: 'OTHER', - id: '1', + group("isSameTokenAs with different id", () { + test('isSameTokenAs should match when serial AND issuer is the same', () { + final other1 = base.copyWith( + id: '2', // same id = same token + serial: () => "serial", + issuer: "issuer", + ); + final other2 = base.copyWith(id: '2', serial: () => "different_serial"); + final other3 = base.copyWith(id: '2', issuer: "different_issuer"); + expect(base.isSameTokenAs(other1), isTrue); + expect(base.isSameTokenAs(other2), isFalse); + expect(base.isSameTokenAs(other3), isFalse); + }); + + test( + 'isSameTokenAs should not match when secret, algorithm or digits differs', + () { + final other1 = base.copyWith( + id: '2', // same id = same token + issuer: "different_issuer", // same serial & issuer = same token + secret: 'different_secret', + ); + final other2 = base.copyWith( + id: '2', + issuer: "different_issuer", + algorithm: Algorithms.SHA256, + ); + final other3 = base.copyWith( + id: '2', + issuer: "different_issuer", + digits: 8, + ); + + expect(base.isSameTokenAs(other1), isFalse); + expect(base.isSameTokenAs(other2), isFalse); + expect(base.isSameTokenAs(other3), isFalse); + }, ); - expect(base.isSameTokenAs(other), isFalse); - }); - test('isSameTokenAs fails if algorithm differs', () { - const other = MockOTPToken( - algorithm: Algorithms.SHA256, - digits: 6, - secret: 'S', - id: '1', + test( + 'isSameTokenAs should not be determined when only other properties differ', + () { + final other = base.copyWith( + id: '2', // same id = same token + label: "different_label", + isLocked: false, + pin: false, + ); + expect(base.isSameTokenAs(other), isNull); + }, ); - expect(base.isSameTokenAs(other), isFalse); }); }); } diff --git a/test/unit_test/model/tokens/token_test.dart b/test/unit_test/model/tokens/token_test.dart index a45173afe..ac1969a26 100644 --- a/test/unit_test/model/tokens/token_test.dart +++ b/test/unit_test/model/tokens/token_test.dart @@ -24,8 +24,8 @@ import 'package:privacyidea_authenticator/model/token_import/token_origin_data.d import 'package:privacyidea_authenticator/model/token_template.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -class MockToken extends Token { - const MockToken({ +class FakeToken extends Token { + const FakeToken({ required super.id, required super.type, super.serial, @@ -52,7 +52,7 @@ class MockToken extends Token { @override Token copyWith({ - String? serial, + String? Function()? serial, String? label, String? issuer, String? Function()? containerSerial, @@ -68,10 +68,10 @@ class MockToken extends Token { bool? isOffline, ForceBiometricOption? forceBiometricOption, }) { - return MockToken( + return FakeToken( id: id ?? this.id, type: type, - serial: serial ?? this.serial, + serial: serial != null ? serial() : this.serial, label: label ?? this.label, issuer: issuer ?? this.issuer, pin: pin ?? this.pin, @@ -121,13 +121,13 @@ void main() { test( 'isLocked returns true if PIN is required regardless of internal state', () { - const t = MockToken(id: '1', type: 'T', pin: true, isLocked: false); + const t = FakeToken(id: '1', type: 'T', pin: true, isLocked: false); expect(t.isLocked, isTrue); }, ); test('isLocked returns true if Biometrics are forced', () { - final t = MockToken( + final t = FakeToken( id: '1', type: 'T', forceBiometricOption: ForceBiometricOption.biometric, @@ -139,7 +139,7 @@ void main() { test( 'isLocked returns false only if PIN, Biometrics, and _isLocked are all false', () { - const t = MockToken( + const t = FakeToken( id: '1', type: 'T', pin: false, @@ -151,8 +151,8 @@ void main() { ); test('isHidden defaults to isLocked value when not specified', () { - const locked = MockToken(id: '1', type: 'T', isLocked: true); - const unlocked = MockToken(id: '2', type: 'T', isLocked: false); + const locked = FakeToken(id: '1', type: 'T', isLocked: true); + const unlocked = FakeToken(id: '2', type: 'T', isLocked: false); expect(locked.isHidden, isTrue); expect(unlocked.isHidden, isFalse); }); @@ -160,7 +160,7 @@ void main() { test( 'isHidden can be false while isLocked is true (explicit override)', () { - const t = MockToken( + const t = FakeToken( id: '1', type: 'T', isLocked: true, @@ -172,41 +172,72 @@ void main() { ); }); - group('Token Identity & Equality contracts', () { - test('Operator == strictly compares ID', () { - const t1 = MockToken(id: 'ID-A', type: 'HOTP'); - const t2 = MockToken(id: 'ID-A', type: 'TOTP'); - const t3 = MockToken(id: 'ID-B', type: 'HOTP'); + group('Token Identity Logic', () { + const base = FakeToken( + id: '1', + type: 'FAKE', + serial: 'serial', + issuer: 'issuer', + label: 'label', + isLocked: true, + pin: true, + ); - expect(t1 == t2, isTrue); - expect(t1 == t3, isFalse); + test('isSameTokenAs matches when id is the same', () { + const other = FakeToken( + id: '1', + type: 'OTHER', + serial: 'different_serial', + issuer: 'different_issuer', + ); + expect(base.isSameTokenAs(other), isTrue); }); - test('isSameTokenAs: match by ID', () { - const t1 = MockToken(id: 'SAME', type: 'A'); - const t2 = MockToken(id: 'SAME', type: 'B'); - expect(t1.isSameTokenAs(t2), isTrue); - }); + group('isSameTokenAs with different id', () { + test('matches when serial AND issuer are the same', () { + final other1 = base.copyWith( + id: '2', + serial: () => 'serial', + issuer: 'issuer', + ); + final other2 = base.copyWith(id: '2', serial: () => 'different_serial'); + final other3 = base.copyWith(id: '2', issuer: 'different_issuer'); + + expect(base.isSameTokenAs(other1), isTrue); + expect(base.isSameTokenAs(other2), isFalse); + expect(base.isSameTokenAs(other3), isFalse); + }); - test('isSameTokenAs: match by Serial and Issuer if IDs differ', () { - const t1 = MockToken(id: '1', type: 'T', serial: 'SN', issuer: 'ISS'); - const t2 = MockToken(id: '2', type: 'T', serial: 'SN', issuer: 'ISS'); - expect(t1.isSameTokenAs(t2), isTrue); + test('returns null if IDs differ and serials are null', () { + final t1 = base.copyWith(id: '1', serial: () => null); + final t2 = base.copyWith(id: '2', serial: () => null); + expect(t1.isSameTokenAs(t2), isNull); + }); + + test('should not be determined when only UI properties differ', () { + final baseNoSerial = base.copyWith( + id: '1', + // have to nullify serial to reach the UI properties check + serial: () => null, + ); + final other = base.copyWith( + id: '2', + serial: () => null, + label: 'different_label', + isLocked: false, + pin: false, + ); + expect(baseNoSerial.isSameTokenAs(other), isNull); + }); }); - test( - 'isSameTokenAs: mismatch if issuer differs even if serial matches', - () { - const t1 = MockToken(id: '1', type: 'T', serial: 'SN', issuer: 'ISS1'); - const t2 = MockToken(id: '2', type: 'T', serial: 'SN', issuer: 'ISS2'); - expect(t1.isSameTokenAs(t2), isFalse); - }, - ); + test('Operator == strictly compares ID', () { + const t1 = FakeToken(id: 'ID-A', type: 'HOTP'); + const t2 = FakeToken(id: 'ID-A', type: 'TOTP'); + const t3 = FakeToken(id: 'ID-B', type: 'HOTP'); - test('isSameTokenAs: return null if IDs differ and serials are null', () { - const t1 = MockToken(id: '1', type: 'T', serial: null); - const t2 = MockToken(id: '2', type: 'T', serial: null); - expect(t1.isSameTokenAs(t2), isNull); + expect(t1 == t2, isTrue); + expect(t1 == t3, isFalse); }); }); @@ -214,15 +245,15 @@ void main() { test( 'toOtpAuthMap transforms PIN bool to proprietary True/False strings', () { - const tTrue = MockToken(id: '1', type: 'T', pin: true); - const tFalse = MockToken(id: '1', type: 'T', pin: false); + const tTrue = FakeToken(id: '1', type: 'T', pin: true); + const tFalse = FakeToken(id: '1', type: 'T', pin: false); expect(tTrue.toOtpAuthMap()[Token.PIN], 'True'); expect(tFalse.toOtpAuthMap()[Token.PIN], 'False'); }, ); test('additionalData exports all internal fields', () { - const t = MockToken( + const t = FakeToken( id: 'uid', type: 'T', sortIndex: 5, @@ -239,8 +270,8 @@ void main() { }); test('toTemplate captures correct state only when serial is present', () { - const tWith = MockToken(id: '1', type: 'T', serial: 'SN-123'); - const tWithout = MockToken(id: '2', type: 'T', serial: null); + const tWith = FakeToken(id: '1', type: 'T', serial: 'SN-123'); + const tWithout = FakeToken(id: '2', type: 'T', serial: null); expect(tWith.toTemplate(), isNotNull); expect(tWith.toTemplate()?.serial, 'SN-123'); diff --git a/test/unit_test/model/tokens/totp_token_test.dart b/test/unit_test/model/tokens/totp_token_test.dart index 92f00f79e..8a1e78d45 100644 --- a/test/unit_test/model/tokens/totp_token_test.dart +++ b/test/unit_test/model/tokens/totp_token_test.dart @@ -188,8 +188,8 @@ void main() { group('isSameTokenAs', () { test('same serial | different id', () { - final t1 = totpToken.copyWith(serial: 'SN1', id: 'id1'); - final t2 = totpToken.copyWith(serial: 'SN1', id: 'id2'); + final t1 = totpToken.copyWith(serial: () => 'SN1', id: 'id1'); + final t2 = totpToken.copyWith(serial: () => 'SN1', id: 'id2'); expect(t1.isSameTokenAs(t2), isTrue); }); diff --git a/test/unit_test/utils/lock_auth_test.dart b/test/unit_test/utils/lock_auth_test.dart index 4b3986159..9656cbf17 100644 --- a/test/unit_test/utils/lock_auth_test.dart +++ b/test/unit_test/utils/lock_auth_test.dart @@ -1,14 +1,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations_en.dart'; import 'package:privacyidea_authenticator/model/enums/force_biometric_option.dart'; import 'package:privacyidea_authenticator/utils/lock_auth.dart'; -import 'lock_auth_test.mocks.dart'; +import '../../tests_app_wrapper.mocks.dart'; -@GenerateMocks([LocalAuthentication]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -17,6 +15,7 @@ void main() { setUp(() { mockLocalAuth = MockLocalAuthentication(); localAuthInstance = mockLocalAuth; // override Local instance for testing + resetAuthMutex(); }); group('lockAuth - Basic Flow', () { diff --git a/test/unit_test/utils/lock_auth_test.mocks.dart b/test/unit_test/utils/lock_auth_test.mocks.dart deleted file mode 100644 index b5fb87f82..000000000 --- a/test/unit_test/utils/lock_auth_test.mocks.dart +++ /dev/null @@ -1,95 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in privacyidea_authenticator/test/unit_test/utils/lock_auth_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; - -import 'package:local_auth/src/local_auth.dart' as _i2; -import 'package:local_auth_android/local_auth_android.dart' as _i4; -import 'package:local_auth_darwin/local_auth_darwin.dart' as _i5; -import 'package:local_auth_windows/local_auth_windows.dart' as _i6; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -/// A class which mocks [LocalAuthentication]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockLocalAuthentication extends _i1.Mock - implements _i2.LocalAuthentication { - MockLocalAuthentication() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Future get canCheckBiometrics => - (super.noSuchMethod( - Invocation.getter(#canCheckBiometrics), - returnValue: _i3.Future.value(false), - ) - as _i3.Future); - - @override - _i3.Future authenticate({ - required String? localizedReason, - Iterable<_i4.AuthMessages>? authMessages = const [ - _i5.IOSAuthMessages(), - _i4.AndroidAuthMessages(), - _i6.WindowsAuthMessages(), - ], - bool? biometricOnly = false, - bool? sensitiveTransaction = true, - bool? persistAcrossBackgrounding = false, - }) => - (super.noSuchMethod( - Invocation.method(#authenticate, [], { - #localizedReason: localizedReason, - #authMessages: authMessages, - #biometricOnly: biometricOnly, - #sensitiveTransaction: sensitiveTransaction, - #persistAcrossBackgrounding: persistAcrossBackgrounding, - }), - returnValue: _i3.Future.value(false), - ) - as _i3.Future); - - @override - _i3.Future stopAuthentication() => - (super.noSuchMethod( - Invocation.method(#stopAuthentication, []), - returnValue: _i3.Future.value(false), - ) - as _i3.Future); - - @override - _i3.Future isDeviceSupported() => - (super.noSuchMethod( - Invocation.method(#isDeviceSupported, []), - returnValue: _i3.Future.value(false), - ) - as _i3.Future); - - @override - _i3.Future> getAvailableBiometrics() => - (super.noSuchMethod( - Invocation.method(#getAvailableBiometrics, []), - returnValue: _i3.Future>.value( - <_i4.BiometricType>[], - ), - ) - as _i3.Future>); -} diff --git a/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart index 68a66cb66..16ae69cde 100644 --- a/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart +++ b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart @@ -574,6 +574,7 @@ void _testTokenNotifier() { verify(mockRepo.saveOrReplaceTokens(any)).called(greaterThan(0)); }); test('addTokenFromOtpAuth: rolloutPushToken', () async { + // -- PREPARE -- WidgetsFlutterBinding.ensureInitialized(); final mockSettingsRepo = MockSettingsRepository(); when( @@ -596,7 +597,7 @@ void _testTokenNotifier() { const publicTokenKeyString = 'MIICCgKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CAwEAAQ=='; const privateTokenKeyString = - 'MIILKAIBAAKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggEBAPReilE/TS0KTk9JFdynw1p9/3mLZCQYNMni5iyQkhdqAobAe3EmZVtWHj0aZtfgMZ3qC9EOJJvYt76m9Gh4UXPI5a9zldQjA2CMaY2yWMGVi8anjI+njB7WhYMtgDdHLajzI2P1bix6mI/bDxhIJBcfV61wlSNz1yArU36cw3SrWUXvGa2LiRJhMNXcALMiuBf9RaFmXQZci8Ae1+PPZ2UAyNdDrO8P8wILFeBTjd1WtZfkYtESBLCX6HdcM5JhaN74MJftWE1rKTQGh6Hg42RfgMDJDXiM/Dh5jg+OP2n5R0n/ua4CN++PNePd3JFODVa8ZvUv3eshoWD3Xc8IMscCggEBAMInwXHcEWNrOAOAL057ZA0WxsZg1IQMyJ1L5WVpYvnyB3jDX91cXhOM/zjC/C5VF1zy2+H6tmQ75C0Fs9Ph676LYnpTd7m8wqkqoI6SPDwsdx9dLZqT5Ps4ILS4ScOwKIN5qsccooZT6GWJyCZhfuTgApq5JE04ZEjrXhqhVcyaT+CJDhBuE1gvtIRmSQyPHa7isM3xrg9jMhdUcDVE/HotgJIxh0TtQmRDCJo2Ltngs3UrHgkGUIqLVVyHI/jZViKEWbnEku+GEE8A8sr52OOM8HpeXLE5rEn/hekf9iV31hLzASIBQWGopxaDpBiQgnFLYi5WSeEIKyqEA23SxSkCggEBAPKLw43Q3rENwZxAVkqk2OlAlgn1qHeK7xpS81LYS6iht9A3zE4KZh+54lmTkvBBvf2XCBN/jiaBfB7nZz8p7O6XQCJc/yGHfxqdQ0c49Y9u90U9l+4dxp31Hp+M0e4L3+4JJd9ZAvly1Woza1AWinvIyCWF0QFXQPbVChJpVja+u+UF5N6z2GE9xlL+AlPK6h4lbK8+AqcFxE/0TSP4AA/oL3A547OEiRZGGniFdhFyttsD/HC3CaCdpkaSZT2tIYHtpY2mLjbpXgQdVxH9PLWrdQfkhlJY3R7Qx4f5EEgG/BMelxV3bj2AT2TUGNDAP80PQsGpuQJgZuTvoVSUNpECggEAVGTBgkN9T3DAlUz3wy6Ba+sVlg9q8Mc5wJ3H5c/sVObudoC+P9MxlV/5ZGvlACK+mAl8qHq5I1KhOSy8YQJX3ahqsu9rIFI7bxr3VWGdSy6szPZMp19X7hcUqFlevu/ofFW7dPcuciMw5koAtSY16TiyCR0m+WXkuYmNixfL2rbMt7X7Zgri37dEyTRI1muzJFynK6280jV1BY0PhSgqctUqiOF8gep7rGcy6w1YSh6RAwIt+RBEnCQ6g5C+gyG9fh13fvdCQ1lL53trDe2SaD7QHPC9a8+84yFtzMq2zMyNQglc2bIgAFo13uRzxLWz7Zkt4SRi0q0hTka50tgGGQKCAQEAoksGQ7xL8E5ZY2sC5EgPenKT2VU89gzNj1F1nJA97CV32Vv+8gSgB2iIokwUVyslPk8y0vZ2n8aF3MVvFzq1FjUlBuGeABPfFuUfRJ6DT+2TwARJhqQuNrn0j3/uKolmpV2PFuqPrEESjbf3rUalubTsCS5XBusdYZgih43tHGE/eDE5sLd8HO7gblnkMwNM9Q0oih5oiMHkGB9xTdfCbZGgRodwlZ+tbyVRyGQ6VRt4IWEmcLsTEYlbisw2TdbT7NeBYW6jOXbHHm3lKeQJoiMEe3YdUKfnjQaVz3JukH2Fk3zjKOTSi0/W0TmXcnvsY3rDhHRBipKvcANhJN/Vg=='; + 'MIILKAIBAAKCAgEAuVWX4JptR4W2NHIMA4feqd/qUXKHAEfVUAKCYWdYEpq8x3tKWsFu9sVERA4rsTG+7Q6fEG1FdOSpJWVXW+paJpt7QDgp0/9VDr0Vn3bd6k7oYL2lDMm5NKEJA/Zk577OOXGogspksUkw3WtEg8meYB6mO8Tk+pPLmJnnLU2C+F8oeftRHQTXJhGMuWRLVhuA/hgMHUW7a7ICARiJhMz0hMWtQAzK0AHVxPDlybggYIYCSa2G5t53m62IDdOkb4LINpZVMCS2/tCDUJzVlzEmJF3G3cxxFaG3R4DkvkoUgLLpwdIj2Kw1FOJVkLyz1BJVfbmt6TvpsXc1G71yXk1p3MCFfilfiPY5U4LQfrR1A+F+rHFZtpQb2Hha1KMGGjBorHu5rpeFqLV1U2pL7CE/qjb/xUkVk1DbXH+26P3gLmrg2pm5TbMogskTUI29WDsklFj1LkH/sXRnWcIbYNp0QdN//FivlYFM4OxAoY1S1ofIu3Xj/rdVRtUvSE8kR7r1v6Xf6oHMkQIbS3mrQgJZNc0eV80TuCnT/YmvsTzT9jXGPQYUeZ4MvENnun7GB2TVdVgJ6srcknZgQGB2zWOUpf1I2xA9wzLTYhVpZKrU10eOxXr/Fao0tf2oNB+QldPRoUFL77z6VYHNIPFr9Yi/WFBVDl7gQ05hu+pVBNmhRN8CggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggIAZqa0329JNcMmnzfH1bDMsFRYSVJg2dPvn0g0hNSjoHJaOzbbgRcAaefrHrKmmpdOA6kEiymqvcrksNTHpR5RXm7hvjkdWdFjgC1Uq6U/1sZrySFhKIsWbMMA5lPzobQ6LvD3/7EwQk2iphECuufSM7TmJ9avaOaxbs1XkO0MrJqwJZgAXk1PCUPRKOIXJBNJx/LzysbTvxuyJn87s/V9PYjro70yHDHYACPZcnfsXun6nGpjfL4di3l7EQV3X1gVor5zYp4DSXGeOekUGJDdamkSe8j/nZabmBwZFhib8IioFnVY62q+X9nYwLjz9XNOLLvKSpOnpWa8YKf2j6rbBboswfKIsN76q0x9w+1+DNrtpVUdKxCmAsIpHMB3dJwU+G5JtcQLuYfz9bR0ALaccizHtumkE/aRjxqv7xwBHxFOMtGUYNkFx51J865nz+PRE3SRIAwF5ArmdFMJyY3xd+hrJDmZtHRW5LorFIurBeTX3l5gfHxdpvjxSZBodLdrw5o/k025K0ZAHr4o+tCYOgRbSryK9ZtYd8s10Jo/QkN6GDFYui67eNw/kf16k3ZEQtTIjCMR3kRQT3gjOLNjYB95FAPmGvCSmhwx5Xb8bzXF6FoQD2qsCgV/nZRL8DwPJR42Fq1lMaIrGqDbBs5nvEpaWg08pF3ks01ayFdOMlECggEBAPReilE/TS0KTk9JFdynw1p9/3mLZCQYNMni5iyQkhdqAobAe3EmZVtWHj0aZtfgMZ3qC9EOJJvYt76m9Gh4UXPI5a9zldQjA2CMaY2yWMGVi8anjI+njB7WhYMtgDdHLajzI2P1bix6mI/bDxhIJBcfV61wlSNz1yArU36cw3SrWUXvGa2LiRJhMNXcALMiuBf9RaFmXQZci8Ae1+PPZ2UAyNdDrO8P8wILFeBTjd1WtZfkYtESBLCX6HdcM5JhaN74MJftWE1rKTQGh6Hg42RfgMDJDXiM/Dh5jg+OP2n5R0n/ua4CN++PNePd3JFODVa8ZvUv3eshoWD3Xc8IMscCggEBAMInwXHcEWNrOAOAL057ZA0WxsZg1IQMyJ1L5WVpYvnyB3jDX91cXhOM/zjC/C5VF1zy2+H6tmQ75C0Fs9Ph676LYnpTd7m8wqkqoI6SPDwsdx9dLZqT5Ps4ILS4ScOwKIN5qsccooZT6GWJyCZhfuTgApq5JE04ZEjrXhqhVcyaT+CJDhBuE1gvtIRmSQyPHa7isM3xrg9jMhdUcDVE/HotgJIxh0TtQmRDCJo2Ltngs3UrHgkGUIqLVVyHI/jZViKEWbnEku+GEE8A8sr52OOM8HpeXLE5rEn/hekf9iV31hLzASIBQWGopxaDpBiQgnFLYi5WSeEIKyqEA23SxSkCggEBAPKLw43Q3rENwZxAVkqk2OlAlgn1qHeK7xpS81LYS6iht9A3zE4KZh+54lmTkvBBvf2XCBN/jiaBfB7nZz8p7O6XQCJc/yGHfxqdQ0c49Y9u90U9l+4dxp31Hp+M0e4L3+4JJd9ZAvly1Woza1AWinvIyCWF0QFXQPbVChJpVja+u+UF5N6z2GE9xlL+AlPK6h4lbK8+AqcFxE/0TSP4AA/oL3A547OEiRZGGniFdhFyttsD/HC3CaCdpkaSZT2tIYHtpY2mLjbpXgQdVxH9PLWrdQfkhlJY3R7Qx4f5EEgG/BMelxV3bj2AT2TUGNDAP80PQsGpuQJgZuTvoVSUNpECggEAVGTBgkN9T3DAlUz3wy6Ba+sVlg9q8Mc5wJ3H5c/sVObudoC+P9MxlV/5ZGvlACK+mAl8qHq5I1KhOSy8YQJX3ahqsu9rIFI7bxr3VWGdSy6szPZMp19X7hcUqFlevu/ofFW7dPcuciMw5koAtSY16TiyCR0m+WXkuYmNixfL2rbMt7X7Zgri37dEyTRI1muzJFynK6280jV1BY0PhSgqctUqiOF8gep7rGcy6w1YSh6RAwIt+RBEnCQ6g5C+gyG9fh13fvdCQ1lL53trDe2SaD7QHPC9a8+84yFtzMq2zMyNQglc2bIgAFo13uRzxLWz7Zkt4SRi0q0hTka50tgGGQKCAQEAoksGQ7xL8E5ZY2sC5EgPenKT2VU89gzNj1F1nJA97CV32Vv+8gSgB2iIokwUVyslPk8y0vZ2n8aF3MVvFzq1FjUlBuGeABPfFuUfRJ6DT+2TwARJhqQuNrn0j3/uKolmpV2PFuqPrEESjbf3rUalubTsCS5XBusdYZgih43tHGE/eDE5sLd8HO7gblnkMwNM9Q0oih5oiMHkGB9xTdfCbZGgRodwlZ+tbyVRyGQ6VRt4IWEmcLsTEYlbisw2TdbT7pNeBYW6jOXbHHm3lKeQJoiMEe3YdUKfnjQaVz3JukH2Fk3zjKOTSi0/W0TmXcnvsY3rDhHRBipKvcANhJN/Vg=='; final publicServerKey = rsaUtils.deserializeRSAPublicKeyPKCS1( publicServerKeyString, ); @@ -700,10 +701,14 @@ void _testTokenNotifier() { final stateBefore = await container.read(testProvider.future); expect(stateBefore.tokens, before); + + // -- ACT -- await scanQrCode( resultHandlerList: [container.read(testProvider.notifier)], qrCode: otpAuth, ); + + // -- ASSERT -- await Future.delayed(const Duration(seconds: 5)); final tokenState = await container.read(testProvider.future); expect(tokenState, isNotNull); diff --git a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart b/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart deleted file mode 100644 index aed6735b6..000000000 --- a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.mocks.dart +++ /dev/null @@ -1,317 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in privacyidea_authenticator/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:basic_utils/basic_utils.dart' as _i10; -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; -import 'package:privacyidea_authenticator/model/container_policies.dart' as _i9; -import 'package:privacyidea_authenticator/model/enums/algorithms.dart' as _i6; -import 'package:privacyidea_authenticator/model/enums/ec_key_algorithm.dart' - as _i5; -import 'package:privacyidea_authenticator/model/enums/rollout_state.dart' - as _i7; -import 'package:privacyidea_authenticator/model/enums/sync_state.dart' as _i8; -import 'package:privacyidea_authenticator/model/token_container.dart' as _i2; -import 'package:privacyidea_authenticator/model/tokens/token.dart' as _i3; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -class _FakeDateTime_0 extends _i1.SmartFake implements DateTime { - _FakeDateTime_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _FakeUri_1 extends _i1.SmartFake implements Uri { - _FakeUri_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -class _Fake$TokenContainerFinalizedCopyWith_2<$Res> extends _i1.SmartFake - implements _i2.$TokenContainerFinalizedCopyWith<$Res> { - _Fake$TokenContainerFinalizedCopyWith_2( - Object parent, - Invocation parentInvocation, - ) : super(parent, parentInvocation); -} - -class _FakeToken_3 extends _i1.SmartFake implements _i3.Token { - _FakeToken_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [TokenContainerFinalized]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTokenContainerFinalized extends _i1.Mock - implements _i2.TokenContainerFinalized { - MockTokenContainerFinalized() { - _i1.throwOnMissingStub(this); - } - - @override - String get issuer => - (super.noSuchMethod( - Invocation.getter(#issuer), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#issuer), - ), - ) - as String); - - @override - String get nonce => - (super.noSuchMethod( - Invocation.getter(#nonce), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#nonce), - ), - ) - as String); - - @override - DateTime get timestamp => - (super.noSuchMethod( - Invocation.getter(#timestamp), - returnValue: _FakeDateTime_0(this, Invocation.getter(#timestamp)), - ) - as DateTime); - - @override - Uri get serverUrl => - (super.noSuchMethod( - Invocation.getter(#serverUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#serverUrl)), - ) - as Uri); - - @override - String get serial => - (super.noSuchMethod( - Invocation.getter(#serial), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#serial), - ), - ) - as String); - - @override - _i5.EcKeyAlgorithm get ecKeyAlgorithm => - (super.noSuchMethod( - Invocation.getter(#ecKeyAlgorithm), - returnValue: _i5.EcKeyAlgorithm.brainpoolp160r1, - ) - as _i5.EcKeyAlgorithm); - - @override - _i6.Algorithms get hashAlgorithm => - (super.noSuchMethod( - Invocation.getter(#hashAlgorithm), - returnValue: _i6.Algorithms.SHA1, - ) - as _i6.Algorithms); - - @override - bool get sslVerify => - (super.noSuchMethod(Invocation.getter(#sslVerify), returnValue: false) - as bool); - - @override - String get serverName => - (super.noSuchMethod( - Invocation.getter(#serverName), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#serverName), - ), - ) - as String); - - @override - _i7.FinalizationState get finalizationState => - (super.noSuchMethod( - Invocation.getter(#finalizationState), - returnValue: _i7.FinalizationState.notStarted, - ) - as _i7.FinalizationState); - - @override - _i8.SyncState get syncState => - (super.noSuchMethod( - Invocation.getter(#syncState), - returnValue: _i8.SyncState.notStarted, - ) - as _i8.SyncState); - - @override - _i9.ContainerPolicies get policies => - (super.noSuchMethod( - Invocation.getter(#policies), - returnValue: _i4.dummyValue<_i9.ContainerPolicies>( - this, - Invocation.getter(#policies), - ), - ) - as _i9.ContainerPolicies); - - @override - bool get initSynced => - (super.noSuchMethod(Invocation.getter(#initSynced), returnValue: false) - as bool); - - @override - String get publicClientKey => - (super.noSuchMethod( - Invocation.getter(#publicClientKey), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#publicClientKey), - ), - ) - as String); - - @override - String get privateClientKey => - (super.noSuchMethod( - Invocation.getter(#privateClientKey), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#privateClientKey), - ), - ) - as String); - - @override - String get $type => - (super.noSuchMethod( - Invocation.getter(#$type), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#$type), - ), - ) - as String); - - @override - _i2.$TokenContainerFinalizedCopyWith<_i2.TokenContainerFinalized> - get copyWith => - (super.noSuchMethod( - Invocation.getter(#copyWith), - returnValue: - _Fake$TokenContainerFinalizedCopyWith_2< - _i2.TokenContainerFinalized - >(this, Invocation.getter(#copyWith)), - ) - as _i2.$TokenContainerFinalizedCopyWith<_i2.TokenContainerFinalized>); - - @override - Uri get registrationUrl => - (super.noSuchMethod( - Invocation.getter(#registrationUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#registrationUrl)), - ) - as Uri); - - @override - Uri get challengeUrl => - (super.noSuchMethod( - Invocation.getter(#challengeUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#challengeUrl)), - ) - as Uri); - - @override - Uri get syncUrl => - (super.noSuchMethod( - Invocation.getter(#syncUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#syncUrl)), - ) - as Uri); - - @override - Uri get transferUrl => - (super.noSuchMethod( - Invocation.getter(#transferUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#transferUrl)), - ) - as Uri); - - @override - Uri get unregisterUrl => - (super.noSuchMethod( - Invocation.getter(#unregisterUrl), - returnValue: _FakeUri_1(this, Invocation.getter(#unregisterUrl)), - ) - as Uri); - - @override - Map toJson() => - (super.noSuchMethod( - Invocation.method(#toJson, []), - returnValue: {}, - ) - as Map); - - @override - _i2.TokenContainer withClientKeyPair( - _i10.AsymmetricKeyPair<_i10.ECPublicKey, _i10.ECPrivateKey>? keyPair, - ) => - (super.noSuchMethod( - Invocation.method(#withClientKeyPair, [keyPair]), - returnValue: _i4.dummyValue<_i2.TokenContainer>( - this, - Invocation.method(#withClientKeyPair, [keyPair]), - ), - ) - as _i2.TokenContainer); - - @override - String signMessage(String? msg) => - (super.noSuchMethod( - Invocation.method(#signMessage, [msg]), - returnValue: _i4.dummyValue( - this, - Invocation.method(#signMessage, [msg]), - ), - ) - as String); - - @override - String? trySignMessage(String? msg) => - (super.noSuchMethod(Invocation.method(#trySignMessage, [msg])) - as String?); - - @override - _i3.Token addOriginToToken({required _i3.Token? token, String? tokenData}) => - (super.noSuchMethod( - Invocation.method(#addOriginToToken, [], { - #token: token, - #tokenData: tokenData, - }), - returnValue: _FakeToken_3( - this, - Invocation.method(#addOriginToToken, [], { - #token: token, - #tokenData: tokenData, - }), - ), - ) - as _i3.Token); -} diff --git a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart b/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart similarity index 97% rename from test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart rename to test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart index 3bdac6996..6b3643252 100644 --- a/test/unit_test/utils/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart +++ b/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart @@ -20,7 +20,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart'; @@ -32,9 +31,8 @@ import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/gene import 'package:privacyidea_authenticator/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart'; import 'package:privacyidea_authenticator/widgets/button_widgets/time_guarded_button.dart'; -import 'rollover_container_tokens_button_test.mocks.dart'; +import '../../../../../tests_app_wrapper.mocks.dart'; -@GenerateMocks([TokenContainerFinalized]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); late MockTokenContainerFinalized mockContainer; diff --git a/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart new file mode 100644 index 000000000..e1b278c02 --- /dev/null +++ b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart'; +import 'package:privacyidea_authenticator/model/enums/sync_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart'; +import 'package:privacyidea_authenticator/model/token_container.dart'; +import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; +import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/has_firebase_provider.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; +import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/buttons/sync_container_button.dart'; +import 'package:privacyidea_authenticator/widgets/button_widgets/time_guarded_button.dart'; + +import '../../../../../tests_app_wrapper.dart'; +import '../../../../../tests_app_wrapper.mocks.dart'; + +class FakeTokenNotifier extends TokenNotifier { + @override + Future build({ + required FirebaseUtils firebaseUtils, + required PrivacyideaIOClient ioClient, + required TokenRepository repo, + required RsaUtils rsaUtils, + }) async => TokenState(tokens: []); +} + +class FakeTokenContainerNotifier extends TokenContainerNotifier { + final MockTokenContainerNotifier mock; + FakeTokenContainerNotifier(this.mock); + + @override + Future build({ + required TokenContainerApi containerApi, + required EccUtils eccUtils, + required TokenContainerRepository repo, + }) async => TokenContainerState(containerList: []); + + @override + Future> syncContainers({ + List? containersToSync, + bool? isInitSync, + required bool isManually, + required TokenState tokenState, + }) => mock.syncContainers( + containersToSync: containersToSync, + isInitSync: isInitSync, + isManually: isManually, + tokenState: tokenState, + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockTokenContainerFinalized mockContainer; + late MockTokenContainerNotifier mockInternalNotifier; + + setUp(() { + mockContainer = MockTokenContainerFinalized(); + mockInternalNotifier = MockTokenContainerNotifier(); + }); + + Future pumpButton( + WidgetTester tester, { + required SyncState state, + bool isPreview = false, + }) async { + when(mockContainer.syncState).thenReturn(state); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [ + tokenProvider.overrideWith(() => FakeTokenNotifier()), + tokenContainerProvider.overrideWith( + () => FakeTokenContainerNotifier(mockInternalNotifier), + ), + hasFirebaseProvider.overrideWith((ref) => Future.value(true)), + ], + child: MaterialApp( + home: Scaffold( + body: SizedBox.expand( + child: Center( + child: SyncContainerButton( + isPreview: isPreview, + container: mockContainer, + ), + ), + ), + ), + ), + ), + ); + + debugPrint('Pump complete for state: $state, isPreview: $isPreview'); + + debugPrint(tester.allWidgets.toString()); + + debugPrint( + 'Looking for TimeGuardedButton: ${find.byType(TimeGuardedButton)}', + ); + // Warte kurz auf das Layout + await tester.pumpAndSettle(); + } + + group('SyncContainerButton - Logic and States', () { + testWidgets('should only show Icon without button when isPreview is true', ( + tester, + ) async { + await pumpButton(tester, state: SyncState.notStarted, isPreview: true); + + await tester.pumpAndSettle(); + + expect(find.byType(TimeGuardedButton), findsNothing); + expect(find.byIcon(Icons.sync), findsOneWidget); + }); + + testWidgets('should be enabled when SyncState is notStarted', ( + tester, + ) async { + await pumpButton(tester, state: SyncState.notStarted); + + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNotNull); + }); + + testWidgets('should be disabled when SyncState is syncing', (tester) async { + await pumpButton(tester, state: SyncState.syncing); + + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNull); + }); + + testWidgets('should be enabled when SyncState is failed', (tester) async { + await pumpButton(tester, state: SyncState.failed); + + final button = tester.widget( + find.byType(TimeGuardedButton), + ); + expect(button.onPressed, isNotNull); + }); + + testWidgets('should call syncContainers when pressed', (tester) async { + await pumpButton(tester, state: SyncState.notStarted); + + when( + mockInternalNotifier.syncContainers( + tokenState: anyNamed('tokenState'), + containersToSync: anyNamed('containersToSync'), + isManually: anyNamed('isManually'), + isInitSync: anyNamed('isInitSync'), + ), + ).thenAnswer((_) async => {}); + + await tester.tap(find.byType(TimeGuardedButton)); + await tester.pump(); + + verify( + mockInternalNotifier.syncContainers( + tokenState: anyNamed('tokenState'), + containersToSync: [mockContainer], + isManually: true, + isInitSync: anyNamed('isInitSync'), + ), + ).called(1); + }); + }); +} diff --git a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart new file mode 100644 index 000000000..4cc9b5807 --- /dev/null +++ b/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart @@ -0,0 +1,135 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/api/interfaces/container_api.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/token_container_repository.dart'; +import 'package:privacyidea_authenticator/interfaces/repo/token_repository.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart'; +import 'package:privacyidea_authenticator/model/token_container.dart'; +import 'package:privacyidea_authenticator/model/tokens/token.dart'; +import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; +import 'package:privacyidea_authenticator/utils/firebase_utils.dart'; +import 'package:privacyidea_authenticator/utils/privacyidea_io_client.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; +import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart'; + +import '../../../../../tests_app_wrapper.dart'; +import '../../../../../tests_app_wrapper.mocks.dart'; + +class FakeTokenNotifier extends TokenNotifier { + final MockTokenNotifier mock; + FakeTokenNotifier(this.mock); + + @override + Future build({ + required FirebaseUtils firebaseUtils, + required PrivacyideaIOClient ioClient, + required TokenRepository repo, + required RsaUtils rsaUtils, + }) async => const TokenState(tokens: []); + + @override + Future removeTokens(List tokens) => mock.removeTokens(tokens); + + @override + Future> updateTokens( + List tokens, + T Function(T) update, + ) => mock.updateTokens(tokens, update); +} + +class FakeTokenContainerNotifier extends TokenContainerNotifier { + final MockTokenContainerNotifier mock; + FakeTokenContainerNotifier(this.mock); + + @override + Future build({ + required TokenContainerApi containerApi, + required EccUtils eccUtils, + required TokenContainerRepository repo, + }) async => const TokenContainerState(containerList: []); + + @override + Future unregisterDelete(TokenContainerFinalized container) => + mock.unregisterDelete(container); + + @override + Future deleteContainer(TokenContainer container) => + mock.deleteContainer(container); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockTokenContainerFinalized mockFinalizedContainer; + late MockTokenNotifier mockTokenInternal; + late MockTokenContainerNotifier mockContainerInternal; + + setUp(() { + mockFinalizedContainer = MockTokenContainerFinalized(); + mockTokenInternal = MockTokenNotifier(); + mockContainerInternal = MockTokenContainerNotifier(); + + when(mockFinalizedContainer.serial).thenReturn('finalized-serial'); + + when(mockTokenInternal.updateTokens(any, any)).thenAnswer(( + invocation, + ) async { + return invocation.positionalArguments[0] as List; + }); + }); + + Future pumpDialog(WidgetTester tester, TokenContainer container) async { + await tester.pumpWidget( + TestsAppWrapper( + overrides: [ + tokenProvider.overrideWith( + () => FakeTokenNotifier(mockTokenInternal), + ), + tokenContainerProvider.overrideWith( + () => FakeTokenContainerNotifier(mockContainerInternal), + ), + ], + child: DeleteContainerDialog(container), + ), + ); + await tester.pumpAndSettle(); + } + + group('DeleteContainerDialog - UI Tests', () { + testWidgets('should show serial in title', (tester) async { + await pumpDialog(tester, mockFinalizedContainer); + expect(find.textContaining('finalized-serial'), findsOneWidget); + }); + + testWidgets('should have cancel and delete buttons', (tester) async { + await pumpDialog(tester, mockFinalizedContainer); + + expect(find.text('Cancel'), findsOneWidget); + expect(find.text('Delete'), findsOneWidget); + }); + }); + + group('DeleteContainerDialog - Logic Tests', () { + testWidgets('should call unregisterDelete and update tokens', ( + tester, + ) async { + await pumpDialog(tester, mockFinalizedContainer); + + when( + mockContainerInternal.unregisterDelete(any), + ).thenAnswer((_) async => true); + + final deleteButton = find.text('Delete'); + await tester.tap(deleteButton); + await tester.pumpAndSettle(); + + verify( + mockContainerInternal.unregisterDelete(mockFinalizedContainer), + ).called(1); + verify(mockTokenInternal.updateTokens(any, any)).called(1); + }); + }); +} diff --git a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart new file mode 100644 index 000000000..006a11591 --- /dev/null +++ b/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart'; + +import '../../../../../tests_app_wrapper.dart'; +import '../../../../../tests_app_wrapper.mocks.dart'; + +void main() { + testWidgets('should call delete via TestsAppWrapper', (tester) async { + final mockNotifier = MockTokenContainerNotifier(); + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('ABC-123'); + when(mockNotifier.deleteContainer(any)).thenAnswer((_) async => true); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [tokenContainerProvider.overrideWith(() => mockNotifier)], + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () => + ForceDeleteContainerDialog.showDialog(mockContainer), + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + verify(mockNotifier.deleteContainer(mockContainer)).called(1); + }); +} From e172ef3d4dd09979c6e40c352e5e279d6caf1307 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:14:29 +0100 Subject: [PATCH 05/13] feat: add ForceDeleteContainerDialog with detailed documentation and implement unit tests for delete functionality --- .../delete_container_force_dialog.dart | 2 + .../model/tokens/otp_token_test.dart | 10 +- .../delete_container_force_dialog_test.dart | 42 ---- .../delete_container_dialog_test.dart | 4 +- .../delete_container_force_dialog_test.dart | 190 ++++++++++++++++++ .../delete_container_token_dialog_test.dart | 148 ++++++++++++++ 6 files changed, 348 insertions(+), 48 deletions(-) delete mode 100644 test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart rename test/unit_test/views/container_view/container_widgets/{ => dialogs}/delete_container_dialogs/delete_container_dialog_test.dart (97%) create mode 100644 test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_force_dialog_test.dart create mode 100644 test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_token_dialog_test.dart diff --git a/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart b/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart index 213608f7e..d4755f484 100644 --- a/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart +++ b/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart @@ -26,6 +26,8 @@ import '../../../../../utils/riverpod/riverpod_providers/generated_providers/tok import '../../../../../utils/view_utils.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; +/// Delete Local state of the container, without unregistering it from the server. +/// This is used when the container cannot be unregistered, e.g. when the server is not reachable, or when the container is already deleted on the server. class ForceDeleteContainerDialog extends ConsumerWidget { final TokenContainer container; diff --git a/test/unit_test/model/tokens/otp_token_test.dart b/test/unit_test/model/tokens/otp_token_test.dart index 1096db9b2..822dc04fa 100644 --- a/test/unit_test/model/tokens/otp_token_test.dart +++ b/test/unit_test/model/tokens/otp_token_test.dart @@ -237,15 +237,17 @@ void main() { ); test( - 'isSameTokenAs should not be determined when only other properties differ', + 'isSameTokenAs should not be determined when other properties differ', () { - final other = base.copyWith( - id: '2', // same id = same token + // Serial must be null to be not determined. + final baseNoSerial = base.copyWith(serial: () => null); + final other = baseNoSerial.copyWith( + id: '2', // Id must be different to not be the same token label: "different_label", isLocked: false, pin: false, ); - expect(base.isSameTokenAs(other), isNull); + expect(baseNoSerial.isSameTokenAs(other), isNull); }, ); }); diff --git a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart deleted file mode 100644 index 006a11591..000000000 --- a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_force_dialog_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; -import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart'; - -import '../../../../../tests_app_wrapper.dart'; -import '../../../../../tests_app_wrapper.mocks.dart'; - -void main() { - testWidgets('should call delete via TestsAppWrapper', (tester) async { - final mockNotifier = MockTokenContainerNotifier(); - final mockContainer = MockTokenContainerFinalized(); - when(mockContainer.serial).thenReturn('ABC-123'); - when(mockNotifier.deleteContainer(any)).thenAnswer((_) async => true); - - await tester.pumpWidget( - TestsAppWrapper( - overrides: [tokenContainerProvider.overrideWith(() => mockNotifier)], - child: MaterialApp( - home: Scaffold( - body: Builder( - builder: (context) => ElevatedButton( - onPressed: () => - ForceDeleteContainerDialog.showDialog(mockContainer), - child: const Text('Open'), - ), - ), - ), - ), - ), - ); - - await tester.tap(find.text('Open')); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete')); - await tester.pumpAndSettle(); - - verify(mockNotifier.deleteContainer(mockContainer)).called(1); - }); -} diff --git a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_dialog_test.dart similarity index 97% rename from test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart rename to test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_dialog_test.dart index 4cc9b5807..988aeb692 100644 --- a/test/unit_test/views/container_view/container_widgets/delete_container_dialogs/delete_container_dialog_test.dart +++ b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_dialog_test.dart @@ -15,8 +15,8 @@ import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/gene import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart'; -import '../../../../../tests_app_wrapper.dart'; -import '../../../../../tests_app_wrapper.mocks.dart'; +import '../../../../../../tests_app_wrapper.dart'; +import '../../../../../../tests_app_wrapper.mocks.dart'; class FakeTokenNotifier extends TokenNotifier { final MockTokenNotifier mock; diff --git a/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_force_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_force_dialog_test.dart new file mode 100644 index 000000000..a1f3f601e --- /dev/null +++ b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_force_dialog_test.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/model/riverpod_states/token_container_state.dart'; +import 'package:privacyidea_authenticator/model/token_container.dart'; +import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_force_dialog.dart'; + +import '../../../../../../tests_app_wrapper.dart'; +import '../../../../../../tests_app_wrapper.mocks.dart'; + +class FakeTokenContainerNotifier extends TokenContainerNotifier { + final TokenContainerState initialState; + final Future Function(TokenContainer) onDelete; + + FakeTokenContainerNotifier(this.initialState, this.onDelete); + + @override + Future build({ + Object? repo, + Object? containerApi, + Object? eccUtils, + }) async => initialState; + + @override + Future deleteContainer(TokenContainer container) async { + return await onDelete(container); + } +} + +void main() { + provideDummy(TokenContainerState(containerList: [])); + + testWidgets('should show correct serial in dialog title', (tester) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('UNIQUE-SERIAL-999'); + + final fakeNotifier = FakeTokenContainerNotifier( + TokenContainerState(containerList: [mockContainer]), + (_) async => true, + ); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [tokenContainerProvider.overrideWith(() => fakeNotifier)], + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () => + ForceDeleteContainerDialog.showDialog(mockContainer), + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + expect(find.textContaining('UNIQUE-SERIAL-999'), findsOneWidget); + }); + + testWidgets('should return false and pop when delete fails', (tester) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('ABC-123'); + bool? dialogResult; + + final fakeNotifier = FakeTokenContainerNotifier( + TokenContainerState(containerList: [mockContainer]), + (_) async => false, + ); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [tokenContainerProvider.overrideWith(() => fakeNotifier)], + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + dialogResult = await ForceDeleteContainerDialog.showDialog( + mockContainer, + ); + }, + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + expect(dialogResult, isFalse); + expect(find.byType(ForceDeleteContainerDialog), findsNothing); + }); + + testWidgets('should pop dialog after deleting the container', (tester) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('ABC-123'); + + var state = TokenContainerState(containerList: [mockContainer]); + var deleteCalled = false; + + final fakeNotifier = FakeTokenContainerNotifier(state, (container) async { + deleteCalled = true; + state = state.copyWith( + containerList: state.containerList + .where((c) => c.serial != 'ABC-123') + .toList(), + ); + return true; + }); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [tokenContainerProvider.overrideWith(() => fakeNotifier)], + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () => + ForceDeleteContainerDialog.showDialog(mockContainer), + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + expect(deleteCalled, isTrue); + expect(state.containerList, isEmpty); + expect(find.textContaining('ABC-123'), findsNothing); + }); + + testWidgets('should pop dialog when pressing cancel', (tester) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('ABC-123'); + + var deleteCalled = false; + final fakeNotifier = FakeTokenContainerNotifier( + TokenContainerState(containerList: [mockContainer]), + (_) async { + deleteCalled = true; + return true; + }, + ); + + await tester.pumpWidget( + TestsAppWrapper( + overrides: [tokenContainerProvider.overrideWith(() => fakeNotifier)], + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () => + ForceDeleteContainerDialog.showDialog(mockContainer), + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + expect(deleteCalled, isFalse); + expect(find.byType(ForceDeleteContainerDialog), findsNothing); + }); +} diff --git a/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_token_dialog_test.dart b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_token_dialog_test.dart new file mode 100644 index 000000000..5825b8d12 --- /dev/null +++ b/test/unit_test/views/container_view/container_widgets/dialogs/delete_container_dialogs/delete_container_token_dialog_test.dart @@ -0,0 +1,148 @@ +/* + * privacyIDEA Authenticator + * + * Author: Frank Merkel + * + * Copyright (c) 2026 NetKnights GmbH + * + * 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 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_token_dialog.dart'; + +import '../../../../../../tests_app_wrapper.dart'; +import '../../../../../../tests_app_wrapper.mocks.dart'; + +void main() { + testWidgets('should return true when "Delete All" is pressed', ( + tester, + ) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('SERIAL-123'); + bool? result; + + await tester.pumpWidget( + TestsAppWrapper( + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + result = await DeleteContainerTokenDialog.showDialog( + mockContainer, + ); + }, + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + final BuildContext context = tester.element( + find.byType(DeleteContainerTokenDialog), + ); + final localizations = AppLocalizations.of(context)!; + + await tester.tap(find.text(localizations.deleteAllButtonText)); + await tester.pumpAndSettle(); + + expect(result, isTrue); + expect(find.byType(DeleteContainerTokenDialog), findsNothing); + }); + + testWidgets('should return false when "Delete Only Container" is pressed', ( + tester, + ) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('SERIAL-123'); + bool? result; + + await tester.pumpWidget( + TestsAppWrapper( + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + result = await DeleteContainerTokenDialog.showDialog( + mockContainer, + ); + }, + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + final BuildContext context = tester.element( + find.byType(DeleteContainerTokenDialog), + ); + final localizations = AppLocalizations.of(context)!; + + await tester.tap(find.text(localizations.deleteOnlyContainerButtonText)); + await tester.pumpAndSettle(); + + expect(result, isFalse); + expect(find.byType(DeleteContainerTokenDialog), findsNothing); + }); + + testWidgets('should return null when closed via close button', ( + tester, + ) async { + final mockContainer = MockTokenContainerFinalized(); + when(mockContainer.serial).thenReturn('SERIAL-123'); + bool? result = false; + + await tester.pumpWidget( + TestsAppWrapper( + child: MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) => ElevatedButton( + onPressed: () async { + result = await DeleteContainerTokenDialog.showDialog( + mockContainer, + ); + }, + child: const Text('Open'), + ), + ), + ), + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.close)); + await tester.pumpAndSettle(); + + expect(result, isNull); + expect(find.byType(DeleteContainerTokenDialog), findsNothing); + }); +} From e1dd964b12c25b3fa4d03838c271b8b15c9b5414 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:58:16 +0100 Subject: [PATCH 06/13] feat: enhance DefaultDeleteAction dialog to use TokenNotifier for token removal --- .../default_delete_action.dart | 113 ++++++++---------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart index 23b68a55c..d7b4f8968 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart @@ -24,11 +24,9 @@ import 'package:privacyidea_authenticator/utils/view_utils.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/token.dart'; import '../../../../../utils/customization/theme_extentions/action_theme.dart'; -import '../../../../../utils/globals.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; -import '../../loading_indicator.dart'; import '../slideable_action.dart'; import 'container_token_indelible_dialog.dart'; @@ -53,6 +51,7 @@ class DefaultDeleteAction extends ConsumerSlideableAction { ).extension()!.actionForegroundColor, onPressed: isEnabled ? (_) async { + final notifier = ref.read(tokenProvider.notifier); if (token.isLocked && !await lockAuth( reason: (localization) => localization.deleteLockedToken, @@ -61,7 +60,9 @@ class DefaultDeleteAction extends ConsumerSlideableAction { )) { return; } - _showDialog(); + if (context.mounted) { + _showDialog(context, notifier); + } } : (_) => ContainerTokenIndelibleDialog.showDialog(), child: Column( @@ -79,70 +80,50 @@ class DefaultDeleteAction extends ConsumerSlideableAction { ); } - void _showDialog() => showAsyncDialog( - builder: (BuildContext context) => DefaultDialog( - scrollable: true, - title: Text( - AppLocalizations.of(context)!.confirmDeletion, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ), - content: Column( - children: [ - Text( - AppLocalizations.of(context)!.confirmDeletionOf(token.label), - style: Theme.of(context).textTheme.bodyMedium, + void _showDialog(BuildContext context, TokenNotifier notifier) => + showAsyncDialog( + builder: (BuildContext context) => DefaultDialog( + scrollable: true, + title: Text( + AppLocalizations.of(context)!.confirmDeletion, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), ), - const SizedBox(height: 8), - Text( - AppLocalizations.of(context)!.confirmTokenDeletionHint, - style: Theme.of(context).textTheme.bodySmall, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.confirmDeletionOf(token.label), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 8), + Text( + AppLocalizations.of(context)!.confirmTokenDeletionHint, + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), - ], - ), - actions: [ - // TextButton( - // onPressed: () => Navigator.of(context).pop(), - // child: Text( - // AppLocalizations.of(context)!.cancel, - // overflow: TextOverflow.fade, - // softWrap: false, - // ), - // ), - // TextButton( - // onPressed: () { - // LoadingIndicator.show( - // context: context, - // action: () async => - // globalRef?.read(tokenProvider.notifier).removeToken(token), - // ); - // Navigator.of(context).pop(); - // }, - // child: Text( - // AppLocalizations.of(context)!.delete, - // style: TextStyle(color: Theme.of(context).colorScheme.error), - // overflow: TextOverflow.fade, - // softWrap: false, - // ), - // ), - DialogAction( - label: AppLocalizations.of(context)!.cancel, - intent: DialogActionIntent.cancel, - onPressed: () => Navigator.of(context).pop(), - ), - DialogAction( - label: AppLocalizations.of(context)!.delete, - intent: DialogActionIntent.destructive, - onPressed: () { - LoadingIndicator.show( - context: context, - action: () async => - globalRef?.read(tokenProvider.notifier).removeToken(token), - ); - }, + actions: [ + DialogAction( + label: AppLocalizations.of(context)!.cancel, + intent: DialogActionIntent.cancel, + onPressed: () => Navigator.of(context).pop(), + ), + DialogAction( + label: AppLocalizations.of(context)!.delete, + intent: DialogActionIntent.destructive, + onPressed: () async { + try { + await notifier.removeToken(token); + } finally { + if (context.mounted) { + Navigator.of(context).pop(); + } + } + }, + ), + ], ), - ], - ), - ); + ); } From 861aec2a3838468acc4af4f925d98b40fb1a3133 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:43:20 +0200 Subject: [PATCH 07/13] Refactor dialog widgets and improve code readability - Removed unused imports and unnecessary code in dialog widgets. - Simplified button logic in DefaultDialog by integrating timed actions directly into IntentButton. - Cleaned up PushCodeToPhoneDialog by removing visibility handling and unnecessary state variables. - Updated PushDefaultDialog to remove redundant intent assignment. - Enhanced PushRequestDialog by removing unused imports. - Improved formatting and readability in various widget files, including DotIndicator, DragItemScroller, and home widget classes. - Refactored test cases for better clarity and consistency in token state tests. --- analysis_options.yaml | 5 +- .../deep_link_listener.dart | 13 +- lib/mains/main_customizer.dart | 1 - lib/mains/main_netknights.dart | 3 +- lib/model/container_policies.dart | 7 +- lib/model/deeplink.dart | 3 +- .../localized_argument_error.dart | 32 +- .../exception_errors/localized_exception.dart | 5 +- .../exception_errors/response_error.dart | 30 +- .../enums/algorithms_extension.dart | 68 ++++- lib/model/pi_server_response.freezed.dart | 4 +- lib/model/processor_result.freezed.dart | 4 +- .../progress_state.freezed.dart | 4 +- lib/model/token_container.dart | 2 - lib/model/token_container.freezed.dart | 10 +- .../token_import/token_import_entry.dart | 21 +- lib/model/token_import/token_origin_data.dart | 96 +++--- lib/model/token_template.freezed.dart | 4 +- lib/model/widget_image.dart | 29 +- .../home_widget_processor.dart | 75 ++++- ...navigation_scheme_processor_interface.dart | 28 +- .../preference_introduction_repository.dart | 37 ++- lib/repo/preference_settings_repository.dart | 64 ++-- .../preference_token_folder_repository.dart | 10 +- lib/repo/secure_storage.dart | 32 +- .../unscaled_animation_controller.dart | 62 ++-- lib/utils/custom_int_buffer.dart | 6 +- .../theme_extentions/action_theme.dart | 96 ++++-- .../theme_extentions/app_dimensions.dart | 2 +- .../elevated_delete_button_theme.dart | 68 +++-- .../theme_extentions/extended_text_theme.dart | 28 +- .../theme_extentions/push_request_theme.dart | 13 +- .../theme_extentions/status_colors.dart | 16 +- lib/utils/default_inkwell.dart | 30 +- .../app_customization_notifier.dart | 14 +- .../deeplink_notifier.dart | 16 +- .../state_notifiers/deeplink_notifier.dart | 16 +- .../add_daypassword_manually.dart | 69 +++-- .../add_hotp_manually.dart | 58 ++-- .../add_steam_manually.dart | 82 +++-- .../add_token_manually_interface.dart | 24 +- lib/views/container_view/container_view.dart | 9 +- .../rollover_container_tokens_button.dart | 3 +- .../buttons/sync_container_button.dart | 29 +- .../delete_container_action.dart | 23 +- .../details_container_action.dart | 25 +- .../transfer_container_action.dart | 25 +- .../container_widgets/container_widget.dart | 18 +- .../container_widget_tile.dart | 111 +++---- .../container_widget_tile_trailing.dart | 3 +- lib/views/feedback_view/feedback_view.dart | 167 ++++++----- .../link_home_widget_view.dart | 24 +- lib/views/main_view/main_view.dart | 69 +++-- .../main_view_widgets/app_bar_item.dart | 29 +- .../connectivity_listener.dart | 8 +- .../custom_paint_navigation_bar.dart | 76 ++++- .../main_view_widgets/expandable_appbar.dart | 12 +- .../filter_token_widget.dart | 31 +- .../lock_token_folder_action.dart | 15 +- .../token_folder_expandable_body.dart | 53 ++-- .../token_folder_expandable_header_icon.dart | 30 +- .../folder_widgets/token_folder_widget.dart | 24 +- .../main_view_background_image.dart | 18 +- .../license_push_view_button.dart | 49 +-- .../qr_scanner_button.dart | 69 +++-- .../main_view_tokens_list_filtered.dart | 27 +- .../main_view_widgets/no_token_screen.dart | 2 +- .../container_token_sync_icon.dart | 8 +- .../day_password_token_widget.dart | 5 +- .../day_password_token_widget_tile.dart | 103 +++++-- .../default_delete_action.dart | 90 +++--- .../hotp_token_widgets/hotp_token_widget.dart | 16 +- .../hotp_token_widget_tile.dart | 13 +- .../push_token_widgets/push_token_widget.dart | 37 ++- .../push_token_widget_tile.dart | 22 +- .../rollout_failed_widget.dart | 6 +- .../push_token_widgets/rollout_widget.dart | 24 +- .../token_widgets/token_image.dart | 32 +- .../token_widgets/token_widget_base.dart | 69 +++-- .../token_widgets/token_widget_tile.dart | 30 +- .../totp_token_widget_tile.dart | 64 ++-- .../totp_token_widget_tile_countdown.dart | 10 +- .../app_wrappers/single_touch_recognizer.dart | 9 +- lib/widgets/button_widgets/intent_button.dart | 189 ++++++++---- lib/widgets/button_widgets/mutex_button.dart | 37 ++- .../button_widgets/push_action_button.dart | 3 +- .../button_widgets/time_guarded_button.dart | 281 +++++++++--------- lib/widgets/custom_texts.dart | 12 +- lib/widgets/custom_trailing.dart | 36 ++- lib/widgets/deactivateable.dart | 11 +- lib/widgets/default_refresh_indicator.dart | 31 +- .../dialog_widgets/default_dialog.dart | 15 +- .../push_code_to_phone_dialog.dart | 101 ++----- .../push_default_dialog.dart | 1 - .../push_request_dialog.dart | 1 - lib/widgets/dot_indicator.dart | 8 +- lib/widgets/drag_item_scroller.dart | 140 ++++++--- lib/widgets/hideable_widget_.dart | 3 +- .../home_widgets/home_widget_action.dart | 47 +-- .../home_widgets/home_widget_background.dart | 31 +- .../home_widgets/home_widget_configure.dart | 22 +- .../home_widgets/home_widget_copied.dart | 41 +-- .../home_widgets/home_widget_hidden.dart | 26 +- lib/widgets/home_widgets/home_widget_otp.dart | 26 +- .../home_widgets/home_widget_unlinked.dart | 48 +-- .../token_introduction.dart | 12 +- .../model/extensions/sortable_list_test.dart | 24 +- .../model/mixins/sortable_mixin_test.dart | 12 +- .../model/states/token_state_test.dart | 59 +++- 109 files changed, 2391 insertions(+), 1500 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 5c6bc0fa3..caa5e0007 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,4 +6,7 @@ include: package:flutter_lints/flutter.yaml linter: rules: unnecessary_string_escapes: false - unnecessary_string_interpolations: false \ No newline at end of file + unnecessary_string_interpolations: false + unawaited_futures: true + avoid_redundant_argument_values: true + avoid_void_async: true diff --git a/lib/interfaces/riverpod/state_listeners/state_notifier_provider_listeners/deep_link_listener.dart b/lib/interfaces/riverpod/state_listeners/state_notifier_provider_listeners/deep_link_listener.dart index 05a4766d0..c8e167b39 100644 --- a/lib/interfaces/riverpod/state_listeners/state_notifier_provider_listeners/deep_link_listener.dart +++ b/lib/interfaces/riverpod/state_listeners/state_notifier_provider_listeners/deep_link_listener.dart @@ -27,14 +27,21 @@ import '../../../../utils/logger.dart'; import '../base_listeners/stream_notifier_listener.dart'; // extends $StreamNotifierProvider -abstract class DeepLinkListener extends BuildlessStreamNotifierListener { - const DeepLinkListener({required super.provider, required super.onNewState, required super.listenerName}); +abstract class DeepLinkListener + extends BuildlessStreamNotifierListener { + const DeepLinkListener({ + required super.provider, + required super.onNewState, + required super.listenerName, + }); @override void buildListen(WidgetRef ref) { Logger.debug('("$listenerName") listening to provider ("$provider")'); ref.listen(provider, (previous, next) { - WidgetsBinding.instance.addPostFrameCallback((_) => onNewState(ref, previous, next)); + WidgetsBinding.instance.addPostFrameCallback( + (_) => onNewState(ref, previous, next), + ); }); } } diff --git a/lib/mains/main_customizer.dart b/lib/mains/main_customizer.dart index fbc5b37d6..6334ac2f4 100644 --- a/lib/mains/main_customizer.dart +++ b/lib/mains/main_customizer.dart @@ -84,7 +84,6 @@ class CustomizationAuthenticator extends ConsumerWidget { PointerDeviceKind.unknown, }, ), - debugShowCheckedModeBanner: true, navigatorKey: globalNavigatorKey, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/lib/mains/main_netknights.dart b/lib/mains/main_netknights.dart index af83af639..fdcd47dca 100644 --- a/lib/mains/main_netknights.dart +++ b/lib/mains/main_netknights.dart @@ -4,7 +4,7 @@ Authors: Timo Sturm Frank Merkel - Copyright (c) 2017-2025 NetKnights GmbH + Copyright (c) 2017-2026 NetKnights GmbH Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. @@ -103,7 +103,6 @@ class PrivacyIDEAAuthenticator extends ConsumerWidget { scrollBehavior: ScrollConfiguration.of( context, ).copyWith(physics: const ClampingScrollPhysics(), overscroll: false), - debugShowCheckedModeBanner: true, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: diff --git a/lib/model/container_policies.dart b/lib/model/container_policies.dart index 475a453b5..3c3340580 100644 --- a/lib/model/container_policies.dart +++ b/lib/model/container_policies.dart @@ -25,12 +25,7 @@ import '../utils/object_validator/object_validators.dart'; part 'container_policies.freezed.dart'; part 'container_policies.g.dart'; -@Freezed( - toStringOverride: false, - addImplicitFinal: true, - toJson: true, - fromJson: true, -) +@Freezed(toStringOverride: false, toJson: true, fromJson: true) sealed class ContainerPolicies with _$ContainerPolicies { static const DISABLED_UNREGISTER = 'disable_client_container_unregister'; static const DISABLED_TOKEN_DELETION = 'disable_client_token_deletion'; diff --git a/lib/model/deeplink.dart b/lib/model/deeplink.dart index 7133e0d24..bb12f0128 100644 --- a/lib/model/deeplink.dart +++ b/lib/model/deeplink.dart @@ -24,7 +24,8 @@ class DeepLink { const DeepLink(this.uri, {this.fromInit = false}); @override - bool operator ==(Object other) => other is DeepLink && other.uri == uri && other.fromInit == fromInit; + bool operator ==(Object other) => + other is DeepLink && other.uri == uri && other.fromInit == fromInit; @override int get hashCode => Object.hash(uri, fromInit); diff --git a/lib/model/exception_errors/localized_argument_error.dart b/lib/model/exception_errors/localized_argument_error.dart index be6757ff6..fa50c1b2d 100644 --- a/lib/model/exception_errors/localized_argument_error.dart +++ b/lib/model/exception_errors/localized_argument_error.dart @@ -20,25 +20,31 @@ import '../../l10n/app_localizations.dart'; import 'localized_exception.dart'; -class LocalizedArgumentError extends LocalizedException implements ArgumentError { +class LocalizedArgumentError extends LocalizedException + implements ArgumentError { final String _invalidValue; final String? _name; final StackTrace? _stackTrace; factory LocalizedArgumentError({ - required String Function(AppLocalizations localizations, String valueString, String name) localizedMessage, + required String Function( + AppLocalizations localizations, + String valueString, + String name, + ) + localizedMessage, required String unlocalizedMessage, required String invalidValue, required String name, StackTrace? stackTrace, - }) => - LocalizedArgumentError._( - unlocalizedMessage: unlocalizedMessage, - localizedMessage: (localizations) => localizedMessage(localizations, invalidValue, name), - invalidValue: invalidValue, - name: name, - stackTrace: stackTrace, - ); + }) => LocalizedArgumentError._( + unlocalizedMessage: unlocalizedMessage, + localizedMessage: (localizations) => + localizedMessage(localizations, invalidValue, name), + invalidValue: invalidValue, + name: name, + stackTrace: stackTrace, + ); const LocalizedArgumentError._({ required super.unlocalizedMessage, @@ -46,9 +52,9 @@ class LocalizedArgumentError extends LocalizedException implements ArgumentError required dynamic invalidValue, String? name, StackTrace? stackTrace, - }) : _invalidValue = invalidValue, - _name = name, - _stackTrace = stackTrace; + }) : _invalidValue = invalidValue, + _name = name, + _stackTrace = stackTrace; @override dynamic get invalidValue => _invalidValue; diff --git a/lib/model/exception_errors/localized_exception.dart b/lib/model/exception_errors/localized_exception.dart index ba2cf16a5..d39b377f7 100644 --- a/lib/model/exception_errors/localized_exception.dart +++ b/lib/model/exception_errors/localized_exception.dart @@ -24,7 +24,10 @@ class LocalizedException implements Exception { final String Function(AppLocalizations localizations) localizedMessage; final String unlocalizedMessage; - const LocalizedException({required this.localizedMessage, required this.unlocalizedMessage}); + const LocalizedException({ + required this.localizedMessage, + required this.unlocalizedMessage, + }); @override String toString() => 'Exception: $unlocalizedMessage'; diff --git a/lib/model/exception_errors/response_error.dart b/lib/model/exception_errors/response_error.dart index e65fde21e..312ce711c 100644 --- a/lib/model/exception_errors/response_error.dart +++ b/lib/model/exception_errors/response_error.dart @@ -25,20 +25,32 @@ class ResponseError { final int _statusCode; int get statusCode => _statusCode; final String _message; - String get message => _message.substring(0, _message.length > 100 ? 100 : _message.length); + String get message => + _message.substring(0, _message.length > 100 ? 100 : _message.length); String get fullMessage => _message; const ResponseError._(int statusCode, String message) - : _statusCode = statusCode, - _message = message; -//405 Method Not Allowed -//Method Not Allowed + : _statusCode = statusCode, + _message = message; + //405 Method Not Allowed + //Method Not Allowed factory ResponseError(Response response) { - assert(HttpStatusChecker.isError(response.statusCode), 'Status code of an response error should not be 200'); + assert( + HttpStatusChecker.isError(response.statusCode), + 'Status code of an response error should not be 200', + ); final regexpCode = RegExp(r'(\d{3})'); - final regexpMessage = RegExp(r'(?<=(<title>\d* ?))[A-Za-z][A-Za-z\s]*(?=)'); - final message = regexpMessage.firstMatch(response.body)?.group(0) ?? response.body; - final statusCode = int.tryParse(regexpCode.firstMatch(response.body)?.group(1) ?? response.statusCode.toString()) ?? response.statusCode; + final regexpMessage = RegExp( + r'(?<=(\d* ?))[A-Za-z][A-Za-z\s]*(?=)', + ); + final message = + regexpMessage.firstMatch(response.body)?.group(0) ?? response.body; + final statusCode = + int.tryParse( + regexpCode.firstMatch(response.body)?.group(1) ?? + response.statusCode.toString(), + ) ?? + response.statusCode; return ResponseError._(statusCode, message); } diff --git a/lib/model/extensions/enums/algorithms_extension.dart b/lib/model/extensions/enums/algorithms_extension.dart index 03393efc8..2992fda8e 100644 --- a/lib/model/extensions/enums/algorithms_extension.dart +++ b/lib/model/extensions/enums/algorithms_extension.dart @@ -1,9 +1,11 @@ +// ignore_for_file: avoid_redundant_argument_values + /* * privacyIDEA Authenticator * * Author: Frank Merkel * - * Copyright (c) 2025 NetKnights GmbH + * Copyright (c) 2025-2026 NetKnights GmbH * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -31,15 +33,32 @@ extension AlgorithmsX on Algorithms { required int length, required Duration interval, bool isGoogle = true, - }) => - switch (this) { - Algorithms.SHA1 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA1, isGoogle: isGoogle), - Algorithms.SHA256 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA256, isGoogle: isGoogle), - Algorithms.SHA512 => OTP.generateTOTPCodeString(secret, time.millisecondsSinceEpoch, - length: length, interval: interval.inSeconds, algorithm: Algorithm.SHA512, isGoogle: isGoogle), - }; + }) => switch (this) { + Algorithms.SHA1 => OTP.generateTOTPCodeString( + secret, + time.millisecondsSinceEpoch, + length: length, + interval: interval.inSeconds, + algorithm: Algorithm.SHA1, + isGoogle: isGoogle, + ), + Algorithms.SHA256 => OTP.generateTOTPCodeString( + secret, + time.millisecondsSinceEpoch, + length: length, + interval: interval.inSeconds, + algorithm: Algorithm.SHA256, + isGoogle: isGoogle, + ), + Algorithms.SHA512 => OTP.generateTOTPCodeString( + secret, + time.millisecondsSinceEpoch, + length: length, + interval: interval.inSeconds, + algorithm: Algorithm.SHA512, + isGoogle: isGoogle, + ), + }; /// Generates a Counter-based one time password code and return as a 0 padded string. /// If isGoogle is true, the secret will be decoded as base32, otherwise it will be decoded as utf8. @@ -48,10 +67,27 @@ extension AlgorithmsX on Algorithms { required int counter, required int length, bool isGoogle = true, - }) => - switch (this) { - Algorithms.SHA1 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA1, isGoogle: isGoogle), - Algorithms.SHA256 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA256, isGoogle: isGoogle), - Algorithms.SHA512 => OTP.generateHOTPCodeString(secret, counter, length: length, algorithm: Algorithm.SHA512, isGoogle: isGoogle), - }; + }) => switch (this) { + Algorithms.SHA1 => OTP.generateHOTPCodeString( + secret, + counter, + length: length, + algorithm: Algorithm.SHA1, + isGoogle: isGoogle, + ), + Algorithms.SHA256 => OTP.generateHOTPCodeString( + secret, + counter, + length: length, + algorithm: Algorithm.SHA256, + isGoogle: isGoogle, + ), + Algorithms.SHA512 => OTP.generateHOTPCodeString( + secret, + counter, + length: length, + algorithm: Algorithm.SHA512, + isGoogle: isGoogle, + ), + }; } diff --git a/lib/model/pi_server_response.freezed.dart b/lib/model/pi_server_response.freezed.dart index 2b0125630..6a71fa4ab 100644 --- a/lib/model/pi_server_response.freezed.dart +++ b/lib/model/pi_server_response.freezed.dart @@ -173,7 +173,7 @@ return error(_that.statusCode,_that.id,_that.jsonrpc,_that.detail,_that.piServer class PiSuccessResponse extends PiServerResponse { PiSuccessResponse({required this.statusCode, required this.id, required this.jsonrpc, required this.result, required this.time, required this.version, required this.versionNumber, this.signature = null, this.detail = null}): super._(); - + @override final int statusCode; @override final int id; @@ -213,7 +213,7 @@ String toString() { class PiErrorResponse extends PiServerResponse { PiErrorResponse({required this.statusCode, required this.id, required this.jsonrpc, this.detail = null, required this.piServerResultError, required this.time, required this.version, required this.versionNumber, this.signature = null}): super._(); - + @override final int statusCode; @override final int id; diff --git a/lib/model/processor_result.freezed.dart b/lib/model/processor_result.freezed.dart index 2ffd85909..810efdf95 100644 --- a/lib/model/processor_result.freezed.dart +++ b/lib/model/processor_result.freezed.dart @@ -213,7 +213,7 @@ return failed(_that.message,_that.error,_that.resultHandlerType);case _: class ProcessorResultSuccess extends ProcessorResult { const ProcessorResultSuccess(this.resultData, {this.resultHandlerType}): super._(); - + final T resultData; @override final RequiredObjectValidator? resultHandlerType; @@ -281,7 +281,7 @@ as RequiredObjectValidator?, class ProcessorResultFailed extends ProcessorResult { const ProcessorResultFailed(this.message, {this.error, this.resultHandlerType}): super._(); - + final String Function(AppLocalizations) message; final dynamic error; diff --git a/lib/model/riverpod_states/progress_state.freezed.dart b/lib/model/riverpod_states/progress_state.freezed.dart index 4a0bf9e41..0719afdca 100644 --- a/lib/model/riverpod_states/progress_state.freezed.dart +++ b/lib/model/riverpod_states/progress_state.freezed.dart @@ -214,7 +214,7 @@ return $default(_that.max,_that.value);case _: class ProgressStateUninitialized extends ProgressState { const ProgressStateUninitialized({this.max = 0, this.value = 0}): super._(); - + @override@JsonKey() final int max; @override@JsonKey() final int value; @@ -282,7 +282,7 @@ as int, class _ProgressState extends ProgressState { const _ProgressState({required this.max, required this.value}): assert(max >= 0, 'max must be greater than or equal to 0'),assert(value <= max, 'value must be less than or equal to max'),super._(); - + @override final int max; @override final int value; diff --git a/lib/model/token_container.dart b/lib/model/token_container.dart index d8adb4952..97b9df212 100644 --- a/lib/model/token_container.dart +++ b/lib/model/token_container.dart @@ -216,10 +216,8 @@ sealed class TokenContainer with _$TokenContainer { hashAlgorithm: hashAlgorithm, sslVerify: sslVerify, passphraseQuestion: passphraseQuestion, - finalizationState: FinalizationState.completed, serverName: serverName, policies: policies, - syncState: SyncState.notStarted, publicClientKey: publicClientKey ?? eccUtils.serializeECPublicKey(clientKeyPair!.publicKey), diff --git a/lib/model/token_container.freezed.dart b/lib/model/token_container.freezed.dart index f526ab7a0..94b79df56 100644 --- a/lib/model/token_container.freezed.dart +++ b/lib/model/token_container.freezed.dart @@ -23,7 +23,7 @@ TokenContainer _$TokenContainerFromJson( return TokenContainerFinalized.fromJson( json ); - + default: throw CheckedFromJsonException( json, @@ -32,7 +32,7 @@ TokenContainer _$TokenContainerFromJson( 'Invalid union type "${json['runtimeType']}"!' ); } - + } /// @nodoc @@ -108,7 +108,7 @@ as String, @override @pragma('vm:prefer-inline') $ContainerPoliciesCopyWith<$Res> get policies { - + return $ContainerPoliciesCopyWith<$Res>(_self.policies, (value) { return _then(_self.copyWith(policies: value)); }); @@ -349,7 +349,7 @@ as bool, @override @pragma('vm:prefer-inline') $ContainerPoliciesCopyWith<$Res> get policies { - + return $ContainerPoliciesCopyWith<$Res>(_self.policies, (value) { return _then(_self.copyWith(policies: value)); }); @@ -457,7 +457,7 @@ as String, @override @pragma('vm:prefer-inline') $ContainerPoliciesCopyWith<$Res> get policies { - + return $ContainerPoliciesCopyWith<$Res>(_self.policies, (value) { return _then(_self.copyWith(policies: value)); }); diff --git a/lib/model/token_import/token_import_entry.dart b/lib/model/token_import/token_import_entry.dart index eb2226e13..7c5fda2a4 100644 --- a/lib/model/token_import/token_import_entry.dart +++ b/lib/model/token_import/token_import_entry.dart @@ -24,19 +24,17 @@ class TokenImportEntry { final Token? oldToken; Token? selectedToken; - TokenImportEntry._( - this.newToken, - this.oldToken, - this.selectedToken, - ); + TokenImportEntry._(this.newToken, this.oldToken, this.selectedToken); - TokenImportEntry({ - required this.newToken, - this.oldToken, - }) : selectedToken = oldToken == null ? newToken : null; + TokenImportEntry({required this.newToken, this.oldToken}) + : selectedToken = oldToken == null ? newToken : null; TokenImportEntry copySelect(Token? selectedToken) { - assert(selectedToken == null || selectedToken == newToken || selectedToken == oldToken); + assert( + selectedToken == null || + selectedToken == newToken || + selectedToken == oldToken, + ); return TokenImportEntry._(newToken, oldToken, selectedToken); } @@ -53,5 +51,6 @@ class TokenImportEntry { int get hashCode => Object.hashAll([newToken, oldToken, selectedToken]); @override - String toString() => 'TokenImportEntry{newToken: $newToken, \noldToken: $oldToken, \nselectedToken: $selectedToken}'; + String toString() => + 'TokenImportEntry{newToken: $newToken, \noldToken: $oldToken, \nselectedToken: $selectedToken}'; } diff --git a/lib/model/token_import/token_origin_data.dart b/lib/model/token_import/token_origin_data.dart index 72add8f2e..1436006f0 100644 --- a/lib/model/token_import/token_origin_data.dart +++ b/lib/model/token_import/token_origin_data.dart @@ -29,18 +29,22 @@ part 'token_origin_data.g.dart'; class TokenOriginData { final TokenOriginSourceType source; final String appName; // Name of the app where the token comes from. - final String data; // The data that was used to create the token. Contains the secret!! - final DateTime createdAt; // The time when the token was created. If imported from another app, this is the time of the import - final bool? isPrivacyIdeaToken; // True if the token was created by a privacyIDEA server. Null if unknown. False if not created by a privacyIDEA server + final String + data; // The data that was used to create the token. Contains the secret!! + final DateTime + createdAt; // The time when the token was created. If imported from another app, this is the time of the import + final bool? + isPrivacyIdeaToken; // True if the token was created by a privacyIDEA server. Null if unknown. False if not created by a privacyIDEA server bool get isExportable { if (isPrivacyIdeaToken == false) return true; if (source == TokenOriginSourceType.manually) return true; return false; } - final String? creator; // like issuer, but only for privacyIDEA servers. This is only set if the token was created by a privacyIDEA server + final String? + creator; // like issuer, but only for privacyIDEA servers. This is only set if the token was created by a privacyIDEA server final Version? - piServerVersion; // The version of the privacyIDEA server that created the token. This is only set if the token was created by a privacyIDEA server + piServerVersion; // The version of the privacyIDEA server that created the token. This is only set if the token was created by a privacyIDEA server const TokenOriginData._({ required this.source, required this.appName, @@ -59,16 +63,15 @@ class TokenOriginData { bool? isPrivacyIdeaToken, String? creator, Version? piServerVersion, - }) => - TokenOriginData._( - source: source, - appName: appName, - data: data, - createdAt: createdAt ?? DateTime.now(), - isPrivacyIdeaToken: isPrivacyIdeaToken, - creator: creator, - piServerVersion: piServerVersion, - ); + }) => TokenOriginData._( + source: source, + appName: appName, + data: data, + createdAt: createdAt ?? DateTime.now(), + isPrivacyIdeaToken: isPrivacyIdeaToken, + creator: creator, + piServerVersion: piServerVersion, + ); @override bool operator ==(Object other) { @@ -91,40 +94,55 @@ class TokenOriginData { bool? Function()? isPrivacyIdeaToken, String? Function()? creator, Version? Function()? piServerVersion, - }) => - TokenOriginData( - source: source ?? this.source, - data: data ?? this.data, - appName: appName ?? this.appName, - createdAt: createdAt ?? this.createdAt, - isPrivacyIdeaToken: isPrivacyIdeaToken != null ? isPrivacyIdeaToken() : this.isPrivacyIdeaToken, - creator: creator != null ? creator() : this.creator, - piServerVersion: piServerVersion != null ? piServerVersion() : this.piServerVersion, - ); + }) => TokenOriginData( + source: source ?? this.source, + data: data ?? this.data, + appName: appName ?? this.appName, + createdAt: createdAt ?? this.createdAt, + isPrivacyIdeaToken: isPrivacyIdeaToken != null + ? isPrivacyIdeaToken() + : this.isPrivacyIdeaToken, + creator: creator != null ? creator() : this.creator, + piServerVersion: piServerVersion != null + ? piServerVersion() + : this.piServerVersion, + ); @override - int get hashCode => Object.hashAll([source, data, appName, isPrivacyIdeaToken, creator, createdAt, piServerVersion]); + int get hashCode => Object.hashAll([ + source, + data, + appName, + isPrivacyIdeaToken, + creator, + createdAt, + piServerVersion, + ]); // toString prints not data because it contains the secret @override String toString() => 'TokenOrigin{source: $source, appName: $appName, isPrivacyIdeaToken: $isPrivacyIdeaToken, creator: $creator, createdAt: $createdAt, piServerVersion: $piServerVersion}'; - factory TokenOriginData.fromJson(Map json) => _$TokenOriginDataFromJson(json); + factory TokenOriginData.fromJson(Map json) => + _$TokenOriginDataFromJson(json); Map toJson() => _$TokenOriginDataToJson(this); - factory TokenOriginData.fromContainer({required TokenContainer container, required String tokenData}) => TokenOriginData( - source: TokenOriginSourceType.container, - appName: container.issuer, - data: tokenData, - createdAt: DateTime.now(), - isPrivacyIdeaToken: true, - ); + factory TokenOriginData.fromContainer({ + required TokenContainer container, + required String tokenData, + }) => TokenOriginData( + source: TokenOriginSourceType.container, + appName: container.issuer, + data: tokenData, + createdAt: DateTime.now(), + isPrivacyIdeaToken: true, + ); factory TokenOriginData.unknown([dynamic data]) => TokenOriginData( - source: TokenOriginSourceType.unknown, - appName: 'Unknown', - data: data.toString(), - createdAt: DateTime.now(), - ); + source: TokenOriginSourceType.unknown, + appName: 'Unknown', + data: data.toString(), + createdAt: DateTime.now(), + ); } diff --git a/lib/model/token_template.freezed.dart b/lib/model/token_template.freezed.dart index 9bdad96d6..d726e6228 100644 --- a/lib/model/token_template.freezed.dart +++ b/lib/model/token_template.freezed.dart @@ -23,7 +23,7 @@ TokenTemplate _$TokenTemplateFromJson( return _TokenTemplateWithOtps.fromJson( json ); - + default: throw CheckedFromJsonException( json, @@ -32,7 +32,7 @@ TokenTemplate _$TokenTemplateFromJson( 'Invalid union type "${json['runtimeType']}"!' ); } - + } /// @nodoc diff --git a/lib/model/widget_image.dart b/lib/model/widget_image.dart index 04afa676a..946122952 100644 --- a/lib/model/widget_image.dart +++ b/lib/model/widget_image.dart @@ -62,7 +62,10 @@ class WidgetImage { } @override - bool operator ==(Object other) => other is WidgetImage && other.imageFormat == imageFormat && other.imageData == imageData; + bool operator ==(Object other) => + other is WidgetImage && + other.imageFormat == imageFormat && + other.imageData == imageData; @override int get hashCode => Object.hash(runtimeType, imageFormat, imageData); @@ -77,7 +80,10 @@ class WidgetImage { try { return imageFormat.buildImageWidget(imageData); } catch (e) { - Logger.error('Image is not an ${imageFormat.name}, or the image data is corrupted.', error: e); + Logger.error( + 'Image is not an ${imageFormat.name}, or the image data is corrupted.', + error: e, + ); rethrow; } } @@ -93,12 +99,16 @@ class WidgetImage { try { return await imageFormat.getImageSize(imageData) ?? Size.zero; } catch (e) { - Logger.error('Image is not an ${imageFormat.name}, or the image data is corrupted.', error: e); + Logger.error( + 'Image is not an ${imageFormat.name}, or the image data is corrupted.', + error: e, + ); rethrow; } } - factory WidgetImage.fromJson(Map json) => _$WidgetImageFromJson(json); + factory WidgetImage.fromJson(Map json) => + _$WidgetImageFromJson(json); Map toJson() => _$WidgetImageToJson(this); XFile? toXFile() { @@ -109,10 +119,9 @@ class WidgetImage { String? fileName, ImageFormat? imageFormat, Uint8List? imageData, - }) => - WidgetImage( - fileName: fileName ?? this.fileName, - imageFormat: imageFormat ?? this.imageFormat, - imageData: imageData ?? this.imageData, - ); + }) => WidgetImage( + fileName: fileName ?? this.fileName, + imageFormat: imageFormat ?? this.imageFormat, + imageData: imageData ?? this.imageData, + ); } diff --git a/lib/processors/scheme_processors/home_widget_processor.dart b/lib/processors/scheme_processors/home_widget_processor.dart index 3a087e0b2..2cacc4dbb 100644 --- a/lib/processors/scheme_processors/home_widget_processor.dart +++ b/lib/processors/scheme_processors/home_widget_processor.dart @@ -25,18 +25,30 @@ import 'scheme_processor_interface.dart'; class HomeWidgetProcessor implements SchemeProcessor { const HomeWidgetProcessor(); - static final Map>?> Function(Uri)> _processors = { + static final Map< + String, + Future>?> Function(Uri) + > + _processors = { 'show': _showProcessor, 'copy': _copyProcessor, 'action': _actionProcessor, }; @override - Future>?> processUri(Uri uri, {bool fromInit = false}) async { + Future>?> processUri( + Uri uri, { + bool fromInit = false, + }) async { if (supportedSchemes.contains(uri.scheme) == false) return []; final processor = _processors[uri.host]; if (processor == null) { - return [ProcessorResult.failed((l) => l.noProcessorFoundForHost(uri.host), resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.noProcessorFoundForHost(uri.host), + resultHandlerType: null, + ), + ]; } return processor.call(uri); } @@ -44,40 +56,79 @@ class HomeWidgetProcessor implements SchemeProcessor { @override Set get supportedSchemes => {'homewidget'}; - static Future>?> _showProcessor(Uri uri, {bool fromInit = false}) async { + static Future>?> _showProcessor( + Uri uri, { + bool fromInit = false, + }) async { Logger.warning('HomeWidgetProcessor: Processing uri show: $uri'); if (uri.host != 'show') { - return [ProcessorResult.failed((l) => l.invalidHostForScheme(uri.host, uri.scheme), resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.invalidHostForScheme(uri.host, uri.scheme), + resultHandlerType: null, + ), + ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ProcessorResult.failed((l) => l.missingWidgetId, resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.missingWidgetId, + resultHandlerType: null, + ), + ]; } HomeWidgetUtils().showOtp(widgetId); return null; } - static Future>?> _copyProcessor(Uri uri, {bool fromInit = false}) async { + static Future>?> _copyProcessor( + Uri uri, { + bool fromInit = false, + }) async { Logger.warning('HomeWidgetProcessor: Processing uri copy: $uri'); if (uri.host != 'copy') { - return [ProcessorResult.failed((l) => l.invalidHostForScheme(uri.host, uri.scheme), resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.invalidHostForScheme(uri.host, uri.scheme), + resultHandlerType: null, + ), + ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ProcessorResult.failed((l) => l.missingWidgetId, resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.missingWidgetId, + resultHandlerType: null, + ), + ]; } HomeWidgetUtils().copyOtp(widgetId); return null; } - static Future>?> _actionProcessor(Uri uri, {bool fromInit = false}) async { + static Future>?> _actionProcessor( + Uri uri, { + bool fromInit = false, + }) async { Logger.warning('HomeWidgetProcessor: Processing uri action: $uri'); if (uri.host != 'action') { - return [ProcessorResult.failed((l) => l.invalidHostForScheme(uri.host, uri.scheme), resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.invalidHostForScheme(uri.host, uri.scheme), + resultHandlerType: null, + ), + ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ProcessorResult.failed((l) => l.missingWidgetId, resultHandlerType: null)]; + return [ + ProcessorResult.failed( + (l) => l.missingWidgetId, + resultHandlerType: null, + ), + ]; } HomeWidgetUtils().performAction(widgetId); return null; diff --git a/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart b/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart index 47a42a5b4..a92470088 100644 --- a/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart +++ b/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart @@ -28,12 +28,22 @@ import 'home_widget_navigate_processor.dart'; abstract class NavigationSchemeProcessor implements SchemeProcessor { const NavigationSchemeProcessor(); - static Set implementations = {HomeWidgetNavigateProcessor()}; + static Set implementations = { + HomeWidgetNavigateProcessor(), + }; @override - Future>?> processUri(Uri uri, {BuildContext? context, bool fromInit = false}); + Future>?> processUri( + Uri uri, { + BuildContext? context, + bool fromInit = false, + }); - static Future processUriByAny(Uri uri, {BuildContext? context, required bool fromInit}) async { + static Future processUriByAny( + Uri uri, { + BuildContext? context, + required bool fromInit, + }) async { if (context == null) { Logger.info('Current context is null, waiting for navigator context'); final key = await contextedGlobalNavigatorKey; @@ -42,12 +52,18 @@ abstract class NavigationSchemeProcessor implements SchemeProcessor { Logger.info('Processing scheme: ${uri.scheme}'); final futures = >[]; for (final processor in implementations) { - Logger.info('Supported schemes [${processor.supportedSchemes}] for processor ${processor.runtimeType}'); + Logger.info( + 'Supported schemes [${processor.supportedSchemes}] for processor ${processor.runtimeType}', + ); if (processor.supportedSchemes.contains(uri.scheme)) { - Logger.info('Processing scheme ${uri.scheme} with ${processor.runtimeType}'); + Logger.info( + 'Processing scheme ${uri.scheme} with ${processor.runtimeType}', + ); // ignoring use_build_context_synchronously is ok because we got the context after the await. The Context cannot be expired. // ignore: use_build_context_synchronously - futures.add(processor.processUri(uri, context: context, fromInit: fromInit)); + futures.add( + processor.processUri(uri, context: context, fromInit: fromInit), + ); } } await Future.wait(futures); diff --git a/lib/repo/preference_introduction_repository.dart b/lib/repo/preference_introduction_repository.dart index fde495649..a367034e7 100644 --- a/lib/repo/preference_introduction_repository.dart +++ b/lib/repo/preference_introduction_repository.dart @@ -28,7 +28,8 @@ import '../utils/logger.dart'; class PreferenceIntroductionRepository implements IntroductionRepository { static const String _completedIntroductionsKey = 'COMPLETED_INTRODUCTIONS'; - static final Future _prefs = SharedPreferences.getInstance(); + static final Future _prefs = + SharedPreferences.getInstance(); /// Function [f] is executed, protected by Mutex [_m]. /// That means, that calls of this method will always be executed serial. @@ -36,28 +37,48 @@ class PreferenceIntroductionRepository implements IntroductionRepository { static Future _protect(Future Function() f) => _m.protect(f); @override - Future loadCompletedIntroductions() async => _protect(_loadCompletedIntroductions); + Future loadCompletedIntroductions() async => + _protect(_loadCompletedIntroductions); Future _loadCompletedIntroductions() async { try { - final encodedIntroductions = (await _prefs).getString(_completedIntroductionsKey); + final encodedIntroductions = (await _prefs).getString( + _completedIntroductionsKey, + ); if (encodedIntroductions == null) return const IntroductionState(); final decodedIntroductions = jsonDecode(encodedIntroductions); return IntroductionState.fromJson(decodedIntroductions); } catch (e, s) { - Logger.warning('Failed to load completed introductions', error: e, stackTrace: s, verbose: true); + Logger.warning( + 'Failed to load completed introductions', + error: e, + stackTrace: s, + verbose: true, + ); return const IntroductionState(); } } @override - Future saveCompletedIntroductions(IntroductionState introductions) async => _protect(() => _saveCompletedIntroductions(introductions)); - Future _saveCompletedIntroductions(IntroductionState introductions) async { + Future saveCompletedIntroductions( + IntroductionState introductions, + ) async => _protect(() => _saveCompletedIntroductions(introductions)); + Future _saveCompletedIntroductions( + IntroductionState introductions, + ) async { try { final encodedIntroductions = jsonEncode(introductions); - await (await _prefs).setString(_completedIntroductionsKey, encodedIntroductions); + await (await _prefs).setString( + _completedIntroductionsKey, + encodedIntroductions, + ); return true; } catch (e, s) { - Logger.warning('Failed to load completed introductions', error: e, stackTrace: s, verbose: true); + Logger.warning( + 'Failed to load completed introductions', + error: e, + stackTrace: s, + verbose: true, + ); return false; } } diff --git a/lib/repo/preference_settings_repository.dart b/lib/repo/preference_settings_repository.dart index 7f8722cec..212cf4e14 100644 --- a/lib/repo/preference_settings_repository.dart +++ b/lib/repo/preference_settings_repository.dart @@ -29,7 +29,8 @@ class PreferenceSettingsRepository extends SettingsRepository { static const String _showGuideOnStartKey = 'KEY_SHOW_GUIDE_ON_START'; static const String _prefHideOtps = 'KEY_HIDE_OTPS'; static const String _prefEnablePoll = 'KEY_ENABLE_POLLING'; - static const String _crashReportRecipientsKey = 'KEY_CRASH_REPORT_RECIPIENTS'; // TODO Use this if the server supports it + static const String _crashReportRecipientsKey = + 'KEY_CRASH_REPORT_RECIPIENTS'; // TODO Use this if the server supports it static const String _localePreferenceKey = 'KEY_LOCALE_PREFERENCE'; static const String _useSystemLocaleKey = 'KEY_USE_SYSTEM_LOCALE'; static const String _enableLoggingKey = 'KEY_VERBOSE_LOGGING'; @@ -38,7 +39,8 @@ class PreferenceSettingsRepository extends SettingsRepository { static const String _showBackgroundImageKey = 'KEY_HIDE_BACKGROUND_IMAGE'; static const String _allowScreenshotKey = 'KEY_ALLOW_SCREENSHOTS'; - static final Future _preferences = SharedPreferences.getInstance(); + static final Future _preferences = + SharedPreferences.getInstance(); static SettingsState? _lastState; /// Function [f] is executed, protected by Mutex [_m]. @@ -54,12 +56,18 @@ class PreferenceSettingsRepository extends SettingsRepository { showGuideOnStart: prefs.getBool(_showGuideOnStartKey), hideOpts: prefs.getBool(_prefHideOtps), enablePolling: prefs.getBool(_prefEnablePoll), - crashReportRecipients: prefs.getStringList(_crashReportRecipientsKey)?.toSet(), - localePreference: prefs.getString(_localePreferenceKey) != null ? SettingsState.decodeLocale(prefs.getString(_localePreferenceKey)!) : null, + crashReportRecipients: prefs + .getStringList(_crashReportRecipientsKey) + ?.toSet(), + localePreference: prefs.getString(_localePreferenceKey) != null + ? SettingsState.decodeLocale(prefs.getString(_localePreferenceKey)!) + : null, useSystemLocale: prefs.getBool(_useSystemLocaleKey), verboseLogging: prefs.getBool(_enableLoggingKey), hidePushTokens: prefs.getBool(_hidePushTokensKey), - latestStartedVersion: prefs.getString(_latestVersionKey) != null ? Version.parse(prefs.getString(_latestVersionKey)!) : null, + latestStartedVersion: prefs.getString(_latestVersionKey) != null + ? Version.parse(prefs.getString(_latestVersionKey)!) + : null, showBackgroundImage: prefs.getBool(_showBackgroundImageKey), allowScreenshots: prefs.getBool(_allowScreenshotKey), ); @@ -68,24 +76,44 @@ class PreferenceSettingsRepository extends SettingsRepository { } @override - Future saveSettings(SettingsState settings) => _protect(() => _saveSettings(settings)); + Future saveSettings(SettingsState settings) => + _protect(() => _saveSettings(settings)); Future _saveSettings(SettingsState settings) async { final prefs = await _preferences; final futures = [ - if (_lastState?.isFirstRun != settings.isFirstRun) prefs.setBool(_isFirstRunKey, settings.isFirstRun), - if (_lastState?.showGuideOnStart != settings.showGuideOnStart) prefs.setBool(_showGuideOnStartKey, settings.showGuideOnStart), - if (_lastState?.hideOpts != settings.hideOpts) prefs.setBool(_prefHideOtps, settings.hideOpts), - if (_lastState?.enablePolling != settings.enablePolling) prefs.setBool(_prefEnablePoll, settings.enablePolling), + if (_lastState?.isFirstRun != settings.isFirstRun) + prefs.setBool(_isFirstRunKey, settings.isFirstRun), + if (_lastState?.showGuideOnStart != settings.showGuideOnStart) + prefs.setBool(_showGuideOnStartKey, settings.showGuideOnStart), + if (_lastState?.hideOpts != settings.hideOpts) + prefs.setBool(_prefHideOtps, settings.hideOpts), + if (_lastState?.enablePolling != settings.enablePolling) + prefs.setBool(_prefEnablePoll, settings.enablePolling), if (_lastState?.crashReportRecipients != settings.crashReportRecipients) - prefs.setStringList(_crashReportRecipientsKey, settings.crashReportRecipients.toList()), + prefs.setStringList( + _crashReportRecipientsKey, + settings.crashReportRecipients.toList(), + ), if (_lastState?.localePreference != settings.localePreference) - prefs.setString(_localePreferenceKey, SettingsState.encodeLocale(settings.localePreference)), - if (_lastState?.useSystemLocale != settings.useSystemLocale) prefs.setBool(_useSystemLocaleKey, settings.useSystemLocale), - if (_lastState?.verboseLogging != settings.verboseLogging) prefs.setBool(_enableLoggingKey, settings.verboseLogging), - if (_lastState?.hidePushTokens != settings.hidePushTokens) prefs.setBool(_hidePushTokensKey, settings.hidePushTokens), - if (_lastState?.latestStartedVersion != settings.latestStartedVersion) prefs.setString(_latestVersionKey, settings.latestStartedVersion.toString()), - if (_lastState?.showBackgroundImage != settings.showBackgroundImage) prefs.setBool(_showBackgroundImageKey, settings.showBackgroundImage), - if (_lastState?.allowScreenshots != settings.allowScreenshots) prefs.setBool(_allowScreenshotKey, settings.allowScreenshots), + prefs.setString( + _localePreferenceKey, + SettingsState.encodeLocale(settings.localePreference), + ), + if (_lastState?.useSystemLocale != settings.useSystemLocale) + prefs.setBool(_useSystemLocaleKey, settings.useSystemLocale), + if (_lastState?.verboseLogging != settings.verboseLogging) + prefs.setBool(_enableLoggingKey, settings.verboseLogging), + if (_lastState?.hidePushTokens != settings.hidePushTokens) + prefs.setBool(_hidePushTokensKey, settings.hidePushTokens), + if (_lastState?.latestStartedVersion != settings.latestStartedVersion) + prefs.setString( + _latestVersionKey, + settings.latestStartedVersion.toString(), + ), + if (_lastState?.showBackgroundImage != settings.showBackgroundImage) + prefs.setBool(_showBackgroundImageKey, settings.showBackgroundImage), + if (_lastState?.allowScreenshots != settings.allowScreenshots) + prefs.setBool(_allowScreenshotKey, settings.allowScreenshots), ]; await Future.wait(futures); _lastState = settings; diff --git a/lib/repo/preference_token_folder_repository.dart b/lib/repo/preference_token_folder_repository.dart index 9602f977f..0726331d2 100644 --- a/lib/repo/preference_token_folder_repository.dart +++ b/lib/repo/preference_token_folder_repository.dart @@ -29,7 +29,8 @@ import '../utils/logger.dart'; class PreferenceTokenFolderRepository extends TokenFolderRepository { static const String _tokenFoldersKey = 'TOKEN_CATEGORIES'; - static final Future _prefs = SharedPreferences.getInstance(); + static final Future _prefs = + SharedPreferences.getInstance(); /// Function [f] is executed, protected by Mutex [_m]. /// That means, that calls of this method will always be executed serial. @@ -40,7 +41,9 @@ class PreferenceTokenFolderRepository extends TokenFolderRepository { Future loadState() async => _protect(_loadFolders); Future _loadFolders() async { try { - final foldersString = await _prefs.then((prefs) => prefs.getString(_tokenFoldersKey)); + final foldersString = await _prefs.then( + (prefs) => prefs.getString(_tokenFoldersKey), + ); if (foldersString == null) return const TokenFolderState(folders: []); final jsons = jsonDecode(foldersString) as List; final folders = jsons.map((e) => TokenFolder.fromJson(e)).toList(); @@ -54,7 +57,8 @@ class PreferenceTokenFolderRepository extends TokenFolderRepository { } @override - Future saveState(TokenFolderState state) => _protect(() => _saveReplaceList(state)); + Future saveState(TokenFolderState state) => + _protect(() => _saveReplaceList(state)); Future _saveReplaceList(TokenFolderState state) async { final folders = state.folders; Logger.info('Saving ${folders.length} folders to preferences...'); diff --git a/lib/repo/secure_storage.dart b/lib/repo/secure_storage.dart index e6965f1a1..1cc1dc2b6 100644 --- a/lib/repo/secure_storage.dart +++ b/lib/repo/secure_storage.dart @@ -25,10 +25,15 @@ import '../interfaces/repo/secure_storage.dart'; class SecureStorage implements SecureStorageInterface { static const defaultStorage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), - iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device, synchronizable: false), + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + synchronizable: false, + ), ); - static const legacyStorage = FlutterSecureStorage(aOptions: AndroidOptions(encryptedSharedPreferences: true)); + static const legacyStorage = FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + ); static final Mutex _m = Mutex(); @override @@ -38,7 +43,11 @@ class SecureStorage implements SecureStorageInterface { @override final String seperator; - SecureStorage({required this.storagePrefix, required this.storage, this.seperator = '_'}); + SecureStorage({ + required this.storagePrefix, + required this.storage, + this.seperator = '_', + }); @override String getFullKey(String key) => "$storagePrefix$seperator$key"; @@ -49,17 +58,21 @@ class SecureStorage implements SecureStorageInterface { /// Writes the given key-value pair to the secure storage. @override - Future write({required String key, required String value}) => _protect(() => storage.write(key: getFullKey(key), value: value)); + Future write({required String key, required String value}) => + _protect(() => storage.write(key: getFullKey(key), value: value)); /// Reads the value for the given key from the secure storage. @override - Future read({required String key}) => _protect(() => storage.read(key: getFullKey(key))); + Future read({required String key}) => + _protect(() => storage.read(key: getFullKey(key))); /// Reads all key-value pairs from the secure storage that start with the storagePrefix. @override Future> readAll() => _protect(() async { final allPairs = await storage.readAll(); - final allKeys = allPairs.keys.where((key) => key.startsWith(storagePrefix)).toList(); + final allKeys = allPairs.keys + .where((key) => key.startsWith(storagePrefix)) + .toList(); final result = {}; for (var key in allKeys) { final shortKey = key.substring(storagePrefix.length + 1); @@ -70,13 +83,16 @@ class SecureStorage implements SecureStorageInterface { /// Deletes the entry with the given key from the secure storage. @override - Future delete({required String key}) => _protect(() => storage.delete(key: getFullKey(key))); + Future delete({required String key}) => + _protect(() => storage.delete(key: getFullKey(key))); /// Deletes all entries from the secure storage that start with the storagePrefix. @override Future deleteAll() => _protect(() async { final allPairs = await storage.readAll(); - final allKeys = allPairs.keys.where((key) => key.startsWith(storagePrefix)).toList(); + final allKeys = allPairs.keys + .where((key) => key.startsWith(storagePrefix)) + .toList(); for (var key in allKeys) { await storage.delete(key: key); } diff --git a/lib/utils/animations/unscaled_animation_controller.dart b/lib/utils/animations/unscaled_animation_controller.dart index 036db5916..5ff5530cf 100644 --- a/lib/utils/animations/unscaled_animation_controller.dart +++ b/lib/utils/animations/unscaled_animation_controller.dart @@ -19,7 +19,8 @@ */ import 'dart:async' show Timer; -import 'package:flutter/material.dart' show Animation, AnimationStatus, AnimationStatusListener, VoidCallback; +import 'package:flutter/material.dart' + show Animation, AnimationStatus, AnimationStatusListener, VoidCallback; class UnscaledAnimationController extends Animation { static const double _refreshRate = 30; // FPS @@ -41,9 +42,12 @@ class UnscaledAnimationController extends Animation { required this.duration, this.lowerBound = 0, this.upperBound = 1, - }) : assert(lowerBound < upperBound, 'lowerBound must be less than upperBound'), - assert(duration.inMicroseconds > 0, 'duration must be greater than 0'), - value = lowerBound; + }) : assert( + lowerBound < upperBound, + 'lowerBound must be less than upperBound', + ), + assert(duration.inMicroseconds > 0, 'duration must be greater than 0'), + value = lowerBound; void _setStatus(AnimationStatus newStatus) { if (status == newStatus) return; @@ -74,32 +78,42 @@ class UnscaledAnimationController extends Animation { if (from != null) _setValue(from); _setStatus(AnimationStatus.forward); _timer?.cancel(); - _timer = Timer.periodic(Duration(milliseconds: (_refreshInterval * 1000).toInt()), (timer) { - final newValue = value + _refreshInterval * (upperBound - lowerBound) / duration.inSeconds; - if (value >= upperBound) { - _setValue(upperBound); - timer.cancel(); - _setStatus(AnimationStatus.completed); - } else { - _setValue(newValue); - } - }); + _timer = Timer.periodic( + Duration(milliseconds: (_refreshInterval * 1000).toInt()), + (timer) { + final newValue = + value + + _refreshInterval * (upperBound - lowerBound) / duration.inSeconds; + if (value >= upperBound) { + _setValue(upperBound); + timer.cancel(); + _setStatus(AnimationStatus.completed); + } else { + _setValue(newValue); + } + }, + ); } void reverse({double? from}) { if (from != null) _setValue(from); _setStatus(AnimationStatus.reverse); _timer?.cancel(); - _timer = Timer.periodic(Duration(milliseconds: (_refreshInterval * 1000).toInt()), (timer) { - final newValue = value + _refreshInterval * (upperBound - lowerBound) / duration.inSeconds; - if (value <= lowerBound) { - _setValue(lowerBound); - timer.cancel(); - _setStatus(AnimationStatus.dismissed); - } else { - _setValue(newValue); - } - }); + _timer = Timer.periodic( + Duration(milliseconds: (_refreshInterval * 1000).toInt()), + (timer) { + final newValue = + value + + _refreshInterval * (upperBound - lowerBound) / duration.inSeconds; + if (value <= lowerBound) { + _setValue(lowerBound); + timer.cancel(); + _setStatus(AnimationStatus.dismissed); + } else { + _setValue(newValue); + } + }, + ); } void stop() { diff --git a/lib/utils/custom_int_buffer.dart b/lib/utils/custom_int_buffer.dart index 3b1a669ba..d5e2ffa5e 100644 --- a/lib/utils/custom_int_buffer.dart +++ b/lib/utils/custom_int_buffer.dart @@ -28,7 +28,8 @@ part 'custom_int_buffer.g.dart'; class CustomIntBuffer { final int maxSize; final List list; - CustomIntBuffer({this.maxSize = 100, List list = const []}) : list = list.sublist(max(list.length - maxSize, 0), list.length); + CustomIntBuffer({this.maxSize = 100, List list = const []}) + : list = list.sublist(max(list.length - maxSize, 0), list.length); CustomIntBuffer copyWith({int? maxSize, List? list}) { return CustomIntBuffer( @@ -66,6 +67,7 @@ class CustomIntBuffer { @override int get hashCode => Object.hashAll([maxSize, ...list]); - factory CustomIntBuffer.fromJson(Map json) => _$CustomIntBufferFromJson(json); + factory CustomIntBuffer.fromJson(Map json) => + _$CustomIntBufferFromJson(json); Map toJson() => _$CustomIntBufferToJson(this); } diff --git a/lib/utils/customization/theme_extentions/action_theme.dart b/lib/utils/customization/theme_extentions/action_theme.dart index bc5ac9bad..2e519cb38 100644 --- a/lib/utils/customization/theme_extentions/action_theme.dart +++ b/lib/utils/customization/theme_extentions/action_theme.dart @@ -57,22 +57,45 @@ class TokenTileTheme extends ThemeExtension { }); @override - ThemeExtension lerp(covariant TokenTileTheme? other, double t) => TokenTileTheme( - deleteColor: Color.lerp(deleteColor, other?.deleteColor, t) ?? deleteColor, - editColor: Color.lerp(editColor, other?.editColor, t) ?? editColor, - lockColor: Color.lerp(lockColor, other?.lockColor, t) ?? lockColor, - transferColor: Color.lerp(transferColor, other?.transferColor, t) ?? transferColor, - actionDisabledColor: Color.lerp(actionDisabledColor, other?.actionDisabledColor, t) ?? actionDisabledColor, - actionForegroundColor: Color.lerp(actionForegroundColor, other?.actionForegroundColor, t) ?? actionForegroundColor, - defaultOtpColor: Color.lerp(defaultOtpColor, other?.defaultOtpColor, t) ?? defaultOtpColor, - warningOtpColor: Color.lerp(warningOtpColor, other?.warningOtpColor, t) ?? warningOtpColor, - criticalOtpColor: Color.lerp(criticalOtpColor, other?.criticalOtpColor, t) ?? criticalOtpColor, - defaultCountdownColor: Color.lerp(defaultCountdownColor, other?.defaultCountdownColor, t) ?? defaultCountdownColor, - warningCountdownColor: Color.lerp(warningCountdownColor, other?.warningCountdownColor, t) ?? warningCountdownColor, - criticalCountdownColor: Color.lerp(criticalCountdownColor, other?.criticalCountdownColor, t) ?? criticalCountdownColor, - tileSubtitleColor: Color.lerp(tileSubtitleColor, other?.tileSubtitleColor, t) ?? tileSubtitleColor, - tileIconColor: Color.lerp(tileIconColor, other?.tileIconColor, t) ?? tileIconColor, - ); + ThemeExtension lerp( + covariant TokenTileTheme? other, + double t, + ) => TokenTileTheme( + deleteColor: Color.lerp(deleteColor, other?.deleteColor, t) ?? deleteColor, + editColor: Color.lerp(editColor, other?.editColor, t) ?? editColor, + lockColor: Color.lerp(lockColor, other?.lockColor, t) ?? lockColor, + transferColor: + Color.lerp(transferColor, other?.transferColor, t) ?? transferColor, + actionDisabledColor: + Color.lerp(actionDisabledColor, other?.actionDisabledColor, t) ?? + actionDisabledColor, + actionForegroundColor: + Color.lerp(actionForegroundColor, other?.actionForegroundColor, t) ?? + actionForegroundColor, + defaultOtpColor: + Color.lerp(defaultOtpColor, other?.defaultOtpColor, t) ?? + defaultOtpColor, + warningOtpColor: + Color.lerp(warningOtpColor, other?.warningOtpColor, t) ?? + warningOtpColor, + criticalOtpColor: + Color.lerp(criticalOtpColor, other?.criticalOtpColor, t) ?? + criticalOtpColor, + defaultCountdownColor: + Color.lerp(defaultCountdownColor, other?.defaultCountdownColor, t) ?? + defaultCountdownColor, + warningCountdownColor: + Color.lerp(warningCountdownColor, other?.warningCountdownColor, t) ?? + warningCountdownColor, + criticalCountdownColor: + Color.lerp(criticalCountdownColor, other?.criticalCountdownColor, t) ?? + criticalCountdownColor, + tileSubtitleColor: + Color.lerp(tileSubtitleColor, other?.tileSubtitleColor, t) ?? + tileSubtitleColor, + tileIconColor: + Color.lerp(tileIconColor, other?.tileIconColor, t) ?? tileIconColor, + ); @override ThemeExtension copyWith({ @@ -90,21 +113,28 @@ class TokenTileTheme extends ThemeExtension { Color? Function()? criticalCountdownColor, Color? tileSubtitleColor, Color? tileIconColor, - }) => - TokenTileTheme( - deleteColor: deleteColor ?? this.deleteColor, - editColor: editColor ?? this.editColor, - lockColor: lockColor ?? this.lockColor, - transferColor: transferColor ?? this.transferColor, - actionDisabledColor: actionDisabledColor ?? this.actionDisabledColor, - actionForegroundColor: actionForegroundColor ?? this.actionForegroundColor, - defaultOtpColor: defaultOtpColor ?? this.defaultOtpColor, - warningOtpColor: warningOtpColor != null ? warningOtpColor() : this.warningOtpColor, - criticalOtpColor: criticalOtpColor != null ? criticalOtpColor() : this.criticalOtpColor, - defaultCountdownColor: defaultCountdownColor ?? this.defaultCountdownColor, - warningCountdownColor: warningCountdownColor != null ? warningCountdownColor() : this.warningCountdownColor, - criticalCountdownColor: criticalCountdownColor != null ? criticalCountdownColor() : this.criticalCountdownColor, - tileSubtitleColor: tileSubtitleColor ?? this.tileSubtitleColor, - tileIconColor: tileIconColor ?? this.tileIconColor, - ); + }) => TokenTileTheme( + deleteColor: deleteColor ?? this.deleteColor, + editColor: editColor ?? this.editColor, + lockColor: lockColor ?? this.lockColor, + transferColor: transferColor ?? this.transferColor, + actionDisabledColor: actionDisabledColor ?? this.actionDisabledColor, + actionForegroundColor: actionForegroundColor ?? this.actionForegroundColor, + defaultOtpColor: defaultOtpColor ?? this.defaultOtpColor, + warningOtpColor: warningOtpColor != null + ? warningOtpColor() + : this.warningOtpColor, + criticalOtpColor: criticalOtpColor != null + ? criticalOtpColor() + : this.criticalOtpColor, + defaultCountdownColor: defaultCountdownColor ?? this.defaultCountdownColor, + warningCountdownColor: warningCountdownColor != null + ? warningCountdownColor() + : this.warningCountdownColor, + criticalCountdownColor: criticalCountdownColor != null + ? criticalCountdownColor() + : this.criticalCountdownColor, + tileSubtitleColor: tileSubtitleColor ?? this.tileSubtitleColor, + tileIconColor: tileIconColor ?? this.tileIconColor, + ); } diff --git a/lib/utils/customization/theme_extentions/app_dimensions.dart b/lib/utils/customization/theme_extentions/app_dimensions.dart index 13a4fb097..cbc156f20 100644 --- a/lib/utils/customization/theme_extentions/app_dimensions.dart +++ b/lib/utils/customization/theme_extentions/app_dimensions.dart @@ -60,7 +60,7 @@ class AppDimensions extends ThemeExtension { /// The thickness of [Divider]s or [Border]s. /// Equivalent to CSS [border-width]. final double strokeWidth; - static const defaultStrokeWidth = 1.0; + static const defaultStrokeWidth = 2.0; /// The standard height for interactive elements like [ElevatedButton] or [TextField]. final double controlHeight; diff --git a/lib/utils/customization/theme_extentions/elevated_delete_button_theme.dart b/lib/utils/customization/theme_extentions/elevated_delete_button_theme.dart index 04ec37ec9..2712e0d5f 100644 --- a/lib/utils/customization/theme_extentions/elevated_delete_button_theme.dart +++ b/lib/utils/customization/theme_extentions/elevated_delete_button_theme.dart @@ -20,7 +20,8 @@ import 'package:flutter/material.dart'; -class ElevatedDeleteButtonTheme extends ThemeExtension { +class ElevatedDeleteButtonTheme + extends ThemeExtension { final ButtonStyle style; const ElevatedDeleteButtonTheme({required this.style}); @@ -50,37 +51,40 @@ class ElevatedDeleteButtonTheme extends ThemeExtension, Widget?)? backgroundBuilder, Widget Function(BuildContext, Set, Widget?)? foregroundBuilder, - }) => - ElevatedDeleteButtonTheme( - style: style.copyWith( - textStyle: textStyle, - backgroundColor: backgroundColor, - foregroundColor: actionForegroundColor, - overlayColor: overlayColor, - shadowColor: shadowColor, - surfaceTintColor: surfaceTintColor, - elevation: elevation, - padding: padding, - minimumSize: minimumSize, - fixedSize: fixedSize, - maximumSize: maximumSize, - iconColor: iconColor, - iconSize: iconSize, - side: side, - shape: shape, - mouseCursor: mouseCursor, - visualDensity: visualDensity, - tapTargetSize: tapTargetSize, - animationDuration: animationDuration, - enableFeedback: enableFeedback, - alignment: alignment, - splashFactory: splashFactory, - backgroundBuilder: backgroundBuilder, - foregroundBuilder: foregroundBuilder, - ), - ); + }) => ElevatedDeleteButtonTheme( + style: style.copyWith( + textStyle: textStyle, + backgroundColor: backgroundColor, + foregroundColor: actionForegroundColor, + overlayColor: overlayColor, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + elevation: elevation, + padding: padding, + minimumSize: minimumSize, + fixedSize: fixedSize, + maximumSize: maximumSize, + iconColor: iconColor, + iconSize: iconSize, + side: side, + shape: shape, + mouseCursor: mouseCursor, + visualDensity: visualDensity, + tapTargetSize: tapTargetSize, + animationDuration: animationDuration, + enableFeedback: enableFeedback, + alignment: alignment, + splashFactory: splashFactory, + backgroundBuilder: backgroundBuilder, + foregroundBuilder: foregroundBuilder, + ), + ); @override - ThemeExtension lerp(covariant ElevatedDeleteButtonTheme? other, double t) => - ElevatedDeleteButtonTheme(style: ButtonStyle.lerp(style, other?.style, t) ?? style); + ThemeExtension lerp( + covariant ElevatedDeleteButtonTheme? other, + double t, + ) => ElevatedDeleteButtonTheme( + style: ButtonStyle.lerp(style, other?.style, t) ?? style, + ); } diff --git a/lib/utils/customization/theme_extentions/extended_text_theme.dart b/lib/utils/customization/theme_extentions/extended_text_theme.dart index e28922074..0fd7b7317 100644 --- a/lib/utils/customization/theme_extentions/extended_text_theme.dart +++ b/lib/utils/customization/theme_extentions/extended_text_theme.dart @@ -28,27 +28,27 @@ class ExtendedTextTheme extends ThemeExtension { this.veilingCharacter = '●', TextStyle? tokenTile, TextStyle? tokenTileSubtitle, - }) : tokenTile = const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ).merge(tokenTile), - tokenTileSubtitle = const TextStyle( - fontFamily: 'monospace', - fontWeight: FontWeight.bold, - ).merge(tokenTileSubtitle); + }) : tokenTile = const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ).merge(tokenTile), + tokenTileSubtitle = const TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ).merge(tokenTileSubtitle); @override ThemeExtension copyWith({ TextStyle? otpTextStyle, TextStyle? otpSubtitleTextStyle, - }) => - ExtendedTextTheme( - tokenTile: otpTextStyle ?? tokenTile, - tokenTileSubtitle: otpSubtitleTextStyle ?? tokenTileSubtitle, - ); + }) => ExtendedTextTheme( + tokenTile: otpTextStyle ?? tokenTile, + tokenTileSubtitle: otpSubtitleTextStyle ?? tokenTileSubtitle, + ); @override - ThemeExtension lerp(ExtendedTextTheme? other, double t) => ExtendedTextTheme( + ThemeExtension lerp(ExtendedTextTheme? other, double t) => + ExtendedTextTheme( tokenTile: TextStyle.lerp(tokenTile, other?.tokenTile, t) ?? tokenTile, ); } diff --git a/lib/utils/customization/theme_extentions/push_request_theme.dart b/lib/utils/customization/theme_extentions/push_request_theme.dart index 2f6023dd1..056094489 100644 --- a/lib/utils/customization/theme_extentions/push_request_theme.dart +++ b/lib/utils/customization/theme_extentions/push_request_theme.dart @@ -23,13 +23,13 @@ class PushRequestTheme extends ThemeExtension { Color acceptColor; Color declineColor; - PushRequestTheme({ - required this.acceptColor, - required this.declineColor, - }); + PushRequestTheme({required this.acceptColor, required this.declineColor}); @override - ThemeExtension copyWith({Color? acceptColor, Color? declineColor}) { + ThemeExtension copyWith({ + Color? acceptColor, + Color? declineColor, + }) { return PushRequestTheme( acceptColor: acceptColor ?? this.acceptColor, declineColor: declineColor ?? this.declineColor, @@ -40,7 +40,8 @@ class PushRequestTheme extends ThemeExtension { ThemeExtension lerp(PushRequestTheme other, double t) { return PushRequestTheme( acceptColor: Color.lerp(acceptColor, other.acceptColor, t) ?? acceptColor, - declineColor: Color.lerp(declineColor, other.declineColor, t) ?? declineColor, + declineColor: + Color.lerp(declineColor, other.declineColor, t) ?? declineColor, ); } } diff --git a/lib/utils/customization/theme_extentions/status_colors.dart b/lib/utils/customization/theme_extentions/status_colors.dart index 9a22b24c7..86cee40b2 100644 --- a/lib/utils/customization/theme_extentions/status_colors.dart +++ b/lib/utils/customization/theme_extentions/status_colors.dart @@ -36,15 +36,17 @@ class StatusColors extends ThemeExtension { Color? error, Color? warning, Color? success, - }) => - StatusColors( - error: error ?? this.error, - warning: warning ?? this.warning, - success: success ?? this.success, - ); + }) => StatusColors( + error: error ?? this.error, + warning: warning ?? this.warning, + success: success ?? this.success, + ); @override - ThemeExtension lerp(covariant ThemeExtension? other, double t) { + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { if (other is StatusColors) { return StatusColors( error: Color.lerp(error, other.error, t)!, diff --git a/lib/utils/default_inkwell.dart b/lib/utils/default_inkwell.dart index 50ac7aedb..24200705a 100644 --- a/lib/utils/default_inkwell.dart +++ b/lib/utils/default_inkwell.dart @@ -34,20 +34,20 @@ class DefaultInkWell extends StatelessWidget { @override Widget build(BuildContext context) => Material( - // Material to draw on for the InkWell - color: Colors.transparent, - child: Container( - decoration: BoxDecoration( - color: highlight ? Theme.of(context).dividerColor : null, - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - child: InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4)), - ), - onTap: onTap, - child: child, - ), + // Material to draw on for the InkWell + color: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: highlight ? Theme.of(context).dividerColor : null, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4)), ), - ); + onTap: onTap, + child: child, + ), + ), + ); } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/app_customization_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/app_customization_notifier.dart index 82e96b0a4..b76d7e6b5 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/app_customization_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/app_customization_notifier.dart @@ -29,7 +29,10 @@ part 'app_customization_notifier.g.dart'; @riverpod class AppCustomizationNotifier extends _$AppCustomizationNotifier { static ApplicationCustomization get initialState => - _initialState ?? ApplicationCustomization.defaultCustomization.copyWith(disabledFeatures: AppFeature.values.toSet()); + _initialState ?? + ApplicationCustomization.defaultCustomization.copyWith( + disabledFeatures: AppFeature.values.toSet(), + ); static ApplicationCustomization? _initialState; static void setInitialState(ApplicationCustomization initialState) { @@ -39,12 +42,17 @@ class AppCustomizationNotifier extends _$AppCustomizationNotifier { @override Future build() async => initialState; - Future setState(ApplicationCustomization newState) async { + Future setState( + ApplicationCustomization newState, + ) async { state = AsyncValue.data(newState); return newState; } - Future updateState(FutureOr Function(ApplicationCustomization) updater) async { + Future updateState( + FutureOr Function(ApplicationCustomization) + updater, + ) async { final oldState = await future; final newState = await updater(oldState); setState(newState); diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/deeplink_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/deeplink_notifier.dart index e45367605..1cc98ebc6 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/deeplink_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/deeplink_notifier.dart @@ -32,8 +32,16 @@ import '../../state_notifiers/deeplink_notifier.dart'; part 'deeplink_notifier.g.dart'; final sources = [ - DeeplinkSource(name: 'uni_links', stream: AppLinks().uriLinkStream, initialUri: AppLinks().getInitialLink()), - DeeplinkSource(name: 'home_widget', stream: HomeWidgetUtils().widgetClicked, initialUri: HomeWidgetUtils().initiallyLaunchedFromHomeWidget()), + DeeplinkSource( + name: 'uni_links', + stream: AppLinks().uriLinkStream, + initialUri: AppLinks().getInitialLink(), + ), + DeeplinkSource( + name: 'home_widget', + stream: HomeWidgetUtils().widgetClicked, + initialUri: HomeWidgetUtils().initiallyLaunchedFromHomeWidget(), + ), ]; @Riverpod(keepAlive: true) @@ -52,7 +60,9 @@ class DeeplinkNotifier extends _$DeeplinkNotifier { /// while already started. Stream _handleIncomingLinks(List sources) async* { if (kIsWeb) return; - final groupedStream = StreamGroup.merge(sources.map((source) => source.stream)); + final groupedStream = StreamGroup.merge( + sources.map((source) => source.stream), + ); await for (var uri in groupedStream) { Logger.info('DeeplinkNotifier got new uri'); if (uri == null) return; diff --git a/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart b/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart index 17fbdaf42..d0664adf8 100644 --- a/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart +++ b/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart @@ -30,7 +30,9 @@ bool _initialUriIsHandled = false; class DeeplinkNotifier extends StateNotifier { final List _subs = []; final List _sources; - DeeplinkNotifier({required List sources}) : _sources = sources, super(null) { + DeeplinkNotifier({required List sources}) + : _sources = sources, + super(null) { _handleInitialUri(); _handleIncomingLinks(); } @@ -59,7 +61,11 @@ class DeeplinkNotifier extends StateNotifier { state = DeepLink(uri); }, onError: (Object err) { - Logger.error('Error getting uri from ${source.name}', error: err, stackTrace: StackTrace.current); + Logger.error( + 'Error getting uri from ${source.name}', + error: err, + stackTrace: StackTrace.current, + ); }, ), ); @@ -88,5 +94,9 @@ class DeeplinkSource { final String name; final Stream stream; final Future initialUri; - DeeplinkSource({required this.name, required this.stream, required this.initialUri}); + DeeplinkSource({ + required this.name, + required this.stream, + required this.initialUri, + }); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_daypassword_manually.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_daypassword_manually.dart index 4674ac978..15bcc2534 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_daypassword_manually.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_daypassword_manually.dart @@ -41,7 +41,11 @@ import '../rows/token_type_dropdown_button.dart'; import 'add_token_manually_interface.dart'; class AddDayPasswordManually extends AddTokenManuallyPage { - static final allowedPeriodsDayPassword = List.generate(24, (i) => Duration(hours: 24 - i), growable: false); + static final allowedPeriodsDayPassword = List.generate( + 24, + (i) => Duration(hours: 24 - i), + growable: false, + ); final TextEditingController labelController; @@ -60,7 +64,11 @@ class AddDayPasswordManually extends AddTokenManuallyPage { autoValidateLabel.value = true; return null; } - if (SecretInputField.validator(secretController.text, encodingNofitier.value) != null) { + if (SecretInputField.validator( + secretController.text, + encodingNofitier.value, + ) != + null) { autoValidateSecret.value = true; return null; } @@ -72,7 +80,10 @@ class AddDayPasswordManually extends AddTokenManuallyPage { id: const Uuid().v4(), algorithm: algorithmsNotifier.value, digits: digitsNotifier.value!, - secret: encodingNofitier.value.encodeStringTo(Encodings.base32, secretController.text), + secret: encodingNofitier.value.encodeStringTo( + Encodings.base32, + secretController.text, + ), type: TokenTypes.DAYPASSWORD.name, origin: TokenOriginSourceType.manually.toTokenOrigin(), period: periodNotifier.value!, @@ -94,30 +105,30 @@ class AddDayPasswordManually extends AddTokenManuallyPage { @override AddTokenManually build(BuildContext context) => AddTokenManually( - fields: [ - LabelInputField( - controller: labelController, - autoValidate: autoValidateLabel, - ), - SecretInputField( - controller: secretController, - autoValidate: autoValidateSecret, - encodingNotifier: encodingNofitier, - ), - TokenTypeDropdownButton(typeNotifier: typeNotifier), - EncodingsDropdownButton(encodingNotifier: encodingNofitier), - AlgorithmsDropdownButton(algorithmsNotifier: algorithmsNotifier), - DigitsDropdownButton(digitsNotifier: digitsNotifier), - DurationDropdownButton( - periodNotifier: periodNotifier, - values: allowedPeriodsDayPassword, - unit: DurationUnit.hours, - ), - ], - button: AddTokenButton( - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - tokenBuilder: _tryBuildToken, - ), - ); + fields: [ + LabelInputField( + controller: labelController, + autoValidate: autoValidateLabel, + ), + SecretInputField( + controller: secretController, + autoValidate: autoValidateSecret, + encodingNotifier: encodingNofitier, + ), + TokenTypeDropdownButton(typeNotifier: typeNotifier), + EncodingsDropdownButton(encodingNotifier: encodingNofitier), + AlgorithmsDropdownButton(algorithmsNotifier: algorithmsNotifier), + DigitsDropdownButton(digitsNotifier: digitsNotifier), + DurationDropdownButton( + periodNotifier: periodNotifier, + values: allowedPeriodsDayPassword, + unit: DurationUnit.hours, + ), + ], + button: AddTokenButton( + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + tokenBuilder: _tryBuildToken, + ), + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_hotp_manually.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_hotp_manually.dart index df8d839c9..849be4ef8 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_hotp_manually.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_hotp_manually.dart @@ -69,11 +69,16 @@ class AddHotpManually extends AddTokenManuallyPage { autoValidateLabel.value = true; return null; } - if (SecretInputField.validator(secretController.text, encodingNofitier.value) != null) { + if (SecretInputField.validator( + secretController.text, + encodingNofitier.value, + ) != + null) { autoValidateSecret.value = true; return null; } - if (CounterInputField.validator(counterNotifier.value?.toString()) != null) { + if (CounterInputField.validator(counterNotifier.value?.toString()) != + null) { return null; } Logger.info('Input is valid, building token'); @@ -82,7 +87,10 @@ class AddHotpManually extends AddTokenManuallyPage { type: TokenTypes.HOTP.name, label: labelController.text, algorithm: algorithmsNotifier.value, - secret: encodingNofitier.value.encodeStringTo(Encodings.base32, secretController.text), + secret: encodingNofitier.value.encodeStringTo( + Encodings.base32, + secretController.text, + ), digits: digitsNotifier.value!, counter: counterNotifier.value!, origin: TokenOriginSourceType.manually.toTokenOrigin(), @@ -91,26 +99,26 @@ class AddHotpManually extends AddTokenManuallyPage { @override AddTokenManually build(BuildContext context) => AddTokenManually( - fields: [ - LabelInputField( - controller: labelController, - autoValidate: autoValidateLabel, - ), - SecretInputField( - controller: secretController, - autoValidate: autoValidateSecret, - encodingNotifier: encodingNofitier, - ), - TokenTypeDropdownButton(typeNotifier: typeNotifier), - EncodingsDropdownButton(encodingNotifier: encodingNofitier), - AlgorithmsDropdownButton(algorithmsNotifier: algorithmsNotifier), - DigitsDropdownButton(digitsNotifier: digitsNotifier), - CounterInputField(counterNotifier: counterNotifier), - ], - button: AddTokenButton( - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - tokenBuilder: _tryBuildToken, - ), - ); + fields: [ + LabelInputField( + controller: labelController, + autoValidate: autoValidateLabel, + ), + SecretInputField( + controller: secretController, + autoValidate: autoValidateSecret, + encodingNotifier: encodingNofitier, + ), + TokenTypeDropdownButton(typeNotifier: typeNotifier), + EncodingsDropdownButton(encodingNotifier: encodingNofitier), + AlgorithmsDropdownButton(algorithmsNotifier: algorithmsNotifier), + DigitsDropdownButton(digitsNotifier: digitsNotifier), + CounterInputField(counterNotifier: counterNotifier), + ], + button: AddTokenButton( + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + tokenBuilder: _tryBuildToken, + ), + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_steam_manually.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_steam_manually.dart index 441a43846..1bb287735 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_steam_manually.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_steam_manually.dart @@ -48,20 +48,22 @@ class AddSteamManually extends AddTokenManuallyPage { final ValueNotifier autoValidateSecret; final ValueNotifier typeNotifier; - const AddSteamManually( - {required this.labelController, - required this.secretController, - required this.autoValidateLabel, - required this.autoValidateSecret, - required this.typeNotifier, - super.key}); + const AddSteamManually({ + required this.labelController, + required this.secretController, + required this.autoValidateLabel, + required this.autoValidateSecret, + required this.typeNotifier, + super.key, + }); SteamToken? _tokenBuilder() { if (LabelInputField.validator(labelController.text) != null) { autoValidateLabel.value = true; return null; } - if (SecretInputField.validator(secretController.text, Encodings.base32) != null) { + if (SecretInputField.validator(secretController.text, Encodings.base32) != + null) { autoValidateSecret.value = true; return null; } @@ -80,39 +82,33 @@ class AddSteamManually extends AddTokenManuallyPage { // Algorithms? algorithm, // unused steam tokens always have SHA1 algorithm @override AddTokenManually build(BuildContext context) => AddTokenManually( - fields: [ - LabelInputField( - controller: labelController, - autoValidate: autoValidateLabel, - ), - SecretInputField( - controller: secretController, - autoValidate: autoValidateSecret, - encodingNotifier: ValueNotifier(Encodings.base32), - ), - TokenTypeDropdownButton(typeNotifier: typeNotifier), - const EncodingsDropdownButton( - enabled: false, - values: [Encodings.base32], - ), - const AlgorithmsDropdownButton( - enabled: false, - allowedAlgorithms: [Algorithms.SHA1], - ), - const DigitsDropdownButton( - enabled: false, - allowedDigits: [5], - ), - const DurationDropdownButton( - enabled: false, - unit: DurationUnit.seconds, - values: [Duration(seconds: 30)], - ), - ], - button: AddTokenButton( - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - tokenBuilder: _tokenBuilder, - ), - ); + fields: [ + LabelInputField( + controller: labelController, + autoValidate: autoValidateLabel, + ), + SecretInputField( + controller: secretController, + autoValidate: autoValidateSecret, + encodingNotifier: ValueNotifier(Encodings.base32), + ), + TokenTypeDropdownButton(typeNotifier: typeNotifier), + const EncodingsDropdownButton(enabled: false, values: [Encodings.base32]), + const AlgorithmsDropdownButton( + enabled: false, + allowedAlgorithms: [Algorithms.SHA1], + ), + const DigitsDropdownButton(enabled: false, allowedDigits: [5]), + const DurationDropdownButton( + enabled: false, + unit: DurationUnit.seconds, + values: [Duration(seconds: 30)], + ), + ], + button: AddTokenButton( + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + tokenBuilder: _tokenBuilder, + ), + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_token_manually_interface.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_token_manually_interface.dart index 27a955a3e..7692bd5b8 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_token_manually_interface.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_tokens_manually/add_token_manually_interface.dart @@ -29,19 +29,19 @@ abstract class AddTokenManuallyPage extends StatelessWidget { class AddTokenManually extends StatelessWidget { final List fields; final Widget button; - const AddTokenManually({super.key, required this.fields, required this.button}); + const AddTokenManually({ + super.key, + required this.fields, + required this.button, + }); @override Widget build(BuildContext context) => Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: fields, - ), - ), - ), - button, - ], - ); + children: [ + Expanded( + child: SingleChildScrollView(child: Column(children: fields)), + ), + button, + ], + ); } diff --git a/lib/views/container_view/container_view.dart b/lib/views/container_view/container_view.dart index 0cdcc4e49..af44bdaa2 100644 --- a/lib/views/container_view/container_view.dart +++ b/lib/views/container_view/container_view.dart @@ -41,7 +41,11 @@ class ContainerView extends ConsumerView { @override Widget build(BuildContext context, WidgetRef ref) { - final containerList = ref.watch(tokenContainerProvider).whenOrNull(data: (data) => data.containerList) ?? []; + final containerList = + ref + .watch(tokenContainerProvider) + .whenOrNull(data: (data) => data.containerList) ?? + []; return Scaffold( appBar: AppBar( title: Text( @@ -58,7 +62,8 @@ class ContainerView extends ConsumerView { mainAxisAlignment: MainAxisAlignment.start, children: [ for (var container in containerList) ...[ - if (containerList.indexOf(container) != 0) const DefaultDivider(), + if (containerList.indexOf(container) != 0) + const DefaultDivider(), ContainerWidget(container: container), ], ], diff --git a/lib/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart b/lib/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart index 62851266c..8700dbc91 100644 --- a/lib/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart +++ b/lib/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart @@ -25,7 +25,6 @@ import 'package:privacyidea_authenticator/model/extensions/enums/sync_state_exte import '../../../../model/token_container.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import '../../../../widgets/button_widgets/intent_button.dart'; -import '../../../../widgets/button_widgets/time_guarded_button.dart'; import '../dialogs/rollover_container_tokens_dialog.dart'; class RolloverContainerTokensButton extends ConsumerWidget { @@ -46,7 +45,7 @@ class RolloverContainerTokensButton extends ConsumerWidget { final bool canPress = currentContainer != null && currentContainer.syncState.isIdle; - return TimeGuardedButton( + return IntentButton( intent: DialogActionIntent.confirm, onPressed: canPress ? () => RolloverContainerTokensDialog.showDialog(context, container) diff --git a/lib/views/container_view/container_widgets/buttons/sync_container_button.dart b/lib/views/container_view/container_widgets/buttons/sync_container_button.dart index 5bd2fc72c..b42816f63 100644 --- a/lib/views/container_view/container_widgets/buttons/sync_container_button.dart +++ b/lib/views/container_view/container_widgets/buttons/sync_container_button.dart @@ -20,13 +20,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacyidea_authenticator/model/extensions/enums/sync_state_extension.dart'; import '../../../../model/token_container.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../widgets/button_widgets/intent_button.dart'; -import '../../../../widgets/button_widgets/time_guarded_button.dart'; class SyncContainerButton extends ConsumerWidget { final TokenContainerFinalized container; @@ -42,21 +40,20 @@ class SyncContainerButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { if (isPreview) return const Icon(Icons.sync, size: 40); - return TimeGuardedButton( - intent: DialogActionIntent.confirm, + return IntentButton( + intent: DialogActionIntent.neutral, // The button is disabled (null) if the container is already syncing - onPressed: container.syncState.isIdle - ? () async { - final tokenState = await ref.read(tokenProvider.future); - await ref - .read(tokenContainerProvider.notifier) - .syncContainers( - tokenState: tokenState, - containersToSync: [container], - isManually: true, - ); - } - : null, + onPressed: () async { + final tokenState = await ref.read(tokenProvider.future); + await ref + .read(tokenContainerProvider.notifier) + .syncContainers( + tokenState: tokenState, + containersToSync: [container], + isManually: true, + ); + return; + }, child: const Icon(Icons.sync, size: 40), ); } diff --git a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart index 5838ef1e4..95be65b84 100644 --- a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart @@ -31,20 +31,25 @@ import '../dialogs/delete_container_dialogs.dart/delete_container_dialog.dart'; class DeleteContainerAction extends ConsumerSlideableAction { final TokenContainer container; - const DeleteContainerAction({ - required this.container, - super.key, - }); + const DeleteContainerAction({required this.container, super.key}); @override CustomSlidableAction build(BuildContext context, WidgetRef ref) { - final deleteAllowed = container is! TokenContainerFinalized || container.policies.disabledUnregister == false; + final deleteAllowed = + container is! TokenContainerFinalized || + container.policies.disabledUnregister == false; return CustomSlidableAction( - onPressed: deleteAllowed ? (BuildContext context) => DeleteContainerDialog.showDialog(container) : null, + onPressed: deleteAllowed + ? (BuildContext context) => + DeleteContainerDialog.showDialog(container) + : null, autoClose: deleteAllowed, - backgroundColor: - deleteAllowed ? Theme.of(context).extension()!.deleteColor : Theme.of(context).extension()!.actionDisabledColor, - foregroundColor: Theme.of(context).extension()!.actionForegroundColor, + backgroundColor: deleteAllowed + ? Theme.of(context).extension()!.deleteColor + : Theme.of(context).extension()!.actionDisabledColor, + foregroundColor: Theme.of( + context, + ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/views/container_view/container_widgets/container_actions/details_container_action.dart b/lib/views/container_view/container_widgets/container_actions/details_container_action.dart index df94ba84d..98be11a05 100644 --- a/lib/views/container_view/container_widgets/container_actions/details_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/details_container_action.dart @@ -31,20 +31,27 @@ import '../dialogs/details_container_action_dialog.dart'; class DetailsContainerAction extends ConsumerSlideableAction { final TokenContainer container; - const DetailsContainerAction({ - required this.container, - super.key, - }); + const DetailsContainerAction({required this.container, super.key}); void _showDetailsContainerDialog(BuildContext context) { - showDialog(useRootNavigator: false, context: context, builder: (_) => DetailsContainerDialog(context, container: container)); + showDialog( + useRootNavigator: false, + context: context, + builder: (_) => DetailsContainerDialog(context, container: container), + ); } @override - CustomSlidableAction build(BuildContext context, WidgetRef ref) => CustomSlidableAction( - onPressed: (BuildContext context) => _showDetailsContainerDialog(context), - backgroundColor: Theme.of(context).extension()!.editColor, - foregroundColor: Theme.of(context).extension()!.actionForegroundColor, + CustomSlidableAction build(BuildContext context, WidgetRef ref) => + CustomSlidableAction( + onPressed: (BuildContext context) => + _showDetailsContainerDialog(context), + backgroundColor: Theme.of( + context, + ).extension()!.editColor, + foregroundColor: Theme.of( + context, + ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart b/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart index ea0785f94..7727856ce 100644 --- a/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart @@ -32,20 +32,27 @@ import '../dialogs/transfer_container_action_dialog.dart'; class TransferContainerAction extends ConsumerSlideableAction { final TokenContainerFinalized container; - const TransferContainerAction({ - required this.container, - super.key, - }); + const TransferContainerAction({required this.container, super.key}); void _showExportContainerDialog(BuildContext context) { - showDialog(useRootNavigator: false, context: context, builder: (_) => TransferContainerDialog(container: container)); + showDialog( + useRootNavigator: false, + context: context, + builder: (_) => TransferContainerDialog(container: container), + ); } @override - CustomSlidableAction build(BuildContext context, WidgetRef ref) => CustomSlidableAction( - onPressed: (BuildContext context) => _showExportContainerDialog(context), - backgroundColor: Theme.of(context).extension()!.transferColor, - foregroundColor: Theme.of(context).extension()!.actionForegroundColor, + CustomSlidableAction build(BuildContext context, WidgetRef ref) => + CustomSlidableAction( + onPressed: (BuildContext context) => + _showExportContainerDialog(context), + backgroundColor: Theme.of( + context, + ).extension()!.transferColor, + foregroundColor: Theme.of( + context, + ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/views/container_view/container_widgets/container_widget.dart b/lib/views/container_view/container_widgets/container_widget.dart index db8d0c24d..4936f8de5 100644 --- a/lib/views/container_view/container_widgets/container_widget.dart +++ b/lib/views/container_view/container_widgets/container_widget.dart @@ -49,10 +49,20 @@ class ContainerWidget extends ConsumerWidget { groupTag: groupTag, identifier: container.serial, actions: [ - DeleteContainerAction(container: container, key: Key('${container.serial}-DeleteContainerAction')), - DetailsContainerAction(container: container, key: Key('${container.serial}-EditContainerAction')), - if (container is TokenContainerFinalized && container.policies.rolloverAllowed) - TransferContainerAction(container: container as TokenContainerFinalized, key: Key('${container.serial}-TransferContainerAction')), + DeleteContainerAction( + container: container, + key: Key('${container.serial}-DeleteContainerAction'), + ), + DetailsContainerAction( + container: container, + key: Key('${container.serial}-EditContainerAction'), + ), + if (container is TokenContainerFinalized && + container.policies.rolloverAllowed) + TransferContainerAction( + container: container as TokenContainerFinalized, + key: Key('${container.serial}-TransferContainerAction'), + ), ], stack: stack, child: Padding( diff --git a/lib/views/container_view/container_widgets/container_widget_tile.dart b/lib/views/container_view/container_widgets/container_widget_tile.dart index 4c0baf45d..0eac59c56 100644 --- a/lib/views/container_view/container_widgets/container_widget_tile.dart +++ b/lib/views/container_view/container_widgets/container_widget_tile.dart @@ -30,65 +30,72 @@ class ContainerWidgetTile extends ConsumerWidget { final TokenContainer container; final bool isPreview; - const ContainerWidgetTile({required this.container, this.isPreview = false, super.key}); + const ContainerWidgetTile({ + required this.container, + this.isPreview = false, + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) => ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 2), - titleAlignment: ListTileTitleAlignment.center, - title: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.topLeft, - child: Align( - alignment: Alignment.centerLeft, - child: Tooltip( - message: AppLocalizations.of(context)!.containerSerial, - triggerMode: TooltipTriggerMode.longPress, - child: Text( - container.serial, - style: Theme.of(context).textTheme.titleMedium, - ), - ), + contentPadding: const EdgeInsets.symmetric(horizontal: 2), + titleAlignment: ListTileTitleAlignment.center, + title: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.topLeft, + child: Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: AppLocalizations.of(context)!.containerSerial, + triggerMode: TooltipTriggerMode.longPress, + child: Text( + container.serial, + style: Theme.of(context).textTheme.titleMedium, ), ), - subtitle: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - for (var line in [ - AppLocalizations.of(context)!.issuerLabel(container.issuer), - '${container.finalizationState.rolloutMsgLocalized(AppLocalizations.of(context)!)}', - ]) - Text( - line, - style: Theme.of(context).listTileTheme.subtitleTextStyle, - textAlign: TextAlign.left, - overflow: TextOverflow.fade, - softWrap: false, - ), - ], - ), - ), + ), + ), + subtitle: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + for (var line in [ + AppLocalizations.of(context)!.issuerLabel(container.issuer), + '${container.finalizationState.rolloutMsgLocalized(AppLocalizations.of(context)!)}', + ]) + Text( + line, + style: Theme.of(context).listTileTheme.subtitleTextStyle, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], ), - ], + ), ), - trailing: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: FittedBox( - fit: BoxFit.contain, - child: ContainerWidgetTileTrailing(container: container, isPreview: isPreview), - ), + ], + ), + trailing: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: FittedBox( + fit: BoxFit.contain, + child: ContainerWidgetTileTrailing( + container: container, + isPreview: isPreview, ), - ], + ), ), - ); + ], + ), + ); } diff --git a/lib/views/container_view/container_widgets/container_widget_tile_trailing.dart b/lib/views/container_view/container_widgets/container_widget_tile_trailing.dart index b524aa99f..33d707e37 100644 --- a/lib/views/container_view/container_widgets/container_widget_tile_trailing.dart +++ b/lib/views/container_view/container_widgets/container_widget_tile_trailing.dart @@ -27,7 +27,6 @@ import '../../../model/token_container.dart'; import '../../../utils/logger.dart'; import '../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import '../../../widgets/button_widgets/intent_button.dart'; -import '../../../widgets/button_widgets/time_guarded_button.dart'; import 'buttons/rollover_container_tokens_button.dart'; import 'buttons/sync_container_button.dart'; @@ -87,7 +86,7 @@ class ContainerWidgetTileTrailing extends ConsumerWidget { ) { if (container.finalizationState.isFailed || container.finalizationState == FinalizationState.notStarted) { - return TimeGuardedButton( + return IntentButton( intent: DialogActionIntent.confirm, onPressed: () async { await ref diff --git a/lib/views/feedback_view/feedback_view.dart b/lib/views/feedback_view/feedback_view.dart index 7bcc5914b..bfeb2d0de 100644 --- a/lib/views/feedback_view/feedback_view.dart +++ b/lib/views/feedback_view/feedback_view.dart @@ -46,7 +46,10 @@ class _FeedbackViewState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - Future.delayed(const Duration(milliseconds: 200), () => _focusNode.requestFocus()); + Future.delayed( + const Duration(milliseconds: 200), + () => _focusNode.requestFocus(), + ); }); } @@ -60,81 +63,103 @@ class _FeedbackViewState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text( - AppLocalizations.of(context)!.feedback, - overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. - maxLines: 2, // Title can be shown on small screens too. - ), - ), - body: Padding( - padding: const EdgeInsets.all(14.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Text( - AppLocalizations.of(context)!.feedbackTitle, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - AppLocalizations.of(context)!.feedbackDescription, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.justify, + appBar: AppBar( + title: Text( + AppLocalizations.of(context)!.feedback, + overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. + maxLines: 2, // Title can be shown on small screens too. + ), + ), + body: Padding( + padding: const EdgeInsets.all(14.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + AppLocalizations.of(context)!.feedbackTitle, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + AppLocalizations.of(context)!.feedbackDescription, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.justify, + ), + const SizedBox(height: 16), + TextField( + onTapOutside: (event) { + _focusNode.unfocus(); + }, + focusNode: _focusNode, + controller: _feedbackController, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - const SizedBox(height: 16), - TextField( - onTapOutside: (event) { - _focusNode.unfocus(); - }, - focusNode: _focusNode, - controller: _feedbackController, - decoration: InputDecoration( - border: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), - enabledBorder: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), - focusedBorder: const OutlineInputBorder(borderSide: BorderSide(width: 1.5)), - labelText: AppLocalizations.of(context)!.feedback, - ), - maxLines: 5, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - const SizedBox(height: 16), - RichText( - textAlign: TextAlign.justify, - text: TextSpan( - children: [ - TextSpan( - text: '${AppLocalizations.of(context)!.feedbackHint} ', - style: Theme.of(context).textTheme.bodySmall, - ), - TextSpan(text: AppLocalizations.of(context)!.feedbackPrivacyPolicy1, style: Theme.of(context).textTheme.bodySmall), - TextSpan( - text: AppLocalizations.of(context)!.feedbackPrivacyPolicy2, - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.blue), - recognizer: TapGestureRecognizer()..onTap = () => launchUrl(policyStatementUri), - ), - TextSpan(text: AppLocalizations.of(context)!.feedbackPrivacyPolicy3, style: Theme.of(context).textTheme.bodySmall), - ], - ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - FeedbackSendRow(feedbackController: _feedbackController), - ], + labelText: AppLocalizations.of(context)!.feedback, + ), + maxLines: 5, ), - ), + const SizedBox(height: 16), + RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: + '${AppLocalizations.of(context)!.feedbackHint} ', + style: Theme.of(context).textTheme.bodySmall, + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy1, + style: Theme.of(context).textTheme.bodySmall, + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy2, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrl(policyStatementUri), + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy3, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + FeedbackSendRow(feedbackController: _feedbackController), + ], ), - ], + ), ), - ), + ], ), - ); + ), + ), + ); } diff --git a/lib/views/link_home_widget_view/link_home_widget_view.dart b/lib/views/link_home_widget_view/link_home_widget_view.dart index 6445b28e7..d035917bc 100644 --- a/lib/views/link_home_widget_view/link_home_widget_view.dart +++ b/lib/views/link_home_widget_view/link_home_widget_view.dart @@ -47,7 +47,9 @@ class _LinkHomeWidgetViewState extends ConsumerState { bool alreadyTapped = false; @override Widget build(BuildContext context) { - final veilingCharacter = Theme.of(context).extension()?.veilingCharacter ?? '●'; + final veilingCharacter = + Theme.of(context).extension()?.veilingCharacter ?? + '●'; final otpTokens = ref.watch(tokenProvider).value?.otpTokens; return Scaffold( appBar: AppBar( @@ -60,17 +62,29 @@ class _LinkHomeWidgetViewState extends ConsumerState { body: ListView.builder( itemBuilder: (context, index) { final otpToken = otpTokens![index]; - final folderIsLocked = ref.watch(tokenFolderProvider).currentOfId(otpToken.folderId)?.isLocked ?? false; - final otpString = otpToken.isLocked || folderIsLocked ? veilingCharacter * otpToken.otpValue.length : otpToken.otpValue; + final folderIsLocked = + ref + .watch(tokenFolderProvider) + .currentOfId(otpToken.folderId) + ?.isLocked ?? + false; + final otpString = otpToken.isLocked || folderIsLocked + ? veilingCharacter * otpToken.otpValue.length + : otpToken.otpValue; return ListTile( title: Text(otpToken.label), - subtitle: Text(insertCharAt(otpString, ' ', (otpString.length / 2).ceil())), + subtitle: Text( + insertCharAt(otpString, ' ', (otpString.length / 2).ceil()), + ), onTap: alreadyTapped ? () {} : () async { if (alreadyTapped) return; setState(() => alreadyTapped = true); - await HomeWidgetUtils().link(widget.homeWidgetId, otpToken.id); + await HomeWidgetUtils().link( + widget.homeWidgetId, + otpToken.id, + ); await SystemNavigator.pop(); await Future.delayed(const Duration(milliseconds: 500)); if (context.mounted) Navigator.pop(context); diff --git a/lib/views/main_view/main_view.dart b/lib/views/main_view/main_view.dart index 4cf6cbd78..379fb65aa 100644 --- a/lib/views/main_view/main_view.dart +++ b/lib/views/main_view/main_view.dart @@ -69,7 +69,10 @@ class _MainViewState extends ConsumerState { @override void initState() { super.initState(); - final latestStartedVersion = globalRef?.read(settingsProvider).whenOrNull(data: (data) => data)?.latestStartedVersion; + final latestStartedVersion = globalRef + ?.read(settingsProvider) + .whenOrNull(data: (data) => data) + ?.latestStartedVersion; Logger.info('Latest started version: $latestStartedVersion'); if (latestStartedVersion == null || widget.disablePatchNotes) return; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -91,37 +94,47 @@ class _MainViewState extends ConsumerState { body: ExpandableAppBar( startExpand: hasFilter, appBar: AppBar( - title: Text( - widget.appName, - overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. - maxLines: 2, // Title can be shown on small screens too. - ), - leading: widget.appbarIcon, - actions: [ - hasFilter - ? AppBarItem( - a11y: AppLocalizations.of(context)!.a11yCloseSearchTokensButton, - onPressed: () { - ref.read(tokenFilterProvider.notifier).state = null; - }, - icon: const Icon(Icons.close), - ) - : AppBarItem( - a11y: AppLocalizations.of(context)!.a11ySearchTokensButton, - onPressed: () { - ref.read(tokenFilterProvider.notifier).state = TokenFilter( - searchQuery: '', - ); - }, - icon: const Icon(Icons.search), - ), - ]), + title: Text( + widget.appName, + overflow: TextOverflow + .ellipsis, // maxLines: 2 only works like this. + maxLines: 2, // Title can be shown on small screens too. + ), + leading: widget.appbarIcon, + actions: [ + hasFilter + ? AppBarItem( + a11y: AppLocalizations.of( + context, + )!.a11yCloseSearchTokensButton, + onPressed: () { + ref.read(tokenFilterProvider.notifier).state = null; + }, + icon: const Icon(Icons.close), + ) + : AppBarItem( + a11y: AppLocalizations.of( + context, + )!.a11ySearchTokensButton, + onPressed: () { + ref.read(tokenFilterProvider.notifier).state = + TokenFilter(searchQuery: ''); + }, + icon: const Icon(Icons.search), + ), + ], + ), body: ConnectivityListener( child: StatusBar( child: Stack( children: [ - if (widget.backgroundImage != null) MainViewBackgroundImage(appImage: widget.backgroundImage!), - hasFilter ? const MainViewTokensListFiltered() : MainViewTokensList(nestedScrollViewKey: globalKey), + if (widget.backgroundImage != null) + MainViewBackgroundImage( + appImage: widget.backgroundImage!, + ), + hasFilter + ? const MainViewTokensListFiltered() + : MainViewTokensList(nestedScrollViewKey: globalKey), if (!hasFilter) MainViewNavigationBar(), ], ), diff --git a/lib/views/main_view/main_view_widgets/app_bar_item.dart b/lib/views/main_view/main_view_widgets/app_bar_item.dart index 1ae3bf481..b36998c43 100644 --- a/lib/views/main_view/main_view_widgets/app_bar_item.dart +++ b/lib/views/main_view/main_view_widgets/app_bar_item.dart @@ -20,7 +20,12 @@ import 'package:flutter/material.dart'; class AppBarItem extends StatelessWidget { - const AppBarItem({super.key, required this.onPressed, required this.icon, required this.a11y}); + const AppBarItem({ + super.key, + required this.onPressed, + required this.icon, + required this.a11y, + }); final VoidCallback onPressed; final String a11y; @@ -28,17 +33,13 @@ class AppBarItem extends StatelessWidget { @override Widget build(BuildContext context) => Semantics( - label: a11y, - child: IconButton( - padding: const EdgeInsets.all(0), - splashRadius: 20, - onPressed: onPressed, - color: Theme.of(context).navigationBarTheme.iconTheme?.resolve({})?.color, - icon: SizedBox( - height: 24, - width: 24, - child: FittedBox(child: icon), - ), - ), - ); + label: a11y, + child: IconButton( + padding: const EdgeInsets.all(0), + splashRadius: 20, + onPressed: onPressed, + color: Theme.of(context).navigationBarTheme.iconTheme?.resolve({})?.color, + icon: SizedBox(height: 24, width: 24, child: FittedBox(child: icon)), + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/connectivity_listener.dart b/lib/views/main_view/main_view_widgets/connectivity_listener.dart index cb8c27c7e..ce1d7adb0 100644 --- a/lib/views/main_view/main_view_widgets/connectivity_listener.dart +++ b/lib/views/main_view/main_view_widgets/connectivity_listener.dart @@ -34,12 +34,16 @@ class ConnectivityListener extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityProvider).asData?.value; - if (connectivity != null && connectivity.contains(ConnectivityResult.none)) { + if (connectivity != null && + connectivity.contains(ConnectivityResult.none)) { ref.read(tokenProvider.future).then((newState) { if (newState.hasPushTokens) { Logger.info("Connectivity changed: $connectivity"); if (!context.mounted) return; - ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => AppLocalizations.of(context)!.noNetworkConnection); + ref.read(statusMessageProvider.notifier).state = StatusMessage( + message: (localization) => + AppLocalizations.of(context)!.noNetworkConnection, + ); } }); } diff --git a/lib/views/main_view/main_view_widgets/custom_paint_navigation_bar.dart b/lib/views/main_view/main_view_widgets/custom_paint_navigation_bar.dart index 9af0e91c3..e3a25b017 100644 --- a/lib/views/main_view/main_view_widgets/custom_paint_navigation_bar.dart +++ b/lib/views/main_view/main_view_widgets/custom_paint_navigation_bar.dart @@ -42,9 +42,13 @@ class CustomPaintNavigationBar extends CustomPainter { @override void paint(Canvas canvas, Size size) { final Color appBarColor = - Theme.of(buildContext).navigationBarTheme.backgroundColor ?? Theme.of(buildContext).appBarTheme.backgroundColor ?? Theme.of(buildContext).primaryColor; + Theme.of(buildContext).navigationBarTheme.backgroundColor ?? + Theme.of(buildContext).appBarTheme.backgroundColor ?? + Theme.of(buildContext).primaryColor; final Color shadowColor = - Theme.of(buildContext).navigationBarTheme.shadowColor ?? Theme.of(buildContext).appBarTheme.shadowColor ?? Theme.of(buildContext).shadowColor; + Theme.of(buildContext).navigationBarTheme.shadowColor ?? + Theme.of(buildContext).appBarTheme.shadowColor ?? + Theme.of(buildContext).shadowColor; final elevation = Theme.of(buildContext).navigationBarTheme.elevation ?? 3; final double radiusPx = min(40, size.height * 0.8); @@ -56,11 +60,35 @@ class CustomPaintNavigationBar extends CustomPainter { // Path for the main navigation bar shape (including the bottom) Path mainPath = Path() ..moveTo(size.width * 0.0, size.height * 0.3) // point 1 - ..quadraticBezierTo(size.width * 0.20, size.height * 0.0, size.width * 0.5 - radiusPx - 15, size.height * 0.0) // point 2 - ..quadraticBezierTo(size.width * 0.5 - radiusPx, size.height * 0.0, size.width * 0.5 - radiusPx, size.height * 0.2) // point 3 - ..arcToPoint(Offset(size.width * 0.5 + radiusPx, size.height * 0.2), radius: Radius.circular(radiusPx), clockwise: false) // point 4 - ..quadraticBezierTo(size.width * 0.5 + radiusPx, size.height * 0.0, size.width * 0.5 + radiusPx + 15, size.height * 0.0) // point 5 - ..quadraticBezierTo(size.width * 0.80, 0, size.width, size.height * 0.3) // point 6 + ..quadraticBezierTo( + size.width * 0.20, + size.height * 0.0, + size.width * 0.5 - radiusPx - 15, + size.height * 0.0, + ) // point 2 + ..quadraticBezierTo( + size.width * 0.5 - radiusPx, + size.height * 0.0, + size.width * 0.5 - radiusPx, + size.height * 0.2, + ) // point 3 + ..arcToPoint( + Offset(size.width * 0.5 + radiusPx, size.height * 0.2), + radius: Radius.circular(radiusPx), + clockwise: false, + ) // point 4 + ..quadraticBezierTo( + size.width * 0.5 + radiusPx, + size.height * 0.0, + size.width * 0.5 + radiusPx + 15, + size.height * 0.0, + ) // point 5 + ..quadraticBezierTo( + size.width * 0.80, + 0, + size.width, + size.height * 0.3, + ) // point 6 ..lineTo(size.width * 1.0, size.height * 1.0) // point 7 ..lineTo(size.width * 0.0, size.height * 1.0) // point 8 ..close(); // point 1 @@ -68,11 +96,35 @@ class CustomPaintNavigationBar extends CustomPainter { // Path for the shadow (only the top curved part) Path shadowPath = Path() ..moveTo(size.width * 0.0, size.height * 0.3) // point 1 - ..quadraticBezierTo(size.width * 0.20, size.height * 0.0, size.width * 0.5 - radiusPx - 15, size.height * 0.0) // point 2 - ..quadraticBezierTo(size.width * 0.5 - radiusPx, size.height * 0.0, size.width * 0.5 - radiusPx, size.height * 0.2) // point 3 - ..arcToPoint(Offset(size.width * 0.5 + radiusPx, size.height * 0.2), radius: Radius.circular(radiusPx), clockwise: false) // point 4 - ..quadraticBezierTo(size.width * 0.5 + radiusPx, size.height * 0.0, size.width * 0.5 + radiusPx + 15, size.height * 0.0) // point 5 - ..quadraticBezierTo(size.width * 0.80, 0, size.width, size.height * 0.3) // point 6 + ..quadraticBezierTo( + size.width * 0.20, + size.height * 0.0, + size.width * 0.5 - radiusPx - 15, + size.height * 0.0, + ) // point 2 + ..quadraticBezierTo( + size.width * 0.5 - radiusPx, + size.height * 0.0, + size.width * 0.5 - radiusPx, + size.height * 0.2, + ) // point 3 + ..arcToPoint( + Offset(size.width * 0.5 + radiusPx, size.height * 0.2), + radius: Radius.circular(radiusPx), + clockwise: false, + ) // point 4 + ..quadraticBezierTo( + size.width * 0.5 + radiusPx, + size.height * 0.0, + size.width * 0.5 + radiusPx + 15, + size.height * 0.0, + ) // point 5 + ..quadraticBezierTo( + size.width * 0.80, + 0, + size.width, + size.height * 0.3, + ) // point 6 //Skip the bottomline but dont cross the middle curve gap. // below point 3 ..lineTo(size.width * 0.5, size.height * 0.9) // point 3 ..close(); diff --git a/lib/views/main_view/main_view_widgets/expandable_appbar.dart b/lib/views/main_view/main_view_widgets/expandable_appbar.dart index 44496af67..6bc86f80d 100644 --- a/lib/views/main_view/main_view_widgets/expandable_appbar.dart +++ b/lib/views/main_view/main_view_widgets/expandable_appbar.dart @@ -112,13 +112,18 @@ class _ExpandableAppBarState extends State { color: Theme.of(context).canvasColor, boxShadow: [ BoxShadow( - color: searchActive && expandTarget != minExpansion ? Theme.of(context).shadowColor : Colors.transparent, + color: searchActive && expandTarget != minExpansion + ? Theme.of(context).shadowColor + : Colors.transparent, blurRadius: 2, offset: const Offset(0, 2), ), ], ), - duration: Duration(milliseconds: 250 * (currentExpansion - expandTarget).abs() ~/ maxExpansion), + duration: Duration( + milliseconds: + 250 * (currentExpansion - expandTarget).abs() ~/ maxExpansion, + ), height: expandTarget, onEnd: () { if (!mounted) return; @@ -141,7 +146,8 @@ class _ExpandableAppBarState extends State { child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: SearchTokenWidget( - searchActive: expandTarget != minExpansion || searchActive, + searchActive: + expandTarget != minExpansion || searchActive, ), ), ), diff --git a/lib/views/main_view/main_view_widgets/filter_token_widget.dart b/lib/views/main_view/main_view_widgets/filter_token_widget.dart index e80ce0c67..dfebe7c87 100644 --- a/lib/views/main_view/main_view_widgets/filter_token_widget.dart +++ b/lib/views/main_view/main_view_widgets/filter_token_widget.dart @@ -29,7 +29,8 @@ class SearchTokenWidget extends StatelessWidget { const SearchTokenWidget({required this.searchActive, super.key}); @override - Widget build(BuildContext context) => searchActive ? const SearchInputField() : const SizedBox(); + Widget build(BuildContext context) => + searchActive ? const SearchInputField() : const SizedBox(); } class SearchInputField extends ConsumerStatefulWidget { @@ -78,16 +79,20 @@ class _SearchInputFieldState extends ConsumerState { @override Widget build(BuildContext context) => TextField( - controller: _controller, - focusNode: _focusNode, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface), - decoration: InputDecoration( - hintText: 'Label / Serial / Issuer / Type', - hintStyle: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface), - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - prefixIcon: Icon(Icons.search), - ), - ); + controller: _controller, + focusNode: _focusNode, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: 'Label / Serial / Issuer / Type', + hintStyle: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + prefixIcon: Icon(Icons.search), + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart index 543c197fb..190c22c3c 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart @@ -36,9 +36,16 @@ class LockTokenFolderAction extends ConsumerSlideableAction { CustomSlidableAction build(BuildContext context, ref) { return CustomSlidableAction( backgroundColor: Theme.of(context).extension()!.lockColor, - foregroundColor: Theme.of(context).extension()!.actionForegroundColor, + foregroundColor: Theme.of( + context, + ).extension()!.actionForegroundColor, onPressed: (context) async { - if (await lockAuth(reason: (localization) => localization.unlock, localization: AppLocalizations.of(context)!) == false) return; + if (await lockAuth( + reason: (localization) => localization.unlock, + localization: AppLocalizations.of(context)!, + ) == + false) + return; globalRef?.read(tokenFolderProvider.notifier).toggleFolderLock(folder); }, child: Column( @@ -47,7 +54,9 @@ class LockTokenFolderAction extends ConsumerSlideableAction { children: [ const Icon(Icons.lock), Text( - folder.isLocked ? AppLocalizations.of(context)!.unlock : AppLocalizations.of(context)!.lock, + folder.isLocked + ? AppLocalizations.of(context)!.unlock + : AppLocalizations.of(context)!.lock, overflow: TextOverflow.fade, softWrap: false, ), diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart index c6dd0573d..d857bde6c 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart @@ -42,30 +42,31 @@ class TokenFolderExpandableBody extends StatelessWidget { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.fromLTRB(14, 0, 14, 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - for (var i = 0; i < tokens.length; i++) ...[ - if (draggingSortable != tokens[i] && (i != 0 || draggingSortable is Token)) - isFilterd - ? const DefaultDivider() - : DragTargetDivider( - dependingFolder: folder, - previousSortable: (i - 1) < 0 ? null : tokens[i - 1], - nextSortable: tokens[i], - ), - TokenWidgetBuilder.fromToken(token: tokens[i]), - ], - if (tokens.isNotEmpty && draggingSortable is Token) - isFilterd - ? const DefaultDivider() - : DragTargetDivider( - dependingFolder: folder, - previousSortable: tokens.last, - nextSortable: null, - ), - ], - ), - ); + padding: const EdgeInsets.fromLTRB(14, 0, 14, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + for (var i = 0; i < tokens.length; i++) ...[ + if (draggingSortable != tokens[i] && + (i != 0 || draggingSortable is Token)) + isFilterd + ? const DefaultDivider() + : DragTargetDivider( + dependingFolder: folder, + previousSortable: (i - 1) < 0 ? null : tokens[i - 1], + nextSortable: tokens[i], + ), + TokenWidgetBuilder.fromToken(token: tokens[i]), + ], + if (tokens.isNotEmpty && draggingSortable is Token) + isFilterd + ? const DefaultDivider() + : DragTargetDivider( + dependingFolder: folder, + previousSortable: tokens.last, + nextSortable: null, + ), + ], + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header_icon.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header_icon.dart index fb76fb476..db863414e 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header_icon.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header_icon.dart @@ -30,7 +30,12 @@ class TokenFolderExpandableHeaderIcon extends StatelessWidget { final bool isLocked; final bool isExpanded; - const TokenFolderExpandableHeaderIcon({super.key, required this.showEmptyFolderIcon, required this.isLocked, required this.isExpanded}); + const TokenFolderExpandableHeaderIcon({ + super.key, + required this.showEmptyFolderIcon, + required this.isLocked, + required this.isExpanded, + }); @override Widget build(BuildContext context) { @@ -54,7 +59,9 @@ class TokenFolderExpandableHeaderIcon extends StatelessWidget { children: [ FaIcon( weight: 0.1, - isExpanded ? FontAwesomeIcons.folderOpen : FontAwesomeIcons.solidFolderClosed, + isExpanded + ? FontAwesomeIcons.folderOpen + : FontAwesomeIcons.solidFolderClosed, color: Theme.of(context).listTileTheme.iconColor, ), if (isLocked) @@ -62,7 +69,10 @@ class TokenFolderExpandableHeaderIcon extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { return Container( - padding: EdgeInsets.only(left: isExpanded ? 2 : 0.0, top: 6), + padding: EdgeInsets.only( + left: isExpanded ? 2 : 0.0, + top: 6, + ), child: Center( child: Transform( transform: isExpanded @@ -74,15 +84,21 @@ class TokenFolderExpandableHeaderIcon extends StatelessWidget { ]) : Matrix4.identity(), child: Icon( - isExpanded ? MdiIcons.lockOpenVariant : MdiIcons.lock, - color: Theme.of(context).extension()?.lockColor, + isExpanded + ? MdiIcons.lockOpenVariant + : MdiIcons.lock, + color: Theme.of( + context, + ).extension()?.lockColor, size: constraints.maxHeight / 2.1, shadows: [ Shadow( - color: Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.3), + color: Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.3), offset: const Offset(0.5, 0.5), blurRadius: 2, - ) + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart index ce525bef4..7c7f98c20 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart @@ -45,7 +45,9 @@ class TokenFolderWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final draggingSortable = ref.watch(draggingSortableProvider); - final TokenFolder? draggingFolder = draggingSortable is TokenFolder ? draggingSortable : null; + final TokenFolder? draggingFolder = draggingSortable is TokenFolder + ? draggingSortable + : null; return draggingSortable == null ? LongPressDraggable( maxSimultaneousDrags: 1, @@ -56,9 +58,13 @@ class TokenFolderWidget extends ConsumerWidget { textScaler: MediaQuery.of(context).textScaler, maxLines: 1, ); - return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); + return Offset( + max(textSize.width / 2, 30), + textSize.height / 2 + 30, + ); }, - onDragStarted: () => ref.read(draggingSortableProvider.notifier).state = folder, + onDragStarted: () => + ref.read(draggingSortableProvider.notifier).state = folder, onDragCompleted: () { Logger.info('Draggable completed'); // Will be handled by the sortableNotifier @@ -94,11 +100,11 @@ class TokenFolderWidget extends ConsumerWidget { ), ) : (draggingFolder == folder) - ? const SizedBox() - : TokenFolderExpandable( - folder: folder, - folderTokens: folderTokens, - filter: filter, - ); + ? const SizedBox() + : TokenFolderExpandable( + folder: folder, + folderTokens: folderTokens, + filter: filter, + ); } } diff --git a/lib/views/main_view/main_view_widgets/main_view_background_image.dart b/lib/views/main_view/main_view_widgets/main_view_background_image.dart index ec6a1925e..0d4b48783 100644 --- a/lib/views/main_view/main_view_widgets/main_view_background_image.dart +++ b/lib/views/main_view/main_view_widgets/main_view_background_image.dart @@ -29,7 +29,9 @@ class MainViewBackgroundImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showBackgroundImage = ref.watch(settingsProvider.selectAsync((v) => v.showBackgroundImage)); + final showBackgroundImage = ref.watch( + settingsProvider.selectAsync((v) => v.showBackgroundImage), + ); return FutureBuilder( future: showBackgroundImage, builder: (context, snapshot) { @@ -37,7 +39,9 @@ class MainViewBackgroundImage extends ConsumerWidget { return const SizedBox(); } final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final scaffoldBackgroundColor = Theme.of(context).scaffoldBackgroundColor; + final scaffoldBackgroundColor = Theme.of( + context, + ).scaffoldBackgroundColor; final base = isDarkMode ? 0.3 : 0.08; final blendMode = isDarkMode ? BlendMode.darken : BlendMode.lighten; return Center( @@ -46,9 +50,15 @@ class MainViewBackgroundImage extends ConsumerWidget { child: FittedBox( child: ClipRect( child: ColorFiltered( - colorFilter: ColorFilter.mode(scaffoldBackgroundColor.withValues(alpha: 1 - base), blendMode), + colorFilter: ColorFilter.mode( + scaffoldBackgroundColor.withValues(alpha: 1 - base), + blendMode, + ), child: ColorFiltered( - colorFilter: ColorFilter.mode(scaffoldBackgroundColor, BlendMode.color), + colorFilter: ColorFilter.mode( + scaffoldBackgroundColor, + BlendMode.color, + ), child: appImage, ), ), diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart index 2a2b5c94e..84ce3978a 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/license_push_view_button.dart @@ -35,21 +35,36 @@ class LicensePushViewButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => - (ref.watch(settingsProvider).whenOrNull(data: (data) => data.hidePushTokens) ?? SettingsState.hidePushTokensDefault) - ? FocusedItemAsOverlay( - isFocused: - ref.watch(introductionNotifierProvider).whenOrNull(data: (data) => data.isConditionFulfilled(ref, Introduction.hidePushTokens)) ?? false, - tooltipWhenFocused: AppLocalizations.of(context)!.introHidePushTokens, - onComplete: () => ref.read(introductionNotifierProvider.notifier).complete(Introduction.hidePushTokens), - child: AppBarItem( - a11y: AppLocalizations.of(context)!.a11yPushTokensButton, - onPressed: () => Navigator.pushNamed(context, PushTokensView.routeName), - icon: const Icon(Icons.notifications), - ), - ) - : AppBarItem( - a11y: AppLocalizations.of(context)!.a11yLicensesButton, - onPressed: () => Navigator.of(context).pushNamed(LicenseView.routeName), - icon: const Icon(Icons.info_outline), - ); + (ref + .watch(settingsProvider) + .whenOrNull(data: (data) => data.hidePushTokens) ?? + SettingsState.hidePushTokensDefault) + ? FocusedItemAsOverlay( + isFocused: + ref + .watch(introductionNotifierProvider) + .whenOrNull( + data: (data) => data.isConditionFulfilled( + ref, + Introduction.hidePushTokens, + ), + ) ?? + false, + tooltipWhenFocused: AppLocalizations.of(context)!.introHidePushTokens, + onComplete: () => ref + .read(introductionNotifierProvider.notifier) + .complete(Introduction.hidePushTokens), + child: AppBarItem( + a11y: AppLocalizations.of(context)!.a11yPushTokensButton, + onPressed: () => + Navigator.pushNamed(context, PushTokensView.routeName), + icon: const Icon(Icons.notifications), + ), + ) + : AppBarItem( + a11y: AppLocalizations.of(context)!.a11yLicensesButton, + onPressed: () => + Navigator.of(context).pushNamed(LicenseView.routeName), + icon: const Icon(Icons.info_outline), + ); } diff --git a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart index 0f72a6c92..7d8a6e4c5 100644 --- a/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart +++ b/lib/views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart @@ -35,33 +35,46 @@ class QrScannerButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => Semantics( - label: AppLocalizations.of(context)!.a11yScanQrCodeButton, - child: FloatingActionButton( - onPressed: () async { - if (await Permission.camera.isPermanentlyDenied) { - showAsyncDialog( - builder: (_) => DefaultDialog( - title: Text(AppLocalizations.of(context)!.grantCameraPermissionDialogTitle), - content: Text(AppLocalizations.of(context)!.grantCameraPermissionDialogPermanentlyDenied), - ), - ); - return; - } - if (!context.mounted) return; + label: AppLocalizations.of(context)!.a11yScanQrCodeButton, + child: FloatingActionButton( + onPressed: () async { + if (await Permission.camera.isPermanentlyDenied) { + showAsyncDialog( + builder: (_) => DefaultDialog( + title: Text( + AppLocalizations.of(context)!.grantCameraPermissionDialogTitle, + ), + content: Text( + AppLocalizations.of( + context, + )!.grantCameraPermissionDialogPermanentlyDenied, + ), + ), + ); + return; + } + if (!context.mounted) return; - /// Open the QR-code scanner and call `handleQrCode`, with the scanned code as the argument. - final qrCode = await Navigator.pushNamed(context, QRScannerView.routeName); - final resultHandlers = [ - ref.read(tokenProvider.notifier), - ref.read(tokenContainerProvider.notifier), - ]; - if (qrCode == null || !context.mounted) return; - final handled = await scanQrCode(context: context, resultHandlerList: resultHandlers, qrCode: qrCode); - if (!handled) { - showErrorStatusMessage(message: (l) => l.invalidQrScan); - } - }, - child: const Icon(Icons.qr_code_scanner_outlined), - ), - ); + /// Open the QR-code scanner and call `handleQrCode`, with the scanned code as the argument. + final qrCode = await Navigator.pushNamed( + context, + QRScannerView.routeName, + ); + final resultHandlers = [ + ref.read(tokenProvider.notifier), + ref.read(tokenContainerProvider.notifier), + ]; + if (qrCode == null || !context.mounted) return; + final handled = await scanQrCode( + context: context, + resultHandlerList: resultHandlers, + qrCode: qrCode, + ); + if (!handled) { + showErrorStatusMessage(message: (l) => l.invalidQrScan); + } + }, + child: const Icon(Icons.qr_code_scanner_outlined), + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart index 1afb854d6..95b27a9ad 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart @@ -45,10 +45,18 @@ class MainViewTokensListFiltered extends ConsumerWidget { sortables = sortables.toList(); if (hidePushTokens) sortables.removeWhere((t) => t is PushToken); sortables = filter?.filterSortables(sortables) ?? sortables; - final tokenFolderIds = sortables.whereType().map((e) => e.folderId).toList(); - sortables.removeWhere((e) => e is TokenFolder && !tokenFolderIds.contains(e.folderId)); + final tokenFolderIds = sortables + .whereType() + .map((e) => e.folderId) + .toList(); + sortables.removeWhere( + (e) => e is TokenFolder && !tokenFolderIds.contains(e.folderId), + ); - return MainViewTokensList.buildSortableWidgets(sortables: sortables, draggingSortable: draggingSortable); + return MainViewTokensList.buildSortableWidgets( + sortables: sortables, + draggingSortable: draggingSortable, + ); } @override @@ -71,12 +79,13 @@ class MainViewTokensListFiltered extends ConsumerWidget { final draggingSortable = ref.watch(draggingSortableProvider); sortables.sort((a, b) => a.compareTo(b)); - final List widgets = MainViewTokensListFiltered._buildSortableWidgets( - sortables: sortables, - draggingSortable: draggingSortable, - filter: filter, - hidePushTokens: ref.watch(hidePushTokensProvider), - ); + final List widgets = + MainViewTokensListFiltered._buildSortableWidgets( + sortables: sortables, + draggingSortable: draggingSortable, + filter: filter, + hidePushTokens: ref.watch(hidePushTokensProvider), + ); return widgets; } diff --git a/lib/views/main_view/main_view_widgets/no_token_screen.dart b/lib/views/main_view/main_view_widgets/no_token_screen.dart index e0d50c7ad..92714cd9d 100644 --- a/lib/views/main_view/main_view_widgets/no_token_screen.dart +++ b/lib/views/main_view/main_view_widgets/no_token_screen.dart @@ -54,7 +54,7 @@ class NoTokenScreen extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium, overflow: TextOverflow.fade, softWrap: false, - ) + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart b/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart index 808004dcc..a49c589d6 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/container_token_sync_icon.dart @@ -31,9 +31,13 @@ class ContainerTokenSyncIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final syncState = ref.watch(tokenContainerProvider).value?.getSyncState(token); + final syncState = ref + .watch(tokenContainerProvider) + .value + ?.getSyncState(token); if (syncState == null) return const SizedBox.shrink(); - final color = Theme.of(context).listTileTheme.subtitleTextStyle?.color ?? Colors.grey; + final color = + Theme.of(context).listTileTheme.subtitleTextStyle?.color ?? Colors.grey; return Icon(color: color, switch (syncState) { SyncState.notStarted => Icons.sync, SyncState.syncing => Icons.cloud_sync_outlined, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget.dart index 103c87149..cd61da279 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget.dart @@ -36,7 +36,10 @@ class DayPasswordTokenWidget extends TokenWidget { token: token, tile: DayPasswordTokenWidgetTile(token), dragIcon: Icons.calendar_month, - editAction: EditDayPassowrdTokenAction(token: token, key: Key('${token.id}editAction')), + editAction: EditDayPassowrdTokenAction( + token: token, + key: Key('${token.id}editAction'), + ), ); } } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index 6386eb2db..06bf5489c 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -39,13 +39,19 @@ import '../token_widget_tile.dart'; class DayPasswordTokenWidgetTile extends ConsumerStatefulWidget { final DayPasswordToken token; final bool isPreview; - const DayPasswordTokenWidgetTile(this.token, {this.isPreview = false, super.key}); + const DayPasswordTokenWidgetTile( + this.token, { + this.isPreview = false, + super.key, + }); @override - ConsumerState createState() => _DayPasswordTokenWidgetTileState(); + ConsumerState createState() => + _DayPasswordTokenWidgetTileState(); } -class _DayPasswordTokenWidgetTileState extends ConsumerState { +class _DayPasswordTokenWidgetTileState + extends ConsumerState { double secondsLeft = 0; late DateTime lastCount; @@ -65,10 +71,17 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState 0) { setState(() => secondsLeft -= msSinceLastCount / 1000); } else { - setState(() => secondsLeft = widget.token.durationUntilNextOTP.inMilliseconds / 1000); + setState( + () => secondsLeft = + widget.token.durationUntilNextOTP.inMilliseconds / 1000, + ); } - final msUntilNextSecond = (secondsLeft * 1000).toInt() % 1000 + 1; // +1 to avoid 0 - Future.delayed(Duration(milliseconds: msUntilNextSecond), () => _startCountDown()); + final msUntilNextSecond = + (secondsLeft * 1000).toInt() % 1000 + 1; // +1 to avoid 0 + Future.delayed( + Duration(milliseconds: msUntilNextSecond), + () => _startCountDown(), + ); } void _copyOtpValue() { @@ -76,7 +89,11 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState data.currentLocale) ?? SettingsState.localeDefault; + final currentLocale = + ref + .watch(settingsProvider) + .whenOrNull(data: (data) => data.currentLocale) ?? + SettingsState.localeDefault; final dateTimeTokenEnd = widget.token.nextOTPTimeStart; final yMdFormat = DateFormat.yMMMd(currentLocale.languageCode); final yMdString = yMdFormat.format(dateTimeTokenEnd); @@ -95,13 +116,20 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - title: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), + ? () async => + await ref.read(tokenProvider.notifier).showToken(widget.token) + : _copyOtpValue, + title: insertCharAt( + widget.token.otpValue, + ' ', + (widget.token.digits / 2).ceil(), + ), additionalSubtitles: widget.isPreview ? [ 'Algorithm: ${widget.token.algorithm.name}', @@ -121,12 +149,28 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDUNTIL)); + if (widget.token.viewMode == + DayPasswordTokenViewMode.VALIDFOR) { + ref + .read(tokenProvider.notifier) + .updateToken( + widget.token, + (p0) => p0.copyWith( + viewMode: DayPasswordTokenViewMode.VALIDUNTIL, + ), + ); return; } - if (widget.token.viewMode == DayPasswordTokenViewMode.VALIDUNTIL) { - ref.read(tokenProvider.notifier).updateToken(widget.token, (p0) => p0.copyWith(viewMode: DayPasswordTokenViewMode.VALIDFOR)); + if (widget.token.viewMode == + DayPasswordTokenViewMode.VALIDUNTIL) { + ref + .read(tokenProvider.notifier) + .updateToken( + widget.token, + (p0) => p0.copyWith( + viewMode: DayPasswordTokenViewMode.VALIDFOR, + ), + ); return; } }, @@ -136,14 +180,22 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState '${AppLocalizations.of(context)!.dayPasswordValidFor}:', - DayPasswordTokenViewMode.VALIDUNTIL => '${AppLocalizations.of(context)!.dayPasswordValidUntil}:', + DayPasswordTokenViewMode.VALIDFOR => + '${AppLocalizations.of(context)!.dayPasswordValidFor}:', + DayPasswordTokenViewMode.VALIDUNTIL => + '${AppLocalizations.of(context)!.dayPasswordValidUntil}:', }, - style: Theme.of(context).listTileTheme.subtitleTextStyle, + style: Theme.of( + context, + ).listTileTheme.subtitleTextStyle, textAlign: TextAlign.center, overflow: TextOverflow.fade, softWrap: false, @@ -153,14 +205,19 @@ class _DayPasswordTokenWidgetTileState extends ConsumerState durationString, - DayPasswordTokenViewMode.VALIDUNTIL => '$yMdString\n$ejmString', + DayPasswordTokenViewMode.VALIDUNTIL => + '$yMdString\n$ejmString', }, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart index d7b4f8968..69de2bb49 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart @@ -60,14 +60,11 @@ class DefaultDeleteAction extends ConsumerSlideableAction { )) { return; } - if (context.mounted) { - _showDialog(context, notifier); - } + _showDialog(notifier); } : (_) => ContainerTokenIndelibleDialog.showDialog(), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.delete), Text( @@ -80,50 +77,49 @@ class DefaultDeleteAction extends ConsumerSlideableAction { ); } - void _showDialog(BuildContext context, TokenNotifier notifier) => - showAsyncDialog( - builder: (BuildContext context) => DefaultDialog( - scrollable: true, - title: Text( - AppLocalizations.of(context)!.confirmDeletion, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - ), + void _showDialog(TokenNotifier notifier) => showAsyncDialog( + builder: (BuildContext context) => DefaultDialog( + scrollable: true, + title: Text( + AppLocalizations.of(context)!.confirmDeletion, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.confirmDeletionOf(token.label), + style: Theme.of(context).textTheme.bodyMedium, ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.confirmDeletionOf(token.label), - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: 8), - Text( - AppLocalizations.of(context)!.confirmTokenDeletionHint, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + const SizedBox(height: 8), + Text( + AppLocalizations.of(context)!.confirmTokenDeletionHint, + style: Theme.of(context).textTheme.bodySmall, ), - actions: [ - DialogAction( - label: AppLocalizations.of(context)!.cancel, - intent: DialogActionIntent.cancel, - onPressed: () => Navigator.of(context).pop(), - ), - DialogAction( - label: AppLocalizations.of(context)!.delete, - intent: DialogActionIntent.destructive, - onPressed: () async { - try { - await notifier.removeToken(token); - } finally { - if (context.mounted) { - Navigator.of(context).pop(); - } - } - }, - ), - ], + ], + ), + actions: [ + DialogAction( + label: AppLocalizations.of(context)!.cancel, + intent: DialogActionIntent.cancel, + onPressed: () => Navigator.of(context).pop(), + ), + DialogAction( + label: AppLocalizations.of(context)!.delete, + intent: DialogActionIntent.destructive, + onPressed: () async { + try { + await notifier.removeToken(token); + } finally { + if (context.mounted) { + Navigator.of(context).pop(); + } + } + }, ), - ); + ], + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart index 00c514233..2ff0ec8d5 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget.dart @@ -29,16 +29,12 @@ class HOTPTokenWidget extends TokenWidget { final HOTPToken token; final bool withDivider; - const HOTPTokenWidget( - this.token, { - this.withDivider = true, - super.key, - }); + const HOTPTokenWidget(this.token, {this.withDivider = true, super.key}); @override TokenWidgetBase build(BuildContext context) => TokenWidgetBase( - token: token, - tile: HOTPTokenWidgetTile(token, key: ValueKey(token.id)), - dragIcon: Icons.replay, - editAction: EditHOTPTokenAction(token: token), - ); + token: token, + tile: HOTPTokenWidgetTile(token, key: ValueKey(token.id)), + dragIcon: Icons.replay, + editAction: EditHOTPTokenAction(token: token), + ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart index fb375ea82..a5d76f479 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart @@ -23,7 +23,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../../../../utils/view_utils.dart'; import '../../../../../../../widgets/button_widgets/intent_button.dart'; -import '../../../../../../../widgets/button_widgets/time_guarded_button.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/tokens/hotp_token.dart'; import '../../../../../utils/globals.dart'; @@ -74,23 +73,17 @@ class HOTPTokenWidgetTile extends ConsumerWidget { : [], trailing: CustomTrailing( child: isPreview - ? const FittedBox( - fit: BoxFit.contain, - child: Icon(size: 100, Icons.replay), - ) + ? const FittedBox(child: Icon(size: 100, Icons.replay)) : HideableWidget( token: token, isHidden: token.isHidden, child: Semantics( label: AppLocalizations.of(context)!.increaseCounter, - child: TimeGuardedButton( + child: IntentButton( intent: DialogActionIntent.neutral, cooldownMs: 1000, onPressed: () async => _updateOtpValue(), - child: const FittedBox( - fit: BoxFit.contain, - child: Icon(size: 100, Icons.replay), - ), + child: const FittedBox(child: Icon(size: 100, Icons.replay)), ), ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget.dart index 1d42c5a9a..6b53a4d30 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget.dart @@ -45,21 +45,26 @@ class PushTokenWidget extends TokenWidget { @override TokenWidgetBase build(BuildContext context) => TokenWidgetBase( - key: Key(token.id), - token: token, - tile: PushTokenWidgetTile(token), - dragIcon: Icons.notifications, - editAction: EditPushTokenAction(token: token, key: Key('${token.id}editAction')), - stack: [ - if (!token.isRolledOut) - Positioned.fill( - child: ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: token.rolloutState.rollOutInProgress ? RolloutWidget(token: token) : PushTokenStartRolloutWidget(token: token), - ), - ), + key: Key(token.id), + token: token, + tile: PushTokenWidgetTile(token), + dragIcon: Icons.notifications, + editAction: EditPushTokenAction( + token: token, + key: Key('${token.id}editAction'), + ), + stack: [ + if (!token.isRolledOut) + Positioned.fill( + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: token.rolloutState.rollOutInProgress + ? RolloutWidget(token: token) + : PushTokenStartRolloutWidget(token: token), ), - ], - ); + ), + ), + ], + ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart index cbf93087c..7da9abf05 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart @@ -41,23 +41,29 @@ class PushTokenWidgetTile extends ConsumerWidget { title: token.label, semanticsLabel: AppLocalizations.of(context)!.containerSerial, trailing: FocusedItemAsOverlay( - tooltipWhenFocused: AppLocalizations.of(context)!.introPollForChallenges, + tooltipWhenFocused: AppLocalizations.of( + context, + )!.introPollForChallenges, alignment: Alignment.centerLeft, - isFocused: ref.watch(introductionNotifierProvider).when( - data: (value) => value.isConditionFulfilled(ref, Introduction.pollForChallenges), + isFocused: ref + .watch(introductionNotifierProvider) + .when( + data: (value) => value.isConditionFulfilled( + ref, + Introduction.pollForChallenges, + ), error: (Object error, StackTrace stackTrace) => false, loading: () => false, ), onComplete: () { - ref.read(introductionNotifierProvider.notifier).complete(Introduction.pollForChallenges); + ref + .read(introductionNotifierProvider.notifier) + .complete(Introduction.pollForChallenges); }, child: const CustomTrailing( child: FittedBox( fit: BoxFit.contain, - child: Icon( - size: 100, - Icons.notifications, - ), + child: Icon(size: 100, Icons.notifications), ), ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart index 597a829f1..3def05a29 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_failed_widget.dart @@ -20,13 +20,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/utils/view_utils.dart'; +import 'package:privacyidea_authenticator/widgets/button_widgets/intent_button.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/extensions/enums/push_token_rollout_state_extension.dart'; import '../../../../../model/tokens/push_token.dart'; import '../../../../../utils/globals.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; -import '../../../../../widgets/button_widgets/time_guarded_button.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; class PushTokenStartRolloutWidget extends ConsumerWidget { @@ -57,7 +57,7 @@ class PushTokenStartRolloutWidget extends ConsumerWidget { const Expanded(flex: 12, child: SizedBox()), Expanded( flex: 35, - child: TimeGuardedButton( + child: IntentButton( intent: DialogActionIntent.confirm, onPressed: () => globalRef @@ -77,7 +77,7 @@ class PushTokenStartRolloutWidget extends ConsumerWidget { const Expanded(flex: 6, child: SizedBox()), Expanded( flex: 35, - child: TimeGuardedButton( + child: IntentButton( intent: DialogActionIntent.destructive, onPressed: () => _showDialog(), child: Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart index 9dc703941..534db18a0 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/rollout_widget.dart @@ -29,16 +29,16 @@ class RolloutWidget extends StatelessWidget { @override Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator.adaptive(), - Text( - token.rolloutState.rolloutMsg(AppLocalizations.of(context)!), - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - ), - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator.adaptive(), + Text( + token.rolloutState.rolloutMsg(AppLocalizations.of(context)!), + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + softWrap: false, + ), + ], + ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart index 64650a868..ccb282a5f 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_image.dart @@ -76,19 +76,19 @@ class _TokenImageState extends State { } Image _createImage(Uint8List uint8List) => Image.memory( - uint8List, - fit: BoxFit.fitHeight, - errorBuilder: (context, error, stackTrace) { - if (!mounted) return const SizedBox(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - setState(() { - hasImage = false; - }); - }); - return const SizedBox(); - }, - ); + uint8List, + fit: BoxFit.fitHeight, + errorBuilder: (context, error, stackTrace) { + if (!mounted) return const SizedBox(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + setState(() { + hasImage = false; + }); + }); + return const SizedBox(); + }, + ); @override void initState() { @@ -125,11 +125,13 @@ class _TokenImageState extends State { padding: const EdgeInsets.only(left: 4, top: 2, right: 4), child: SizedBox( height: 32, - child: tokenImage ?? + child: + tokenImage ?? const SizedBox( width: 32, child: CircularProgressIndicator.adaptive(), ), - )) + ), + ) : const SizedBox(); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index 9cde8a67e..d4169794d 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -93,7 +93,12 @@ class _TokenWidgetBaseState extends ConsumerState { if (widget.token.containerSerial == null) { deletationDisabled = Future.value(false); } else { - final selector = tokenContainerProvider.selectAsync((state) => state.containerOf(widget.token.containerSerial!)?.policies.disabledTokenDeletion); + final selector = tokenContainerProvider.selectAsync( + (state) => state + .containerOf(widget.token.containerSerial!) + ?.policies + .disabledTokenDeletion, + ); deletationDisabled = ref.watch(selector); } @@ -112,21 +117,26 @@ class _TokenWidgetBaseState extends ConsumerState { isEnabled: !tokenDeletationDisabled, key: Key('${widget.token.id}deleteAction'), ), - widget.editAction ?? DefaultEditAction(token: widget.token, key: Key('${widget.token.id}editAction')), + widget.editAction ?? + DefaultEditAction( + token: widget.token, + key: Key('${widget.token.id}editAction'), + ), ]; if ((widget.token.pin == false)) { actions.add( - widget.lockAction ?? DefaultLockAction(token: widget.token, key: Key('${widget.token.id}lockAction')), + widget.lockAction ?? + DefaultLockAction( + token: widget.token, + key: Key('${widget.token.id}lockAction'), + ), ); } if (draggingSortable == widget.token) return const SizedBox(); final child = Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: DefaultInkWell( - onTap: () {}, - child: widget.tile, - ), + child: DefaultInkWell(onTap: () {}, child: widget.tile), ); if (draggingSortable != null) { @@ -146,7 +156,8 @@ class _TokenWidgetBaseState extends ConsumerState { stack: widget.stack, child: LongPressDraggable( maxSimultaneousDrags: 1, - onDragStarted: () => ref.read(draggingSortableProvider.notifier).state = widget.token, + onDragStarted: () => + ref.read(draggingSortableProvider.notifier).state = widget.token, onDragCompleted: () { Logger.info('Draggable completed'); // Will be handled by the sortableNotifier @@ -155,34 +166,36 @@ class _TokenWidgetBaseState extends ConsumerState { Logger.info('Draggable canceled'); globalRef?.read(draggingSortableProvider.notifier).state = null; }, - dragAnchorStrategy: (Draggable d, BuildContext context, Offset point) { - final textSize = textSizeOf( - text: widget.token.label, - style: Theme.of(context).textTheme.titleMedium!, - textScaler: MediaQuery.of(context).textScaler, - maxLines: 1, - ); - return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); - }, + dragAnchorStrategy: + (Draggable d, BuildContext context, Offset point) { + final textSize = textSizeOf( + text: widget.token.label, + style: Theme.of(context).textTheme.titleMedium!, + textScaler: MediaQuery.of(context).textScaler, + maxLines: 1, + ); + return Offset( + max(textSize.width / 2, 30), + textSize.height / 2 + 30, + ); + }, feedback: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(widget.dragIcon, size: 60), Material( - color: Colors.transparent, - child: Text( - widget.token.label, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - )), + color: Colors.transparent, + child: Text( + widget.token.label, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), ], ), data: widget.token, - child: Material( - color: Colors.transparent, - child: child, - ), + child: Material(color: Colors.transparent, child: child), ), ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart index 59e090bf7..145e005f4 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart @@ -75,7 +75,12 @@ class TokenWidgetTile extends ConsumerWidget { label: semanticsLabel, child: InkWell( onTap: titleOnTap, - child: HideableText(textScaleFactor: 1.9, isHidden: token.isHidden, text: title, textStyle: titleStyle), + child: HideableText( + textScaleFactor: 1.9, + isHidden: token.isHidden, + text: title, + textStyle: titleStyle, + ), ), ), ), @@ -91,18 +96,35 @@ class TokenWidgetTile extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - if (subtitle1.isNotEmpty) Text(subtitle1, textAlign: TextAlign.left, overflow: TextOverflow.fade, softWrap: false), + if (subtitle1.isNotEmpty) + Text( + subtitle1, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), if (subtitle2.isNotEmpty) Row( children: [ Flexible( - child: Text(subtitle2, textAlign: TextAlign.left, overflow: TextOverflow.fade, softWrap: false), + child: Text( + subtitle2, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), ), SizedBox(width: 6), ContainerTokenSyncIcon(token), ], ), - for (var line in additionalSubtitles) Text(line, textAlign: TextAlign.left, overflow: TextOverflow.fade, softWrap: false), + for (var line in additionalSubtitles) + Text( + line, + textAlign: TextAlign.left, + overflow: TextOverflow.fade, + softWrap: false, + ), ], ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart index 30ba8e1d4..bc52d6a3a 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart @@ -42,10 +42,12 @@ class TOTPTokenWidgetTile extends ConsumerStatefulWidget { const TOTPTokenWidgetTile(this.token, {super.key, this.isPreview = false}); @override - ConsumerState createState() => _TOTPTokenWidgetTileState(); + ConsumerState createState() => + _TOTPTokenWidgetTileState(); } -class _TOTPTokenWidgetTileState extends ConsumerState with SingleTickerProviderStateMixin { +class _TOTPTokenWidgetTileState extends ConsumerState + with SingleTickerProviderStateMixin { late String currentOtpValue = widget.token.otpValue; late UnscaledAnimationController _animationController; Color? _currentOtpColor; @@ -57,8 +59,15 @@ class _TOTPTokenWidgetTileState extends ConsumerState with globalRef?.read(disableCopyOtpProvider.notifier).state = true; Clipboard.setData(ClipboardData(text: widget.token.otpValue)); - showSnackBar(AppLocalizations.of(context)!.otpValueCopiedMessage(widget.token.otpValue)); - Future.delayed(const Duration(seconds: 5), () => globalRef?.read(disableCopyOtpProvider.notifier).state = false); + showSnackBar( + AppLocalizations.of( + context, + )!.otpValueCopiedMessage(widget.token.otpValue), + ); + Future.delayed( + const Duration(seconds: 5), + () => globalRef?.read(disableCopyOtpProvider.notifier).state = false, + ); } @override @@ -92,12 +101,24 @@ class _TOTPTokenWidgetTileState extends ConsumerState with totalDuration: Duration(seconds: widget.token.period), warningDuration: Duration(seconds: 2), criticalDuration: Duration(seconds: 3), - defaultOtpColor: Theme.of(context).extension()!.defaultOtpColor, - warningOtpColor: Theme.of(context).extension()!.warningOtpColor, - criticalOtpColor: Theme.of(context).extension()!.criticalOtpColor, - defaultCountdownColor: Theme.of(context).extension()!.defaultCountdownColor, - warningCountdownColor: Theme.of(context).extension()!.warningCountdownColor, - criticalCountdownColor: Theme.of(context).extension()!.criticalCountdownColor, + defaultOtpColor: Theme.of( + context, + ).extension()!.defaultOtpColor, + warningOtpColor: Theme.of( + context, + ).extension()!.warningOtpColor, + criticalOtpColor: Theme.of( + context, + ).extension()!.criticalOtpColor, + defaultCountdownColor: Theme.of( + context, + ).extension()!.defaultCountdownColor, + warningCountdownColor: Theme.of( + context, + ).extension()!.warningCountdownColor, + criticalCountdownColor: Theme.of( + context, + ).extension()!.criticalCountdownColor, ).createAnimation(); } @@ -105,17 +126,22 @@ class _TOTPTokenWidgetTileState extends ConsumerState with Widget build(BuildContext context) { return TokenWidgetTile( key: Key('${widget.token.hashCode}TokenWidgetTile'), - semanticsLabel: widget.token.isHidden ? AppLocalizations.of(context)!.authenticateToShowOtp : AppLocalizations.of(context)!.copyOTPToClipboard, + semanticsLabel: widget.token.isHidden + ? AppLocalizations.of(context)!.authenticateToShowOtp + : AppLocalizations.of(context)!.copyOTPToClipboard, titleOnTap: widget.isPreview ? null : widget.token.isLocked && widget.token.isHidden - ? () async => await ref.read(tokenProvider.notifier).showToken(widget.token) - : () => _copyOtpValue(context), + ? () async => + await ref.read(tokenProvider.notifier).showToken(widget.token) + : () => _copyOtpValue(context), token: widget.token, - title: insertCharAt(widget.token.otpValue, ' ', (widget.token.digits / 2).ceil()), - titleStyle: TextStyle( - color: _currentOtpColor, + title: insertCharAt( + widget.token.otpValue, + ' ', + (widget.token.digits / 2).ceil(), ), + titleStyle: TextStyle(color: _currentOtpColor), additionalSubtitles: widget.isPreview ? [ 'Algorithm: ${widget.token.algorithm.name}', @@ -128,7 +154,11 @@ class _TOTPTokenWidgetTileState extends ConsumerState with isHidden: widget.token.isHidden && !widget.isPreview, child: TotpTokenWidgetTileCountdown( period: widget.token.period, - currentColor: _currentCountdownColor ?? Theme.of(context).extension()!.defaultCountdownColor, + currentColor: + _currentCountdownColor ?? + Theme.of( + context, + ).extension()!.defaultCountdownColor, secondsUntilNextOTP: _secondsUntilNextOTP, ), ), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart index 4b7c640a5..f29757e35 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart @@ -43,15 +43,13 @@ class TotpTokenWidgetTileCountdown extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ - Text( - '$value', - overflow: TextOverflow.fade, - softWrap: false, - ), + Text('$value', overflow: TextOverflow.fade, softWrap: false), PiCircularProgressIndicator( 1 - (secondsUntilNextOTP / period), foregroundColor: currentColor, - semanticsLabel: AppLocalizations.of(context)!.a11ySecondsUntilNextOTP(value), + semanticsLabel: AppLocalizations.of( + context, + )!.a11ySecondsUntilNextOTP(value), semanticsValue: '$value', ), ], diff --git a/lib/widgets/app_wrappers/single_touch_recognizer.dart b/lib/widgets/app_wrappers/single_touch_recognizer.dart index c5de625cd..2f2146549 100644 --- a/lib/widgets/app_wrappers/single_touch_recognizer.dart +++ b/lib/widgets/app_wrappers/single_touch_recognizer.dart @@ -9,10 +9,11 @@ class SingleTouchRecognizer extends StatelessWidget { Widget build(BuildContext context) { return RawGestureDetector( gestures: { - _SingleTouchRecognizer: GestureRecognizerFactoryWithHandlers<_SingleTouchRecognizer>( - () => _SingleTouchRecognizer(), - (_SingleTouchRecognizer instance) {}, - ), + _SingleTouchRecognizer: + GestureRecognizerFactoryWithHandlers<_SingleTouchRecognizer>( + () => _SingleTouchRecognizer(), + (_SingleTouchRecognizer instance) {}, + ), }, child: child, ); diff --git a/lib/widgets/button_widgets/intent_button.dart b/lib/widgets/button_widgets/intent_button.dart index f5b5c4e53..cc3f01b6e 100644 --- a/lib/widgets/button_widgets/intent_button.dart +++ b/lib/widgets/button_widgets/intent_button.dart @@ -17,19 +17,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * privacyIDEA Authenticator - * - * Author: Frank Merkel - * - * Copyright (c) 2026 NetKnights GmbH - */ - import 'dart:async'; import 'package:flutter/material.dart'; import '../../utils/customization/theme_extentions/app_dimensions.dart'; +import '../pi_circular_progress_indicator.dart'; extension DialogActionIntentX on DialogActionIntent { int get priority { @@ -55,63 +48,119 @@ class IntentButton extends StatefulWidget { final DialogActionIntent intent; final FutureOr Function()? onPressed; final Widget child; + final int delaySeconds; + final int cooldownMs; const IntentButton({ super.key, required this.intent, required this.onPressed, required this.child, + this.delaySeconds = 0, + this.cooldownMs = 0, }); @override State createState() => _IntentButtonState(); } -class _IntentButtonState extends State { +class _IntentButtonState extends State + with SingleTickerProviderStateMixin { bool _isLoading = false; + bool _isCooldown = false; + late int _currentDelay; + late AnimationController _animation; @override - Widget build(BuildContext context) { - return switch (widget.intent) { - DialogActionIntent.confirm || - DialogActionIntent.destructive => _buildElevatedButton(context), - DialogActionIntent.info => _buildOutlinedButton(context), - DialogActionIntent.neutral || - DialogActionIntent.cancel || - DialogActionIntent.external => _buildTextButton(context), - }; + void initState() { + super.initState(); + _currentDelay = widget.delaySeconds; + _animation = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + if (_currentDelay > 0) _startDelayTimer(); + } + + Future _startDelayTimer() async { + while (_currentDelay > 0 && mounted) { + _animation.forward(from: 0); + await Future.delayed(const Duration(seconds: 1)); + if (mounted) setState(() => _currentDelay--); + } } @override void dispose() { - _isLoading = false; + _animation.dispose(); super.dispose(); } - FutureOr Function()? get effectiveOnPressed { - if (widget.onPressed == null) return null; - - return () async { - try { - final futureOr = widget.onPressed!(); - if (futureOr is Future) { - setState(() => _isLoading = true); - await futureOr; - } - } finally { - if (mounted) setState(() => _isLoading = false); - } - }; + Future _handlePress() async { + if (widget.onPressed == null || + _isCooldown || + _isLoading || + _currentDelay > 0) + return; + + final result = widget.onPressed!.call(); + final isFuture = result is Future; + + if (!isFuture && widget.cooldownMs == 0) return; + + if (mounted) { + setState(() { + if (isFuture) _isLoading = true; + if (widget.cooldownMs > 0) _isCooldown = true; + }); + } + + final List> tasks = []; + if (isFuture) tasks.add(result); + if (widget.cooldownMs > 0) { + tasks.add(Future.delayed(Duration(milliseconds: widget.cooldownMs))); + } + + if (tasks.isNotEmpty) { + await Future.wait(tasks); + } + + if (mounted) { + setState(() { + _isLoading = false; + _isCooldown = false; + }); + } + } + + VoidCallback? get _effectiveOnPressed { + if (widget.onPressed == null || _isCooldown || _isLoading) return null; + if (_currentDelay > 0) return () {}; + return _handlePress; } - Widget _buildElevatedButton(BuildContext context) { + @override + Widget build(BuildContext context) { final theme = Theme.of(context); final dimensions = theme.extension() ?? const AppDimensions(); + + return switch (widget.intent) { + DialogActionIntent.confirm || DialogActionIntent.destructive => + _buildElevatedButton(context, dimensions), + DialogActionIntent.info => _buildOutlinedButton(context, dimensions), + DialogActionIntent.neutral || + DialogActionIntent.cancel || + DialogActionIntent.external => _buildTextButton(context, dimensions), + }; + } + + Widget _buildElevatedButton(BuildContext context, AppDimensions dimensions) { + final theme = Theme.of(context); final isDestructive = widget.intent == DialogActionIntent.destructive; return ElevatedButton( - onPressed: effectiveOnPressed, + onPressed: _effectiveOnPressed, style: ElevatedButton.styleFrom( backgroundColor: isDestructive ? theme.colorScheme.error : null, foregroundColor: isDestructive ? theme.colorScheme.onError : null, @@ -127,17 +176,15 @@ class _IntentButtonState extends State { borderRadius: BorderRadius.circular(dimensions.borderRadius), ), ), - child: _buildChildWithLoading(), + child: _buildChildWithStatus(dimensions), ); } - Widget _buildOutlinedButton(BuildContext context) { + Widget _buildOutlinedButton(BuildContext context, AppDimensions dimensions) { final theme = Theme.of(context); - final dimensions = - theme.extension() ?? const AppDimensions(); return OutlinedButton( - onPressed: effectiveOnPressed, + onPressed: _effectiveOnPressed, style: OutlinedButton.styleFrom( minimumSize: Size(0, dimensions.controlHeight), padding: const EdgeInsets.symmetric(horizontal: 16), @@ -145,7 +192,7 @@ class _IntentButtonState extends State { alpha: 0.38, ), side: BorderSide( - color: effectiveOnPressed == null + color: _effectiveOnPressed == null ? theme.colorScheme.onSurface.withValues(alpha: 0.12) : theme.dividerColor, ), @@ -153,17 +200,15 @@ class _IntentButtonState extends State { borderRadius: BorderRadius.circular(dimensions.borderRadius), ), ), - child: _buildChildWithLoading(), + child: _buildChildWithStatus(dimensions), ); } - Widget _buildTextButton(BuildContext context) { + Widget _buildTextButton(BuildContext context, AppDimensions dimensions) { final theme = Theme.of(context); - final dimensions = - theme.extension() ?? const AppDimensions(); return TextButton( - onPressed: effectiveOnPressed, + onPressed: _effectiveOnPressed, style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, minimumSize: Size(0, dimensions.controlHeight), @@ -179,30 +224,62 @@ class _IntentButtonState extends State { ? Row( mainAxisSize: MainAxisSize.min, children: [ - _buildChildWithLoading(), + _buildChildWithStatus(dimensions), const SizedBox(width: 4), Icon( Icons.open_in_new, size: 16, - // Icon passt sich automatisch der Vordergrundfarbe an, - // außer im disabled State - color: effectiveOnPressed == null + color: _effectiveOnPressed == null ? theme.colorScheme.onSurface.withValues(alpha: 0.38) : theme.colorScheme.onSurface, ), ], ) - : _buildChildWithLoading(), + : _buildChildWithStatus(dimensions), ); } - Widget _buildChildWithLoading() { + Widget _buildChildWithStatus(AppDimensions dimensions) { + if (_currentDelay > 0) return _buildCountdownStack(dimensions); if (!_isLoading) return widget.child; - return const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator(strokeWidth: 2), + return Stack( + alignment: Alignment.center, + children: [ + Opacity(opacity: 0.0, child: widget.child), + SizedBox( + width: dimensions.iconSizeMedium, + height: dimensions.iconSizeMedium, + child: CircularProgressIndicator(strokeWidth: dimensions.strokeWidth), + ), + ], + ); + } + + Widget _buildCountdownStack(AppDimensions dimensions) { + return SizedBox( + height: dimensions.iconSizeMedium, + width: dimensions.iconSizeMedium, + child: Stack( + alignment: Alignment.center, + children: [ + AnimatedBuilder( + animation: _animation, + builder: (context, _) => PiCircularProgressIndicator( + _animation.value, + strokeWidth: 3, + swapColors: _currentDelay % 2 == 0, + ), + ), + Text( + _currentDelay.toString(), + style: TextStyle( + fontSize: dimensions.spacingMedium * 0.8, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ); } } diff --git a/lib/widgets/button_widgets/mutex_button.dart b/lib/widgets/button_widgets/mutex_button.dart index 52e39255a..2ed8623d0 100644 --- a/lib/widgets/button_widgets/mutex_button.dart +++ b/lib/widgets/button_widgets/mutex_button.dart @@ -24,7 +24,12 @@ class MutexButton extends StatefulWidget { final Future Function()? onPressed; final Widget child; final ButtonStyle? style; - const MutexButton({super.key, required this.onPressed, required this.child, this.style}); + const MutexButton({ + super.key, + required this.onPressed, + required this.child, + this.style, + }); @override State createState() => _MutexButtonState(); @@ -46,18 +51,20 @@ class _MutexButtonState extends State { @override Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.all(3), - child: ElevatedButton( - onPressed: isPressable - ? () => m.protect(() async { - setState(() => isPressable = false); - await widget.onPressed!(); - if (!mounted) return; - setState(() => isPressable = true); - }) - : null, - style: widget.style?.merge(Theme.of(context).elevatedButtonTheme.style) ?? Theme.of(context).elevatedButtonTheme.style, - child: widget.child, - ), - ); + padding: const EdgeInsets.all(3), + child: ElevatedButton( + onPressed: isPressable + ? () => m.protect(() async { + setState(() => isPressable = false); + await widget.onPressed!(); + if (!mounted) return; + setState(() => isPressable = true); + }) + : null, + style: + widget.style?.merge(Theme.of(context).elevatedButtonTheme.style) ?? + Theme.of(context).elevatedButtonTheme.style, + child: widget.child, + ), + ); } diff --git a/lib/widgets/button_widgets/push_action_button.dart b/lib/widgets/button_widgets/push_action_button.dart index 13578f6c1..aa6114c01 100644 --- a/lib/widgets/button_widgets/push_action_button.dart +++ b/lib/widgets/button_widgets/push_action_button.dart @@ -23,7 +23,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'intent_button.dart'; -import 'time_guarded_button.dart'; /// A specialized button for Push Notification actions with a distinct border and typography. /// Uses [TimeGuardedButton] to handle asynchronous execution and a minimum threshold to prevent double-taps. @@ -48,7 +47,7 @@ class PushActionButton extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - return TimeGuardedButton( + return IntentButton( intent: intent, onPressed: onPressed, cooldownMs: minThreshold, diff --git a/lib/widgets/button_widgets/time_guarded_button.dart b/lib/widgets/button_widgets/time_guarded_button.dart index ffdd8273e..24ea130a7 100644 --- a/lib/widgets/button_widgets/time_guarded_button.dart +++ b/lib/widgets/button_widgets/time_guarded_button.dart @@ -1,137 +1,144 @@ -/* - * privacyIDEA Authenticator - * - * Author: Frank Merkel - * - * Copyright (c) 2026 NetKnights GmbH - * - * 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 '../../utils/customization/theme_extentions/app_dimensions.dart'; -import '../pi_circular_progress_indicator.dart'; -import 'intent_button.dart'; - -class TimeGuardedButton extends StatefulWidget { - final FutureOr Function()? onPressed; - final Widget child; - final int delaySeconds; - final int cooldownMs; - final DialogActionIntent intent; - - const TimeGuardedButton({ - super.key, - required this.onPressed, - required this.child, - this.intent = DialogActionIntent.confirm, - this.delaySeconds = 0, - this.cooldownMs = 0, - }); - - @override - State createState() => _TimeGuardedButtonState(); -} - -class _TimeGuardedButtonState extends State - with SingleTickerProviderStateMixin { - bool _isCooldown = false; - late int _currentDelay; - late AnimationController _animation; - - @override - void initState() { - super.initState(); - _currentDelay = widget.delaySeconds; - _animation = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - if (_currentDelay > 0) _startDelayTimer(); - } - - void _startDelayTimer() async { - while (_currentDelay > 0 && mounted) { - _animation.forward(from: 0); - await Future.delayed(const Duration(seconds: 1)); - if (mounted) setState(() => _currentDelay--); - } - } - - void _handlePress() { - if (widget.cooldownMs > 0) setState(() => _isCooldown = true); - - widget.onPressed?.call(); - - if (widget.cooldownMs > 0 && mounted) { - Future.delayed(Duration(milliseconds: widget.cooldownMs)).then((_) { - if (mounted) setState(() => _isCooldown = false); - }); - } - } - - @override - void dispose() { - _animation.dispose(); - super.dispose(); - } - - FutureOr Function()? get effectiveOnPressed { - if (widget.onPressed == null || _isCooldown) return null; - if (_currentDelay > 0) return () {}; - return _handlePress; - } - - @override - Widget build(BuildContext context) { - final dimensions = - Theme.of(context).extension() ?? const AppDimensions(); - - return IntentButton( - intent: widget.intent, - onPressed: effectiveOnPressed, - child: _currentDelay > 0 - ? _buildCountdownStack(dimensions) - : widget.child, - ); - } - - Widget _buildCountdownStack(AppDimensions dimensions) { - return SizedBox( - height: dimensions.iconSizeMedium, - width: dimensions.iconSizeMedium, - child: Stack( - alignment: Alignment.center, - children: [ - AnimatedBuilder( - animation: _animation, - builder: (context, _) => PiCircularProgressIndicator( - _animation.value, - strokeWidth: 3, - swapColors: _currentDelay % 2 == 0, - ), - ), - Text( - _currentDelay.toString(), - style: TextStyle( - fontSize: dimensions.spacingMedium * 0.8, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } -} +// /* +// * privacyIDEA Authenticator +// * +// * Author: Frank Merkel +// * +// * Copyright (c) 2026 NetKnights GmbH +// * +// * 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 '../../utils/customization/theme_extentions/app_dimensions.dart'; +// import '../pi_circular_progress_indicator.dart'; +// import 'intent_button.dart'; + +// class TimeGuardedButton extends StatefulWidget { +// final FutureOr Function()? onPressed; +// final Widget child; +// final int delaySeconds; +// final int cooldownMs; +// final DialogActionIntent intent; + +// const IntentButton({ +// super.key, +// required this.onPressed, +// required this.child, +// this.intent = DialogActionIntent.confirm, +// this.delaySeconds = 0, +// this.cooldownMs = 0, +// }); + +// @override +// State createState() => _TimeGuardedButtonState(); +// } + +// class _TimeGuardedButtonState extends State +// with SingleTickerProviderStateMixin { +// bool _isCooldown = false; +// late int _currentDelay; +// late AnimationController _animation; + +// @override +// void initState() { +// super.initState(); +// _currentDelay = widget.delaySeconds; +// _animation = AnimationController( +// vsync: this, +// duration: const Duration(seconds: 1), +// ); +// if (_currentDelay > 0) _startDelayTimer(); +// } + +// Future _startDelayTimer() async { +// while (_currentDelay > 0 && mounted) { +// await Future.wait([ +// _animation.forward(from: 0), +// Future.delayed(const Duration(seconds: 1)), +// ]); +// if (mounted) setState(() => _currentDelay--); +// } +// } + +// Future _handlePress() async { +// if (widget.onPressed == null) return; +// final result = widget.onPressed!.call(); +// final isFuture = result is Future; +// if (!isFuture && widget.cooldownMs == 0) return; + +// if (mounted) setState(() => _isCooldown = true); +// await Future.wait([ +// if (isFuture) result, +// Future.delayed(Duration(milliseconds: widget.cooldownMs)), +// ]); +// if (mounted) setState(() => _isCooldown = false); +// } + +// @override +// void dispose() { +// _animation.dispose(); +// super.dispose(); +// } + +// FutureOr Function()? get effectiveOnPressed { +// if (widget.onPressed == null || _isCooldown) { +// return null; +// } +// if (_currentDelay > 0) { +// return () {}; +// } +// return _handlePress; +// } + +// @override +// Widget build(BuildContext context) { +// final dimensions = +// Theme.of(context).extension() ?? const AppDimensions(); + +// return IntentButton( +// intent: widget.intent, +// onPressed: () async { +// effectiveOnPressed?.call(); +// }, +// child: _currentDelay > 0 +// ? _buildCountdownStack(dimensions) +// : widget.child, +// ); +// } + +// Widget _buildCountdownStack(AppDimensions dimensions) { +// return Stack( +// alignment: Alignment.center, +// children: [ +// AnimatedBuilder( +// animation: _animation, +// builder: (context, _) => PiCircularProgressIndicator( +// size: dimensions.iconSizeMedium, +// _animation.value, +// strokeWidth: 3, +// swapColors: _currentDelay % 2 == 0, +// ), +// ), +// Text( +// _currentDelay.toString(), +// style: TextStyle( +// fontSize: dimensions.spacingMedium * 0.8, +// fontWeight: FontWeight.bold, +// ), +// ), +// ], +// ); +// } +// } diff --git a/lib/widgets/custom_texts.dart b/lib/widgets/custom_texts.dart index 744313169..a29bc3662 100644 --- a/lib/widgets/custom_texts.dart +++ b/lib/widgets/custom_texts.dart @@ -55,10 +55,18 @@ class HideableText extends StatelessWidget { @override Widget build(BuildContext context) { return Text( - isHidden ? text.replaceAll(RegExp(replaceWhitespaces ? r'.' : r'[^\s]'), replaceCharacter) : text, + isHidden + ? text.replaceAll( + RegExp(replaceWhitespaces ? r'.' : r'[^\s]'), + replaceCharacter, + ) + : text, textScaler: const TextScaler.linear(1.9), style: textStyle != null - ? textStyle!.copyWith(fontFamily: 'monospace', fontWeight: FontWeight.bold) + ? textStyle!.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + ) : const TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.bold, diff --git a/lib/widgets/custom_trailing.dart b/lib/widgets/custom_trailing.dart index 59b9db5b6..d26f3b2a3 100644 --- a/lib/widgets/custom_trailing.dart +++ b/lib/widgets/custom_trailing.dart @@ -12,22 +12,30 @@ class CustomTrailing extends StatelessWidget { /// Creates a widget that limits the width of [child] to [maxPercentWidth] of /// the parent width or [maxPixelsWidth] if the parent width is too small. /// Defaults: [maxPercentWidth] = 27.5, [maxPixelsWidth] = 85 - const CustomTrailing({required this.child, super.key, double? maxPercentWidth, double? maxPixelsWidth, this.padding, this.fit = BoxFit.contain}) - : maxPercentWidth = maxPercentWidth ?? 27.5, - maxPixelsWidth = maxPixelsWidth ?? 85; + const CustomTrailing({ + required this.child, + super.key, + double? maxPercentWidth, + double? maxPixelsWidth, + this.padding, + this.fit = BoxFit.contain, + }) : maxPercentWidth = maxPercentWidth ?? 27.5, + maxPixelsWidth = maxPixelsWidth ?? 85; @override Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - final boxSize = min(maxPixelsWidth, constraints.maxWidth * maxPercentWidth / 100); - return SizedBox( - width: boxSize, - height: boxSize, - child: FittedBox( - fit: fit, - child: child, - ), - ); - }); + return LayoutBuilder( + builder: (context, constraints) { + final boxSize = min( + maxPixelsWidth, + constraints.maxWidth * maxPercentWidth / 100, + ); + return SizedBox( + width: boxSize, + height: boxSize, + child: FittedBox(fit: fit, child: child), + ); + }, + ); } } diff --git a/lib/widgets/deactivateable.dart b/lib/widgets/deactivateable.dart index ed0687911..c6490aa48 100644 --- a/lib/widgets/deactivateable.dart +++ b/lib/widgets/deactivateable.dart @@ -23,13 +23,14 @@ class Deactivateable extends StatelessWidget { final bool deactivated; final Widget child; - const Deactivateable({super.key, required this.deactivated, required this.child}); + const Deactivateable({ + super.key, + required this.deactivated, + required this.child, + }); @override Widget build(BuildContext context) => deactivated - ? Opacity( - opacity: 0.3, - child: AbsorbPointer(child: child), - ) + ? Opacity(opacity: 0.3, child: AbsorbPointer(child: child)) : child; } diff --git a/lib/widgets/default_refresh_indicator.dart b/lib/widgets/default_refresh_indicator.dart index 48bcfb619..187a86c73 100644 --- a/lib/widgets/default_refresh_indicator.dart +++ b/lib/widgets/default_refresh_indicator.dart @@ -12,18 +12,27 @@ class DefaultRefreshIndicator extends ConsumerStatefulWidget { final GlobalKey? listViewKey; final ScrollController? scrollController; - const DefaultRefreshIndicator({super.key, this.listViewKey, this.scrollController, required this.child}); + const DefaultRefreshIndicator({ + super.key, + this.listViewKey, + this.scrollController, + required this.child, + }); @override - ConsumerState createState() => _DefaultRefreshIndicatorState(); + ConsumerState createState() => + _DefaultRefreshIndicatorState(); } -class _DefaultRefreshIndicatorState extends ConsumerState { +class _DefaultRefreshIndicatorState + extends ConsumerState { bool isRefreshing = false; @override Widget build(BuildContext context) { - final hasRolledOutPushTokens = ref.watch(tokenProvider).value?.hasRolledOutPushTokens ?? false; - final hasFinalizedContainers = ref.watch(tokenContainerProvider).value?.hasFinalizedContainers == true; + final hasRolledOutPushTokens = + ref.watch(tokenProvider).value?.hasRolledOutPushTokens ?? false; + final hasFinalizedContainers = + ref.watch(tokenContainerProvider).value?.hasFinalizedContainers == true; final allowToRefresh = hasRolledOutPushTokens || hasFinalizedContainers; return DeactivateableRefreshIndicator( onRefresh: () async { @@ -36,8 +45,11 @@ class _DefaultRefreshIndicatorState extends ConsumerState - allowToRefresh ? const AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics()) : const ClampingScrollPhysics(); + ScrollPhysics _getScrollPhysics(bool allowToRefresh) => allowToRefresh + ? const AlwaysScrollableScrollPhysics(parent: ClampingScrollPhysics()) + : const ClampingScrollPhysics(); } diff --git a/lib/widgets/dialog_widgets/default_dialog.dart b/lib/widgets/dialog_widgets/default_dialog.dart index cd6cecfaf..6f1870ab7 100644 --- a/lib/widgets/dialog_widgets/default_dialog.dart +++ b/lib/widgets/dialog_widgets/default_dialog.dart @@ -25,7 +25,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../utils/customization/theme_extentions/app_dimensions.dart'; import '../button_widgets/intent_button.dart'; -import '../button_widgets/time_guarded_button.dart'; export '../button_widgets/intent_button.dart' show DialogActionIntent; @@ -45,9 +44,6 @@ class DialogAction { this.cooldownMs = 0, this.formState, }); - - // Determines if this action requires the TimeGuardedButton logic - bool get isTimed => delaySeconds > 0 || cooldownMs > 0; } class DefaultDialog extends ConsumerWidget { @@ -140,18 +136,11 @@ class DefaultDialog extends ConsumerWidget { ..sort((a, b) => a.intent.priority.compareTo(b.intent.priority)); return sortedActions.map((action) { - if (action.isTimed) { - return TimeGuardedButton( - intent: action.intent, - onPressed: action.onPressed, - delaySeconds: action.delaySeconds, - cooldownMs: action.cooldownMs, - child: Text(action.label), - ); - } return IntentButton( intent: action.intent, onPressed: action.onPressed, + delaySeconds: action.delaySeconds, + cooldownMs: action.cooldownMs, child: Text(action.label), ); }).toList(); diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_code_to_phone_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_code_to_phone_dialog.dart index 4d6e55502..ac69c0748 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_code_to_phone_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_code_to_phone_dialog.dart @@ -38,40 +38,9 @@ class PushCodeToPhoneDialog extends ConsumerStatefulWidget } class _PushCodeToPhoneDialogState extends ConsumerState { - bool _isRevealed = false; - bool _hasBeenSeen = false; bool _isCopyOnCooldown = false; final _formKey = GlobalKey(); - @override - void initState() { - super.initState(); - if (!widget.token.isLocked) { - _isRevealed = true; - _hasBeenSeen = true; - } - } - - Future _handleVisibility() async { - if (!_isRevealed) { - if (widget.token.isLocked) { - final authenticated = await lockAuth( - reason: (l) => l.authenticateToShowOtp, - localization: AppLocalizations.of(context)!, - forceBiometricOption: widget.token.forceBiometricOption, - ); - if (!authenticated) return; - } - setState(() { - _isRevealed = true; - _hasBeenSeen = true; - }); - _formKey.currentState?.validate(); - } else { - setState(() => _isRevealed = false); - } - } - void _copyToClipboard() { if (_isCopyOnCooldown) return; @@ -108,53 +77,33 @@ class _PushCodeToPhoneDialogState extends ConsumerState { children: [ PushRequestBaseInfo(pushRequest: widget.pushRequest), const SizedBox(height: 12), - FormField( - initialValue: _hasBeenSeen, - validator: (_) => !_hasBeenSeen ? "" : null, - builder: (state) => Center( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(width: 40), - GestureDetector( - onTap: _isRevealed ? _copyToClipboard : _handleVisibility, - child: Text( - insertCharAt( - _isRevealed - ? widget.pushRequest.displayCode - : '•' * widget.pushRequest.displayCode.length, - ' ', - (widget.pushRequest.displayCode.length / 2).ceil(), - ), - textAlign: TextAlign.center, - style: theme.textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.bold, - color: _hasBeenSeen - ? theme.colorScheme.primary - : theme.colorScheme.error, - ), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 40), + GestureDetector( + onTap: _copyToClipboard, + child: Text( + widget.pushRequest.displayCode, + + textAlign: TextAlign.center, + style: theme.textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, ), ), - _isRevealed - ? IconButton( - icon: Icon( - Icons.copy, - color: _isCopyOnCooldown - ? theme.disabledColor - : theme.colorScheme.primary, - ), - onPressed: _copyToClipboard, - ) - : IconButton( - icon: Icon( - Icons.visibility, - color: theme.colorScheme.primary, - ), - onPressed: _handleVisibility, - ), - ], - ), + ), + IconButton( + icon: Icon( + Icons.copy, + color: _isCopyOnCooldown + ? theme.disabledColor + : theme.colorScheme.primary, + ), + onPressed: _copyToClipboard, + ), + ], ), ), ], diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_default_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_default_dialog.dart index 752e480af..2dbfe179f 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_default_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_default_dialog.dart @@ -46,7 +46,6 @@ class PushDefaultDialog extends ConsumerWidget with PushDialogMixin { PushRequestBaseInfo(pushRequest: pushRequest), const SizedBox(height: 24), PushActionButton( - intent: DialogActionIntent.confirm, onPressed: () => _handleAccept(context, ref), child: Text(l10n.accept), ), diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart index f3e3bc1a1..315550159 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart @@ -37,7 +37,6 @@ import '../../../utils/lock_auth.dart'; import '../../../utils/logger.dart'; import '../../../utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart'; import '../../../utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; -import '../../../utils/utils.dart'; import '../../../utils/view_utils.dart'; import '../default_dialog.dart'; import 'widgets/push_decline_confirm_dialog.dart'; diff --git a/lib/widgets/dot_indicator.dart b/lib/widgets/dot_indicator.dart index 819737321..46520dff2 100644 --- a/lib/widgets/dot_indicator.dart +++ b/lib/widgets/dot_indicator.dart @@ -8,9 +8,13 @@ class DotIndicator extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData mode = Theme.of(context); - Color circleBackgroundColor = mode.brightness == Brightness.dark ? Colors.white38 : Colors.grey; + Color circleBackgroundColor = mode.brightness == Brightness.dark + ? Colors.white38 + : Colors.grey; - Color selectedColor = mode.brightness == Brightness.dark ? Colors.white : Colors.black; + Color selectedColor = mode.brightness == Brightness.dark + ? Colors.white + : Colors.black; return Padding( padding: const EdgeInsets.only(right: 6.0), diff --git a/lib/widgets/drag_item_scroller.dart b/lib/widgets/drag_item_scroller.dart index df569e34f..bc32dd9e0 100644 --- a/lib/widgets/drag_item_scroller.dart +++ b/lib/widgets/drag_item_scroller.dart @@ -12,10 +12,12 @@ final dragItemScrollerStateProvider = StateProvider((ref) => false); class DragItemScroller extends StatefulWidget { static const maxScrollingSpeed = 800.0; // px per second static const minScrollingSpeed = 100.0; // px per second - static const minScrollingSpeedDetectDistanceTop = 40.0; // px distance to top it starts to scroll up + static const minScrollingSpeedDetectDistanceTop = + 40.0; // px distance to top it starts to scroll up // When the dragitem reached the end of this zone, it has max speed. if this is smaller than minScrollingSpeedDetectDistanceTop, its possible it will never scroll up with max speed static const maxSpeedZoneHeightTop = 40.0; - static const minScrollingSpeedDetectDistanceBottom = 120.0; // px distance to bottom it starts to scroll down + static const minScrollingSpeedDetectDistanceBottom = + 120.0; // px distance to bottom it starts to scroll down // When the dragitem reached the end of this zone, it has max speed. if this is smaller than minScrollingSpeedDetectDistanceBottom, its possible it will never scroll down with max speed static const maxSpeedZoneHeightBottom = 40; static const refreshRate = 30; // fps @@ -33,7 +35,10 @@ class DragItemScroller extends StatefulWidget { this.nestedScrollViewKey, this.scrollController, super.key, - }) : assert(nestedScrollViewKey != null || scrollController != null, 'Either nestedScrollViewKey or scrollController must be set'); + }) : assert( + nestedScrollViewKey != null || scrollController != null, + 'Either nestedScrollViewKey or scrollController must be set', + ); @override State createState() => _DragItemScrollerState(); @@ -47,49 +52,75 @@ class _DragItemScrollerState extends State { super.initState(); Future.doWhile(() async { _scrollJump(); - await Future.delayed(const Duration(milliseconds: 1000 ~/ DragItemScroller.refreshRate)); // wait for next frame + await Future.delayed( + const Duration(milliseconds: 1000 ~/ DragItemScroller.refreshRate), + ); // wait for next frame return mounted; }); } bool canScroll(ScrollController? controller) => - controller != null && ((controller.offset > 0 && currentSpeed < 0) || (controller.offset < controller.position.maxScrollExtent && currentSpeed > 0)); + controller != null && + ((controller.offset > 0 && currentSpeed < 0) || + (controller.offset < controller.position.maxScrollExtent && + currentSpeed > 0)); void _scrollJump() { if (currentSpeed == 0) return; // no speed, no jump - final innerController = widget.nestedScrollViewKey?.currentState?.innerController ?? widget.scrollController; - final outerController = widget.nestedScrollViewKey?.currentState?.outerController; + final innerController = + widget.nestedScrollViewKey?.currentState?.innerController ?? + widget.scrollController; + final outerController = + widget.nestedScrollViewKey?.currentState?.outerController; if (canScroll(outerController)) { - final distanceOneFrame = currentSpeed / DragItemScroller.refreshRate; // px this frame - final nextPosition = clampDouble(outerController!.offset + distanceOneFrame, 0, outerController.position.maxScrollExtent); + final distanceOneFrame = + currentSpeed / DragItemScroller.refreshRate; // px this frame + final nextPosition = clampDouble( + outerController!.offset + distanceOneFrame, + 0, + outerController.position.maxScrollExtent, + ); outerController.position.setPixels(nextPosition); // jump to next position return; } if (canScroll(innerController)) { - final distanceOneFrame = currentSpeed / DragItemScroller.refreshRate; // px this frame - final nextPosition = clampDouble(innerController!.offset + distanceOneFrame, 0, innerController.position.maxScrollExtent); + final distanceOneFrame = + currentSpeed / DragItemScroller.refreshRate; // px this frame + final nextPosition = clampDouble( + innerController!.offset + distanceOneFrame, + 0, + innerController.position.maxScrollExtent, + ); innerController.position.jumpTo(nextPosition); // jump to next position return; } } void _startScrolling(double speedInPercent, {bool moveUp = false}) { - double nextScrollingSpeed = max(DragItemScroller.minScrollingSpeed, DragItemScroller.maxScrollingSpeed * speedInPercent); - if (moveUp) nextScrollingSpeed = -nextScrollingSpeed; // if moveUp is true, the speed is negative + double nextScrollingSpeed = max( + DragItemScroller.minScrollingSpeed, + DragItemScroller.maxScrollingSpeed * speedInPercent, + ); + if (moveUp) + nextScrollingSpeed = + -nextScrollingSpeed; // if moveUp is true, the speed is negative if (currentSpeed == nextScrollingSpeed) return; setState(() { currentSpeed = nextScrollingSpeed; // set new speed }); - if (globalRef?.read(dragItemScrollerStateProvider.notifier).state != true && currentSpeed != 0) { - globalRef?.read(dragItemScrollerStateProvider.notifier).state = true; // set scrolling state to true if there is speed + if (globalRef?.read(dragItemScrollerStateProvider.notifier).state != true && + currentSpeed != 0) { + globalRef?.read(dragItemScrollerStateProvider.notifier).state = + true; // set scrolling state to true if there is speed } } void _stopScrolling() { currentSpeed = 0; // to stop set speed to 0 if (globalRef?.read(dragItemScrollerStateProvider.notifier).state == true) { - globalRef?.read(dragItemScrollerStateProvider.notifier).state = false; // set scrolling state to false if there is no speed + globalRef?.read(dragItemScrollerStateProvider.notifier).state = + false; // set scrolling state to false if there is no speed } } @@ -110,34 +141,75 @@ class _DragItemScrollerState extends State { child: widget.child, onPointerMove: (PointerMoveEvent event) { if (widget.itemIsDragged == false) return; - final innerController = widget.nestedScrollViewKey?.currentState?.innerController ?? widget.scrollController; - final innerControllerOffset = innerController?.offset ?? widget.scrollController?.offset ?? 0.0; - final innerControllerMaxScrollExtent = innerController?.position.maxScrollExtent ?? widget.scrollController?.position.maxScrollExtent ?? 0.0; - final outerControllerOffset = widget.nestedScrollViewKey?.currentState?.outerController.offset ?? 0.0; - final outerControllerMaxScrollExtent = widget.nestedScrollViewKey?.currentState?.outerController.position.maxScrollExtent ?? 0.0; - final render = widget.listViewKey.currentContext?.findRenderObject() as RenderBox; + final innerController = + widget.nestedScrollViewKey?.currentState?.innerController ?? + widget.scrollController; + final innerControllerOffset = + innerController?.offset ?? widget.scrollController?.offset ?? 0.0; + final innerControllerMaxScrollExtent = + innerController?.position.maxScrollExtent ?? + widget.scrollController?.position.maxScrollExtent ?? + 0.0; + final outerControllerOffset = + widget.nestedScrollViewKey?.currentState?.outerController.offset ?? + 0.0; + final outerControllerMaxScrollExtent = + widget + .nestedScrollViewKey + ?.currentState + ?.outerController + .position + .maxScrollExtent ?? + 0.0; + final render = + widget.listViewKey.currentContext?.findRenderObject() as RenderBox; final position = render.localToGlobal(Offset.zero); final topY = position.dy; // top position of the widget - final bottomY = topY + render.size.height; // bottom position of the widget - final minScrollingSpeedDetectDistanceTopWithOuterOffset = DragItemScroller.minScrollingSpeedDetectDistanceTop + outerControllerOffset; - if (event.position.dy < topY + minScrollingSpeedDetectDistanceTopWithOuterOffset && (innerControllerOffset > 0 || outerControllerOffset > 0)) { + final bottomY = + topY + render.size.height; // bottom position of the widget + final minScrollingSpeedDetectDistanceTopWithOuterOffset = + DragItemScroller.minScrollingSpeedDetectDistanceTop + + outerControllerOffset; + if (event.position.dy < + topY + minScrollingSpeedDetectDistanceTopWithOuterOffset && + (innerControllerOffset > 0 || outerControllerOffset > 0)) { // scroll up if the pointer is in the top range and the scrollController is not at the top final distanceToTop = event.position.dy - topY; - final distanceToMaxSpeed = distanceToTop - (minScrollingSpeedDetectDistanceTopWithOuterOffset - DragItemScroller.maxSpeedZoneHeightTop); - final scrollSpeedPercent = 1 - distanceToMaxSpeed / DragItemScroller.maxSpeedZoneHeightTop; + final distanceToMaxSpeed = + distanceToTop - + (minScrollingSpeedDetectDistanceTopWithOuterOffset - + DragItemScroller.maxSpeedZoneHeightTop); + final scrollSpeedPercent = + 1 - distanceToMaxSpeed / DragItemScroller.maxSpeedZoneHeightTop; Logger.info('scrollSpeedPercent: $scrollSpeedPercent'); - _startScrolling(clampDouble(scrollSpeedPercent, 0.0, 1.0), moveUp: true); + _startScrolling( + clampDouble(scrollSpeedPercent, 0.0, 1.0), + moveUp: true, + ); return; } - if (event.position.dy > bottomY - DragItemScroller.minScrollingSpeedDetectDistanceBottom && - (innerControllerOffset < innerControllerMaxScrollExtent || outerControllerOffset < outerControllerMaxScrollExtent)) { + if (event.position.dy > + bottomY - + DragItemScroller.minScrollingSpeedDetectDistanceBottom && + (innerControllerOffset < innerControllerMaxScrollExtent || + outerControllerOffset < outerControllerMaxScrollExtent)) { // scroll down if the pointer is in the bottom range and the scrollController is not at the bottom - final distanceToBottom = bottomY - event.position.dy; // distance to bottom of the widget in px - final distanceToMaxSpeed = distanceToBottom - (DragItemScroller.minScrollingSpeedDetectDistanceBottom - DragItemScroller.maxSpeedZoneHeightBottom); - final scrollSpeedPercent = 1 - distanceToMaxSpeed / DragItemScroller.maxSpeedZoneHeightBottom; + final distanceToBottom = + bottomY - + event.position.dy; // distance to bottom of the widget in px + final distanceToMaxSpeed = + distanceToBottom - + (DragItemScroller.minScrollingSpeedDetectDistanceBottom - + DragItemScroller.maxSpeedZoneHeightBottom); + final scrollSpeedPercent = + 1 - + distanceToMaxSpeed / DragItemScroller.maxSpeedZoneHeightBottom; Logger.info('scrollSpeedPercent: $scrollSpeedPercent'); - _startScrolling(clampDouble(scrollSpeedPercent, 0.0, 1.0), moveUp: false); + _startScrolling( + clampDouble(scrollSpeedPercent, 0.0, 1.0), + moveUp: false, + ); return; } _stopScrolling(); diff --git a/lib/widgets/hideable_widget_.dart b/lib/widgets/hideable_widget_.dart index bc546e8ea..262dd43ef 100644 --- a/lib/widgets/hideable_widget_.dart +++ b/lib/widgets/hideable_widget_.dart @@ -21,7 +21,8 @@ class HideableWidget extends ConsumerWidget { return token.isLocked && isHidden ? IconButton( tooltip: AppLocalizations.of(context)!.authenticateToShowOtp, - onPressed: () async => ref.read(tokenProvider.notifier).showToken(token), + onPressed: () async => + ref.read(tokenProvider.notifier).showToken(token), icon: const Icon(Icons.remove_red_eye_outlined), ) : child; diff --git a/lib/widgets/home_widgets/home_widget_action.dart b/lib/widgets/home_widgets/home_widget_action.dart index 1b61b9c54..bf764b3f7 100644 --- a/lib/widgets/home_widgets/home_widget_action.dart +++ b/lib/widgets/home_widgets/home_widget_action.dart @@ -19,22 +19,26 @@ class HomeWidgetAction extends FlutterHomeWidgetBase { }); @override - Widget build(BuildContext context) => (aditionalSuffix == HomeWidgetUtils.keySuffixActive) + Widget build(BuildContext context) => + (aditionalSuffix == HomeWidgetUtils.keySuffixActive) ? Icon( icon, size: min(logicalSize.width, logicalSize.height), color: theme.listTileTheme.iconColor, ) : (aditionalSuffix == HomeWidgetUtils.keySuffixInactive) - ? Icon( - icon, - size: min(logicalSize.width, logicalSize.height), - color: theme.listTileTheme.iconColor?.mixWith(theme.scaffoldBackgroundColor), - ) - : const SizedBox(); + ? Icon( + icon, + size: min(logicalSize.width, logicalSize.height), + color: theme.listTileTheme.iconColor?.mixWith( + theme.scaffoldBackgroundColor, + ), + ) + : const SizedBox(); } -class HomeWidgetActionBuilder extends FlutterHomeWidgetBuilder { +class HomeWidgetActionBuilder + extends FlutterHomeWidgetBuilder { final IconData icon; HomeWidgetActionBuilder({ super.key, @@ -45,19 +49,24 @@ class HomeWidgetActionBuilder extends FlutterHomeWidgetBuilder required super.homeWidgetKey, required super.utils, }) : super( - formWidget: (key, theme, logicalSize, additionalSuffix) => HomeWidgetAction( - icon: icon, - key: key, - theme: theme, - logicalSize: logicalSize, - aditionalSuffix: additionalSuffix ?? '', - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, additionalSuffix) => + HomeWidgetAction( + icon: icon, + key: key, + theme: theme, + logicalSize: logicalSize, + aditionalSuffix: additionalSuffix ?? '', + utils: utils, + ), + ); @override Future renderFlutterWidgets({String additionalSuffix = ''}) async { - await super.renderFlutterWidgets(additionalSuffix: '$additionalSuffix${HomeWidgetUtils.keySuffixActive}'); - await super.renderFlutterWidgets(additionalSuffix: '$additionalSuffix${HomeWidgetUtils.keySuffixInactive}'); + await super.renderFlutterWidgets( + additionalSuffix: '$additionalSuffix${HomeWidgetUtils.keySuffixActive}', + ); + await super.renderFlutterWidgets( + additionalSuffix: '$additionalSuffix${HomeWidgetUtils.keySuffixInactive}', + ); } } diff --git a/lib/widgets/home_widgets/home_widget_background.dart b/lib/widgets/home_widgets/home_widget_background.dart index 53f8b98c1..d0c4d2021 100644 --- a/lib/widgets/home_widgets/home_widget_background.dart +++ b/lib/widgets/home_widgets/home_widget_background.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; -class HomeWidgetBackgroundBuilder extends FlutterHomeWidgetBuilder { +class HomeWidgetBackgroundBuilder + extends FlutterHomeWidgetBuilder { final Color? color; HomeWidgetBackgroundBuilder({ super.key, @@ -14,13 +15,13 @@ class HomeWidgetBackgroundBuilder extends FlutterHomeWidgetBuilder HomeWidgetBackground( - key: key, - theme: theme, - logicalSize: logicalSize, - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetBackground( + key: key, + theme: theme, + logicalSize: logicalSize, + utils: utils, + ), + ); } class HomeWidgetBackground extends FlutterHomeWidgetBase { @@ -33,11 +34,11 @@ class HomeWidgetBackground extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) => Container( - width: logicalSize.width, - height: logicalSize.height, - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(logicalSize.height / 4), - ), - ); + width: logicalSize.width, + height: logicalSize.height, + decoration: BoxDecoration( + color: theme.scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(logicalSize.height / 4), + ), + ); } diff --git a/lib/widgets/home_widgets/home_widget_configure.dart b/lib/widgets/home_widgets/home_widget_configure.dart index e34c199c3..750e0d7c2 100644 --- a/lib/widgets/home_widgets/home_widget_configure.dart +++ b/lib/widgets/home_widgets/home_widget_configure.dart @@ -14,13 +14,13 @@ class HomeWidgetConfigBuilder extends FlutterHomeWidgetBuilder { required super.homeWidgetKey, required super.utils, }) : super( - formWidget: (key, theme, logicalSize, _) => HomeWidgetIcon( - key: key, - theme: theme, - logicalSize: logicalSize, - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetIcon( + key: key, + theme: theme, + logicalSize: logicalSize, + utils: utils, + ), + ); } class HomeWidgetIcon extends FlutterHomeWidgetBase { @@ -33,8 +33,8 @@ class HomeWidgetIcon extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) => Icon( - Icons.settings, - size: min(logicalSize.width, logicalSize.height), - color: theme.listTileTheme.iconColor, - ); + Icons.settings, + size: min(logicalSize.width, logicalSize.height), + color: theme.listTileTheme.iconColor, + ); } diff --git a/lib/widgets/home_widgets/home_widget_copied.dart b/lib/widgets/home_widgets/home_widget_copied.dart index 5fb9961ba..7098b2403 100644 --- a/lib/widgets/home_widgets/home_widget_copied.dart +++ b/lib/widgets/home_widgets/home_widget_copied.dart @@ -4,7 +4,8 @@ import '../../utils/customization/theme_extentions/extended_text_theme.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; -class HomeWidgetCopiedBuilder extends FlutterHomeWidgetBuilder { +class HomeWidgetCopiedBuilder + extends FlutterHomeWidgetBuilder { HomeWidgetCopiedBuilder({ required super.lightTheme, required super.darkTheme, @@ -12,13 +13,13 @@ class HomeWidgetCopiedBuilder extends FlutterHomeWidgetBuilder required super.homeWidgetKey, required super.utils, }) : super( - formWidget: (key, theme, logicalSize, _) => HomeWidgetCopied( - key: key, - theme: theme, - logicalSize: logicalSize, - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetCopied( + key: key, + theme: theme, + logicalSize: logicalSize, + utils: utils, + ), + ); } class HomeWidgetCopied extends FlutterHomeWidgetBase { @@ -31,16 +32,16 @@ class HomeWidgetCopied extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) => SizedBox( - width: logicalSize.width, - height: logicalSize.height, - child: FittedBox( - fit: BoxFit.contain, - alignment: Alignment.center, - child: Text( - 'Password copied\nto Clipboard', - textAlign: TextAlign.center, - style: theme.extension()?.tokenTile, - ), - ), - ); + width: logicalSize.width, + height: logicalSize.height, + child: FittedBox( + fit: BoxFit.contain, + alignment: Alignment.center, + child: Text( + 'Password copied\nto Clipboard', + textAlign: TextAlign.center, + style: theme.extension()?.tokenTile, + ), + ), + ); } diff --git a/lib/widgets/home_widgets/home_widget_hidden.dart b/lib/widgets/home_widgets/home_widget_hidden.dart index c5713ebe2..6351f0b97 100644 --- a/lib/widgets/home_widgets/home_widget_hidden.dart +++ b/lib/widgets/home_widgets/home_widget_hidden.dart @@ -5,7 +5,8 @@ import 'home_widget_otp.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; -class HomeWidgetHiddenBuilder extends FlutterHomeWidgetBuilder { +class HomeWidgetHiddenBuilder + extends FlutterHomeWidgetBuilder { final int otpLength; final String? issuer; final String? label; @@ -20,16 +21,16 @@ class HomeWidgetHiddenBuilder extends FlutterHomeWidgetBuilder required super.homeWidgetKey, required super.utils, }) : super( - formWidget: (key, theme, logicalSize, _) => HomeWidgetHidden( - key: key, - theme: theme, - logicalSize: logicalSize, - issuer: issuer ?? '', - label: label ?? '', - otpLength: otpLength, - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetHidden( + key: key, + theme: theme, + logicalSize: logicalSize, + issuer: issuer ?? '', + label: label ?? '', + otpLength: otpLength, + utils: utils, + ), + ); } class HomeWidgetHidden extends FlutterHomeWidgetBase { @@ -48,7 +49,8 @@ class HomeWidgetHidden extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) { - final veilingCharacter = theme.extension()?.veilingCharacter ?? '●'; + final veilingCharacter = + theme.extension()?.veilingCharacter ?? '●'; return HomeWidgetOtp( theme: theme, logicalSize: logicalSize, diff --git a/lib/widgets/home_widgets/home_widget_otp.dart b/lib/widgets/home_widgets/home_widget_otp.dart index 2d1bf666e..3991d7411 100644 --- a/lib/widgets/home_widgets/home_widget_otp.dart +++ b/lib/widgets/home_widgets/home_widget_otp.dart @@ -20,16 +20,16 @@ class HomeWidgetOtpBuilder extends FlutterHomeWidgetBuilder { required super.homeWidgetKey, required super.utils, }) : super( - formWidget: (key, theme, logicalSize, _) => HomeWidgetOtp( - key: key, - theme: theme, - logicalSize: logicalSize, - otp: otp, - issuer: issuer ?? '', - label: label ?? '', - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetOtp( + key: key, + theme: theme, + logicalSize: logicalSize, + otp: otp, + issuer: issuer ?? '', + label: label ?? '', + utils: utils, + ), + ); } class HomeWidgetOtp extends FlutterHomeWidgetBase { @@ -48,7 +48,11 @@ class HomeWidgetOtp extends FlutterHomeWidgetBase { @override Widget build(BuildContext context) { - String text = insertCharAt(otp, otp.length > 10 ? '\n' : ' ', (otp.length / 2).ceil()); + String text = insertCharAt( + otp, + otp.length > 10 ? '\n' : ' ', + (otp.length / 2).ceil(), + ); return SizedBox( width: logicalSize.width, height: logicalSize.height, diff --git a/lib/widgets/home_widgets/home_widget_unlinked.dart b/lib/widgets/home_widgets/home_widget_unlinked.dart index 2d820d9ce..9295011ee 100644 --- a/lib/widgets/home_widgets/home_widget_unlinked.dart +++ b/lib/widgets/home_widgets/home_widget_unlinked.dart @@ -4,7 +4,8 @@ import '../../utils/customization/theme_extentions/extended_text_theme.dart'; import 'interfaces/flutter_home_widget_base.dart'; import 'interfaces/flutter_home_widget_builder.dart'; -class HomeWidgetUnlinkedBuilder extends FlutterHomeWidgetBuilder { +class HomeWidgetUnlinkedBuilder + extends FlutterHomeWidgetBuilder { HomeWidgetUnlinkedBuilder({ required super.lightTheme, required super.darkTheme, @@ -12,30 +13,35 @@ class HomeWidgetUnlinkedBuilder extends FlutterHomeWidgetBuilder HomeWidgetUnlinked( - key: key, - theme: theme, - logicalSize: logicalSize, - utils: utils, - ), - ); + formWidget: (key, theme, logicalSize, _) => HomeWidgetUnlinked( + key: key, + theme: theme, + logicalSize: logicalSize, + utils: utils, + ), + ); } class HomeWidgetUnlinked extends FlutterHomeWidgetBase { - const HomeWidgetUnlinked({super.key, required super.logicalSize, required super.theme, required super.utils}); + const HomeWidgetUnlinked({ + super.key, + required super.logicalSize, + required super.theme, + required super.utils, + }); @override Widget build(BuildContext context) => SizedBox( - width: logicalSize.width, - height: logicalSize.height, - child: FittedBox( - fit: BoxFit.contain, - alignment: Alignment.topRight, - child: Text( - 'Tap to link\nyour token', - textAlign: TextAlign.center, - style: theme.extension()?.tokenTileSubtitle, - ), - ), - ); + width: logicalSize.width, + height: logicalSize.height, + child: FittedBox( + fit: BoxFit.contain, + alignment: Alignment.topRight, + child: Text( + 'Tap to link\nyour token', + textAlign: TextAlign.center, + style: theme.extension()?.tokenTileSubtitle, + ), + ), + ); } diff --git a/lib/widgets/introduction_widgets/token_introduction.dart b/lib/widgets/introduction_widgets/token_introduction.dart index e805ba30a..b94796258 100644 --- a/lib/widgets/introduction_widgets/token_introduction.dart +++ b/lib/widgets/introduction_widgets/token_introduction.dart @@ -11,14 +11,18 @@ class TokenIntroduction extends ConsumerWidget { const TokenIntroduction({required this.child, super.key}); @override - Widget build(context, ref) => ref.watch(introductionNotifierProvider).when( + Widget build(context, ref) => ref + .watch(introductionNotifierProvider) + .when( data: (value) { if (value.isConditionFulfilled(ref, Introduction.tokenSwipe)) { return FocusedItemAsOverlay( isFocused: true, tooltipWhenFocused: AppLocalizations.of(context)!.introTokenSwipe, alignment: Alignment.bottomCenter, - onComplete: () => ref.read(introductionNotifierProvider.notifier).complete(Introduction.tokenSwipe), + onComplete: () => ref + .read(introductionNotifierProvider.notifier) + .complete(Introduction.tokenSwipe), overlayChild: Column(children: [child]), child: child, ); @@ -29,7 +33,9 @@ class TokenIntroduction extends ConsumerWidget { isFocused: true, tooltipWhenFocused: AppLocalizations.of(context)!.introDragToken, alignment: Alignment.bottomCenter, - onComplete: () => ref.read(introductionNotifierProvider.notifier).complete(Introduction.dragToken), + onComplete: () => ref + .read(introductionNotifierProvider.notifier) + .complete(Introduction.dragToken), overlayChild: Column(children: [child]), child: child, ); diff --git a/test/unit_test/model/extensions/sortable_list_test.dart b/test/unit_test/model/extensions/sortable_list_test.dart index 503e4e49f..74767527c 100644 --- a/test/unit_test/model/extensions/sortable_list_test.dart +++ b/test/unit_test/model/extensions/sortable_list_test.dart @@ -14,16 +14,18 @@ class _SortableTestClass with SortableMixin { @override SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( - sortIndex: sortIndex ?? this.sortIndex, - name: name ?? this.name, - ); + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); @override - operator ==(Object other) => other is _SortableTestClass && other.name == name; + operator ==(Object other) => + other is _SortableTestClass && other.name == name; @override int get hashCode => name.hashCode; @override - String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; + String toString() => + "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; } void _testSortableList() { @@ -151,7 +153,11 @@ void _testSortableList() { _SortableTestClass(sortIndex: 4, name: '4'), ]; // Act - final result = list.moveBetween(moveAfter: null, movedItem: movedItem, moveBefore: list[1]); + final result = list.moveBetween( + moveAfter: null, + movedItem: movedItem, + moveBefore: list[1], + ); // Assert expect(result, [ _SortableTestClass(sortIndex: 0, name: '2'), @@ -176,7 +182,11 @@ void _testSortableList() { _SortableTestClass(sortIndex: 8, name: '8'), ]; // Act - final result = list.moveBetween(moveAfter: moveAfter, movedItem: movedItem, moveBefore: moveBefore); + final result = list.moveBetween( + moveAfter: moveAfter, + movedItem: movedItem, + moveBefore: moveBefore, + ); // Assert expect(result, [ _SortableTestClass(sortIndex: 0, name: '1'), diff --git a/test/unit_test/model/mixins/sortable_mixin_test.dart b/test/unit_test/model/mixins/sortable_mixin_test.dart index 83861ab47..711c961ab 100644 --- a/test/unit_test/model/mixins/sortable_mixin_test.dart +++ b/test/unit_test/model/mixins/sortable_mixin_test.dart @@ -13,16 +13,18 @@ class _SortableTestClass with SortableMixin { @override SortableMixin copyWith({int? sortIndex, String? name}) => _SortableTestClass( - sortIndex: sortIndex ?? this.sortIndex, - name: name ?? this.name, - ); + sortIndex: sortIndex ?? this.sortIndex, + name: name ?? this.name, + ); @override - operator ==(Object other) => other is _SortableTestClass && other.name == name; + operator ==(Object other) => + other is _SortableTestClass && other.name == name; @override int get hashCode => name.hashCode; @override - String toString() => "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; + String toString() => + "_SortableTestClass(sortIndex: $sortIndex, name: '$name')"; } void _testSortableMixin() { diff --git a/test/unit_test/model/states/token_state_test.dart b/test/unit_test/model/states/token_state_test.dart index 4ae58d10c..f88139f9b 100644 --- a/test/unit_test/model/states/token_state_test.dart +++ b/test/unit_test/model/states/token_state_test.dart @@ -33,7 +33,10 @@ void _testTokenState() { }); test('withTokens', () { final state = TokenState(tokens: [_TokenMock(id: 'id')]); - final newState = state.withTokens([_TokenMock(id: 'newid'), _TokenMock(id: 'newid2')]); + final newState = state.withTokens([ + _TokenMock(id: 'newid'), + _TokenMock(id: 'newid2'), + ]); expect(state.tokens.length, 1); expect((state.tokens.first as _TokenMock).id, 'id'); expect(newState.tokens.length, 3); @@ -42,7 +45,12 @@ void _testTokenState() { expect((newState.tokens[2] as _TokenMock).id, 'newid2'); }); test('withoutToken', () { - final state = TokenState(tokens: [_TokenMock(id: 'id'), _TokenMock(id: 'id2')]); + final state = TokenState( + tokens: [ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + ], + ); final newState = state.withoutToken(_TokenMock(id: 'id')); expect(state.tokens.length, 2); expect((state.tokens.first as _TokenMock).id, 'id'); @@ -51,8 +59,17 @@ void _testTokenState() { expect((newState.tokens.first as _TokenMock).id, 'id2'); }); test('withoutTokens', () { - final state = TokenState(tokens: [_TokenMock(id: 'id'), _TokenMock(id: 'id2'), _TokenMock(id: 'id3')]); - final newState = state.withoutTokens([_TokenMock(id: 'id'), _TokenMock(id: 'id2')]); + final state = TokenState( + tokens: [ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + _TokenMock(id: 'id3'), + ], + ); + final newState = state.withoutTokens([ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + ]); expect(state.tokens.length, 3); expect((state.tokens[0] as _TokenMock).id, 'id'); expect((state.tokens[1] as _TokenMock).id, 'id2'); @@ -62,8 +79,15 @@ void _testTokenState() { }); group('addOrReplaceToken', () { test('existing id', () { - final state = TokenState(tokens: [_TokenMock(id: 'id'), _TokenMock(id: 'id2')]); - final newState = state.addOrReplaceToken(_TokenMock(id: 'id', label: 'labelUpdated')); + final state = TokenState( + tokens: [ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + ], + ); + final newState = state.addOrReplaceToken( + _TokenMock(id: 'id', label: 'labelUpdated'), + ); expect(state.tokens.length, 2); expect((state.tokens.first as _TokenMock).id, 'id'); expect((state.tokens.last as _TokenMock).id, 'id2'); @@ -72,8 +96,15 @@ void _testTokenState() { expect((newState.tokens.first as _TokenMock).label, 'labelUpdated'); }); test('new id', () { - final state = TokenState(tokens: [_TokenMock(id: 'id'), _TokenMock(id: 'id2')]); - final newState = state.addOrReplaceToken(_TokenMock(id: 'newId', label: 'labelUpdated')); + final state = TokenState( + tokens: [ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + ], + ); + final newState = state.addOrReplaceToken( + _TokenMock(id: 'newId', label: 'labelUpdated'), + ); expect(state.tokens.length, 2); expect((state.tokens.first as _TokenMock).id, 'id'); expect((state.tokens.last as _TokenMock).id, 'id2'); @@ -84,8 +115,16 @@ void _testTokenState() { }); test('addOrReplaceTokens', () { - final state = TokenState(tokens: [_TokenMock(id: 'id'), _TokenMock(id: 'id2')]); - final newState = state.addOrReplaceTokens([_TokenMock(id: 'id', label: 'labelUpdated'), _TokenMock(id: 'id3')]); + final state = TokenState( + tokens: [ + _TokenMock(id: 'id'), + _TokenMock(id: 'id2'), + ], + ); + final newState = state.addOrReplaceTokens([ + _TokenMock(id: 'id', label: 'labelUpdated'), + _TokenMock(id: 'id3'), + ]); expect(state.tokens.length, 2); expect((state.tokens.first as _TokenMock).id, 'id'); expect((state.tokens.last as _TokenMock).id, 'id2'); From 4cf4ae1ae6ea22c5922a86e198d8696b5a04854b Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:00:20 +0200 Subject: [PATCH 08/13] lints --- analysis_options.yaml | 2 +- integration_test/add_tokens_test.dart | 8 +- integration_test/copy_to_clipboard_test.dart | 1 - integration_test/rename_and_delete_test.dart | 1 - .../enums/image_format_extension.dart | 134 +- lib/model/token_container.dart | 7 +- lib/model/tokens/day_password_token.dart | 1 - lib/model/tokens/hotp_token.dart | 2 - lib/model/tokens/totp_token.dart | 1 - .../home_widget_processor.dart | 29 +- ...navigation_scheme_processor_interface.dart | 3 +- .../pia_scheme_processor.dart | 2 +- lib/repo/secure_storage.dart | 1 - lib/utils/animations/totp_animation.dart | 75 +- lib/utils/home_widget_utils.dart | 18 +- lib/utils/lock_auth.dart | 6 +- lib/utils/logger.dart | 5 +- lib/utils/push_provider.dart | 2 +- .../token_container_notifier.dart | 1 - .../generated_providers/token_notifier.dart | 10 +- .../state_notifiers/deeplink_notifier.dart | 2 +- lib/utils/utils.dart | 4 +- lib/utils/view_utils.dart | 3 +- .../add_token_manually_view.dart | 82 +- .../add_token_manually_row.dart | 40 +- .../labeled_dropdown_button.dart | 7 +- .../link_input_field.dart | 126 +- .../page_view_dot_indicator.dart | 24 +- .../page_view_indicator.dart | 24 +- .../rows/add_token_button.dart | 34 +- .../rows/algorithms_dropdown_button.dart | 12 +- .../rows/counter_input_field.dart | 16 +- .../rows/digits_dropdown_button.dart | 10 +- .../rows/duration_dropdown_button.dart | 16 +- .../rows/encoding_dropdown_button.dart | 19 +- .../rows/label_input_field.dart | 19 +- .../rows/secret_input_field.dart | 28 +- .../rows/token_type_dropdown_button.dart | 10 +- lib/views/container_view/container_view.dart | 1 - .../delete_container_action.dart | 1 - .../details_container_action.dart | 1 - .../transfer_container_action.dart | 1 - .../container_widget_tile.dart | 2 - .../transfer_delete_dontainer_dialog.dart | 2 +- lib/views/feedback_view/feedback_view.dart | 2 - .../widgets/feedback_send_row.dart | 5 - .../import_tokens_view.dart | 17 +- .../pages/import_encrypted_data_page.dart | 231 ++-- .../pages/import_plain_tokens_page.dart | 122 +- .../pages/import_start_page.dart | 3 - .../pages/select_import_type_page.dart | 41 +- .../conflicted_import_tokens_list.dart | 18 +- .../widgets/failed_imports_list.dart | 22 +- .../no_conflict_import_tokens_list.dart | 29 +- .../no_conflict_import_tokens_tile.dart | 30 +- lib/views/license_view/license_view.dart | 33 +- .../main_view_widgets/expandable_appbar.dart | 1 - .../delete_token_folder_action.dart | 1 - .../lock_token_folder_action.dart | 4 +- .../rename_token_folder_action.dart | 1 - .../token_folder_expandable_body.dart | 1 - .../token_folder_expandable_header.dart | 1 - .../main_view_tokens_list.dart | 1 - .../main_view_tokens_list_filtered.dart | 5 +- .../edit_day_password_token_action.dart | 1 - .../day_password_token_widget_tile.dart | 1 - .../default_edit_action.dart | 1 - .../default_edit_action_dialog.dart | 7 +- .../default_lock_action.dart | 1 - .../actions/edit_hotp_token_action.dart | 1 - .../actions/edit_push_token_action.dart | 1 - .../push_token_widget_tile.dart | 5 +- .../token_widgets/token_widget_tile.dart | 1 - .../actions/edit_totp_token_action.dart | 1 - .../totp_token_widget_tile_countdown.dart | 1 - .../push_token_view/push_tokens_view.dart | 6 +- .../widgets/push_tokens_view_list.dart | 16 +- .../qr_scanner_view/qr_scanner_view.dart | 2 +- .../qr_scanner_widget.dart | 4 - .../dialogs/export_tokens_to_file_dialog.dart | 4 +- .../dialogs/select_export_type_dialog.dart | 4 +- .../dialogs/select_tokens_dialog.dart | 2 +- .../settings_group_allow_screenshot.dart | 4 +- .../settings_group_background_image.dart | 32 +- .../settings_group_container.dart | 17 +- .../settings_group_error_log.dart | 16 +- .../settings_group_feedback.dart | 8 +- .../settings_group_import_export_tokens.dart | 2 +- .../settings_group_language.dart | 72 +- .../settings_group_push_token.dart | 13 +- .../settings_groups/settings_group_theme.dart | 12 +- lib/views/settings_view/settings_view.dart | 56 +- .../delete_errorlog_button.dart | 6 +- .../errorlog_buttons/errorlog_button.dart | 38 +- .../send_errorlog_button.dart | 6 +- .../show_errorlog_button.dart | 1 - .../settings_view_widgets/logging_menu.dart | 1 - .../settings_view_widgets/settings_group.dart | 16 +- .../settings_list_tile_button.dart | 57 +- .../update_firebase_token_dialog.dart | 2 +- lib/views/splash_screen/splash_screen.dart | 62 +- lib/widgets/app_wrapper.dart | 1 - lib/widgets/button_widgets/intent_button.dart | 3 +- .../button_widgets/push_action_button.dart | 2 +- .../button_widgets/time_guarded_button.dart | 6 +- .../container_already_exists_dialog.dart | 6 +- .../initial_token_assignment_dialog.dart | 1 - .../dialog_widgets/patch_notes_dialog.dart | 1 - .../push_choice_dialog.dart | 1 - .../push_request_dialog.dart | 3 +- .../widgets/push_decline_confirm_dialog.dart | 1 - .../widgets/push_request_base_info.dart | 1 - .../dialog_widgets/two_step_dialog.dart | 2 +- lib/widgets/drag_item_scroller.dart | 4 +- .../enable_text_edit_after_many_taps.dart | 1 - .../home_widgets/home_widget_copied.dart | 2 - lib/widgets/home_widgets/home_widget_otp.dart | 2 - .../home_widgets/home_widget_unlinked.dart | 1 - lib/widgets/padded_row.dart | 25 +- lib/widgets/pi_slidable.dart | 22 +- lib/widgets/pi_text_field.dart | 25 +- lib/widgets/pulse_icon.dart | 58 +- lib/widgets/select_tokens_widget.dart | 33 +- lib/widgets/status_bar.dart | 1 - lib/widgets/tooltip_container.dart | 44 +- .../api/privacy_idea_container_api_test.dart | 475 +++++--- .../model/extensions/enum_extension_test.dart | 46 +- .../enums/algorithms_extension_test.dart | 129 +- .../enums/encodings_extension_test.dart | 175 ++- ...sh_token_rollout_state_extension_test.dart | 60 +- ...ken_origin_source_type_extension_test.dart | 30 +- .../model/extensions/int_extension_test.dart | 184 ++- .../model/extensions/sortable_list_test.dart | 18 +- .../model/mixins/sortable_mixin_test.dart | 8 +- .../model/processor_result_test.dart | 12 +- .../push_code_to_phone_request_test.dart | 2 +- .../model/push_default_request_test.dart | 2 +- .../model/states/introduction_state_test.dart | 40 +- .../model/states/settings_state_test.dart | 15 +- .../model/states/token_folder_state_test.dart | 12 +- .../model/states/token_state_test.dart | 2 +- test/unit_test/model/token_folder_test.dart | 29 +- .../model/tokens/day_password_test.dart | 2 +- .../model/tokens/hotp_token_test.dart | 15 - .../model/tokens/push_token_test.dart | 2 +- test/unit_test/model/tokens/token_test.dart | 10 +- ...token_container_scheme_processor_test.dart | 29 +- ...oogle_authenticator_qr_processor_test.dart | 6 +- .../aegis_import_file_processor_test.dart | 63 +- ...icator_pro_import_file_processor_test.dart | 46 +- ...e_otp_plus_import_file_processor_test.dart | 39 +- ...henticator_import_file_processor_test.dart | 59 +- test/unit_test/repo/secure_storage_test.dart | 37 +- ...ecure_token_container_repository_test.dart | 134 +- .../allow_screenshot_notifier_test.dart | 173 ++- .../deeplink_notifier_test.dart | 140 ++- .../push_request_notifier_test.dart | 4 - .../sortable_notifier_test.dart | 93 +- .../token_container_notifier_test.dart | 9 +- .../token_folder_notifier_test.dart | 70 +- test/unit_test/utils/crypto_utils_test.dart | 1080 ++++++++++++----- .../utils/custom_int_buffer_test.dart | 9 +- test/unit_test/utils/firebase_utils_test.dart | 102 +- .../token_notifier_test.dart | 23 - test/unit_test/utils/rsa_utils_test.dart | 79 +- ...rollover_container_tokens_button_test.dart | 22 +- .../buttons/sync_container_button_test.dart | 22 +- 167 files changed, 3647 insertions(+), 1947 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index caa5e0007..a0af6e5a6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,6 @@ linter: rules: unnecessary_string_escapes: false unnecessary_string_interpolations: false - unawaited_futures: true + # unawaited_futures: true avoid_redundant_argument_values: true avoid_void_async: true diff --git a/integration_test/add_tokens_test.dart b/integration_test/add_tokens_test.dart index 05ec00ff0..33f38f014 100644 --- a/integration_test/add_tokens_test.dart +++ b/integration_test/add_tokens_test.dart @@ -126,7 +126,6 @@ void main() { tester, find.byType(TokenWidgetBase).hitTestable(), 3, - timeout: const Duration(seconds: 5), ); expect(find.byType(TokenWidgetBase).hitTestable(), findsNWidgets(3)); }, timeout: const Timeout(Duration(minutes: 20))); @@ -251,12 +250,7 @@ Future _moveDayPasswordTokenWidgetIntoFolder(WidgetTester tester) async { } Future _openFolder(WidgetTester tester) async { - await pumpUntilFindNWidgets( - tester, - find.byType(TokenFolderWidget), - 1, - timeout: const Duration(seconds: 5), - ); + await pumpUntilFindNWidgets(tester, find.byType(TokenFolderWidget), 1); await tester.tap(find.byType(TokenFolderWidget)); await tester.pump(); } diff --git a/integration_test/copy_to_clipboard_test.dart b/integration_test/copy_to_clipboard_test.dart index 528b6a6ca..0b3168ee0 100644 --- a/integration_test/copy_to_clipboard_test.dart +++ b/integration_test/copy_to_clipboard_test.dart @@ -49,7 +49,6 @@ void main() { algorithm: Algorithms.SHA256, digits: 6, secret: 'secret', - counter: 0, ), ], ); diff --git a/integration_test/rename_and_delete_test.dart b/integration_test/rename_and_delete_test.dart index 988c4174f..f0012687c 100644 --- a/integration_test/rename_and_delete_test.dart +++ b/integration_test/rename_and_delete_test.dart @@ -52,7 +52,6 @@ void main() { algorithm: Algorithms.SHA256, digits: 6, secret: 'secret', - counter: 0, ), ]; when(mockTokenRepository.loadTokens()).thenAnswer((_) async { diff --git a/lib/model/extensions/enums/image_format_extension.dart b/lib/model/extensions/enums/image_format_extension.dart index e55db217d..7efffb64a 100644 --- a/lib/model/extensions/enums/image_format_extension.dart +++ b/lib/model/extensions/enums/image_format_extension.dart @@ -29,57 +29,51 @@ import '../../enums/image_format.dart'; extension ImageFormatX on ImageFormat { static ImageFormat fromExtensionString(String ex) => switch (ex) { - 'svg' => ImageFormat.svg, - 'svgz' => ImageFormat.svgz, - 'png' => ImageFormat.png, - 'jpg' => ImageFormat.jpg, - 'jpeg' => ImageFormat.jpeg, - 'gif' => ImageFormat.gif, - 'bmp' => ImageFormat.bmp, - 'webp' => ImageFormat.webp, - _ => throw Exception('Unknown extension: $ex'), - }; + 'svg' => ImageFormat.svg, + 'svgz' => ImageFormat.svgz, + 'png' => ImageFormat.png, + 'jpg' => ImageFormat.jpg, + 'jpeg' => ImageFormat.jpeg, + 'gif' => ImageFormat.gif, + 'bmp' => ImageFormat.bmp, + 'webp' => ImageFormat.webp, + _ => throw Exception('Unknown extension: $ex'), + }; Widget buildImageWidget(Uint8List imageData) => switch (this) { - ImageFormat.svg => SvgPicture.memory( - imageData, - colorFilter: ColorFilter.mode( - Colors.transparent, - BlendMode.srcOver, - ), - ), - ImageFormat.svgz => SvgPicture.memory( - imageData, - colorFilter: ColorFilter.mode( - Colors.transparent, - BlendMode.srcOver, - ), - ), - ImageFormat.png => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - ImageFormat.jpg => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - ImageFormat.jpeg => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - ImageFormat.gif => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - ImageFormat.bmp => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - ImageFormat.webp => Image.memory( - imageData, - colorBlendMode: BlendMode.srcOver, - ), - }; + ImageFormat.svg => SvgPicture.memory( + imageData, + colorFilter: ColorFilter.mode(Colors.transparent, BlendMode.srcOver), + ), + ImageFormat.svgz => SvgPicture.memory( + imageData, + colorFilter: ColorFilter.mode(Colors.transparent, BlendMode.srcOver), + ), + ImageFormat.png => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + ImageFormat.jpg => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + ImageFormat.jpeg => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + ImageFormat.gif => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + ImageFormat.bmp => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + ImageFormat.webp => Image.memory( + imageData, + colorBlendMode: BlendMode.srcOver, + ), + }; Future getImageSize(Uint8List imageData) async { double? width; @@ -113,32 +107,34 @@ extension ImageFormatX on ImageFormat { String get extension => toString().split('.').last; String get name => switch (this) { - ImageFormat.svg => 'Scalable Vector Graphic', - ImageFormat.svgz => 'Scalable Vector Graphic (compressed)', - ImageFormat.png => 'PNG', - ImageFormat.jpg => 'JPEG', - ImageFormat.jpeg => 'JPEG', - ImageFormat.gif => 'GIF', - ImageFormat.bmp => 'Bitmap', - ImageFormat.webp => 'WebP', - }; + ImageFormat.svg => 'Scalable Vector Graphic', + ImageFormat.svgz => 'Scalable Vector Graphic (compressed)', + ImageFormat.png => 'PNG', + ImageFormat.jpg => 'JPEG', + ImageFormat.jpeg => 'JPEG', + ImageFormat.gif => 'GIF', + ImageFormat.bmp => 'Bitmap', + ImageFormat.webp => 'WebP', + }; String get mimeType => switch (this) { - ImageFormat.svg => 'image/svg+xml', - ImageFormat.svgz => 'image/svg+xml', - ImageFormat.png => 'image/png', - ImageFormat.jpg => 'image/jpeg', - ImageFormat.jpeg => 'image/jpeg', - ImageFormat.gif => 'image/gif', - ImageFormat.bmp => 'image/bmp', - ImageFormat.webp => 'image/webp', - }; + ImageFormat.svg => 'image/svg+xml', + ImageFormat.svgz => 'image/svg+xml', + ImageFormat.png => 'image/png', + ImageFormat.jpg => 'image/jpeg', + ImageFormat.jpeg => 'image/jpeg', + ImageFormat.gif => 'image/gif', + ImageFormat.bmp => 'image/bmp', + ImageFormat.webp => 'image/webp', + }; /// Builds an [XFile] from the given [imageData] and [fileName]. /// The [fileName] is used as the name of the file. /// The file extension is determined by the [ImageFormat]. - XFile buildXFile(Uint8List imageData, String fileName) => XFile.fromData(imageData, name: "$fileName.$extension"); + XFile buildXFile(Uint8List imageData, String fileName) => + XFile.fromData(imageData, name: "$fileName.$extension"); /// Compares the given [extension] with the extension of this [ImageFormat] case-insensitive. - bool matches(String extension) => extension.toLowerCase() == this.extension.toLowerCase(); + bool matches(String extension) => + extension.toLowerCase() == this.extension.toLowerCase(); } diff --git a/lib/model/token_container.dart b/lib/model/token_container.dart index 97b9df212..1f76075d1 100644 --- a/lib/model/token_container.dart +++ b/lib/model/token_container.dart @@ -38,12 +38,7 @@ import 'token_import/token_origin_data.dart'; part 'token_container.freezed.dart'; part 'token_container.g.dart'; -@Freezed( - toStringOverride: false, - addImplicitFinal: true, - toJson: true, - fromJson: true, -) +@Freezed(toStringOverride: false, toJson: true, fromJson: true) sealed class TokenContainer with _$TokenContainer { static const String CONTAINER_SERIAL = 'container_serial'; diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 93fb0adb6..be6d7606d 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -169,7 +169,6 @@ class DayPasswordToken extends OTPToken { time: time, length: digits, interval: period, - isGoogle: true, ); @override diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index 01e55861d..3eb8c07d2 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -68,7 +68,6 @@ class HOTPToken extends OTPToken { secret: secret, counter: counter, length: digits, - isGoogle: true, ); @override @@ -76,7 +75,6 @@ class HOTPToken extends OTPToken { secret: secret, counter: counter + 1, length: digits, - isGoogle: true, ); // --- Constructor --- diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index 3ff5bdd10..5e47cb4bb 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -171,7 +171,6 @@ class TOTPToken extends OTPToken { time: time, length: digits, interval: Duration(seconds: period), - isGoogle: true, ); @override diff --git a/lib/processors/scheme_processors/home_widget_processor.dart b/lib/processors/scheme_processors/home_widget_processor.dart index 2cacc4dbb..ded685434 100644 --- a/lib/processors/scheme_processors/home_widget_processor.dart +++ b/lib/processors/scheme_processors/home_widget_processor.dart @@ -44,10 +44,7 @@ class HomeWidgetProcessor implements SchemeProcessor { final processor = _processors[uri.host]; if (processor == null) { return [ - ProcessorResult.failed( - (l) => l.noProcessorFoundForHost(uri.host), - resultHandlerType: null, - ), + ProcessorResult.failed((l) => l.noProcessorFoundForHost(uri.host)), ]; } return processor.call(uri); @@ -65,18 +62,12 @@ class HomeWidgetProcessor implements SchemeProcessor { return [ ProcessorResult.failed( (l) => l.invalidHostForScheme(uri.host, uri.scheme), - resultHandlerType: null, ), ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ - ProcessorResult.failed( - (l) => l.missingWidgetId, - resultHandlerType: null, - ), - ]; + return [ProcessorResult.failed((l) => l.missingWidgetId)]; } HomeWidgetUtils().showOtp(widgetId); return null; @@ -91,18 +82,12 @@ class HomeWidgetProcessor implements SchemeProcessor { return [ ProcessorResult.failed( (l) => l.invalidHostForScheme(uri.host, uri.scheme), - resultHandlerType: null, ), ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ - ProcessorResult.failed( - (l) => l.missingWidgetId, - resultHandlerType: null, - ), - ]; + return [ProcessorResult.failed((l) => l.missingWidgetId)]; } HomeWidgetUtils().copyOtp(widgetId); return null; @@ -117,18 +102,12 @@ class HomeWidgetProcessor implements SchemeProcessor { return [ ProcessorResult.failed( (l) => l.invalidHostForScheme(uri.host, uri.scheme), - resultHandlerType: null, ), ]; } final widgetId = uri.queryParameters['widgetId']; if (widgetId == null) { - return [ - ProcessorResult.failed( - (l) => l.missingWidgetId, - resultHandlerType: null, - ), - ]; + return [ProcessorResult.failed((l) => l.missingWidgetId)]; } HomeWidgetUtils().performAction(widgetId); return null; diff --git a/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart b/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart index a92470088..e550aa967 100644 --- a/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart +++ b/lib/processors/scheme_processors/navigation_scheme_processors/navigation_scheme_processor_interface.dart @@ -49,6 +49,7 @@ abstract class NavigationSchemeProcessor implements SchemeProcessor { final key = await contextedGlobalNavigatorKey; context = key.currentContext; } + if (context!.mounted == false) return; Logger.info('Processing scheme: ${uri.scheme}'); final futures = >[]; for (final processor in implementations) { @@ -59,8 +60,6 @@ abstract class NavigationSchemeProcessor implements SchemeProcessor { Logger.info( 'Processing scheme ${uri.scheme} with ${processor.runtimeType}', ); - // ignoring use_build_context_synchronously is ok because we got the context after the await. The Context cannot be expired. - // ignore: use_build_context_synchronously futures.add( processor.processUri(uri, context: context, fromInit: fromInit), ); diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/pia_scheme_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/pia_scheme_processor.dart index c2be55b11..13c3a78f3 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/pia_scheme_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/pia_scheme_processor.dart @@ -84,6 +84,6 @@ class PiaSchemeProcessor extends TokenImportSchemeProcessor { queryParameters: uri.queryParameters, fragment: uri.fragment, ); - return OtpAuthProcessor().processUri(uri, fromInit: false); + return OtpAuthProcessor().processUri(uri); } } diff --git a/lib/repo/secure_storage.dart b/lib/repo/secure_storage.dart index 1cc1dc2b6..67d872ed8 100644 --- a/lib/repo/secure_storage.dart +++ b/lib/repo/secure_storage.dart @@ -27,7 +27,6 @@ class SecureStorage implements SecureStorageInterface { aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, - synchronizable: false, ), ); diff --git a/lib/utils/animations/totp_animation.dart b/lib/utils/animations/totp_animation.dart index 699219fcf..c7e0159b7 100644 --- a/lib/utils/animations/totp_animation.dart +++ b/lib/utils/animations/totp_animation.dart @@ -56,39 +56,72 @@ class TotpAnimation { required this.defaultCountdownColor, Color? warningCountdownColor, Color? criticalCountdownColor, - }) : warningOtpColor = warningOtpColor ?? defaultOtpColor, - criticalOtpColor = criticalOtpColor ?? warningOtpColor ?? defaultOtpColor, - warningCountdownColor = warningCountdownColor ?? defaultCountdownColor, - criticalCountdownColor = criticalCountdownColor ?? warningCountdownColor ?? defaultCountdownColor; + }) : warningOtpColor = warningOtpColor ?? defaultOtpColor, + criticalOtpColor = + criticalOtpColor ?? warningOtpColor ?? defaultOtpColor, + warningCountdownColor = warningCountdownColor ?? defaultCountdownColor, + criticalCountdownColor = + criticalCountdownColor ?? + warningCountdownColor ?? + defaultCountdownColor; DateTime lastResync = DateTime.now(); /// The initial elapsed time is [initPassedTime]. UnscaledAnimationController createAnimation() { final colorAnimation = UnscaledAnimationController( - lowerBound: 0, upperBound: totalDuration.inSeconds.toDouble(), duration: totalDuration, ); colorAnimation.addStatusListener((status) { if (status == AnimationStatus.completed) { onPeriodEnd(); - colorAnimation.forward(from: DateTime.now().millisecondsSinceEpoch % (totalDuration.inMilliseconds) / 1000); + colorAnimation.forward( + from: + DateTime.now().millisecondsSinceEpoch % + (totalDuration.inMilliseconds) / + 1000, + ); } }); - colorAnimation.forward(from: DateTime.now().millisecondsSinceEpoch % (totalDuration.inMilliseconds) / 1000); + colorAnimation.forward( + from: + DateTime.now().millisecondsSinceEpoch % + (totalDuration.inMilliseconds) / + 1000, + ); colorAnimation.addListener(() { - final passedDuration = Duration(milliseconds: (colorAnimation.value * 1000).toInt()); + final passedDuration = Duration( + milliseconds: (colorAnimation.value * 1000).toInt(), + ); Color otpColor; Color countdownColor; if (passedDuration > (totalDuration - criticalDuration)) { - final factor = (totalDuration - passedDuration).inMilliseconds / criticalDuration.inMilliseconds; - otpColor = criticalOtpColor.mixWith(warningDuration.inMilliseconds < 1 ? defaultOtpColor : warningOtpColor, factor); - countdownColor = criticalCountdownColor.mixWith(warningDuration.inMilliseconds < 1 ? defaultCountdownColor : warningCountdownColor, factor); - } else if (passedDuration > (totalDuration - warningDuration - criticalDuration)) { - final factor = (totalDuration - passedDuration - criticalDuration).inMilliseconds / warningDuration.inMilliseconds; // 0-1 + final factor = + (totalDuration - passedDuration).inMilliseconds / + criticalDuration.inMilliseconds; + otpColor = criticalOtpColor.mixWith( + warningDuration.inMilliseconds < 1 + ? defaultOtpColor + : warningOtpColor, + factor, + ); + countdownColor = criticalCountdownColor.mixWith( + warningDuration.inMilliseconds < 1 + ? defaultCountdownColor + : warningCountdownColor, + factor, + ); + } else if (passedDuration > + (totalDuration - warningDuration - criticalDuration)) { + final factor = + (totalDuration - passedDuration - criticalDuration).inMilliseconds / + warningDuration.inMilliseconds; // 0-1 otpColor = warningOtpColor.mixWith(defaultOtpColor, factor); - countdownColor = warningCountdownColor.mixWith(defaultCountdownColor, factor); + countdownColor = warningCountdownColor.mixWith( + defaultCountdownColor, + factor, + ); } else { otpColor = defaultOtpColor; countdownColor = defaultCountdownColor; @@ -96,14 +129,22 @@ class TotpAnimation { final callbackValue = TotpAnimationCallback( otpColor: otpColor, countdownColor: countdownColor, - secondsUntilNextOTP: (totalDuration.inMilliseconds - passedDuration.inMilliseconds) / 1000, + secondsUntilNextOTP: + (totalDuration.inMilliseconds - passedDuration.inMilliseconds) / + 1000, ); callback(callbackValue); // Resync every second but do not skip the AnimationStatus.completed - if (DateTime.now().difference(lastResync).inSeconds > 1 && colorAnimation.value > 0.5) { + if (DateTime.now().difference(lastResync).inSeconds > 1 && + colorAnimation.value > 0.5) { lastResync = DateTime.now(); - colorAnimation.forward(from: DateTime.now().millisecondsSinceEpoch % (totalDuration.inMilliseconds) / 1000); + colorAnimation.forward( + from: + DateTime.now().millisecondsSinceEpoch % + (totalDuration.inMilliseconds) / + 1000, + ); } }); return colorAnimation; diff --git a/lib/utils/home_widget_utils.dart b/lib/utils/home_widget_utils.dart index fbdac39ce..a39459f9f 100644 --- a/lib/utils/home_widget_utils.dart +++ b/lib/utils/home_widget_utils.dart @@ -56,7 +56,7 @@ const appGroupId = 'group.authenticator_home_widget_group'; /// This function is called on any interaction with the HomeWidget @pragma('vm:entry-point') -void homeWidgetBackgroundCallback(Uri? uri) async { +Future homeWidgetBackgroundCallback(Uri? uri) async { if (uri == null) return; const HomeWidgetProcessor().processUri(uri); } @@ -740,7 +740,7 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Map get _copyTimers => {}; @override - Future _folderOf(Token token) => Future.value(null); + Future _folderOf(Token token) => Future.value(); @override Future _getThemeData({bool dark = false}) => Future.value(ThemeData.light()); @@ -752,9 +752,9 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Map get _hideTimers => {}; @override - Future _hotpTokenAction(String widgetId) => Future.value(null); + Future _hotpTokenAction(String widgetId) => Future.value(); @override - Future _link(String widgetId, OTPToken token) => Future.value(null); + Future _link(String widgetId, OTPToken token) => Future.value(); @override Map Function(String p1)> get _mapTokenAction => {}; @override @@ -812,9 +812,9 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Future copyOtp(String widgetId) async {} @override - Future getTokenIdOfWidgetId(String widgetId) => Future.value(null); + Future getTokenIdOfWidgetId(String widgetId) => Future.value(); @override - Future getTokenOfWidgetId(String? widgetId) => Future.value(null); + Future getTokenOfWidgetId(String? widgetId) => Future.value(); @override Future homeWidgetInit({TokenRepository? repository}) async {} @override @@ -838,15 +838,15 @@ class UnsupportedHomeWidgetUtils implements HomeWidgetUtils { @override Future updateTokensIfLinked(List tokens) async {} @override - Future initiallyLaunchedFromHomeWidget() => Future.value(null); + Future initiallyLaunchedFromHomeWidget() => Future.value(); @override Stream get widgetClicked => const Stream.empty(); @override Future registerInteractivityCallback( void Function(Uri? uri) homeWidgetBackgroundCallback, - ) => Future.value(null); + ) => Future.value(); @override - Future setAppGroupId(String appGroupId) => Future.value(null); + Future setAppGroupId(String appGroupId) => Future.value(); @override Future _hideOtp(String widgetId, int otpLength) async {} @override diff --git a/lib/utils/lock_auth.dart b/lib/utils/lock_auth.dart index df4cf888d..97d5e2066 100644 --- a/lib/utils/lock_auth.dart +++ b/lib/utils/lock_auth.dart @@ -169,7 +169,6 @@ Future _showBiometricUnsupportedDialog() async { leading: const Icon(Symbols.fingerprint_off), ), content: Row( - mainAxisSize: MainAxisSize.max, children: [ Text(AppLocalizations.of(context)!.biometricAuthNotSupportedBody), const Icon(Symbols.fingerprint_off), @@ -191,7 +190,6 @@ Future _showBiometricUnavailableDialog() async { ), ), content: Row( - mainAxisSize: MainAxisSize.max, children: [ IconButton( iconSize: 48, @@ -203,7 +201,7 @@ Future _showBiometricUnavailableDialog() async { ); } if (Platform.isIOS) { - AppSettings.openAppSettings(type: AppSettingsType.settings); + AppSettings.openAppSettings(); } }, ), @@ -226,7 +224,7 @@ Future _showBiometricUnavailableDialog() async { ); } if (Platform.isIOS) { - await AppSettings.openAppSettings(type: AppSettingsType.settings); + await AppSettings.openAppSettings(); } final hasBiometricsEnrolled = (await _localAuth.getAvailableBiometrics()).isNotEmpty; diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index ee4074b65..ac18ced7d 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -57,8 +57,6 @@ class Logger { printer: printer.PrettyPrinter( methodCount: 0, levelColors: {printer.Level.debug: const printer.AnsiColor.fg(040)}, - colors: true, - printEmojis: true, ), ); @@ -176,7 +174,6 @@ class Logger { error: error, stackTrace: stackTrace, name: name ?? _getCallerMethodName(depth: 2), - logLevel: LogLevel.INFO, ); infoString = _textFilter(infoString); if (_verboseLogging || verbose) { @@ -370,7 +367,7 @@ Device Parameters $deviceInfo"""; Future _clearLog() async { final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/$_filename'); - await file.writeAsString('', mode: FileMode.write); + await file.writeAsString(''); showSnackBar( _context != null ? AppLocalizations.of(_context!)!.errorLogCleared diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index 009d70701..54057c1fd 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -544,7 +544,7 @@ class PlaceholderPushProvider implements PushProvider { @override Future<(List, List)?> updateAllFirebaseTokens({ String? firebaseToken, - }) => Future.value(null); + }) => Future.value(); @override Future initFirebase() async {} } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart index 8ef1e5421..ed6681f70 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart @@ -345,7 +345,6 @@ class TokenContainerNotifier extends _$TokenContainerNotifier if (uri == null) throw ArgumentError('Invalid rollover uri'); final result = (await TokenContainerProcessor().processUri( uri, - fromInit: false, ))?.firstOrNull; if (result == null) throw StateError('Failed to process rollover uri'); final success = await handleProcessorResult( diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart index 9513d57b7..9275f70ef 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart @@ -432,7 +432,7 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { final token = await getTokenById(tokenId); if (token is! OTPToken) { Logger.warning('Tried to show a token that is not an OTPToken.'); - return Future.value(null); + return Future.value(); } return showToken(token); } @@ -520,10 +520,7 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { final fbToken = await firebaseUtils.getFBToken(); if (fbToken == null) { - await _updateTokens( - (await future).pushTokens, - (p0) => p0.copyWith(fbToken: null), - ); + await _updateTokens((await future).pushTokens, (p0) => p0.copyWith()); Logger.warning( 'Could not update firebase token because no firebase token is available.', ); @@ -542,7 +539,7 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { firebaseToken: fbToken, )) ?? ([], []); - await _updateTokens(notUpdated, (p0) => p0.copyWith(fbToken: null)); + await _updateTokens(notUpdated, (p0) => p0.copyWith()); if (notUpdated.isNotEmpty) { Logger.warning( 'Could not update firebase token for ${notUpdated.length} tokens.', @@ -988,7 +985,6 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { e.origin?.copyWith(source: tokenOriginSourceType) ?? TokenOriginSourceType.unknown.toTokenOrigin( data: 'No Origindata available', - isPrivacyIdeaToken: null, ), ), ) diff --git a/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart b/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart index d0664adf8..928355d8a 100644 --- a/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart +++ b/lib/utils/riverpod/state_notifiers/deeplink_notifier.dart @@ -47,7 +47,7 @@ class DeeplinkNotifier extends StateNotifier { /// Handle incoming links - the ones that the app will recieve from the OS /// while already started. - void _handleIncomingLinks() async { + Future _handleIncomingLinks() async { if (kIsWeb) return; for (var source in _sources) { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 230a09737..4c7c84265 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -77,7 +77,7 @@ String splitPeriodically(String str, int period) { } /// If permission is already given, this function does nothing -void checkNotificationPermission() async { +Future checkNotificationPermission() async { if (kIsWeb || !Platform.isAndroid && !Platform.isIOS) return; var status = await Permission.notification.status; Logger.info('Notification permission status: $status'); @@ -154,7 +154,7 @@ dynamic tryJsonDecode(String json) { } } -void dragSortableOnAccept({ +Future dragSortableOnAccept({ required SortableMixin? previousSortable, required SortableMixin dragedSortable, required SortableMixin? nextSortable, diff --git a/lib/utils/view_utils.dart b/lib/utils/view_utils.dart index 9cee3e35c..80b9bceb6 100644 --- a/lib/utils/view_utils.dart +++ b/lib/utils/view_utils.dart @@ -98,7 +98,6 @@ void showErrorStatusMessage({ ref.read(statusMessageProvider.notifier).state = StatusMessage( message: message, details: details, - isError: true, ); } @@ -125,7 +124,7 @@ Future showAsyncDialog({ }) { if (globalContextSync == null) { Logger.error('globalContextSync is null'); - return Future.value(null); + return Future.value(); } return showDialog( context: globalContextSync!, diff --git a/lib/views/add_token_manually_view/add_token_manually_view.dart b/lib/views/add_token_manually_view/add_token_manually_view.dart index b22244003..7fd7216b2 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view.dart @@ -39,7 +39,8 @@ class AddTokenManuallyView extends ConsumerStatefulWidget { const AddTokenManuallyView({super.key}); @override - ConsumerState createState() => _AddTokenManuallyViewState(); + ConsumerState createState() => + _AddTokenManuallyViewState(); } class _AddTokenManuallyViewState extends ConsumerState { @@ -98,45 +99,45 @@ class _AddTokenManuallyViewState extends ConsumerState { Widget build(BuildContext context) { final AddTokenManuallyPage page = switch (selectedTypeNotifier.value) { const (TokenTypes.HOTP) => AddHotpManually( - labelController: labelController, - secretController: secretController, - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - encodingNofitier: encodingNofitier, - algorithmsNotifier: algorithmsNotifier, - digitsNotifier: digitsNotifier, - counterNotifier: counterNotifier, - typeNotifier: selectedTypeNotifier, - ), + labelController: labelController, + secretController: secretController, + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + encodingNofitier: encodingNofitier, + algorithmsNotifier: algorithmsNotifier, + digitsNotifier: digitsNotifier, + counterNotifier: counterNotifier, + typeNotifier: selectedTypeNotifier, + ), TokenTypes.TOTP => AddTotpManually( - labelController: labelController, - secretController: secretController, - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - encodingNofitier: encodingNofitier, - algorithmsNotifier: algorithmsNotifier, - digitsNotifier: digitsNotifier, - periodNotifier: periodNotifierTOTP, - typeNotifier: selectedTypeNotifier, - ), + labelController: labelController, + secretController: secretController, + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + encodingNofitier: encodingNofitier, + algorithmsNotifier: algorithmsNotifier, + digitsNotifier: digitsNotifier, + periodNotifier: periodNotifierTOTP, + typeNotifier: selectedTypeNotifier, + ), TokenTypes.DAYPASSWORD => AddDayPasswordManually( - labelController: labelController, - secretController: secretController, - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - encodingNofitier: encodingNofitier, - algorithmsNotifier: algorithmsNotifier, - digitsNotifier: digitsNotifier, - periodNotifier: periodNotifierDayPassword, - typeNotifier: selectedTypeNotifier, - ), + labelController: labelController, + secretController: secretController, + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + encodingNofitier: encodingNofitier, + algorithmsNotifier: algorithmsNotifier, + digitsNotifier: digitsNotifier, + periodNotifier: periodNotifierDayPassword, + typeNotifier: selectedTypeNotifier, + ), TokenTypes.STEAM => AddSteamManually( - labelController: labelController, - secretController: secretController, - autoValidateLabel: autoValidateLabel, - autoValidateSecret: autoValidateSecret, - typeNotifier: selectedTypeNotifier, - ), + labelController: labelController, + secretController: secretController, + autoValidateLabel: autoValidateLabel, + autoValidateSecret: autoValidateSecret, + typeNotifier: selectedTypeNotifier, + ), TokenTypes.PIPUSH => throw UnimplementedError(), TokenTypes.PUSH => throw UnimplementedError(), }; @@ -155,10 +156,7 @@ class _AddTokenManuallyViewState extends ConsumerState { children: [ PageViewIndicator( controller: pageController, - icons: [ - Icon(Icons.edit), - Icon(Icons.link), - ], + icons: [Icon(Icons.edit), Icon(Icons.link)], ), Expanded( child: PageView( @@ -171,7 +169,7 @@ class _AddTokenManuallyViewState extends ConsumerState { Padding( padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), child: LinkInputView(), - ) + ), ], ), ), diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_token_manually_row.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_token_manually_row.dart index 8b8d97f31..699b762cd 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_token_manually_row.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/add_token_manually_row.dart @@ -32,24 +32,24 @@ class AddTokenManuallyRow extends StatelessWidget { @override Widget build(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - flex: 5, - child: Text( - label, - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - Flexible( - flex: 3, - child: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: child, - ), - ), - ], - ); + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 5, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + Flexible( + flex: 3, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 100), + child: child, + ), + ), + ], + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart index 566fb8416..8dcd245fd 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/labeled_dropdown_button.dart @@ -43,7 +43,8 @@ class LabeledDropdownButton extends StatefulWidget { }); @override - State> createState() => _LabeledDropdownButtonState(); + State> createState() => + _LabeledDropdownButtonState(); } class _LabeledDropdownButtonState extends State> { @@ -69,7 +70,9 @@ class _LabeledDropdownButtonState extends State> { child: AddTokenManuallyRow( label: widget.label, child: DropdownButton( - value: widget.valueNotifier?.value != null && widget.values.contains(widget.valueNotifier!.value) + value: + widget.valueNotifier?.value != null && + widget.values.contains(widget.valueNotifier!.value) ? widget.valueNotifier!.value : widget.values.firstOrNull, isExpanded: true, diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart index 725d39cf0..3d427bd21 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart @@ -38,7 +38,9 @@ class _LinkInputViewState extends ConsumerState { Future addToken(Uri link) async { final linkHandled = await ref.read(tokenProvider.notifier).handleLink(link); if (!linkHandled) { - ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => localization.linkMustOtpAuth); + ref.read(statusMessageProvider.notifier).state = StatusMessage( + message: (localization) => localization.linkMustOtpAuth, + ); return; } if (!mounted) return; @@ -47,64 +49,76 @@ class _LinkInputViewState extends ConsumerState { @override Widget build(BuildContext context) => Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + children: [ + Row( children: [ - Row( - children: [ - Expanded( - child: TextFormField( - controller: textController, - decoration: InputDecoration(labelText: AppLocalizations.of(context)!.tokenLinkImport), - keyboardType: TextInputType.url, - textInputAction: TextInputAction.done, - onFieldSubmitted: (text) { - try { - addToken(Uri.parse(text)); - } catch (e) { - ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => localization.invalidUrl); - } - }, - validator: (value) => value != null - ? Uri.tryParse(value) == null - ? AppLocalizations.of(context)!.invalidUrl - : null - : null, - ), - ), - SizedBox(width: 8), - IconButton( - icon: Icon(Icons.paste), - onPressed: () async { - ClipboardData? data = await Clipboard.getData('text/plain'); - if (data == null || data.text == null || data.text!.isEmpty) { - if (context.mounted) ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => localization.clipboardEmpty); - return; - } - setState(() => textController.text = data.text ?? ''); - }, - ), - ], - ), - Expanded(child: SizedBox()), - SizedBox( - width: double.infinity, - child: ElevatedButton( - child: Text( - AppLocalizations.of(context)!.addToken, - style: Theme.of(context).textTheme.headlineSmall, - overflow: TextOverflow.fade, - softWrap: false, + Expanded( + child: TextFormField( + controller: textController, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.tokenLinkImport, ), - onPressed: () { - try { - addToken(Uri.parse(textController.text)); - } catch (e) { - ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => localization.invalidUrl); - } - }, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.done, + onFieldSubmitted: (text) { + try { + addToken(Uri.parse(text)); + } catch (e) { + ref + .read(statusMessageProvider.notifier) + .state = StatusMessage( + message: (localization) => localization.invalidUrl, + ); + } + }, + validator: (value) => value != null + ? Uri.tryParse(value) == null + ? AppLocalizations.of(context)!.invalidUrl + : null + : null, ), ), + SizedBox(width: 8), + IconButton( + icon: Icon(Icons.paste), + onPressed: () async { + ClipboardData? data = await Clipboard.getData('text/plain'); + if (data == null || data.text == null || data.text!.isEmpty) { + if (context.mounted) { + ref + .read(statusMessageProvider.notifier) + .state = StatusMessage( + message: (localization) => localization.clipboardEmpty, + ); + } + return; + } + setState(() => textController.text = data.text ?? ''); + }, + ), ], - ); + ), + Expanded(child: SizedBox()), + SizedBox( + width: double.infinity, + child: ElevatedButton( + child: Text( + AppLocalizations.of(context)!.addToken, + style: Theme.of(context).textTheme.headlineSmall, + overflow: TextOverflow.fade, + softWrap: false, + ), + onPressed: () { + try { + addToken(Uri.parse(textController.text)); + } catch (e) { + ref.read(statusMessageProvider.notifier).state = StatusMessage( + message: (localization) => localization.invalidUrl, + ); + } + }, + ), + ), + ], + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_dot_indicator.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_dot_indicator.dart index a60b27502..20edcc70e 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_dot_indicator.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_dot_indicator.dart @@ -23,10 +23,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; class PageViewDotIndicator extends ConsumerStatefulWidget { final PageController controller; final List icons; - const PageViewDotIndicator({super.key, required this.controller, required this.icons}); + const PageViewDotIndicator({ + super.key, + required this.controller, + required this.icons, + }); @override - ConsumerState createState() => _PageViewDotIndicatorState(); + ConsumerState createState() => + _PageViewDotIndicatorState(); } class _PageViewDotIndicatorState extends ConsumerState { @@ -53,27 +58,34 @@ class _PageViewDotIndicatorState extends ConsumerState { width: constraints.maxWidth, height: 50, child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < widget.icons.length; i++) ...[ GestureDetector( onTap: () { final pageDifference = (i - _currentPage).abs(); - widget.controller.animateToPage(i, duration: Duration(milliseconds: 200 * pageDifference + 150), curve: Curves.easeInOut); + widget.controller.animateToPage( + i, + duration: Duration( + milliseconds: 200 * pageDifference + 150, + ), + curve: Curves.easeInOut, + ); }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: _currentPage == i ? iconWidth * 2 : iconWidth, decoration: BoxDecoration( - color: _currentPage == i ? Theme.of(context).primaryColor : Theme.of(context).disabledColor, + color: _currentPage == i + ? Theme.of(context).primaryColor + : Theme.of(context).disabledColor, borderRadius: BorderRadius.circular(5), ), child: widget.icons[i], ), ), if (i < widget.icons.length - 1) SizedBox(width: space), - ] + ], ], ), ); diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_indicator.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_indicator.dart index 4267ea314..452534eb1 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_indicator.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/page_view_indicator.dart @@ -23,10 +23,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; class PageViewIndicator extends ConsumerStatefulWidget { final PageController controller; final List icons; - const PageViewIndicator({super.key, required this.controller, required this.icons}); + const PageViewIndicator({ + super.key, + required this.controller, + required this.icons, + }); @override - ConsumerState createState() => _PageViewDotIndicatorState(); + ConsumerState createState() => + _PageViewDotIndicatorState(); } class _PageViewDotIndicatorState extends ConsumerState { @@ -50,27 +55,34 @@ class _PageViewDotIndicatorState extends ConsumerState { final space = widthPerIcon * 0.1; final double iconWidth = widthPerIcon - space; return Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < widget.icons.length; i++) ...[ GestureDetector( onTap: () { final pageDifference = (i - _currentPage).abs(); - widget.controller.animateToPage(i, duration: Duration(milliseconds: 200 * pageDifference + 150), curve: Curves.easeInOut); + widget.controller.animateToPage( + i, + duration: Duration( + milliseconds: 200 * pageDifference + 150, + ), + curve: Curves.easeInOut, + ); }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: _currentPage == i ? iconWidth * 2 : iconWidth, decoration: BoxDecoration( - color: _currentPage == i ? Theme.of(context).primaryColor : Theme.of(context).disabledColor, + color: _currentPage == i + ? Theme.of(context).primaryColor + : Theme.of(context).disabledColor, borderRadius: BorderRadius.circular(99), ), child: widget.icons[i], ), ), if (i < widget.icons.length - 1) SizedBox(width: space), - ] + ], ], ); }, diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/add_token_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/add_token_button.dart index 49b4e34b9..37e873a4b 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/add_token_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/add_token_button.dart @@ -38,21 +38,23 @@ class AddTokenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => SizedBox( - width: double.infinity, - child: MutexButton( - onPressed: () async { - if (!context.mounted) return; - final token = tokenBuilder(); - if (token == null) return; - Navigator.pop(context); - await ref.read(tokenProvider.notifier).addOrReplaceToken(token); - }, - child: Text( - AppLocalizations.of(context)!.addToken, - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimary), - overflow: TextOverflow.fade, - softWrap: false, - ), + width: double.infinity, + child: MutexButton( + onPressed: () async { + if (!context.mounted) return; + final token = tokenBuilder(); + if (token == null) return; + Navigator.pop(context); + await ref.read(tokenProvider.notifier).addOrReplaceToken(token); + }, + child: Text( + AppLocalizations.of(context)!.addToken, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, ), - ); + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/algorithms_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/algorithms_dropdown_button.dart index e86ef7b85..2bc78ea03 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/algorithms_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/algorithms_dropdown_button.dart @@ -35,10 +35,10 @@ class AlgorithmsDropdownButton extends StatelessWidget { }); @override Widget build(BuildContext context) => LabeledDropdownButton( - label: AppLocalizations.of(context)!.algorithm, - enabled: enabled, - valueNotifier: algorithmsNotifier, - values: allowedAlgorithms, - valueLabels: [for (final value in allowedAlgorithms) value.name], - ); + label: AppLocalizations.of(context)!.algorithm, + enabled: enabled, + valueNotifier: algorithmsNotifier, + values: allowedAlgorithms, + valueLabels: [for (final value in allowedAlgorithms) value.name], + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/counter_input_field.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/counter_input_field.dart index 79bb0ee5d..640183ad4 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/counter_input_field.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/counter_input_field.dart @@ -23,20 +23,21 @@ import '../../../../l10n/app_localizations.dart'; import '../add_token_manually_row.dart'; class CounterInputField extends StatelessWidget { - static final FocusNode counterFieldFocus = FocusNode(debugLabel: 'CounterInputField'); + static final FocusNode counterFieldFocus = FocusNode( + debugLabel: 'CounterInputField', + ); static String? validator(String? value, {AppLocalizations? locale}) { if (value == null || value.isEmpty) { counterFieldFocus.requestFocus(); return locale?.mustNotBeEmpty(locale.counter) ?? 'Must not be empty'; } - return int.tryParse(value) == null ? locale?.notAnInteger ?? 'Must be a number' : null; + return int.tryParse(value) == null + ? locale?.notAnInteger ?? 'Must be a number' + : null; } final ValueNotifier counterNotifier; - const CounterInputField({ - super.key, - required this.counterNotifier, - }); + const CounterInputField({super.key, required this.counterNotifier}); @override Widget build(BuildContext context) { counterNotifier.value ??= 0; @@ -53,7 +54,8 @@ class CounterInputField extends StatelessWidget { counterNotifier.value = int.tryParse(value); }, autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) => validator(value, locale: AppLocalizations.of(context)), + validator: (value) => + validator(value, locale: AppLocalizations.of(context)), focusNode: CounterInputField.counterFieldFocus, ), ); diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/digits_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/digits_dropdown_button.dart index cb52ab0ee..5e378a80a 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/digits_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/digits_dropdown_button.dart @@ -34,9 +34,9 @@ class DigitsDropdownButton extends StatelessWidget { }); @override Widget build(BuildContext context) => LabeledDropdownButton( - label: AppLocalizations.of(context)!.digits, - enabled: enabled, - valueNotifier: digitsNotifier, - values: allowedDigits, - ); + label: AppLocalizations.of(context)!.digits, + enabled: enabled, + valueNotifier: digitsNotifier, + values: allowedDigits, + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/duration_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/duration_dropdown_button.dart index cbb39db30..8b340e53b 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/duration_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/duration_dropdown_button.dart @@ -38,11 +38,13 @@ class DurationDropdownButton extends StatelessWidget { }); @override Widget build(BuildContext context) => LabeledDropdownButton( - label: AppLocalizations.of(context)!.period, - enabled: enabled, - valueNotifier: periodNotifier, - values: values, - valueLabels: [for (final value in values) unit.durationToUnitInt(value).toString()], - postFix: unit.postfix, - ); + label: AppLocalizations.of(context)!.period, + enabled: enabled, + valueNotifier: periodNotifier, + values: values, + valueLabels: [ + for (final value in values) unit.durationToUnitInt(value).toString(), + ], + postFix: unit.postfix, + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/encoding_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/encoding_dropdown_button.dart index 24eae2ecb..c8a9ac302 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/encoding_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/encoding_dropdown_button.dart @@ -28,13 +28,18 @@ class EncodingsDropdownButton extends StatelessWidget { final List values; final bool enabled; - const EncodingsDropdownButton({super.key, this.encodingNotifier, this.values = Encodings.values, this.enabled = true}); + const EncodingsDropdownButton({ + super.key, + this.encodingNotifier, + this.values = Encodings.values, + this.enabled = true, + }); @override Widget build(BuildContext context) => LabeledDropdownButton( - label: AppLocalizations.of(context)!.encoding, - enabled: enabled, - valueNotifier: encodingNotifier, - values: values, - valueLabels: [for (final value in values) value.name], - ); + label: AppLocalizations.of(context)!.encoding, + enabled: enabled, + valueNotifier: encodingNotifier, + values: values, + valueLabels: [for (final value in values) value.name], + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/label_input_field.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/label_input_field.dart index 6d1c9c634..915c4ce60 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/label_input_field.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/label_input_field.dart @@ -23,7 +23,9 @@ import '../../../../../../../widgets/pi_text_field.dart'; import '../../../../l10n/app_localizations.dart'; class LabelInputField extends StatefulWidget { - static final FocusNode labelFieldFocus = FocusNode(debugLabel: 'LabelInputField'); + static final FocusNode labelFieldFocus = FocusNode( + debugLabel: 'LabelInputField', + ); static String? validator(String? value, {AppLocalizations? locale}) { if (value == null || value.isEmpty) { labelFieldFocus.requestFocus(); @@ -62,10 +64,13 @@ class _LabelInputFieldState extends State { @override Widget build(BuildContext context) => PiTextField( - controller: widget.controller, - autovalidateMode: widget.autoValidate.value ? AutovalidateMode.always : AutovalidateMode.disabled, - labelText: AppLocalizations.of(context)!.name, - validator: (value) => LabelInputField.validator(value, locale: AppLocalizations.of(context)), - focusNode: LabelInputField.labelFieldFocus, - ); + controller: widget.controller, + autovalidateMode: widget.autoValidate.value + ? AutovalidateMode.always + : AutovalidateMode.disabled, + labelText: AppLocalizations.of(context)!.name, + validator: (value) => + LabelInputField.validator(value, locale: AppLocalizations.of(context)), + focusNode: LabelInputField.labelFieldFocus, + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/secret_input_field.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/secret_input_field.dart index 032397bd2..329253faf 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/secret_input_field.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/secret_input_field.dart @@ -25,8 +25,14 @@ import '../../../../l10n/app_localizations.dart'; import '../../../../model/enums/encodings.dart'; class SecretInputField extends StatefulWidget { - static final FocusNode secretFieldFocus = FocusNode(debugLabel: 'SecretInputField'); - static String? validator(String? value, Encodings encoding, {AppLocalizations? locale}) { + static final FocusNode secretFieldFocus = FocusNode( + debugLabel: 'SecretInputField', + ); + static String? validator( + String? value, + Encodings encoding, { + AppLocalizations? locale, + }) { if (value == null || value.isEmpty) { secretFieldFocus.requestFocus(); return locale?.pleaseEnterASecretForThisToken ?? 'Not Valid'; @@ -73,10 +79,16 @@ class _SecretInputFieldState extends State { @override Widget build(BuildContext context) => PiTextField( - controller: widget.controller, - autovalidateMode: widget.autoValidate.value ? AutovalidateMode.always : AutovalidateMode.disabled, - labelText: AppLocalizations.of(context)!.secretKey, - validator: (value) => SecretInputField.validator(value, widget.encodingNotifier.value, locale: AppLocalizations.of(context)), - focusNode: SecretInputField.secretFieldFocus, - ); + controller: widget.controller, + autovalidateMode: widget.autoValidate.value + ? AutovalidateMode.always + : AutovalidateMode.disabled, + labelText: AppLocalizations.of(context)!.secretKey, + validator: (value) => SecretInputField.validator( + value, + widget.encodingNotifier.value, + locale: AppLocalizations.of(context), + ), + focusNode: SecretInputField.secretFieldFocus, + ); } diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/token_type_dropdown_button.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/token_type_dropdown_button.dart index 9e4844bc1..79dfe8753 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/token_type_dropdown_button.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/rows/token_type_dropdown_button.dart @@ -35,9 +35,9 @@ class TokenTypeDropdownButton extends StatelessWidget { const TokenTypeDropdownButton({super.key, required this.typeNotifier}); @override Widget build(BuildContext context) => LabeledDropdownButton( - label: AppLocalizations.of(context)!.type, - valueNotifier: typeNotifier, - values: values, - valueLabels: [for (final value in values) value.name], - ); + label: AppLocalizations.of(context)!.type, + valueNotifier: typeNotifier, + values: values, + valueLabels: [for (final value in values) value.name], + ); } diff --git a/lib/views/container_view/container_view.dart b/lib/views/container_view/container_view.dart index af44bdaa2..d2946c2e7 100644 --- a/lib/views/container_view/container_view.dart +++ b/lib/views/container_view/container_view.dart @@ -59,7 +59,6 @@ class ContainerView extends ConsumerView { body: Center( child: SlidableAutoCloseBehavior( child: Column( - mainAxisAlignment: MainAxisAlignment.start, children: [ for (var container in containerList) ...[ if (containerList.indexOf(container) != 0) diff --git a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart index 95be65b84..ae3e656c6 100644 --- a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart @@ -52,7 +52,6 @@ class DeleteContainerAction extends ConsumerSlideableAction { ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.delete_forever), Text( diff --git a/lib/views/container_view/container_widgets/container_actions/details_container_action.dart b/lib/views/container_view/container_widgets/container_actions/details_container_action.dart index 98be11a05..f947afbe4 100644 --- a/lib/views/container_view/container_widgets/container_actions/details_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/details_container_action.dart @@ -54,7 +54,6 @@ class DetailsContainerAction extends ConsumerSlideableAction { ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart b/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart index 7727856ce..b9f0138f0 100644 --- a/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/transfer_container_action.dart @@ -55,7 +55,6 @@ class TransferContainerAction extends ConsumerSlideableAction { ).extension()!.actionForegroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(MdiIcons.transfer), Text( diff --git a/lib/views/container_view/container_widgets/container_widget_tile.dart b/lib/views/container_view/container_widgets/container_widget_tile.dart index 0eac59c56..e8c9b73e9 100644 --- a/lib/views/container_view/container_widgets/container_widget_tile.dart +++ b/lib/views/container_view/container_widgets/container_widget_tile.dart @@ -83,12 +83,10 @@ class ContainerWidgetTile extends ConsumerWidget { ], ), trailing: Column( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: FittedBox( - fit: BoxFit.contain, child: ContainerWidgetTileTrailing( container: container, isPreview: isPreview, diff --git a/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart b/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart index c3642ee21..f52e0fad3 100644 --- a/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart +++ b/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart @@ -139,7 +139,7 @@ class _TransferDeleteContainerDialogState }; } - void confirmDeleteLocaly(BuildContext context) async { + Future confirmDeleteLocaly(BuildContext context) async { final containerTokens = (await ref.read( tokenProvider.future, )).containerTokens(widget.container.serial); diff --git a/lib/views/feedback_view/feedback_view.dart b/lib/views/feedback_view/feedback_view.dart index bfeb2d0de..3829fe5a5 100644 --- a/lib/views/feedback_view/feedback_view.dart +++ b/lib/views/feedback_view/feedback_view.dart @@ -74,8 +74,6 @@ class _FeedbackViewState extends State { padding: const EdgeInsets.all(14.0), child: SingleChildScrollView( child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), diff --git a/lib/views/feedback_view/widgets/feedback_send_row.dart b/lib/views/feedback_view/widgets/feedback_send_row.dart index 758bc1bc5..637b4a0c4 100644 --- a/lib/views/feedback_view/widgets/feedback_send_row.dart +++ b/lib/views/feedback_view/widgets/feedback_send_row.dart @@ -44,14 +44,11 @@ class _FeedbackSendRowState extends State { @override Widget build(BuildContext context) { return Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Row( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Expanded(child: SizedBox()), Expanded( @@ -60,9 +57,7 @@ class _FeedbackSendRowState extends State { onPressed: () => setState(() => _addDeviceInfo = !_addDeviceInfo), child: Row( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: Text( diff --git a/lib/views/import_tokens_view/import_tokens_view.dart b/lib/views/import_tokens_view/import_tokens_view.dart index 2f95b51bb..337811fb3 100644 --- a/lib/views/import_tokens_view/import_tokens_view.dart +++ b/lib/views/import_tokens_view/import_tokens_view.dart @@ -63,12 +63,15 @@ class _ImportTokensViewState extends ConsumerState { } else { tokensToImport = await Navigator.of(context).push( MaterialPageRoute( - builder: (context) => SelectImportTypePage(tokenImportOrigin: tokenImportOrigin), + builder: (context) => + SelectImportTypePage(tokenImportOrigin: tokenImportOrigin), ), ); } if (tokensToImport == null) return; - if (tokensToImport.isNotEmpty) ref.read(tokenProvider.notifier).addOrReplaceTokens(tokensToImport); + if (tokensToImport.isNotEmpty) { + ref.read(tokenProvider.notifier).addOrReplaceTokens(tokensToImport); + } if (mounted) return Navigator.of(context).pop(tokensToImport.isNotEmpty); } @@ -87,8 +90,6 @@ class _ImportTokensViewState extends ConsumerState { child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, children: [ for (final item in TokenImportOrigins.appList) ListTile( @@ -99,12 +100,8 @@ class _ImportTokensViewState extends ConsumerState { ), trailing: Theme( data: ThemeData( - iconTheme: const IconThemeData( - color: Colors.red, - ), - primaryIconTheme: const IconThemeData( - color: Colors.blue, - ), + iconTheme: const IconThemeData(color: Colors.red), + primaryIconTheme: const IconThemeData(color: Colors.blue), iconButtonTheme: const IconButtonThemeData( style: ButtonStyle( foregroundColor: WidgetStatePropertyAll(Colors.green), diff --git a/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart b/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart index 8bb8ee0a3..5b313b788 100644 --- a/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart +++ b/lib/views/import_tokens_view/pages/import_encrypted_data_page.dart @@ -44,7 +44,8 @@ class ImportEncryptedDataPage extends StatefulWidget { @override // ignore: no_logic_in_create_state - State createState() => _ImportEncryptedDataPageState(); + State createState() => + _ImportEncryptedDataPageState(); } class _ImportEncryptedDataPageState extends State { @@ -56,120 +57,138 @@ class _ImportEncryptedDataPageState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text( - widget.appName, - overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. - maxLines: 2, // Title can be shown on small screens too. + appBar: AppBar( + title: Text( + widget.appName, + overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. + maxLines: 2, // Title can be shown on small screens too. + ), + ), + body: Center( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: ImportTokensView.pagePaddingHorizontal, ), - ), - body: Center( - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: ImportTokensView.pagePaddingHorizontal), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.file_present, - size: ImportTokensView.iconSize, - ), - const SizedBox(height: ImportTokensView.itemSpacingHorizontal), - Text( - AppLocalizations.of(context)!.tokensAreEncrypted, - textAlign: TextAlign.center, - ), - SizedBox( - height: ImportTokensView.itemSpacingHorizontal * 4, - child: Row( - children: [ - Flexible( - flex: 9, - child: TextField( - controller: _passwordController, - focusNode: _passwordFocusNode, - enabled: processingFuture == null, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.password, - labelStyle: Theme.of(context).textTheme.titleSmall, - errorText: wrongPassword ? AppLocalizations.of(context)!.wrongPassword : null, - ), - onChanged: (value) => setState(() { - wrongPassword = false; - }), - obscureText: !isPasswordVisible, - ), + child: Column( + children: [ + const Icon(Icons.file_present, size: ImportTokensView.iconSize), + const SizedBox(height: ImportTokensView.itemSpacingHorizontal), + Text( + AppLocalizations.of(context)!.tokensAreEncrypted, + textAlign: TextAlign.center, + ), + SizedBox( + height: ImportTokensView.itemSpacingHorizontal * 4, + child: Row( + children: [ + Flexible( + flex: 9, + child: TextField( + controller: _passwordController, + focusNode: _passwordFocusNode, + enabled: processingFuture == null, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.password, + labelStyle: Theme.of(context).textTheme.titleSmall, + errorText: wrongPassword + ? AppLocalizations.of(context)!.wrongPassword + : null, ), - const SizedBox(width: ImportTokensView.itemSpacingVertical), - Flexible( - child: GestureDetector( - child: const SizedBox( - height: 200, - width: 200, - child: Center( - child: Icon( - Icons.visibility, - size: 36, - ), - ), - ), - onPanDown: (_) => setState(() => isPasswordVisible = true), - onPanStart: (_) => setState(() => isPasswordVisible = true), - onPanCancel: () => setState(() => isPasswordVisible = false), - onPanEnd: (_) => setState(() => isPasswordVisible = false), - ), - ) - ], + onChanged: (value) => setState(() { + wrongPassword = false; + }), + obscureText: !isPasswordVisible, + ), ), - ), - const SizedBox(height: ImportTokensView.itemSpacingHorizontal), - processingFuture != null - ? const CircularProgressIndicator() - : SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _passwordController.text.isEmpty || wrongPassword - ? null - : () async { - setState(() { - processingFuture = Future( - () async { - try { - final processorResults = await widget.processor.processTokenMigrate(widget.data, args: _passwordController.text); - if (processorResults == null) return; - _pushImportPlainTokensPage(processorResults); - } on BadDecryptionPasswordException catch (_) { - setState(() { - wrongPassword = true; - processingFuture = null; - WidgetsBinding.instance.addPostFrameCallback((_) { - _passwordFocusNode.requestFocus(); - }); - }); - return; - } - }, - )..then((_) => setState(() => processingFuture = null)); - }); - }, - child: Text( - AppLocalizations.of(context)!.decrypt, - style: Theme.of(context).textTheme.headlineSmall, - overflow: TextOverflow.fade, - softWrap: false, - ), + const SizedBox(width: ImportTokensView.itemSpacingVertical), + Flexible( + child: GestureDetector( + child: const SizedBox( + height: 200, + width: 200, + child: Center( + child: Icon(Icons.visibility, size: 36), ), ), - ], + onPanDown: (_) => + setState(() => isPasswordVisible = true), + onPanStart: (_) => + setState(() => isPasswordVisible = true), + onPanCancel: () => + setState(() => isPasswordVisible = false), + onPanEnd: (_) => + setState(() => isPasswordVisible = false), + ), + ), + ], + ), ), - ), + const SizedBox(height: ImportTokensView.itemSpacingHorizontal), + processingFuture != null + ? const CircularProgressIndicator() + : SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: + _passwordController.text.isEmpty || wrongPassword + ? null + : () async { + setState(() { + processingFuture = + Future(() async { + try { + final processorResults = await widget + .processor + .processTokenMigrate( + widget.data, + args: _passwordController.text, + ); + if (processorResults == null) return; + _pushImportPlainTokensPage( + processorResults, + ); + } on BadDecryptionPasswordException catch ( + _ + ) { + setState(() { + wrongPassword = true; + processingFuture = null; + WidgetsBinding.instance + .addPostFrameCallback((_) { + _passwordFocusNode + .requestFocus(); + }); + }); + return; + } + })..then( + (_) => setState( + () => processingFuture = null, + ), + ); + }); + }, + child: Text( + AppLocalizations.of(context)!.decrypt, + style: Theme.of(context).textTheme.headlineSmall, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + ), + ], ), ), - ); + ), + ), + ); - void _pushImportPlainTokensPage(List> processorResults) async { + Future _pushImportPlainTokensPage( + List> processorResults, + ) async { final tokensToImport = await Navigator.of(context).push>( MaterialPageRoute( builder: (context) => ImportPlainTokensPage( diff --git a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart index 4346aba0b..41a2ecefa 100644 --- a/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart +++ b/lib/views/import_tokens_view/pages/import_plain_tokens_page.dart @@ -45,12 +45,20 @@ class ImportPlainTokensPage extends ConsumerStatefulWidget { required String titleName, required TokenImportType selectedType, }) { - final importedTokens = processorResults.whereType>().map((e) => e.resultData).toList(); - final failedImports = processorResults.whereType().map((e) => e.message).toList(); + final importedTokens = processorResults + .whereType>() + .map((e) => e.resultData) + .toList(); + final failedImports = processorResults + .whereType() + .map((e) => e.message) + .toList(); return ImportPlainTokensPage._( key: key, importedTokens: importedTokens, - failedImports: failedImports.map((failedImport) => failedImport(AppLocalizationsEn())).toList(), + failedImports: failedImports + .map((failedImport) => failedImport(AppLocalizationsEn())) + .toList(), titleName: titleName, selectedType: selectedType, // numOfDuplicates: duplicates.length, @@ -79,10 +87,15 @@ class _ImportFileNoPwState extends ConsumerState { final List appDuplicates = []; final List importDuplicates = []; - List _initBuildLists(List importTokenEntrys) { + List _initBuildLists( + List importTokenEntrys, + ) { for (var i = 0; i < importTokenEntrys.length; i++) { final importTokenEntry = importTokenEntrys[i]; - if ([...newImports, ...appDuplicates, ...conflictedImports].any((import) => import.newToken.isSameTokenAs(importTokenEntry.newToken) == true)) { + if ([...newImports, ...appDuplicates, ...conflictedImports].any( + (import) => + import.newToken.isSameTokenAs(importTokenEntry.newToken) == true, + )) { importDuplicates.add(importTokenEntry); importTokenEntrys.remove(importTokenEntry); i--; @@ -106,11 +119,15 @@ class _ImportFileNoPwState extends ConsumerState { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - final map = (await ref.read(tokenProvider.future)).getSameTokens(widget.importedTokens); + final map = (await ref.read( + tokenProvider.future, + )).getSameTokens(widget.importedTokens); final importTokenEntrys = []; setState(() { map.forEach((key, value) { - importTokenEntrys.add(TokenImportEntry(newToken: key, oldToken: value)); + importTokenEntrys.add( + TokenImportEntry(newToken: key, oldToken: value), + ); }); _initBuildLists(importTokenEntrys); }); @@ -126,11 +143,12 @@ class _ImportFileNoPwState extends ConsumerState { super.dispose(); } - void _updateIsMaxScrollExtent() async { + Future _updateIsMaxScrollExtent() async { WidgetsBinding.instance.addPostFrameCallback((_) async { await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; - if (scrollController.position.maxScrollExtent <= scrollController.offset) { + if (scrollController.position.maxScrollExtent <= + scrollController.offset) { if (isMaxScrollOffset || !mounted) return; setState(() { isMaxScrollOffset = true; @@ -157,9 +175,7 @@ class _ImportFileNoPwState extends ConsumerState { ), ), body: Column( - mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: SingleChildScrollView( @@ -167,50 +183,72 @@ class _ImportFileNoPwState extends ConsumerState { child: Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: ImportTokensView.pagePaddingHorizontal), + padding: const EdgeInsets.symmetric( + horizontal: ImportTokensView.pagePaddingHorizontal, + ), child: Icon( widget.selectedType.icon, color: Colors.green, size: ImportTokensView.iconSize, ), ), - const SizedBox(height: ImportTokensView.itemSpacingHorizontal), + const SizedBox( + height: ImportTokensView.itemSpacingHorizontal, + ), Column( children: [ if (widget.failedImports.isNotEmpty) - FailedImportsList( - failedImports: widget.failedImports, - ), + FailedImportsList(failedImports: widget.failedImports), if (conflictedImports.isNotEmpty) ConflictedImportTokensList( - title: AppLocalizations.of(context)!.importConflictToken(conflictedImports.length), - titlePadding: const EdgeInsets.symmetric(horizontal: 40), + title: AppLocalizations.of( + context, + )!.importConflictToken(conflictedImports.length), + titlePadding: const EdgeInsets.symmetric( + horizontal: 40, + ), leadingDivider: widget.failedImports.isNotEmpty, importEntries: conflictedImports, onTap: _updateConflicted, ), if (newImports.isNotEmpty) NoConflictImportTokensList( - title: AppLocalizations.of(context)!.importNewToken(newImports.length), - titlePadding: const EdgeInsets.symmetric(horizontal: 40), + title: AppLocalizations.of( + context, + )!.importNewToken(newImports.length), + titlePadding: const EdgeInsets.symmetric( + horizontal: 40, + ), leadingDivider: conflictedImports.isNotEmpty, importEntries: newImports, onTap: _updateNewImports, ), if (appDuplicates.isNotEmpty) NoConflictImportTokensList( - title: AppLocalizations.of(context)!.importExistingToken(appDuplicates.length), - titlePadding: const EdgeInsets.symmetric(horizontal: 40), - leadingDivider: newImports.isNotEmpty || conflictedImports.isNotEmpty, + title: AppLocalizations.of( + context, + )!.importExistingToken(appDuplicates.length), + titlePadding: const EdgeInsets.symmetric( + horizontal: 40, + ), + leadingDivider: + newImports.isNotEmpty || + conflictedImports.isNotEmpty, importEntries: appDuplicates, // borderColor: null, ), if (importDuplicates.isNotEmpty) NoConflictImportTokensList( - title: 'The contained duplicates (${importDuplicates.length}) will be ignored.', + title: + 'The contained duplicates (${importDuplicates.length}) will be ignored.', // AppLocalizations.of(context)!.importDuplicateToken(importDuplicates.length),'' - titlePadding: const EdgeInsets.symmetric(horizontal: 40), - leadingDivider: newImports.isNotEmpty || conflictedImports.isNotEmpty || appDuplicates.isNotEmpty, + titlePadding: const EdgeInsets.symmetric( + horizontal: 40, + ), + leadingDivider: + newImports.isNotEmpty || + conflictedImports.isNotEmpty || + appDuplicates.isNotEmpty, importEntries: importDuplicates, borderColor: null, ), @@ -223,11 +261,7 @@ class _ImportFileNoPwState extends ConsumerState { AnimatedOpacity( opacity: isMaxScrollOffset ? 0 : 1, duration: const Duration(milliseconds: 250), - child: const Divider( - thickness: 2, - indent: 4, - endIndent: 4, - ), + child: const Divider(thickness: 2, indent: 4, endIndent: 4), ), Padding( padding: const EdgeInsets.fromLTRB( @@ -241,9 +275,15 @@ class _ImportFileNoPwState extends ConsumerState { child: ElevatedButton( onPressed: tokensToKeep == null || tokensToKeep!.contains(null) ? null - : () => Navigator.of(context).pop>(tokensToKeep!.whereType().toList()), + : () => Navigator.of(context).pop>( + tokensToKeep!.whereType().toList(), + ), child: Text( - tokensToKeep != null ? AppLocalizations.of(context)!.importNTokens(tokensToKeep!.length) : AppLocalizations.of(context)!.ok, + tokensToKeep != null + ? AppLocalizations.of( + context, + )!.importNTokens(tokensToKeep!.length) + : AppLocalizations.of(context)!.ok, style: Theme.of(context).textTheme.headlineSmall, overflow: TextOverflow.fade, softWrap: false, @@ -276,10 +316,20 @@ class _ImportFileNoPwState extends ConsumerState { tokensToKeep = []; for (final importTokenEntry in importTokenEntrys) { if (importTokenEntry.oldToken != null) { - if (importTokenEntry.newToken.sameValuesAs(importTokenEntry.oldToken!)) continue; - tokensToKeep!.add(importTokenEntry.selectedToken?.copyWith(id: importTokenEntry.oldToken?.id)); + if (importTokenEntry.newToken.sameValuesAs( + importTokenEntry.oldToken!, + )) { + continue; + } + tokensToKeep!.add( + importTokenEntry.selectedToken?.copyWith( + id: importTokenEntry.oldToken?.id, + ), + ); } else { - if (importTokenEntry.selectedToken != null) tokensToKeep!.add(importTokenEntry.selectedToken); + if (importTokenEntry.selectedToken != null) { + tokensToKeep!.add(importTokenEntry.selectedToken); + } } } } diff --git a/lib/views/import_tokens_view/pages/import_start_page.dart b/lib/views/import_tokens_view/pages/import_start_page.dart index 3867f9654..1224118cc 100644 --- a/lib/views/import_tokens_view/pages/import_start_page.dart +++ b/lib/views/import_tokens_view/pages/import_start_page.dart @@ -91,7 +91,6 @@ class _ImportStartPageState extends ConsumerState { horizontal: ImportTokensView.pagePaddingHorizontal, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( widget.selectedSource.type.icon, @@ -279,10 +278,8 @@ class _ImportStartPageState extends ConsumerState { final DecodeParams params = DecodeParams( imageFormat: zxing.ImageFormat.rgb, - format: Format.any, tryHarder: tryHarder, tryInverted: tryInverted, - isMultiScan: false, ); final text = (await zx.readBarcodeImagePath(file, params)).text; if (text == null) { diff --git a/lib/views/import_tokens_view/pages/select_import_type_page.dart b/lib/views/import_tokens_view/pages/select_import_type_page.dart index 5e6cc5eee..58d4dca77 100644 --- a/lib/views/import_tokens_view/pages/select_import_type_page.dart +++ b/lib/views/import_tokens_view/pages/select_import_type_page.dart @@ -49,9 +49,10 @@ class SelectImportTypePage extends StatelessWidget { child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: ImportTokensView.pagePaddingHorizontal), + padding: const EdgeInsets.symmetric( + horizontal: ImportTokensView.pagePaddingHorizontal, + ), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ const RotatedBox( quarterTurns: 1, @@ -67,7 +68,10 @@ class SelectImportTypePage extends StatelessWidget { ), const SizedBox(height: ImportTokensView.itemSpacingHorizontal), for (final importEntity in tokenImportOrigin.importSources) ...[ - if (importEntity != tokenImportOrigin.importSources.first) const SizedBox(height: ImportTokensView.itemSpacingHorizontal / 2), + if (importEntity != tokenImportOrigin.importSources.first) + const SizedBox( + height: ImportTokensView.itemSpacingHorizontal / 2, + ), SizedBox( width: double.infinity, child: ElevatedButton( @@ -77,10 +81,14 @@ class SelectImportTypePage extends StatelessWidget { flex: 8, child: Text( switch (importEntity.type) { - const (TokenImportType.backupFile) => localizations.selectFile, - const (TokenImportType.qrScan) => localizations.scanQrCode, - const (TokenImportType.qrFile) => localizations.selectFile, - const (TokenImportType.link) => localizations.enterLink, + const (TokenImportType.backupFile) => + localizations.selectFile, + const (TokenImportType.qrScan) => + localizations.scanQrCode, + const (TokenImportType.qrFile) => + localizations.selectFile, + const (TokenImportType.link) => + localizations.enterLink, }, style: Theme.of(context).textTheme.headlineSmall, overflow: TextOverflow.fade, @@ -88,13 +96,14 @@ class SelectImportTypePage extends StatelessWidget { softWrap: false, ), ), - Expanded( - child: Icon(importEntity.type.icon), - ), + Expanded(child: Icon(importEntity.type.icon)), const Expanded(child: SizedBox()), ], ), - onPressed: () => _routeStartPage(context: context, importSource: importEntity), + onPressed: () => _routeStartPage( + context: context, + importSource: importEntity, + ), ), ), ], @@ -107,10 +116,16 @@ class SelectImportTypePage extends StatelessWidget { ); } - Future _routeStartPage({required TokenImportSource importSource, required BuildContext context}) async { + Future _routeStartPage({ + required TokenImportSource importSource, + required BuildContext context, + }) async { final tokensToImport = await Navigator.of(context).push>( MaterialPageRoute( - builder: (context) => ImportStartPage(appName: tokenImportOrigin.appName, selectedSource: importSource), + builder: (context) => ImportStartPage( + appName: tokenImportOrigin.appName, + selectedSource: importSource, + ), ), ); if (tokensToImport != null) { diff --git a/lib/views/import_tokens_view/widgets/conflicted_import_tokens_list.dart b/lib/views/import_tokens_view/widgets/conflicted_import_tokens_list.dart index 199126c19..25fe82938 100644 --- a/lib/views/import_tokens_view/widgets/conflicted_import_tokens_list.dart +++ b/lib/views/import_tokens_view/widgets/conflicted_import_tokens_list.dart @@ -37,7 +37,8 @@ class ConflictedImportTokensList extends StatelessWidget { final bool leadingDivider; final List importEntries; - final void Function(TokenImportEntry oldEntry, TokenImportEntry newEntry) onTap; + final void Function(TokenImportEntry oldEntry, TokenImportEntry newEntry) + onTap; @override Widget build(BuildContext context) { @@ -47,27 +48,20 @@ class ConflictedImportTokensList extends StatelessWidget { children: [ if (leadingDivider) ...[ const SizedBox(height: 16), - const Divider( - thickness: 2, - height: 2, - indent: 4, - endIndent: 4, - ), + const Divider(thickness: 2, height: 2, indent: 4, endIndent: 4), const SizedBox(height: 16), ], if (title != null) ...[ Padding( padding: titlePadding, - child: Text( - title!, - textAlign: TextAlign.center, - ), + child: Text(title!, textAlign: TextAlign.center), ), const SizedBox(height: 8), ], for (var i = 0; i < importEntries.length; i++) ConflictedImportTokensTile( - selectTokenCallback: (newEntry) => onTap(importEntries[i], newEntry), + selectTokenCallback: (newEntry) => + onTap(importEntries[i], newEntry), importTokenEntry: importEntries[i], initialScreenSize: MediaQuery.of(context).size, key: Key('ConflictedImportTokensTile_$i'), diff --git a/lib/views/import_tokens_view/widgets/failed_imports_list.dart b/lib/views/import_tokens_view/widgets/failed_imports_list.dart index e06a975b7..2a30b5dc1 100644 --- a/lib/views/import_tokens_view/widgets/failed_imports_list.dart +++ b/lib/views/import_tokens_view/widgets/failed_imports_list.dart @@ -24,10 +24,7 @@ import '../../../l10n/app_localizations.dart'; class FailedImportsList extends StatelessWidget { final List failedImports; - const FailedImportsList({ - super.key, - required this.failedImports, - }); + const FailedImportsList({super.key, required this.failedImports}); @override Widget build(BuildContext context) { @@ -37,7 +34,9 @@ class FailedImportsList extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( - AppLocalizations.of(context)!.importFailedToken(failedImports.length), + AppLocalizations.of( + context, + )!.importFailedToken(failedImports.length), textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), @@ -46,21 +45,12 @@ class FailedImportsList extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( - mainAxisSize: MainAxisSize.max, children: [ - Expanded( - child: Text( - '${i + 1}.', - textAlign: TextAlign.right, - ), - ), + Expanded(child: Text('${i + 1}.', textAlign: TextAlign.right)), const SizedBox(width: 8), Expanded( flex: 5, - child: Text( - '${failedImports[i]}', - textAlign: TextAlign.left, - ), + child: Text('${failedImports[i]}', textAlign: TextAlign.left), ), ], ), diff --git a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart index 85d94b282..eed12ef01 100644 --- a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart +++ b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_list.dart @@ -37,15 +37,18 @@ class NoConflictImportTokensList extends StatefulWidget { final EdgeInsetsGeometry titlePadding; final Color? borderColor; final bool leadingDivider; - final void Function(TokenImportEntry oldEntry, TokenImportEntry newEntry)? onTap; + final void Function(TokenImportEntry oldEntry, TokenImportEntry newEntry)? + onTap; final List importEntries; @override - State createState() => _NoConflictImportTokensListState(); + State createState() => + _NoConflictImportTokensListState(); } -class _NoConflictImportTokensListState extends State { +class _NoConflictImportTokensListState + extends State { @override Widget build(BuildContext context) { return Column( @@ -54,21 +57,13 @@ class _NoConflictImportTokensListState extends State children: [ if (widget.leadingDivider) ...[ const SizedBox(height: 16), - const Divider( - thickness: 2, - height: 2, - indent: 4, - endIndent: 4, - ), + const Divider(thickness: 2, height: 2, indent: 4, endIndent: 4), const SizedBox(height: 16), ], if (widget.title != null) ...[ Padding( padding: widget.titlePadding, - child: Text( - widget.title!, - textAlign: TextAlign.center, - ), + child: Text(widget.title!, textAlign: TextAlign.center), ), const SizedBox(height: 8), ], @@ -76,7 +71,11 @@ class _NoConflictImportTokensListState extends State GestureDetector( onTap: widget.onTap != null ? () { - final newTokenEntry = tokenEntry.copySelect(tokenEntry.selectedToken == null ? tokenEntry.newToken : null); + final newTokenEntry = tokenEntry.copySelect( + tokenEntry.selectedToken == null + ? tokenEntry.newToken + : null, + ); widget.onTap!(tokenEntry, newTokenEntry); } : null, @@ -85,7 +84,7 @@ class _NoConflictImportTokensListState extends State selected: tokenEntry.selectedToken, borderColor: widget.borderColor, ), - ) + ), ], ); } diff --git a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_tile.dart b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_tile.dart index d50564bdd..310de10d0 100644 --- a/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_tile.dart +++ b/lib/views/import_tokens_view/widgets/no_conflict_import_tokens_tile.dart @@ -41,22 +41,22 @@ class NoConflictImportTokensTile extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( - width: width, - child: Card( - elevation: 2, - color: selected == token ? borderColor : null, - child: InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.all(4), - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - children: [TokenWidgetBuilder.previewFromToken(token)], - ), - ), + width: width, + child: Card( + elevation: 2, + color: selected == token ? borderColor : null, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + children: [TokenWidgetBuilder.previewFromToken(token)], ), ), ), - ); + ), + ), + ); } diff --git a/lib/views/license_view/license_view.dart b/lib/views/license_view/license_view.dart index 9bd9424bf..d3e35d3cf 100644 --- a/lib/views/license_view/license_view.dart +++ b/lib/views/license_view/license_view.dart @@ -31,21 +31,28 @@ class LicenseView extends StatelessView { final Widget appImage; final String websiteLink; - const LicenseView({required this.appName, required this.websiteLink, required this.appImage, super.key}); + const LicenseView({ + required this.appName, + required this.websiteLink, + required this.appImage, + super.key, + }); @override Widget build(BuildContext context) => PushRequestListener( - child: FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, platformInfo) => LicensePage( - applicationName: appName, - applicationIcon: Padding( - padding: const EdgeInsets.all(32), - child: appImage, - ), - applicationLegalese: '© $websiteLink', - applicationVersion: platformInfo.data == null ? '' : '${platformInfo.data?.version}+${platformInfo.data?.buildNumber}', - ), + child: FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, platformInfo) => LicensePage( + applicationName: appName, + applicationIcon: Padding( + padding: const EdgeInsets.all(32), + child: appImage, ), - ); + applicationLegalese: '© $websiteLink', + applicationVersion: platformInfo.data == null + ? '' + : '${platformInfo.data?.version}+${platformInfo.data?.buildNumber}', + ), + ), + ); } diff --git a/lib/views/main_view/main_view_widgets/expandable_appbar.dart b/lib/views/main_view/main_view_widgets/expandable_appbar.dart index 6bc86f80d..0835d224b 100644 --- a/lib/views/main_view/main_view_widgets/expandable_appbar.dart +++ b/lib/views/main_view/main_view_widgets/expandable_appbar.dart @@ -102,7 +102,6 @@ class _ExpandableAppBarState extends State { onVerticalDragEnd: _stopExpansion, child: Column( verticalDirection: VerticalDirection.up, - mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded(child: widget.body), AnimatedContainer( diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart index 23da106c3..cc11fc3be 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/delete_token_folder_action.dart @@ -56,7 +56,6 @@ class DeleteTokenFolderAction extends ConsumerSlideableAction { }, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.delete), Text( diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart index 190c22c3c..1a0faadce 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/lock_token_folder_action.dart @@ -44,13 +44,13 @@ class LockTokenFolderAction extends ConsumerSlideableAction { reason: (localization) => localization.unlock, localization: AppLocalizations.of(context)!, ) == - false) + false) { return; + } globalRef?.read(tokenFolderProvider.notifier).toggleFolderLock(folder); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.lock), Text( diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart index 8e0f82bbb..5a931783c 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_actions.dart/rename_token_folder_action.dart @@ -55,7 +55,6 @@ class RenameTokenFolderAction extends ConsumerSlideableAction { }, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart index d857bde6c..8fa4b4a5d 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_body.dart @@ -44,7 +44,6 @@ class TokenFolderExpandableBody extends StatelessWidget { Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 4), child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ for (var i = 0; i < tokens.length; i++) ...[ if (draggingSortable != tokens[i] && diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart index ef7d41ccd..539faea2d 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart @@ -144,7 +144,6 @@ class _TokenFolderExpandableHeaderState widget.expandableController.value = true; }, child: Row( - mainAxisSize: MainAxisSize.max, children: [ const SizedBox(width: 8), RotationTransition( diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart index 7e9e21701..da42b630f 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list.dart @@ -143,7 +143,6 @@ class MainViewTokensList extends ConsumerStatefulWidget { key: ValueKey( 'mainview_${sortable.runtimeType}_${sortable.folderId}', ), - filter: null, ), ); continue; diff --git a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart index 95b27a9ad..18b42d254 100644 --- a/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart +++ b/lib/views/main_view/main_view_widgets/main_view_tokens_list_filtered.dart @@ -62,10 +62,7 @@ class MainViewTokensListFiltered extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.max, - children: [..._mapTokensToWidgets(ref: ref)], - ), + child: Column(children: [..._mapTokensToWidgets(ref: ref)]), ); } diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart index 6a6b4164b..450755e30 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/actions/edit_day_password_token_action.dart @@ -71,7 +71,6 @@ class EditDayPassowrdTokenAction extends ConsumerSlideableAction { .complete(Introduction.editToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index 06bf5489c..ea93936e3 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -204,7 +204,6 @@ class _DayPasswordTokenWidgetTileState ), ConstrainedBox( constraints: BoxConstraints( - maxWidth: double.infinity, maxHeight: Theme.of(context).textTheme.bodyLarge!.fontSize! * 3, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart index 4b0f5724e..00076165c 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action.dart @@ -73,7 +73,6 @@ class DefaultEditAction extends ConsumerSlideableAction { .complete(Introduction.editToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action_dialog.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action_dialog.dart index 8f4bb8659..4541ad7af 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action_dialog.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_edit_action_dialog.dart @@ -107,7 +107,6 @@ class _DefaultEditActionDialogState padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ PiTextField( @@ -203,14 +202,14 @@ class _DefaultEditActionDialogState ); } - void _saveButtonPressed() async { + Future _saveButtonPressed() async { widget.onSaveButtonPressed!( newLabel: nameInputController.text, newImageUrl: imageUrlController.text, ); } - void _defaultSaveAction() async { + Future _defaultSaveAction() async { final newLabel = nameInputController.text; final newImageUrl = imageUrlController.text; if (newLabel.isEmpty) return; @@ -315,7 +314,7 @@ class _EditActionExpansionTileState extends State animation: animation!, builder: (buildContext, _) => Container( margin: const EdgeInsets.symmetric(vertical: 8.0), - padding: EdgeInsets.only(bottom: animation!.value * 16.0, right: 0), + padding: EdgeInsets.only(bottom: animation!.value * 16.0), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(12.0), diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart index 1e2abd1fa..21c55e987 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_lock_action.dart @@ -81,7 +81,6 @@ class DefaultLockAction extends ConsumerSlideableAction { .complete(Introduction.lockToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.lock), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart index e059faa3a..d3072a512 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/actions/edit_hotp_token_action.dart @@ -74,7 +74,6 @@ class EditHOTPTokenAction extends ConsumerSlideableAction { .complete(Introduction.editToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart index d1b8e36a6..fc1ca9992 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/actions/edit_push_token_action.dart @@ -76,7 +76,6 @@ class EditPushTokenAction extends ConsumerSlideableAction { .complete(Introduction.editToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart index 7da9abf05..02b2918a4 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/push_token_widgets/push_token_widget_tile.dart @@ -61,10 +61,7 @@ class PushTokenWidgetTile extends ConsumerWidget { .complete(Introduction.pollForChallenges); }, child: const CustomTrailing( - child: FittedBox( - fit: BoxFit.contain, - child: Icon(size: 100, Icons.notifications), - ), + child: FittedBox(child: Icon(size: 100, Icons.notifications)), ), ), ); diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart index 145e005f4..9a2000978 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_tile.dart @@ -94,7 +94,6 @@ class TokenWidgetTile extends ConsumerWidget { padding: const EdgeInsets.only(left: 4.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, children: [ if (subtitle1.isNotEmpty) Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart index e418aa1ae..f9e36b217 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/actions/edit_totp_token_action.dart @@ -71,7 +71,6 @@ class EditTOTPTokenAction extends ConsumerSlideableAction { .complete(Introduction.editToken), child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon(Icons.edit), Text( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart index f29757e35..ea74e3927 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile_countdown.dart @@ -39,7 +39,6 @@ class TotpTokenWidgetTileCountdown extends StatelessWidget { final value = secondsUntilNextOTP.ceil(); return FittedBox( clipBehavior: Clip.hardEdge, - fit: BoxFit.contain, child: Stack( alignment: Alignment.center, children: [ diff --git a/lib/views/push_token_view/push_tokens_view.dart b/lib/views/push_token_view/push_tokens_view.dart index 0f12ca948..21076939e 100644 --- a/lib/views/push_token_view/push_tokens_view.dart +++ b/lib/views/push_token_view/push_tokens_view.dart @@ -45,7 +45,11 @@ class PushTokensView extends StatelessView { child: Stack( children: [ Center( - child: Icon(Icons.notifications_none, size: 300, color: Colors.grey.withValues(alpha: 0.2)), + child: Icon( + Icons.notifications_none, + size: 300, + color: Colors.grey.withValues(alpha: 0.2), + ), ), const PushTokensViwList(), ], diff --git a/lib/views/push_token_view/widgets/push_tokens_view_list.dart b/lib/views/push_token_view/widgets/push_tokens_view_list.dart index 925fe3bfc..9f812542e 100644 --- a/lib/views/push_token_view/widgets/push_tokens_view_list.dart +++ b/lib/views/push_token_view/widgets/push_tokens_view_list.dart @@ -42,7 +42,10 @@ class PushTokensViwList extends ConsumerStatefulWidget { if (sortables.isEmpty) return []; sortables = sortables.toList(); sortables = sortables.whereType().toList(); - return MainViewTokensList.buildSortableWidgets(sortables: sortables, draggingSortable: draggingSortable); + return MainViewTokensList.buildSortableWidgets( + sortables: sortables, + draggingSortable: draggingSortable, + ); } @override @@ -67,7 +70,11 @@ class _PushTokensViwListState extends ConsumerState { height: 9999, child: Opacity( opacity: 0, - child: DragTargetDivider(dependingFolder: null, previousSortable: pushTokens.lastOrNull, nextSortable: null, bottomPadding: 0), + child: DragTargetDivider( + dependingFolder: null, + previousSortable: pushTokens.lastOrNull, + nextSortable: null, + ), ), ), ), @@ -84,7 +91,10 @@ class _PushTokensViwListState extends ConsumerState { scrollController: scrollController, child: Column( mainAxisSize: MainAxisSize.min, - children: PushTokensViwList._buildSortableWidgets(sortables: pushTokens, draggingSortable: draggingSortable), + children: PushTokensViwList._buildSortableWidgets( + sortables: pushTokens, + draggingSortable: draggingSortable, + ), ), ), ), diff --git a/lib/views/qr_scanner_view/qr_scanner_view.dart b/lib/views/qr_scanner_view/qr_scanner_view.dart index d650518fd..73774d0d3 100644 --- a/lib/views/qr_scanner_view/qr_scanner_view.dart +++ b/lib/views/qr_scanner_view/qr_scanner_view.dart @@ -109,7 +109,7 @@ class _QRScannerViewState extends State { DialogAction( label: AppLocalizations.of(context)!.cancel, intent: DialogActionIntent.cancel, - onPressed: () => Navigator.pop(context, null), + onPressed: () => Navigator.pop(context), ), DialogAction( label: AppLocalizations.of( diff --git a/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart b/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart index a503a0654..93d62a07f 100644 --- a/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart +++ b/lib/views/qr_scanner_view/qr_scanner_view_widgets/qr_scanner_widget.dart @@ -51,7 +51,6 @@ class _QRScannerWidgetState extends State { setState(() => isInitialized = controller != null); }, actionButtonsAlignment: Alignment.bottomRight, - showFlashlight: true, flashOnIcon: Semantics( label: AppLocalizations.of( context, @@ -64,7 +63,6 @@ class _QRScannerWidgetState extends State { )!.a11yScanQrCodeViewFlashlightOff, child: const Icon(Icons.flash_off), ), - showGallery: true, galleryIcon: Semantics( label: AppLocalizations.of(context)!.a11yScanQrCodeViewGallery, child: const Icon(Icons.image), @@ -73,9 +71,7 @@ class _QRScannerWidgetState extends State { codeFormat: Format.qrCode, cropPercent: 0.70, scannerOverlay: ScannerOverlayBorder( - borderColor: Colors.white, overlayColor: Colors.black54, - borderLength: 32, borderWidth: 6, cutOutSize: MediaQuery.of(context).size.width * 0.7, ), diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart index f6b070388..a20518f3e 100644 --- a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart @@ -159,7 +159,7 @@ class _ExportTokensToFileDialogState ); } - void _exportTokens() async { + Future _exportTokens() async { if (_passwordTextController.text.isEmpty || _passwordTextController.text != _confirmTextController.text) { return; @@ -190,7 +190,7 @@ class _ExportTokensToFileDialogState ); } - void _saveToFile(String encryptedTokens) async { + Future _saveToFile(String encryptedTokens) async { if (kIsWeb) return; bool isExported = false; diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart index fe338ce5f..aa70de6da 100644 --- a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_export_type_dialog.dart @@ -59,7 +59,7 @@ class SelectExportTypeDialog extends StatelessWidget { ); } - void _selectTokensDialog(BuildContext context) async { + Future _selectTokensDialog(BuildContext context) async { final isExported = await showDialog( useRootNavigator: false, context: context, @@ -79,7 +79,7 @@ class SelectExportTypeDialog extends StatelessWidget { } } - void _selectTokenDialog(BuildContext context) async { + Future _selectTokenDialog(BuildContext context) async { final localization = AppLocalizations.of(context)!; final authenticated = await lockAuth( reason: (l) => l.exportLockedTokenReason, diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart index 930f9a928..79d5155a9 100644 --- a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/select_tokens_dialog.dart @@ -101,7 +101,7 @@ class _SelectTokensDialogState extends ConsumerState { ); } - void _showExportDialog(Set tokens) async { + Future _showExportDialog(Set tokens) async { if (tokens.isEmpty) return; final isExported = await showDialog( useRootNavigator: false, diff --git a/lib/views/settings_view/settings_groups/settings_group_allow_screenshot/settings_group_allow_screenshot.dart b/lib/views/settings_view/settings_groups/settings_group_allow_screenshot/settings_group_allow_screenshot.dart index a63d2b467..19c5c5985 100644 --- a/lib/views/settings_view/settings_groups/settings_group_allow_screenshot/settings_group_allow_screenshot.dart +++ b/lib/views/settings_view/settings_groups/settings_group_allow_screenshot/settings_group_allow_screenshot.dart @@ -55,7 +55,9 @@ class SettingsGroupAllowScreenshot extends ConsumerWidget { if (allowed != true) return; ref.read(allowScreenshotProvider.notifier).allowScreenshots(); } else { - ref.read(allowScreenshotProvider.notifier).disallowScreenshots(); + ref + .read(allowScreenshotProvider.notifier) + .disallowScreenshots(); } }, ), diff --git a/lib/views/settings_view/settings_groups/settings_group_background_image.dart b/lib/views/settings_view/settings_groups/settings_group_background_image.dart index 53b7f8d65..40ad978df 100644 --- a/lib/views/settings_view/settings_groups/settings_group_background_image.dart +++ b/lib/views/settings_view/settings_groups/settings_group_background_image.dart @@ -30,21 +30,29 @@ class SettingsGroupBackroundImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - if (PrivacyIDEAAuthenticator.currentCustomization?.backgroundImage == null) return const SizedBox(); + if (PrivacyIDEAAuthenticator.currentCustomization?.backgroundImage == null) { + return const SizedBox(); + } return SettingsGroup( title: AppLocalizations.of(context)!.backgroundImage, - onPressed: () => ref.read(settingsProvider.notifier).toggleShowBackgroundImage(), + onPressed: () => + ref.read(settingsProvider.notifier).toggleShowBackgroundImage(), trailingWidget: FutureBuilder( - future: ref.watch(settingsProvider.selectAsync((v) => v.showBackgroundImage)), - builder: (context, snapshot) { - if (snapshot.hasError || snapshot.data == null) { - return const SizedBox(); - } - return Switch( - value: snapshot.data!, - onChanged: (value) => ref.read(settingsProvider.notifier).setShowBackgroundImage(value), - ); - }), + future: ref.watch( + settingsProvider.selectAsync((v) => v.showBackgroundImage), + ), + builder: (context, snapshot) { + if (snapshot.hasError || snapshot.data == null) { + return const SizedBox(); + } + return Switch( + value: snapshot.data!, + onChanged: (value) => ref + .read(settingsProvider.notifier) + .setShowBackgroundImage(value), + ); + }, + ), ); } } diff --git a/lib/views/settings_view/settings_groups/settings_group_container.dart b/lib/views/settings_view/settings_groups/settings_group_container.dart index e614c5bc3..4552b93b0 100644 --- a/lib/views/settings_view/settings_groups/settings_group_container.dart +++ b/lib/views/settings_view/settings_groups/settings_group_container.dart @@ -31,9 +31,16 @@ class SettingsGroupContainer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => SettingsGroup( - title: AppLocalizations.of(context)!.container, - onPressed: () => Navigator.of(context).pushNamed(ContainerView.routeName), - isActive: ref.watch(tokenContainerProvider).whenOrNull(data: (data) => data)?.containerList.isNotEmpty ?? false, - trailingIcon: Icons.arrow_forward_ios, // TODO: Change to container icon when we have one - ); + title: AppLocalizations.of(context)!.container, + onPressed: () => Navigator.of(context).pushNamed(ContainerView.routeName), + isActive: + ref + .watch(tokenContainerProvider) + .whenOrNull(data: (data) => data) + ?.containerList + .isNotEmpty ?? + false, + trailingIcon: Icons + .arrow_forward_ios, // TODO: Change to container icon when we have one + ); } diff --git a/lib/views/settings_view/settings_groups/settings_group_error_log.dart b/lib/views/settings_view/settings_groups/settings_group_error_log.dart index cc9044b07..49e4c659f 100644 --- a/lib/views/settings_view/settings_groups/settings_group_error_log.dart +++ b/lib/views/settings_view/settings_groups/settings_group_error_log.dart @@ -28,14 +28,14 @@ class SettingsGroupErrorLog extends StatelessWidget { @override Widget build(BuildContext context) => SettingsGroup( - title: AppLocalizations.of(context)!.errorLogTitle, - onPressed: () => showDialog( - useRootNavigator: false, - context: context, - builder: (_) => const SettingsGroupErrorLogDialog(), - ), - trailingIcon: Icons.error_outline, - ); + title: AppLocalizations.of(context)!.errorLogTitle, + onPressed: () => showDialog( + useRootNavigator: false, + context: context, + builder: (_) => const SettingsGroupErrorLogDialog(), + ), + trailingIcon: Icons.error_outline, + ); } class SettingsGroupErrorLogDialog extends StatelessWidget { diff --git a/lib/views/settings_view/settings_groups/settings_group_feedback.dart b/lib/views/settings_view/settings_groups/settings_group_feedback.dart index 475229bff..a7b3be21c 100644 --- a/lib/views/settings_view/settings_groups/settings_group_feedback.dart +++ b/lib/views/settings_view/settings_groups/settings_group_feedback.dart @@ -29,8 +29,8 @@ class SettingsGroupFeedback extends StatelessWidget { @override Widget build(BuildContext context) => SettingsGroup( - title: AppLocalizations.of(context)!.feedback, - onPressed: () => Navigator.pushNamed(context, FeedbackView.routeName), - trailingIcon: Icons.feedback, - ); + title: AppLocalizations.of(context)!.feedback, + onPressed: () => Navigator.pushNamed(context, FeedbackView.routeName), + trailingIcon: Icons.feedback, + ); } diff --git a/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart b/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart index d3b505767..cac69e7de 100644 --- a/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart +++ b/lib/views/settings_view/settings_groups/settings_group_import_export_tokens.dart @@ -77,7 +77,7 @@ class _SettingsGroupImportExportTokensState ); } - void _exportDialog() async { + Future _exportDialog() async { bool? isAccepted = (await ref.read( introductionNotifierProvider.future, diff --git a/lib/views/settings_view/settings_groups/settings_group_language.dart b/lib/views/settings_view/settings_groups/settings_group_language.dart index 7d99a094d..0ff8d7b7c 100644 --- a/lib/views/settings_view/settings_groups/settings_group_language.dart +++ b/lib/views/settings_view/settings_groups/settings_group_language.dart @@ -44,16 +44,18 @@ class SettingsGroupLanguage extends ConsumerWidget { } class SettingsGroupLanguageDialog extends ConsumerWidget { - const SettingsGroupLanguageDialog({ - super.key, - }); + const SettingsGroupLanguageDialog({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final localizations = AppLocalizations.of(context)!; - final settings = ref.watch(settingsProvider).whenOrNull(data: (data) => data); - final useSystemLocale = settings?.useSystemLocale ?? SettingsState.useSystemLocaleDefault; - final currentLocale = settings?.currentLocale ?? SettingsState.localeDefault; + final settings = ref + .watch(settingsProvider) + .whenOrNull(data: (data) => data); + final useSystemLocale = + settings?.useSystemLocale ?? SettingsState.useSystemLocaleDefault; + final currentLocale = + settings?.currentLocale ?? SettingsState.localeDefault; return DefaultDialog( title: Text(localizations.language), content: Column( @@ -61,39 +63,51 @@ class SettingsGroupLanguageDialog extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( - title: Text( - localizations.useDeviceLocaleTitle, - style: Theme.of(context).textTheme.bodyMedium, - ), - subtitle: Text( - localizations.useDeviceLocaleDescription, - overflow: TextOverflow.fade, - ), - value: useSystemLocale, - onChanged: (value) => ref.read(settingsProvider.notifier).setUseSystemLocale(value)), + title: Text( + localizations.useDeviceLocaleTitle, + style: Theme.of(context).textTheme.bodyMedium, + ), + subtitle: Text( + localizations.useDeviceLocaleDescription, + overflow: TextOverflow.fade, + ), + value: useSystemLocale, + onChanged: (value) => + ref.read(settingsProvider.notifier).setUseSystemLocale(value), + ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: DropdownButton( disabledHint: Text( '$currentLocale', - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Colors.grey), overflow: TextOverflow.fade, softWrap: false, ), isExpanded: true, value: currentLocale, - items: AppLocalizations.supportedLocales.map>((Locale itemLocale) { - return DropdownMenuItem( - value: itemLocale, - child: Text( - '$itemLocale', - overflow: TextOverflow.fade, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: useSystemLocale ? Colors.grey : null), - softWrap: false, - ), - ); - }).toList(), - onChanged: useSystemLocale ? null : (value) => ref.read(settingsProvider.notifier).setLocalePreference(value!), + items: AppLocalizations.supportedLocales + .map>((Locale itemLocale) { + return DropdownMenuItem( + value: itemLocale, + child: Text( + '$itemLocale', + overflow: TextOverflow.fade, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: useSystemLocale ? Colors.grey : null, + ), + softWrap: false, + ), + ); + }) + .toList(), + onChanged: useSystemLocale + ? null + : (value) => ref + .read(settingsProvider.notifier) + .setLocalePreference(value!), ), ), ], diff --git a/lib/views/settings_view/settings_groups/settings_group_push_token/settings_group_push_token.dart b/lib/views/settings_view/settings_groups/settings_group_push_token/settings_group_push_token.dart index 81cdb834c..13eb09a79 100644 --- a/lib/views/settings_view/settings_groups/settings_group_push_token/settings_group_push_token.dart +++ b/lib/views/settings_view/settings_groups/settings_group_push_token/settings_group_push_token.dart @@ -32,15 +32,22 @@ class SettingsGroupPushToken extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final tokens = ref.watch(tokenProvider).value?.tokens ?? []; - final enrolledPushTokenList = tokens.whereType().where((e) => e.isRolledOut).toList(); - final unsupportedPushTokens = enrolledPushTokenList.where((e) => e.url == null).toList(); + final enrolledPushTokenList = tokens + .whereType() + .where((e) => e.isRolledOut) + .toList(); + final unsupportedPushTokens = enrolledPushTokenList + .where((e) => e.url == null) + .toList(); return SettingsGroup( title: AppLocalizations.of(context)!.pushToken, isActive: enrolledPushTokenList.isNotEmpty, onPressed: () => showDialog( useRootNavigator: false, context: context, - builder: (_) => SettingsGroupPushTokenDialog(unsupportedPushTokens: unsupportedPushTokens), + builder: (_) => SettingsGroupPushTokenDialog( + unsupportedPushTokens: unsupportedPushTokens, + ), ), trailingIcon: Icons.notifications, ); diff --git a/lib/views/settings_view/settings_groups/settings_group_theme.dart b/lib/views/settings_view/settings_groups/settings_group_theme.dart index a0e1e8c2c..32f29efe3 100644 --- a/lib/views/settings_view/settings_groups/settings_group_theme.dart +++ b/lib/views/settings_view/settings_groups/settings_group_theme.dart @@ -35,7 +35,9 @@ class SettingsGroupTheme extends StatelessWidget { onPressed: () { switch (current) { case ThemeMode.light: - EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: true); + EasyDynamicTheme.of( + context, + ).changeTheme(dynamic: false, dark: true); HomeWidgetUtils().setCurrentThemeMode(ThemeMode.dark); break; case ThemeMode.dark: @@ -43,11 +45,15 @@ class SettingsGroupTheme extends StatelessWidget { HomeWidgetUtils().setCurrentThemeMode(ThemeMode.system); break; case ThemeMode.system: - EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false); + EasyDynamicTheme.of( + context, + ).changeTheme(dynamic: false, dark: false); HomeWidgetUtils().setCurrentThemeMode(ThemeMode.light); break; case null: - EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false); + EasyDynamicTheme.of( + context, + ).changeTheme(dynamic: false, dark: false); HomeWidgetUtils().setCurrentThemeMode(ThemeMode.light); break; } diff --git a/lib/views/settings_view/settings_view.dart b/lib/views/settings_view/settings_view.dart index 444fe6a26..46cbf2650 100644 --- a/lib/views/settings_view/settings_view.dart +++ b/lib/views/settings_view/settings_view.dart @@ -42,34 +42,34 @@ class SettingsView extends ConsumerView { @override Widget build(BuildContext context, WidgetRef ref) => PushRequestListener( - child: Scaffold( - appBar: AppBar( - title: Text( - AppLocalizations.of(context)!.settings, - overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. - maxLines: 2, // Title can be shown on small screens too. - ), - ), - body: SafeArea( - child: const SingleChildScrollView( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsGroupFeedback(), - SettingsGroupImportExportTokens(), - SettingsGroupPushToken(), - SettingsGroupContainer(), - SettingsGroupLanguage(), - SettingsGroupTheme(), - SettingsGroupBackroundImage(), - SettingsGroupAllowScreenshot(), - SettingsGroupErrorLog(), - SettingsGroupGeneral(), - ], - ), - ), + child: Scaffold( + appBar: AppBar( + title: Text( + AppLocalizations.of(context)!.settings, + overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. + maxLines: 2, // Title can be shown on small screens too. + ), + ), + body: SafeArea( + child: const SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsGroupFeedback(), + SettingsGroupImportExportTokens(), + SettingsGroupPushToken(), + SettingsGroupContainer(), + SettingsGroupLanguage(), + SettingsGroupTheme(), + SettingsGroupBackroundImage(), + SettingsGroupAllowScreenshot(), + SettingsGroupErrorLog(), + SettingsGroupGeneral(), + ], ), ), - ); + ), + ), + ); } diff --git a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/delete_errorlog_button.dart b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/delete_errorlog_button.dart index c3886d1cc..87a1b782a 100644 --- a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/delete_errorlog_button.dart +++ b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/delete_errorlog_button.dart @@ -28,9 +28,9 @@ class DeleteErrorlogButton extends StatelessWidget { @override Widget build(BuildContext context) => ErrorlogButton( - onPressed: () => _pressClearErrorLog(context), - text: AppLocalizations.of(context)!.clearErrorLog, - ); + onPressed: () => _pressClearErrorLog(context), + text: AppLocalizations.of(context)!.clearErrorLog, + ); void _pressClearErrorLog(BuildContext context) { Navigator.pop(context); diff --git a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/errorlog_button.dart b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/errorlog_button.dart index 25e83fa21..45e78f82a 100644 --- a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/errorlog_button.dart +++ b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/errorlog_button.dart @@ -22,27 +22,27 @@ import 'package:flutter/material.dart'; class ErrorlogButton extends StatelessWidget { final Function() onPressed; final String text; - const ErrorlogButton({super.key, required this.onPressed, required this.text}); + const ErrorlogButton({ + super.key, + required this.onPressed, + required this.text, + }); @override Widget build(BuildContext context) => ListTile( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Flexible(child: SizedBox()), - Expanded( - flex: 4, - child: ElevatedButton( - onPressed: onPressed, - child: Text( - text, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - ), - const Flexible(child: SizedBox()), - ], + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Flexible(child: SizedBox()), + Expanded( + flex: 4, + child: ElevatedButton( + onPressed: onPressed, + child: Text(text, overflow: TextOverflow.fade, softWrap: false), + ), ), - ); + const Flexible(child: SizedBox()), + ], + ), + ); } diff --git a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/send_errorlog_button.dart b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/send_errorlog_button.dart index 08ac353ab..780a24eac 100644 --- a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/send_errorlog_button.dart +++ b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/send_errorlog_button.dart @@ -29,9 +29,9 @@ class SendErrorLogButton extends StatelessWidget { @override Widget build(BuildContext context) => ErrorlogButton( - onPressed: () => _pressSendErrorLog(context), - text: AppLocalizations.of(context)!.send, - ); + onPressed: () => _pressSendErrorLog(context), + text: AppLocalizations.of(context)!.send, + ); } void _pressSendErrorLog(BuildContext context) { diff --git a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart index d4a2ec16a..d0e3b221f 100644 --- a/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart +++ b/lib/views/settings_view/settings_view_widgets/errorlog_buttons/show_errorlog_button.dart @@ -65,7 +65,6 @@ void _pressShowErrorLog(BuildContext context) { content: SingleChildScrollView( reverse: true, physics: const BouncingScrollPhysics(), - scrollDirection: Axis.vertical, child: FutureBuilder( future: Logger.getErrorLog(), builder: (context, errorLog) { diff --git a/lib/views/settings_view/settings_view_widgets/logging_menu.dart b/lib/views/settings_view/settings_view_widgets/logging_menu.dart index 7f5909b61..95b8bb51c 100644 --- a/lib/views/settings_view/settings_view_widgets/logging_menu.dart +++ b/lib/views/settings_view/settings_view_widgets/logging_menu.dart @@ -41,7 +41,6 @@ class LoggingMenu extends ConsumerWidget { style: Theme.of(context).listTileTheme.titleTextStyle, ), content: Column( - mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ListTile( diff --git a/lib/views/settings_view/settings_view_widgets/settings_group.dart b/lib/views/settings_view/settings_view_widgets/settings_group.dart index aa26f894b..ca643b528 100644 --- a/lib/views/settings_view/settings_view_widgets/settings_group.dart +++ b/lib/views/settings_view/settings_view_widgets/settings_group.dart @@ -39,7 +39,10 @@ class SettingsGroup extends StatelessWidget { this.onPressed, this.trailingIcon, this.trailingWidget, - }) : assert(trailingIcon == null || trailingWidget == null, 'Only one of trailingIcon or trailingWidget can be set.'); + }) : assert( + trailingIcon == null || trailingWidget == null, + 'Only one of trailingIcon or trailingWidget can be set.', + ); @override Widget build(BuildContext context) { @@ -72,7 +75,9 @@ class SettingsGroup extends StatelessWidget { isThreeLine: false, title: Text( title, - style: theme.textTheme.titleMedium?.copyWith(color: isActive ? null : Colors.grey), + style: theme.textTheme.titleMedium?.copyWith( + color: isActive ? null : Colors.grey, + ), overflow: TextOverflow.fade, softWrap: false, ), @@ -83,7 +88,8 @@ class SettingsGroup extends StatelessWidget { : DefaultIconButton( semanticsLabel: title, onPressed: isActive ? onPressed! : null, - icon: trailingIcon ?? Icons.arrow_forward_ios, + icon: + trailingIcon ?? Icons.arrow_forward_ios, ), ), ), @@ -93,7 +99,9 @@ class SettingsGroup extends StatelessWidget { isThreeLine: false, title: Text( title, - style: theme.textTheme.titleMedium?.copyWith(color: isActive ? null : Colors.grey), + style: theme.textTheme.titleMedium?.copyWith( + color: isActive ? null : Colors.grey, + ), overflow: TextOverflow.fade, softWrap: false, ), diff --git a/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart b/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart index fdab32af0..b4534ed8b 100644 --- a/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart +++ b/lib/views/settings_view/settings_view_widgets/settings_list_tile_button.dart @@ -25,34 +25,41 @@ class SettingsListTileButton extends StatelessWidget { final Widget? icon; static const double tileHeight = 40; - const SettingsListTileButton({super.key, required this.title, this.icon, required this.onPressed}); + const SettingsListTileButton({ + super.key, + required this.title, + this.icon, + required this.onPressed, + }); @override Widget build(BuildContext context) => TextButton( - onPressed: onPressed, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: SizedBox( - height: tileHeight, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded(child: title), - if (icon != null) - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minHeight: tileHeight, minWidth: tileHeight), - onPressed: onPressed, - splashRadius: 26, - icon: icon!, - ) - ], - ), - ), + onPressed: onPressed, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: SizedBox( + height: tileHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Expanded(child: title), + if (icon != null) + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minHeight: tileHeight, + minWidth: tileHeight, + ), + onPressed: onPressed, + splashRadius: 26, + icon: icon!, + ), + ], ), ), - ); + ), + ), + ); } diff --git a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart index 628f33c7f..a92a28778 100644 --- a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart +++ b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart @@ -66,7 +66,7 @@ class _UpdateFirebaseTokenDialogState ); } - void _updateFbTokens(AppLocalizations localizations) async { + Future _updateFbTokens(AppLocalizations localizations) async { Logger.info('Starting update of firebase token.'); // TODO What to do with poll only tokens if google-services is used? diff --git a/lib/views/splash_screen/splash_screen.dart b/lib/views/splash_screen/splash_screen.dart index 5a008da47..6024f36b5 100644 --- a/lib/views/splash_screen/splash_screen.dart +++ b/lib/views/splash_screen/splash_screen.dart @@ -61,32 +61,42 @@ class _SplashScreenState extends ConsumerState { if (mounted) setState(() => _appIconIsVisible = true); Future.wait( - [ - Future.delayed(_splashScreenDuration), - ref.read(tokenProvider.future), - InfoUtils.init(), - HomeWidgetUtils().homeWidgetInit(), - ref.read(allowScreenshotProvider.future), - ref.read(tokenFolderProvider.notifier).initState, - ], - eagerError: true, - cleanUp: (_) { - _navigate(); - }, - ).catchError((error) async { - Logger.error('Error while loading the app.', error: error, stackTrace: StackTrace.current); + [ + Future.delayed(_splashScreenDuration), + ref.read(tokenProvider.future), + InfoUtils.init(), + HomeWidgetUtils().homeWidgetInit(), + ref.read(allowScreenshotProvider.future), + ref.read(tokenFolderProvider.notifier).initState, + ], + eagerError: true, + cleanUp: (_) { + _navigate(); + }, + ) + .catchError((error) async { + Logger.error( + 'Error while loading the app.', + error: error, + stackTrace: StackTrace.current, + ); - if (!mounted) return []; - final tokenState = await ref.read(tokenProvider.future); - ref.read(tokenContainerProvider.notifier).syncContainers(tokenState: tokenState, isManually: false); - _navigate(); - return []; - }).then((values) async { - if (!mounted) return; - final tokenState = await ref.read(tokenProvider.future); - ref.read(tokenContainerProvider.notifier).syncContainers(tokenState: tokenState, isManually: false); - return _navigate(); - }); + if (!mounted) return []; + final tokenState = await ref.read(tokenProvider.future); + ref + .read(tokenContainerProvider.notifier) + .syncContainers(tokenState: tokenState, isManually: false); + _navigate(); + return []; + }) + .then((values) async { + if (!mounted) return; + final tokenState = await ref.read(tokenProvider.future); + ref + .read(tokenContainerProvider.notifier) + .syncContainers(tokenState: tokenState, isManually: false); + return _navigate(); + }); }); } @@ -96,7 +106,7 @@ class _SplashScreenState extends ConsumerState { super.dispose(); } - void _navigate() async { + Future _navigate() async { if (_customization.disabledFeatures.isNotEmpty) { Logger.info('Disabled features: ${_customization.disabledFeatures}'); } diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index ab85f8e5a..d29bf0926 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -92,7 +92,6 @@ class _AppWrapperState extends ConsumerState<_AppWrapper> { Widget build(BuildContext context) { return SingleTouchRecognizer( child: StateObserver( - stateNotifierProviderListeners: const [], buildlessProviderListener: [], streamNotifierProviderListeners: [ NavigationDeepLinkListener(provider: deeplinkProvider), diff --git a/lib/widgets/button_widgets/intent_button.dart b/lib/widgets/button_widgets/intent_button.dart index cc3f01b6e..078f3083e 100644 --- a/lib/widgets/button_widgets/intent_button.dart +++ b/lib/widgets/button_widgets/intent_button.dart @@ -100,8 +100,9 @@ class _IntentButtonState extends State if (widget.onPressed == null || _isCooldown || _isLoading || - _currentDelay > 0) + _currentDelay > 0) { return; + } final result = widget.onPressed!.call(); final isFuture = result is Future; diff --git a/lib/widgets/button_widgets/push_action_button.dart b/lib/widgets/button_widgets/push_action_button.dart index aa6114c01..8e5b76277 100644 --- a/lib/widgets/button_widgets/push_action_button.dart +++ b/lib/widgets/button_widgets/push_action_button.dart @@ -25,7 +25,7 @@ import 'package:flutter/material.dart'; import 'intent_button.dart'; /// A specialized button for Push Notification actions with a distinct border and typography. -/// Uses [TimeGuardedButton] to handle asynchronous execution and a minimum threshold to prevent double-taps. +/// Uses [IntentButton] to handle asynchronous execution and a minimum threshold to prevent double-taps. class PushActionButton extends StatelessWidget { final FutureOr Function()? onPressed; final Widget child; diff --git a/lib/widgets/button_widgets/time_guarded_button.dart b/lib/widgets/button_widgets/time_guarded_button.dart index 24ea130a7..55f1a99b2 100644 --- a/lib/widgets/button_widgets/time_guarded_button.dart +++ b/lib/widgets/button_widgets/time_guarded_button.dart @@ -25,7 +25,7 @@ // import '../pi_circular_progress_indicator.dart'; // import 'intent_button.dart'; -// class TimeGuardedButton extends StatefulWidget { +// class IntentButton extends StatefulWidget { // final FutureOr Function()? onPressed; // final Widget child; // final int delaySeconds; @@ -42,10 +42,10 @@ // }); // @override -// State createState() => _TimeGuardedButtonState(); +// State createState() => _IntentButtonState(); // } -// class _TimeGuardedButtonState extends State +// class _IntentButtonState extends State // with SingleTickerProviderStateMixin { // bool _isCooldown = false; // late int _currentDelay; diff --git a/lib/widgets/dialog_widgets/container_dialogs/container_already_exists_dialog.dart b/lib/widgets/dialog_widgets/container_dialogs/container_already_exists_dialog.dart index 7bd505ea1..64e8de3f6 100644 --- a/lib/widgets/dialog_widgets/container_dialogs/container_already_exists_dialog.dart +++ b/lib/widgets/dialog_widgets/container_dialogs/container_already_exists_dialog.dart @@ -86,10 +86,11 @@ class _ContainerAlreadyExistsDialogState void _dismiss(TokenContainer container) { setState(() => unhandledContainers.remove(container)); - if (unhandledContainers.isEmpty) + if (unhandledContainers.isEmpty) { Navigator.of( context, ).pop>(replaceContainers); + } } Future _replace( @@ -102,9 +103,10 @@ class _ContainerAlreadyExistsDialogState ); replaceContainers.add(newContainer); }); - if (unhandledContainers.isEmpty) + if (unhandledContainers.isEmpty) { Navigator.of( context, ).pop>(replaceContainers); + } } } diff --git a/lib/widgets/dialog_widgets/container_dialogs/initial_token_assignment_dialog.dart b/lib/widgets/dialog_widgets/container_dialogs/initial_token_assignment_dialog.dart index 8f98779f7..82b934c01 100644 --- a/lib/widgets/dialog_widgets/container_dialogs/initial_token_assignment_dialog.dart +++ b/lib/widgets/dialog_widgets/container_dialogs/initial_token_assignment_dialog.dart @@ -85,7 +85,6 @@ class _InitialTokenAssignmentDialogState child: Text(localizations.initialTokenAssignmentDialogQuestion), ), SelectTokensWidget( - multiSelect: true, tokens: widget.tokens.toSet(), onSelect: (selected, unselected) => setState(() { _selectedTokens = selected; diff --git a/lib/widgets/dialog_widgets/patch_notes_dialog.dart b/lib/widgets/dialog_widgets/patch_notes_dialog.dart index 254b8ea55..607127e80 100644 --- a/lib/widgets/dialog_widgets/patch_notes_dialog.dart +++ b/lib/widgets/dialog_widgets/patch_notes_dialog.dart @@ -55,7 +55,6 @@ class PatchNotesDialog extends StatelessWidget { (note) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( - mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_choice_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_choice_dialog.dart index 9b86b0650..2e11d4138 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_choice_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_choice_dialog.dart @@ -92,7 +92,6 @@ class PushChoiceDialog extends ConsumerWidget with PushDialogMixin { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: PushActionButton( - intent: DialogActionIntent.confirm, onPressed: () => handleAccept(context, ref, answer: answer), child: Text(answer), diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart index 315550159..328bbd08f 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart @@ -110,7 +110,6 @@ mixin PushDialogMixin { Logger.error('Error accepting push request: $e'); ref.read(statusMessageProvider.notifier).state = StatusMessage( message: (l10n) => "Error accepting push request: $e", - isError: true, ); return; } @@ -164,7 +163,7 @@ mixin PushDialogMixin { } } - void + Future _onHandled({ required BuildContext context, required WidgetRef ref, diff --git a/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_decline_confirm_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_decline_confirm_dialog.dart index 44e9c4149..d71ec0ced 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_decline_confirm_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_decline_confirm_dialog.dart @@ -80,7 +80,6 @@ class PushDeclineConfirmDialog extends StatelessWidget { ), ), PushActionButton( - intent: DialogActionIntent.confirm, onPressed: () async { await onDiscard(); if (context.mounted && Navigator.of(context).canPop()) { diff --git a/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_request_base_info.dart b/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_request_base_info.dart index b14ab1102..c365e6aa0 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_request_base_info.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/widgets/push_request_base_info.dart @@ -30,7 +30,6 @@ class PushRequestBaseInfo extends StatelessWidget { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( pushRequest.question, diff --git a/lib/widgets/dialog_widgets/two_step_dialog.dart b/lib/widgets/dialog_widgets/two_step_dialog.dart index 4f885928e..8c048c0eb 100644 --- a/lib/widgets/dialog_widgets/two_step_dialog.dart +++ b/lib/widgets/dialog_widgets/two_step_dialog.dart @@ -48,7 +48,7 @@ class GenerateTwoStepDialog extends StatelessWidget { _keyLength = keyLength, _password = password; - void _do2Step(BuildContext context) async { + Future _do2Step(BuildContext context) async { // 1. Generate salt. final Uint8List salt = secureRandom().nextBytes(_saltLength); diff --git a/lib/widgets/drag_item_scroller.dart b/lib/widgets/drag_item_scroller.dart index bc32dd9e0..1a32b8d1d 100644 --- a/lib/widgets/drag_item_scroller.dart +++ b/lib/widgets/drag_item_scroller.dart @@ -101,9 +101,10 @@ class _DragItemScrollerState extends State { DragItemScroller.minScrollingSpeed, DragItemScroller.maxScrollingSpeed * speedInPercent, ); - if (moveUp) + if (moveUp) { nextScrollingSpeed = -nextScrollingSpeed; // if moveUp is true, the speed is negative + } if (currentSpeed == nextScrollingSpeed) return; setState(() { currentSpeed = nextScrollingSpeed; // set new speed @@ -208,7 +209,6 @@ class _DragItemScrollerState extends State { Logger.info('scrollSpeedPercent: $scrollSpeedPercent'); _startScrolling( clampDouble(scrollSpeedPercent, 0.0, 1.0), - moveUp: false, ); return; } diff --git a/lib/widgets/enable_text_edit_after_many_taps.dart b/lib/widgets/enable_text_edit_after_many_taps.dart index 90d17d750..f621d8865 100644 --- a/lib/widgets/enable_text_edit_after_many_taps.dart +++ b/lib/widgets/enable_text_edit_after_many_taps.dart @@ -51,7 +51,6 @@ class _EnableTextEditAfterManyTapsState Widget build(BuildContext context) => enabled ? TextFormField( key: Key('${widget.controller.hashCode}_enableTextEditAfterManyTaps'), - style: null, controller: widget.controller, decoration: InputDecoration(labelText: widget.labelText), autovalidateMode: widget.autovalidateMode, diff --git a/lib/widgets/home_widgets/home_widget_copied.dart b/lib/widgets/home_widgets/home_widget_copied.dart index 7098b2403..5a821491f 100644 --- a/lib/widgets/home_widgets/home_widget_copied.dart +++ b/lib/widgets/home_widgets/home_widget_copied.dart @@ -35,8 +35,6 @@ class HomeWidgetCopied extends FlutterHomeWidgetBase { width: logicalSize.width, height: logicalSize.height, child: FittedBox( - fit: BoxFit.contain, - alignment: Alignment.center, child: Text( 'Password copied\nto Clipboard', textAlign: TextAlign.center, diff --git a/lib/widgets/home_widgets/home_widget_otp.dart b/lib/widgets/home_widgets/home_widget_otp.dart index 3991d7411..f69d18c12 100644 --- a/lib/widgets/home_widgets/home_widget_otp.dart +++ b/lib/widgets/home_widgets/home_widget_otp.dart @@ -64,7 +64,6 @@ class HomeWidgetOtp extends FlutterHomeWidgetBase { Expanded( flex: 3, child: FittedBox( - fit: BoxFit.contain, alignment: Alignment.centerLeft, child: Text( text, @@ -74,7 +73,6 @@ class HomeWidgetOtp extends FlutterHomeWidgetBase { ), ), Expanded( - flex: 1, child: FittedBox( fit: BoxFit.fitHeight, alignment: Alignment.topLeft, diff --git a/lib/widgets/home_widgets/home_widget_unlinked.dart b/lib/widgets/home_widgets/home_widget_unlinked.dart index 9295011ee..7be0511e9 100644 --- a/lib/widgets/home_widgets/home_widget_unlinked.dart +++ b/lib/widgets/home_widgets/home_widget_unlinked.dart @@ -35,7 +35,6 @@ class HomeWidgetUnlinked extends FlutterHomeWidgetBase { width: logicalSize.width, height: logicalSize.height, child: FittedBox( - fit: BoxFit.contain, alignment: Alignment.topRight, child: Text( 'Tap to link\nyour token', diff --git a/lib/widgets/padded_row.dart b/lib/widgets/padded_row.dart index 6bae74d4e..6677bfcdb 100644 --- a/lib/widgets/padded_row.dart +++ b/lib/widgets/padded_row.dart @@ -28,24 +28,15 @@ class PaddedRow extends StatelessWidget { /// [ 0.125 | child | 0.125 ] /// /// Assert that [peddingPercent] is higher than 0 and lower than 1. - const PaddedRow({super.key, required this.child, this.peddingPercent = 0.25}) : assert(peddingPercent > 0 && peddingPercent < 1); + const PaddedRow({super.key, required this.child, this.peddingPercent = 0.25}) + : assert(peddingPercent > 0 && peddingPercent < 1); @override Widget build(BuildContext context) => Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: (peddingPercent * 50).toInt(), - child: const SizedBox(), - ), - Expanded( - flex: 100 - (peddingPercent * 100).toInt(), - child: child, - ), - Expanded( - flex: (peddingPercent * 50).toInt(), - child: const SizedBox(), - ), - ], - ); + children: [ + Expanded(flex: (peddingPercent * 50).toInt(), child: const SizedBox()), + Expanded(flex: 100 - (peddingPercent * 100).toInt(), child: child), + Expanded(flex: (peddingPercent * 50).toInt(), child: const SizedBox()), + ], + ); } diff --git a/lib/widgets/pi_slidable.dart b/lib/widgets/pi_slidable.dart index 17fd728e8..2e719ec23 100644 --- a/lib/widgets/pi_slidable.dart +++ b/lib/widgets/pi_slidable.dart @@ -34,13 +34,21 @@ class PiSliable extends ConsumerStatefulWidget { final List stack; final Widget child; - const PiSliable({required this.groupTag, required this.identifier, required this.actions, required this.child, this.stack = const [], super.key}); + const PiSliable({ + required this.groupTag, + required this.identifier, + required this.actions, + required this.child, + this.stack = const [], + super.key, + }); @override ConsumerState createState() => _PiSliableState(); } -class _PiSliableState extends ConsumerState with TickerProviderStateMixin { +class _PiSliableState extends ConsumerState + with TickerProviderStateMixin { late SlidableController controller; @override @@ -69,14 +77,20 @@ class _PiSliableState extends ConsumerState with TickerProviderStateM @override Widget build(BuildContext context) { - final childStack = Stack(children: [widget.child, for (var item in widget.stack) item]); + final childStack = Stack( + children: [widget.child, for (var item in widget.stack) item], + ); return widget.actions.isNotEmpty ? ClipRRect( child: Slidable( controller: controller, key: ValueKey('${widget.groupTag}-${widget.identifier}'), groupTag: widget.groupTag, - endActionPane: ActionPane(motion: const DrawerMotion(), extentRatio: 1, children: widget.actions), + endActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 1, + children: widget.actions, + ), child: childStack, ), ) diff --git a/lib/widgets/pi_text_field.dart b/lib/widgets/pi_text_field.dart index 1951fbb23..5c7cedb80 100644 --- a/lib/widgets/pi_text_field.dart +++ b/lib/widgets/pi_text_field.dart @@ -45,18 +45,15 @@ class PiTextField extends StatelessWidget { @override Widget build(BuildContext context) => TextFormField( - decoration: InputDecoration( - labelText: labelText, - errorMaxLines: 2, - ), - onChanged: onChanged, - controller: controller, - initialValue: initialValue, - keyboardType: keyboardType, - focusNode: focusNode, - autofocus: autofocus, - style: Theme.of(context).textTheme.titleSmall, - autovalidateMode: autovalidateMode, - validator: validator, - ); + decoration: InputDecoration(labelText: labelText, errorMaxLines: 2), + onChanged: onChanged, + controller: controller, + initialValue: initialValue, + keyboardType: keyboardType, + focusNode: focusNode, + autofocus: autofocus, + style: Theme.of(context).textTheme.titleSmall, + autovalidateMode: autovalidateMode, + validator: validator, + ); } diff --git a/lib/widgets/pulse_icon.dart b/lib/widgets/pulse_icon.dart index 8bd6075dc..fdbada205 100644 --- a/lib/widgets/pulse_icon.dart +++ b/lib/widgets/pulse_icon.dart @@ -20,7 +20,8 @@ class PulseIcon extends StatefulWidget { State createState() => _PulseIconState(); } -class _PulseIconState extends State with SingleTickerProviderStateMixin { +class _PulseIconState extends State + with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; @@ -30,15 +31,16 @@ class _PulseIconState extends State with SingleTickerProviderStateMix void initState() { super.initState(); if (widget.isPulsing) { - _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500))..repeat(); - _scaleAnimation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); - _opacityAnimation = Tween(begin: 0.8, end: 0.05).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(); + _scaleAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + _opacityAnimation = Tween(begin: 0.8, end: 0.05).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); } } @@ -50,25 +52,25 @@ class _PulseIconState extends State with SingleTickerProviderStateMix @override Widget build(BuildContext context) => Stack( - children: [ - if (widget.isPulsing) - Center( - child: FadeTransition( - opacity: _opacityAnimation, - child: ScaleTransition( - scale: _scaleAnimation, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: widget.borderRadius, - ), - width: widget.width, - height: widget.height, - ), + children: [ + if (widget.isPulsing) + Center( + child: FadeTransition( + opacity: _opacityAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: widget.borderRadius, ), + width: widget.width, + height: widget.height, ), ), - Center(child: widget.child), - ], - ); + ), + ), + Center(child: widget.child), + ], + ); } diff --git a/lib/widgets/select_tokens_widget.dart b/lib/widgets/select_tokens_widget.dart index 50041c355..a0f9a5dc6 100644 --- a/lib/widgets/select_tokens_widget.dart +++ b/lib/widgets/select_tokens_widget.dart @@ -28,7 +28,12 @@ class SelectTokensWidget extends StatefulWidget { final bool multiSelect; final Set tokens; final void Function(Set selected, Set unselected) onSelect; - const SelectTokensWidget({this.multiSelect = true, required this.onSelect, super.key, required this.tokens}); + const SelectTokensWidget({ + this.multiSelect = true, + required this.onSelect, + super.key, + required this.tokens, + }); @override State createState() => _SelectTokensWidgetState(); @@ -57,7 +62,9 @@ class _SelectTokensWidgetState extends State { } else { _selectedTokens.clear(); _selectedTokens.add(token); - _unselectedTokens = widget.tokens.where((element) => element != token).toSet(); + _unselectedTokens = widget.tokens + .where((element) => element != token) + .toSet(); } }); widget.onSelect(_selectedTokens, _unselectedTokens); @@ -85,10 +92,7 @@ class _SelectTokensWidgetState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: (tokens.isEmpty) - ? Text( - appLocalizations.nothingToSelect, - textAlign: TextAlign.center, - ) + ? Text(appLocalizations.nothingToSelect, textAlign: TextAlign.center) : Column( mainAxisSize: MainAxisSize.min, children: [ @@ -104,7 +108,10 @@ class _SelectTokensWidgetState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Checkbox(value: _selectedTokens.length == tokens.length, onChanged: (_) => _selectAll()), + child: Checkbox( + value: _selectedTokens.length == tokens.length, + onChanged: (_) => _selectAll(), + ), ), ], ), @@ -121,10 +128,18 @@ class _SelectTokensWidgetState extends State { padding: const EdgeInsets.symmetric(vertical: 4), child: TextButton( style: _selectedTokens.contains(token) - ? ButtonStyle(backgroundColor: WidgetStateProperty.all(theme.colorScheme.secondary.withAlpha(80))) + ? ButtonStyle( + backgroundColor: + WidgetStateProperty.all( + theme.colorScheme.secondary + .withAlpha(80), + ), + ) : null, onPressed: () => _select(token), - child: TokenWidgetBuilder.previewFromToken(token), + child: TokenWidgetBuilder.previewFromToken( + token, + ), ), ), ], diff --git a/lib/widgets/status_bar.dart b/lib/widgets/status_bar.dart index 38bee7273..f52fd026c 100644 --- a/lib/widgets/status_bar.dart +++ b/lib/widgets/status_bar.dart @@ -228,7 +228,6 @@ class _StatusBarOverlayEntryState extends State child: SizedBox( width: maxWidth, child: Column( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( widget.statusText, diff --git a/lib/widgets/tooltip_container.dart b/lib/widgets/tooltip_container.dart index a8ac4ac36..b8e727cfe 100644 --- a/lib/widgets/tooltip_container.dart +++ b/lib/widgets/tooltip_container.dart @@ -19,29 +19,25 @@ class TooltipContainer extends StatelessWidget { @override Widget build(BuildContext context) => Container( - padding: padding, - margin: margin, - decoration: BoxDecoration( - color: Theme.of(context).navigationBarTheme.backgroundColor, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Theme.of(context).primaryColor, width: border), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), - blurRadius: 8, - spreadRadius: 1, - ), - ], + padding: padding, + margin: margin, + decoration: BoxDecoration( + color: Theme.of(context).navigationBarTheme.backgroundColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Theme.of(context).primaryColor, width: border), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 1, ), - child: GestureDetector( - onTapDown: (details) { - onComplete?.call(); - }, - child: Text( - tooltip, - style: textStyle, - textAlign: TextAlign.center, - ), - ), - ); + ], + ), + child: GestureDetector( + onTapDown: (details) { + onComplete?.call(); + }, + child: Text(tooltip, style: textStyle, textAlign: TextAlign.center), + ), + ); } diff --git a/test/unit_test/api/privacy_idea_container_api_test.dart b/test/unit_test/api/privacy_idea_container_api_test.dart index 9ad98ce3d..87a0696ba 100644 --- a/test/unit_test/api/privacy_idea_container_api_test.dart +++ b/test/unit_test/api/privacy_idea_container_api_test.dart @@ -49,10 +49,7 @@ void _testPrivacyIdeaContainerApi() { 'jsonrpc': '2.0', 'result': { 'status': false, - 'error': { - 'message': 'Error message', - 'code': 400, - }, + 'error': {'message': 'Error message', 'code': 400}, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -73,7 +70,7 @@ void _testPrivacyIdeaContainerApi() { 'enc_key_algorithm': 'secp384r1', 'nonce': 'b33d3a11c8d1b45f19640035e27944ccf0b2383d', 'time_stamp': '2024-12-06T11:14:26.885409+00:00', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -86,19 +83,22 @@ void _testPrivacyIdeaContainerApi() { // final privateServerKey = // "35d6e41baa53d43545058e4f89d2644f:dc62c1d9da9fa0a6f2c7229cd12fdf0b0ea863be31d0e7be9433ea57f344b8d2f1f2745cef28120809b7dd24efc4f48ae6040bc11fb5112c5effb9eca4d27a0a6d54bacdadd47056a6f9cd160264002636382e670c4da67bef75a5104ee3b874c1e7af5dec39692c7daddb24fb45e48d8d1c4300c846d0578a5282991010b489e4d7a49ce0fd7d71c313e78740253b80110aa4945c7124e35be094fadace0ba8edf777daae1cde4152a092e0f9310d479a97004443b1fa4950bd5869fbd80bf05969563e6efe05dd41f7f5bf7f6e3792218ff63bd485a6e90a13144ba65f2d63d737fdf146b040b9bdebffbb67433d00dd38df487d7ab815a5249d399aeec37636f13eec6ebd6687eef1180a80c27b2a981821b149fd7cafe4f19b35ec7d8bff5a436802cf68255f84a41db91cc5e09ff1ae58cd4405e30e9ac3c5b5d30320f0"; - final publicServerKey = "-----BEGIN PUBLIC KEY-----\n" + final publicServerKey = + "-----BEGIN PUBLIC KEY-----\n" "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd7thB7AwR3xgK7etKmFJfn4SrNhCAMB5\n" "V1ERqwhYj7QlmBe2pp8k07Ti6vZ1hue0Vf7utqIcTDPAK52qGcZ4fs8mpKkEeNIZ\n" "8yUPuPv9weXsVm/h7fBqHYQs82fMnzKz\n" "-----END PUBLIC KEY-----"; - final privateClientKey = "-----BEGIN EC PRIVATE KEY-----\n" + final privateClientKey = + "-----BEGIN EC PRIVATE KEY-----\n" "MIGkAgEBBDCleRofxXJwTtc0HUeE/Af8P4depFM0KY7oT4hMQdt3geK5uDWEOZn4\n" "DaCMTGrsSP2gBwYFK4EEACKhZANiAATxezSrY8++QiUpNxCQzEwOe//i0fd0OqCU\n" "rjZoc3XWhP7AkOfXVwYnlvm667ajB94+A0POVPCErcG/HbHk0Gb8lbO1Q5pYjb3N\n" "3ATXIlK0HJJqETYIgZ8pzVF9wBKnn/g=\n" "-----END EC PRIVATE KEY-----"; - final publicClientKey = "-----BEGIN PUBLIC KEY-----\n" + final publicClientKey = + "-----BEGIN PUBLIC KEY-----\n" "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8Xs0q2PPvkIlKTcQkMxMDnv/4tH3dDqg\n" "lK42aHN11oT+wJDn11cGJ5b5uuu2owfePgNDzlTwhK3Bvx2x5NBm/JWztUOaWI29\n" "zdwE1yJStBySahE2CIGfKc1RfcASp5/4\n" @@ -123,31 +123,31 @@ void _testPrivacyIdeaContainerApi() { Duration? withTtl, bool? addDeviceInfos, FinalizationState? withFinalizationState, - }) => - TokenContainerUnfinalized( - issuer: withIssuer ?? 'privacyIDEA', - nonce: withNonce ?? 'b33d3a11c8d1b45f19640035e27944ccf0b2383d', - timestamp: withTimestamp ?? DateTime(2024, 12, 6, 11, 14, 26, 885, 409), - serverUrl: withServerUrl ?? Uri.parse('http://example.com'), - serial: withSerial ?? 'SMPH00067A2F', - ecKeyAlgorithm: withEcKeyAlgorithm ?? EcKeyAlgorithm.secp384r1, - hashAlgorithm: withHashAlgorithm ?? Algorithms.SHA256, - sslVerify: withSslVerify ?? false, - passphraseQuestion: withPassphraseQuestion, - publicClientKey: publicClientKey, - privateClientKey: privateClientKey, - policies: withPolicies ?? - ContainerPolicies( - rolloverAllowed: true, - initialTokenAssignment: true, - disabledTokenDeletion: false, - disabledUnregister: false, - ), - serverName: withServerName ?? 'privacyIDEA', - ttl: withTtl ?? Duration(minutes: 10), - addDeviceInfos: addDeviceInfos ?? false, - finalizationState: withFinalizationState ?? FinalizationState.notStarted, - ); + }) => TokenContainerUnfinalized( + issuer: withIssuer ?? 'privacyIDEA', + nonce: withNonce ?? 'b33d3a11c8d1b45f19640035e27944ccf0b2383d', + timestamp: withTimestamp ?? DateTime(2024, 12, 6, 11, 14, 26, 885, 409), + serverUrl: withServerUrl ?? Uri.parse('http://example.com'), + serial: withSerial ?? 'SMPH00067A2F', + ecKeyAlgorithm: withEcKeyAlgorithm ?? EcKeyAlgorithm.secp384r1, + hashAlgorithm: withHashAlgorithm ?? Algorithms.SHA256, + sslVerify: withSslVerify ?? false, + passphraseQuestion: withPassphraseQuestion, + publicClientKey: publicClientKey, + privateClientKey: privateClientKey, + policies: + withPolicies ?? + ContainerPolicies( + rolloverAllowed: true, + initialTokenAssignment: true, + disabledTokenDeletion: false, + disabledUnregister: false, + ), + serverName: withServerName ?? 'privacyIDEA', + ttl: withTtl ?? Duration(minutes: 10), + addDeviceInfos: addDeviceInfos ?? false, + finalizationState: withFinalizationState ?? FinalizationState.notStarted, + ); TokenContainerFinalized getFinalizedTokenContainer({ String? withIssuer, @@ -165,40 +165,44 @@ void _testPrivacyIdeaContainerApi() { ContainerPolicies? withPolicies, SyncState? withSyncState, String? withServerName, - }) => - TokenContainerFinalized( - issuer: withIssuer ?? 'privacyIDEA', - nonce: withNonce ?? 'b33d3a11c8d1b45f19640035e27944ccf0b2383d', - timestamp: withTimestamp ?? DateTime(2024, 12, 6, 11, 14, 26, 885, 409), - serverUrl: withServerUrl ?? Uri.parse('http://example.com'), - serial: withSerial ?? 'SMPH00067A2F', - ecKeyAlgorithm: withEcKeyAlgorithm ?? EcKeyAlgorithm.secp384r1, - hashAlgorithm: withHashAlgorithm ?? Algorithms.SHA256, - sslVerify: withSslVerify ?? false, - passphraseQuestion: withPassphraseQuestion, - publicClientKey: publicClientKey, - privateClientKey: privateClientKey, - policies: withPolicies ?? - ContainerPolicies( - rolloverAllowed: true, - initialTokenAssignment: true, - disabledTokenDeletion: false, - disabledUnregister: false, - ), - syncState: withSyncState ?? SyncState.completed, - serverName: withServerName ?? 'privacyIDEA', - ); + }) => TokenContainerFinalized( + issuer: withIssuer ?? 'privacyIDEA', + nonce: withNonce ?? 'b33d3a11c8d1b45f19640035e27944ccf0b2383d', + timestamp: withTimestamp ?? DateTime(2024, 12, 6, 11, 14, 26, 885, 409), + serverUrl: withServerUrl ?? Uri.parse('http://example.com'), + serial: withSerial ?? 'SMPH00067A2F', + ecKeyAlgorithm: withEcKeyAlgorithm ?? EcKeyAlgorithm.secp384r1, + hashAlgorithm: withHashAlgorithm ?? Algorithms.SHA256, + sslVerify: withSslVerify ?? false, + passphraseQuestion: withPassphraseQuestion, + publicClientKey: publicClientKey, + privateClientKey: privateClientKey, + policies: + withPolicies ?? + ContainerPolicies( + rolloverAllowed: true, + initialTokenAssignment: true, + disabledTokenDeletion: false, + disabledUnregister: false, + ), + syncState: withSyncState ?? SyncState.completed, + serverName: withServerName ?? 'privacyIDEA', + ); group('PrivacyIdeaContainerApi', () { test('finalizeContainer', () async { final tokenContainer = getNewTokenContainer(); - final message = '${tokenContainer.nonce}' + final message = + '${tokenContainer.nonce}' '|${tokenContainer.timestamp.toIso8601String().replaceFirst('Z', '+00:00')}' '|${tokenContainer.serial}' '|${tokenContainer.registrationUrl}'; final EccUtils eccUtils = EccUtils(); - final signature = eccUtils.signWithPrivateKey(tokenContainer.ecPrivateClientKey!, message); + final signature = eccUtils.signWithPrivateKey( + tokenContainer.ecPrivateClientKey!, + message, + ); final body = { 'container_serial': tokenContainer.serial, 'public_client_key': tokenContainer.publicClientKey, @@ -208,14 +212,25 @@ void _testPrivacyIdeaContainerApi() { // Arrange final mockIoClient = MockPrivacyideaIOClient(); final containerApi = PiContainerApi(ioClient: mockIoClient); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final invocationUrl = invocation.namedArguments[Symbol('url')]; final invocationBody = invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == tokenContainer.registrationUrl.toString() && + if (invocationUrl.toString() == + tokenContainer.registrationUrl.toString() && invocationBody['container_serial'] == body['container_serial'] && invocationBody['public_client_key'] == body['public_client_key'] && - eccUtils.validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature'], message)) { + eccUtils.validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature'], + message, + )) { final exampleSuccess = { 'id': 5, 'jsonrpc': '2.0', @@ -246,10 +261,7 @@ void _testPrivacyIdeaContainerApi() { 'jsonrpc': '2.0', 'result': { 'status': false, - 'error': { - 'message': 'Error message', - 'code': 400, - }, + 'error': {'message': 'Error message', 'code': 400}, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -277,19 +289,36 @@ void _testPrivacyIdeaContainerApi() { final mockIoClient = MockPrivacyideaIOClient(); final containerApi = PiContainerApi(ioClient: mockIoClient); final tokenContainer = getFinalizedTokenContainer(); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/rollover') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/rollover') { return containerChallengeResponse; } - final signMessage = '$containerChallengeNonce|$containerChallengeTimeStamp|${tokenContainer.serial}|$invocationUrl'; - if (invocationUrl.toString() == 'http://example.com/container/rollover' && - invocationBody['scope'] == 'http://example.com/container/rollover' && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage)) { + final signMessage = + '$containerChallengeNonce|$containerChallengeTimeStamp|${tokenContainer.serial}|$invocationUrl'; + if (invocationUrl.toString() == + 'http://example.com/container/rollover' && + invocationBody['scope'] == + 'http://example.com/container/rollover' && + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage, + )) { return Response( jsonEncode({ 'id': 5, @@ -300,8 +329,8 @@ void _testPrivacyIdeaContainerApi() { 'container_url': { 'description': qrCodeDescription, 'value': qrCodeDataValue, - } - } + }, + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -316,7 +345,9 @@ void _testPrivacyIdeaContainerApi() { return Response(jsonEncode(exampleError), 400); }); // // Act - final responseTransferQrData = await containerApi.getRolloverQrData(tokenContainer); + final responseTransferQrData = await containerApi.getRolloverQrData( + tokenContainer, + ); // // Assert expect(responseTransferQrData.description, qrCodeDescription); expect(responseTransferQrData.value, qrCodeDataValue); @@ -327,27 +358,41 @@ void _testPrivacyIdeaContainerApi() { final containerApi = PiContainerApi(ioClient: mockIoClient); final tokenContainer = getFinalizedTokenContainer(); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && - invocationBody['scope'] == 'http://example.com/container/register/terminate/client') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/register/terminate/client') { return containerChallengeResponse; } - final signMessage = '$containerChallengeNonce|$containerChallengeTimeStamp|${tokenContainer.serial}|$invocationUrl'; - if (invocationUrl.toString() == 'http://example.com/container/register/terminate/client' && - invocationBody['scope'] == 'http://example.com/container/register/terminate/client' && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage)) { + final signMessage = + '$containerChallengeNonce|$containerChallengeTimeStamp|${tokenContainer.serial}|$invocationUrl'; + if (invocationUrl.toString() == + 'http://example.com/container/register/terminate/client' && + invocationBody['scope'] == + 'http://example.com/container/register/terminate/client' && + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage, + )) { return Response( jsonEncode({ 'id': 5, 'jsonrpc': '2.0', 'result': { 'status': true, - 'value': { - 'success': true, - } + 'value': {'success': true}, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -375,30 +420,54 @@ void _testPrivacyIdeaContainerApi() { final tokenState = TokenState( tokens: [ // Token is not in the container - HOTPToken(label: "label1", issuer: "privacyIDEA", counter: 5, id: 'id1', algorithm: Algorithms.SHA1, digits: 6, secret: 'AAAAAAAA'), + HOTPToken( + label: "label1", + issuer: "privacyIDEA", + counter: 5, + id: 'id1', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'AAAAAAAA', + ), ], ); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/synchronize') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/synchronize') { return containerChallengeResponse; } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"tokentype":"HOTP","label":"label1","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["435986","964213"],"counter":"5"}]}'; - final signMessage2 = '$containerChallengeNonce|' + final signMessage2 = + '$containerChallengeNonce|' '$containerChallengeTimeStamp|' '${tokenContainer.serial}|' '$invocationUrl|' '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == 'http://example.com/container/synchronize' && + if (invocationUrl.toString() == + 'http://example.com/container/synchronize' && invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage2)) { + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { return Response( jsonEncode({ 'id': 5, @@ -421,9 +490,10 @@ void _testPrivacyIdeaContainerApi() { 'container_client_rollover': false, 'initially_add_tokens_to_container': false, }, - 'public_server_key': 'AMc1nbpqrEOgQLe1-nR2ExnqE1IM8qMDETYw65IU6wQ=', + 'public_server_key': + 'AMc1nbpqrEOgQLe1-nR2ExnqE1IM8qMDETYw65IU6wQ=', 'server_url': 'http://example.com/container/synchronize', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -440,12 +510,21 @@ void _testPrivacyIdeaContainerApi() { final type = X25519().keyPairType; final publicSimpleKeyPair = SimpleKeyPairData( base64Decode("mJp57a9lmpXXpT9bTUDLz1/Kcngtzmz/yodWlIw7UXo="), - publicKey: SimplePublicKey(base64Decode("sGSaA8sawDbkglDHYRJPwgKoSghTvJz1ejlJe4USrDA="), type: type), + publicKey: SimplePublicKey( + base64Decode("sGSaA8sawDbkglDHYRJPwgKoSghTvJz1ejlJe4USrDA="), + type: type, + ), type: type, ); // Act - final result = await containerApi.sync(tokenContainer, tokenState, withX25519Key: publicSimpleKeyPair, isInitSync: true, sendAllOTPs: true); + final result = await containerApi.sync( + tokenContainer, + tokenState, + withX25519Key: publicSimpleKeyPair, + isInitSync: true, + sendAllOTPs: true, + ); // Asserta expect(result, isNotNull); @@ -494,27 +573,43 @@ void _testPrivacyIdeaContainerApi() { ), ], ); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/synchronize') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/synchronize') { return containerChallengeResponse; } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"OATH00068B93","tokentype":"HOTP","label":"OATH00068B93","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","counter":"1"}]}'; - final signMessage2 = '$containerChallengeNonce|' + final signMessage2 = + '$containerChallengeNonce|' '$containerChallengeTimeStamp|' '${tokenContainer.serial}|' '$invocationUrl|' '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == 'http://example.com/container/synchronize' && + if (invocationUrl.toString() == + 'http://example.com/container/synchronize' && invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage2)) { + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { return Response( jsonEncode({ 'id': 5, @@ -537,9 +632,10 @@ void _testPrivacyIdeaContainerApi() { 'container_client_rollover': false, 'initially_add_tokens_to_container': false, }, - 'public_server_key': '4HUJxDV2j1dguSOUGmupScPqjNJL-sATSvmYujP3STo=', + 'public_server_key': + '4HUJxDV2j1dguSOUGmupScPqjNJL-sATSvmYujP3STo=', 'server_url': 'http://example.com/container/synchronize', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -556,12 +652,21 @@ void _testPrivacyIdeaContainerApi() { final type = X25519().keyPairType; final publicSimpleKeyPair = SimpleKeyPairData( base64Decode("2NEmGL31xJBYWSQ72+oqeMvwn+liM3Nwq6qG4NzRd04="), - publicKey: SimplePublicKey(base64Decode("+4KfIuxEB8z6CDlXjbFpirmJ3tNuTIzhCV6L21lxkw8="), type: type), + publicKey: SimplePublicKey( + base64Decode("+4KfIuxEB8z6CDlXjbFpirmJ3tNuTIzhCV6L21lxkw8="), + type: type, + ), type: type, ); // Act - final result = await containerApi.sync(tokenContainer, tokenState, withX25519Key: publicSimpleKeyPair, isInitSync: true, sendAllOTPs: true); + final result = await containerApi.sync( + tokenContainer, + tokenState, + withX25519Key: publicSimpleKeyPair, + isInitSync: true, + sendAllOTPs: true, + ); // Assert expect(result, isNotNull); @@ -621,27 +726,43 @@ void _testPrivacyIdeaContainerApi() { ), ], ); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/synchronize') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/synchronize') { return containerChallengeResponse; } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; - final signMessage2 = '$containerChallengeNonce|' + final signMessage2 = + '$containerChallengeNonce|' '$containerChallengeTimeStamp|' '${tokenContainer.serial}|' '$invocationUrl|' '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == 'http://example.com/container/synchronize' && + if (invocationUrl.toString() == + 'http://example.com/container/synchronize' && invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage2)) { + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { return Response( jsonEncode({ 'id': 5, @@ -664,9 +785,10 @@ void _testPrivacyIdeaContainerApi() { 'container_client_rollover': false, 'initially_add_tokens_to_container': false, }, - 'public_server_key': 'aK_oH0ycoKrXoIMbTlQ7_adxUe7JVAuPCbcoOUBKYBY=', + 'public_server_key': + 'aK_oH0ycoKrXoIMbTlQ7_adxUe7JVAuPCbcoOUBKYBY=', 'server_url': 'http://example.com/container/synchronize', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -683,12 +805,21 @@ void _testPrivacyIdeaContainerApi() { final type = X25519().keyPairType; final publicSimpleKeyPair = SimpleKeyPairData( base64Decode("uCyfofJSNWX08K8omYeR43nwoPUE++niUrxDB43noVc="), - publicKey: SimplePublicKey(base64Decode("4/d5K2gycwPxeIVHuHQvlq6tb7BDQ7HkQ/g8JBBmVHw="), type: type), + publicKey: SimplePublicKey( + base64Decode("4/d5K2gycwPxeIVHuHQvlq6tb7BDQ7HkQ/g8JBBmVHw="), + type: type, + ), type: type, ); // Act - final result = await containerApi.sync(tokenContainer, tokenState, withX25519Key: publicSimpleKeyPair, isInitSync: true, sendAllOTPs: true); + final result = await containerApi.sync( + tokenContainer, + tokenState, + withX25519Key: publicSimpleKeyPair, + isInitSync: true, + sendAllOTPs: true, + ); // Asserta expect(result, isNotNull); final newPolicies = result!.newPolicies; @@ -746,27 +877,43 @@ void _testPrivacyIdeaContainerApi() { ), ], ); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/synchronize') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/synchronize') { return containerChallengeResponse; } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"serial":"OATH00166051","tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","counter":"1"}]}'; - final signMessage2 = '$containerChallengeNonce|' + final signMessage2 = + '$containerChallengeNonce|' '$containerChallengeTimeStamp|' '${tokenContainer.serial}|' '$invocationUrl|' '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == 'http://example.com/container/synchronize' && + if (invocationUrl.toString() == + 'http://example.com/container/synchronize' && invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage2)) { + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { return Response( jsonEncode({ 'id': 5, @@ -789,9 +936,10 @@ void _testPrivacyIdeaContainerApi() { 'container_client_rollover': false, 'initially_add_tokens_to_container': false, }, - 'public_server_key': 'Od5nNdvC3iVYTK5aA5e-c1-f3FhSe4MH4apaNDRkSQA=', + 'public_server_key': + 'Od5nNdvC3iVYTK5aA5e-c1-f3FhSe4MH4apaNDRkSQA=', 'server_url': 'http://example.com/container/synchronize', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -808,12 +956,21 @@ void _testPrivacyIdeaContainerApi() { final type = X25519().keyPairType; final publicSimpleKeyPair = SimpleKeyPairData( base64Decode("YIgUiisLPu5dq3KQUMksNVEq12NG2mIM32E13UkQwWQ="), - publicKey: SimplePublicKey(base64Decode("ScZtrNZ3Zay12x+eQDyz4a2wafvZqk7BVzBNTchXc2w="), type: type), + publicKey: SimplePublicKey( + base64Decode("ScZtrNZ3Zay12x+eQDyz4a2wafvZqk7BVzBNTchXc2w="), + type: type, + ), type: type, ); // Act - final result = await containerApi.sync(tokenContainer, tokenState, withX25519Key: publicSimpleKeyPair, isInitSync: true, sendAllOTPs: true); + final result = await containerApi.sync( + tokenContainer, + tokenState, + withX25519Key: publicSimpleKeyPair, + isInitSync: true, + sendAllOTPs: true, + ); // Asserta expect(result, isNotNull); final newPolicies = result!.newPolicies; @@ -864,11 +1021,21 @@ void _testPrivacyIdeaContainerApi() { ), ], ); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { final Uri invocationUrl = invocation.namedArguments[Symbol('url')]; - final Map invocationBody = invocation.namedArguments[Symbol('body')]; + final Map invocationBody = + invocation.namedArguments[Symbol('body')]; Logger.info('Body: $invocationBody'); - if (invocationUrl.toString() == 'http://example.com/container/challenge' && invocationBody['scope'] == 'http://example.com/container/synchronize') { + if (invocationUrl.toString() == + 'http://example.com/container/challenge' && + invocationBody['scope'] == + 'http://example.com/container/synchronize') { return containerChallengeResponse; } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; @@ -876,16 +1043,22 @@ void _testPrivacyIdeaContainerApi() { final containerDictClient = '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; - final signMessage2 = '$containerChallengeNonce|' + final signMessage2 = + '$containerChallengeNonce|' '$containerChallengeTimeStamp|' '${tokenContainer.serial}|' '$invocationUrl|' '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == 'http://example.com/container/synchronize' && + if (invocationUrl.toString() == + 'http://example.com/container/synchronize' && invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature(tokenContainer.ecPublicClientKey!, invocationBody['signature']!, signMessage2)) { + EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { return Response( jsonEncode({ 'id': 5, @@ -908,9 +1081,10 @@ void _testPrivacyIdeaContainerApi() { 'container_client_rollover': false, 'initially_add_tokens_to_container': false, }, - 'public_server_key': 'Od5nNdvC3iVYTK5aA5e-c1-f3FhSe4MH4apaNDRkSQA=', + 'public_server_key': + 'Od5nNdvC3iVYTK5aA5e-c1-f3FhSe4MH4apaNDRkSQA=', 'server_url': 'http://example.com/container/synchronize', - } + }, }, 'time': 1.0, 'version': 'privacyIDEA 3.6.2', @@ -927,12 +1101,21 @@ void _testPrivacyIdeaContainerApi() { final type = X25519().keyPairType; final publicSimpleKeyPair = SimpleKeyPairData( base64Decode("YIgUiisLPu5dq3KQUMksNVEq12NG2mIM32E13UkQwWQ="), - publicKey: SimplePublicKey(base64Decode("ScZtrNZ3Zay12x+eQDyz4a2wafvZqk7BVzBNTchXc2w="), type: type), + publicKey: SimplePublicKey( + base64Decode("ScZtrNZ3Zay12x+eQDyz4a2wafvZqk7BVzBNTchXc2w="), + type: type, + ), type: type, ); // Act - final result = await containerApi.sync(tokenContainer, tokenState, withX25519Key: publicSimpleKeyPair, isInitSync: true, sendAllOTPs: true); + final result = await containerApi.sync( + tokenContainer, + tokenState, + withX25519Key: publicSimpleKeyPair, + isInitSync: true, + sendAllOTPs: true, + ); // Asserta expect(result, isNotNull); final newPolicies = result!.newPolicies; @@ -970,13 +1153,22 @@ void _testPrivacyIdeaContainerApi() { ), ); // Act & Assert - expect(() => containerApi.getRolloverQrData(tokenContainer), throwsA(isA())); + expect( + () => containerApi.getRolloverQrData(tokenContainer), + throwsA(isA()), + ); }); test('unregister', () { // Arrange final mockIoClient = MockPrivacyideaIOClient(); - when(mockIoClient.doPost(url: anyNamed('url'), body: anyNamed('body'), sslVerify: anyNamed('sslVerify'))).thenAnswer((invocation) async { + when( + mockIoClient.doPost( + url: anyNamed('url'), + body: anyNamed('body'), + sslVerify: anyNamed('sslVerify'), + ), + ).thenAnswer((invocation) async { return Response( '{"id": 1, "jsonrpc": "2.0", "result": {"status": true, "value": {"enc_key_algorithm": "x25519", "nonce": "d77ff7bf0174815aeea29f68aef4ae6cec6616c2", "time_stamp": "2025-02-11T08:56:45.696499+00:00"}}, "time": 1739264205.7145326, "version": "privacyIDEA 3.11.dev2", "versionnumber": "3.11.dev2", "signature": "rsa_sha256_pss:03a857d6e1941488c368286d1f55c6896c018729d17fb68e0fc5b7c1d956ba54cc657c785b9d284ad6fc34ec17370c7fdd0a0f6255a0fd630dfb97e6659b7af6fc9370cb2a7d0b0d055904145fdf21af40d15b15727bacac59bc79a4941df75d24efbb0b74e6e40561984ac73ca8392382100623bc51cb9e043915535a96fe9ac2b417cbd1e55977a04fdd992ae3758db66a9dcf265f956c9e37faeea3fd5614fd8c88030364a9ef4021cb79128a3bdeb0694bdf45e9cedf4507ee5e5715b9b1f68454b67c5642416c4b226302a50b887233c364acbf1cbc07bf7b3bdda884ca052c15f65b0724ef4bfafe411311ffe85683946e5f0c899377d4d95c66db4147"}', 200, @@ -993,7 +1185,10 @@ void _testPrivacyIdeaContainerApi() { ), ); // Act & Assert - expect(() => containerApi.unregister(tokenContainer), throwsA(isA())); + expect( + () => containerApi.unregister(tokenContainer), + throwsA(isA()), + ); }); }); } diff --git a/test/unit_test/model/extensions/enum_extension_test.dart b/test/unit_test/model/extensions/enum_extension_test.dart index 322a496e9..b9139e456 100644 --- a/test/unit_test/model/extensions/enum_extension_test.dart +++ b/test/unit_test/model/extensions/enum_extension_test.dart @@ -5,11 +5,7 @@ void main() { _testEnumExtension(); } -enum _TestEnum { - entryOne, - entryTwo, - entryThree, -} +enum _TestEnum { entryOne, entryTwo, entryThree } void _testEnumExtension() { group('Enum Extension', () { @@ -25,14 +21,38 @@ void _testEnumExtension() { expect(_TestEnum.entryThree.isName('entrythree'), false); }); test('caseInsensitive', () { - expect(_TestEnum.entryOne.isName('entryone', caseSensitive: false), true); - expect(_TestEnum.entryOne.isName('ENTRYONE', caseSensitive: false), true); - expect(_TestEnum.entryOne.isName('entryTwo', caseSensitive: false), false); - expect(_TestEnum.entryTwo.isName('entrytwo', caseSensitive: false), true); - expect(_TestEnum.entryTwo.isName('ENTRYTWO', caseSensitive: false), true); - expect(_TestEnum.entryTwo.isName('entryThree', caseSensitive: false), false); - expect(_TestEnum.entryThree.isName('entrythree', caseSensitive: false), true); - expect(_TestEnum.entryThree.isName('ENTRYTHREE', caseSensitive: false), true); + expect( + _TestEnum.entryOne.isName('entryone', caseSensitive: false), + true, + ); + expect( + _TestEnum.entryOne.isName('ENTRYONE', caseSensitive: false), + true, + ); + expect( + _TestEnum.entryOne.isName('entryTwo', caseSensitive: false), + false, + ); + expect( + _TestEnum.entryTwo.isName('entrytwo', caseSensitive: false), + true, + ); + expect( + _TestEnum.entryTwo.isName('ENTRYTWO', caseSensitive: false), + true, + ); + expect( + _TestEnum.entryTwo.isName('entryThree', caseSensitive: false), + false, + ); + expect( + _TestEnum.entryThree.isName('entrythree', caseSensitive: false), + true, + ); + expect( + _TestEnum.entryThree.isName('ENTRYTHREE', caseSensitive: false), + true, + ); }); }); }); diff --git a/test/unit_test/model/extensions/enums/algorithms_extension_test.dart b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart index a315586c0..e98c28da7 100644 --- a/test/unit_test/model/extensions/enums/algorithms_extension_test.dart +++ b/test/unit_test/model/extensions/enums/algorithms_extension_test.dart @@ -115,7 +115,11 @@ void _testAlgorithmsExtension() { final oneYear60SecCounter = 31536000 ~/ 60; final halfYear30SecCounter = 15768000 ~/ 30; expect(oneYear60SecCounter, equals(halfYear30SecCounter)); - final hotpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: oneYear60SecCounter, length: 6); + final hotpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: oneYear60SecCounter, + length: 6, + ); final otpValueOneYear60Sec = Algorithms.SHA1.generateTOTPCodeString( secret: 'secret', @@ -135,13 +139,23 @@ void _testAlgorithmsExtension() { }); group('is not google', () { test('OTP for zero seconds from epoch', () { - final otpValue = - Algorithms.SHA1.generateTOTPCodeString(secret: 'secret', length: 6, interval: Duration(seconds: 30), time: zeroTimestamp, isGoogle: false); + final otpValue = Algorithms.SHA1.generateTOTPCodeString( + secret: 'secret', + length: 6, + interval: Duration(seconds: 30), + time: zeroTimestamp, + isGoogle: false, + ); expect(otpValue, equals('862089')); }); test('OTP for one year from epoch', () { - final otpValue = - Algorithms.SHA1.generateTOTPCodeString(secret: 'secret', length: 6, interval: Duration(seconds: 30), time: oneYearFromEpoch, isGoogle: false); + final otpValue = Algorithms.SHA1.generateTOTPCodeString( + secret: 'secret', + length: 6, + interval: Duration(seconds: 30), + time: oneYearFromEpoch, + isGoogle: false, + ); expect(otpValue, equals('265498')); }); }); @@ -150,91 +164,162 @@ void _testAlgorithmsExtension() { group('generateHOTPCodeString', () { group('different couters 6 digits', () { test('OTP for counter == 0', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + ); expect(otpValue, equals('328482')); }); test('OTP for counter == 1', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 6); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 1, + length: 6, + ); expect(otpValue, equals('812658')); }); test('OTP for counter == 2', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 6); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 2, + length: 6, + ); expect(otpValue, equals('073348')); }); test('OTP for counter == 8', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 6); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 8, + length: 6, + ); expect(otpValue, equals('985814')); }); }); group('different couters 8 digits', () { test('OTP for counter == 0', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 8, + ); expect(otpValue, equals('35328482')); }); test('OTP for counter == 1', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 1, length: 8); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 1, + length: 8, + ); expect(otpValue, equals('30812658')); }); test('OTP for counter == 2', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 2, length: 8); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 2, + length: 8, + ); expect(otpValue, equals('41073348')); }); test('OTP for counter == 8', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 8, length: 8); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 8, + length: 8, + ); expect(otpValue, equals('12985814')); }); }); group('different algorithms 6 digits', () { test('OTP for sha1', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + ); expect(otpValue, equals('328482')); }); test('OTP for sha256', () { - final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + final otpValue = Algorithms.SHA256.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + ); expect(otpValue, equals('356306')); }); test('OTP for sha512', () { - final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6); + final otpValue = Algorithms.SHA512.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + ); expect(otpValue, equals('674061')); }); }); group('different algorithms 8 digits', () { test('OTP for sha1', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 8, + ); expect(otpValue, equals('35328482')); }); test('OTP for sha256', () { - final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + final otpValue = Algorithms.SHA256.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 8, + ); expect(otpValue, equals('03356306')); }); test('OTP for sha512', () { - final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 8); + final otpValue = Algorithms.SHA512.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 8, + ); expect(otpValue, equals('66674061')); }); }); group('is not google', () { test('OTP for sha1', () { - final otpValue = Algorithms.SHA1.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + final otpValue = Algorithms.SHA1.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + isGoogle: false, + ); expect(otpValue, equals('814628')); }); test('OTP for sha256', () { - final otpValue = Algorithms.SHA256.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + final otpValue = Algorithms.SHA256.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + isGoogle: false, + ); expect(otpValue, equals('059019')); }); test('OTP for sha512', () { - final otpValue = Algorithms.SHA512.generateHOTPCodeString(secret: 'secret', counter: 0, length: 6, isGoogle: false); + final otpValue = Algorithms.SHA512.generateHOTPCodeString( + secret: 'secret', + counter: 0, + length: 6, + isGoogle: false, + ); expect(otpValue, equals('377469')); }); }); diff --git a/test/unit_test/model/extensions/enums/encodings_extension_test.dart b/test/unit_test/model/extensions/enums/encodings_extension_test.dart index c5c53aa43..ea8fe86a0 100644 --- a/test/unit_test/model/extensions/enums/encodings_extension_test.dart +++ b/test/unit_test/model/extensions/enums/encodings_extension_test.dart @@ -12,55 +12,156 @@ void _testEncodingsExtension() { group('Encodings Extension', () { group('encode', () { group('valid', () { - test('base32', () => expect(Encodings.base32.encode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); - test('hex', () => expect(Encodings.hex.encode(Uint8List.fromList([153, 37, 57])), equals('992539'))); - test('none', () => expect(Encodings.none.encode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + test( + 'base32', + () => expect( + Encodings.base32.encode(Uint8List.fromList([153, 37, 57])), + equals('TESTS==='), + ), + ); + test( + 'hex', + () => expect( + Encodings.hex.encode(Uint8List.fromList([153, 37, 57])), + equals('992539'), + ), + ); + test( + 'none', + () => expect( + Encodings.none.encode( + Uint8List.fromList([116, 101, 115, 116, 115]), + ), + equals('tests'), + ), + ); }); group('invalid', () { - test('none', () => expect(() => Encodings.none.encode(Uint8List.fromList([153, 37, 57])), throwsException)); + test( + 'none', + () => expect( + () => Encodings.none.encode(Uint8List.fromList([153, 37, 57])), + throwsException, + ), + ); }); }); group('encodeStringTo', () { - test('base32 to hex', () => expect(Encodings.base32.encodeStringTo(Encodings.hex, 'TESTS==='), equals('992539'))); - test('hex to base32', () => expect(Encodings.hex.encodeStringTo(Encodings.base32, '992539'), equals('TESTS==='))); + test( + 'base32 to hex', + () => expect( + Encodings.base32.encodeStringTo(Encodings.hex, 'TESTS==='), + equals('992539'), + ), + ); + test( + 'hex to base32', + () => expect( + Encodings.hex.encodeStringTo(Encodings.base32, '992539'), + equals('TESTS==='), + ), + ); }); group('decode', () { group('valid', () { - test('base32', () => expect(Encodings.base32.decode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); - test('hex', () => expect(Encodings.hex.decode('992539'), equals(Uint8List.fromList([153, 37, 57])))); - test('none', () => expect(Encodings.none.decode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + test( + 'base32', + () => expect( + Encodings.base32.decode('TESTS==='), + equals(Uint8List.fromList([153, 37, 57])), + ), + ); + test( + 'hex', + () => expect( + Encodings.hex.decode('992539'), + equals(Uint8List.fromList([153, 37, 57])), + ), + ); + test( + 'none', + () => expect( + Encodings.none.decode('tests'), + equals(Uint8List.fromList([116, 101, 115, 116, 115])), + ), + ); }); group('invalid', () { - test('base32', () => expect(() => Encodings.base32.decode('TESTS+++'), throwsException)); - test('hex', () => expect(() => Encodings.hex.decode('abcdefg'), throwsException)); + test( + 'base32', + () => expect( + () => Encodings.base32.decode('TESTS+++'), + throwsException, + ), + ); + test( + 'hex', + () => expect(() => Encodings.hex.decode('abcdefg'), throwsException), + ); // Every utf8 string has a valid binary representation }); }); group('isValidEncoding', () { - test('base32', () => expect(Encodings.base32.isValidEncoding('TESTS==='), isTrue)); - test('hex', () => expect(Encodings.hex.isValidEncoding('992539'), isTrue)); - test('none', () => expect(Encodings.none.isValidEncoding('tests'), isTrue)); + test( + 'base32', + () => expect(Encodings.base32.isValidEncoding('TESTS==='), isTrue), + ); + test( + 'hex', + () => expect(Encodings.hex.isValidEncoding('992539'), isTrue), + ); + test( + 'none', + () => expect(Encodings.none.isValidEncoding('tests'), isTrue), + ); }); group('isInvalidEncoding', () { - test('base32', () => expect(Encodings.base32.isInvalidEncoding('TESTS==='), isFalse)); - test('hex', () => expect(Encodings.hex.isInvalidEncoding('992539'), isFalse)); + test( + 'base32', + () => expect(Encodings.base32.isInvalidEncoding('TESTS==='), isFalse), + ); + test( + 'hex', + () => expect(Encodings.hex.isInvalidEncoding('992539'), isFalse), + ); // Every utf8 string has a valid binary representation }); group('tryDecode', () { group('valid', () { - test('base32', () => expect(Encodings.base32.tryDecode('TESTS==='), equals(Uint8List.fromList([153, 37, 57])))); - test('hex', () => expect(Encodings.hex.tryDecode('992539'), equals(Uint8List.fromList([153, 37, 57])))); - test('none', () => expect(Encodings.none.tryDecode('tests'), equals(Uint8List.fromList([116, 101, 115, 116, 115])))); + test( + 'base32', + () => expect( + Encodings.base32.tryDecode('TESTS==='), + equals(Uint8List.fromList([153, 37, 57])), + ), + ); + test( + 'hex', + () => expect( + Encodings.hex.tryDecode('992539'), + equals(Uint8List.fromList([153, 37, 57])), + ), + ); + test( + 'none', + () => expect( + Encodings.none.tryDecode('tests'), + equals(Uint8List.fromList([116, 101, 115, 116, 115])), + ), + ); }); group('invalid', () { - test('base32', () => expect(Encodings.base32.tryDecode('TESTS+++'), isNull)); + test( + 'base32', + () => expect(Encodings.base32.tryDecode('TESTS+++'), isNull), + ); test('hex', () => expect(Encodings.hex.tryDecode('abcdefg'), isNull)); // Every utf8 string has a valid binary representation }); @@ -68,13 +169,39 @@ void _testEncodingsExtension() { group('tryEncode', () { group('valid', () { - test('base32', () => expect(Encodings.base32.tryEncode(Uint8List.fromList([153, 37, 57])), equals('TESTS==='))); - test('hex', () => expect(Encodings.hex.tryEncode(Uint8List.fromList([153, 37, 57])), equals('992539'))); - test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([116, 101, 115, 116, 115])), equals('tests'))); + test( + 'base32', + () => expect( + Encodings.base32.tryEncode(Uint8List.fromList([153, 37, 57])), + equals('TESTS==='), + ), + ); + test( + 'hex', + () => expect( + Encodings.hex.tryEncode(Uint8List.fromList([153, 37, 57])), + equals('992539'), + ), + ); + test( + 'none', + () => expect( + Encodings.none.tryEncode( + Uint8List.fromList([116, 101, 115, 116, 115]), + ), + equals('tests'), + ), + ); }); group('invalid', () { // Every binary data can be encoded to base32 and hex - test('none', () => expect(Encodings.none.tryEncode(Uint8List.fromList([153, 37, 57])), isNull)); + test( + 'none', + () => expect( + Encodings.none.tryEncode(Uint8List.fromList([153, 37, 57])), + isNull, + ), + ); }); }); }); diff --git a/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart index dd2fa493f..a89182048 100644 --- a/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart +++ b/test/unit_test/model/extensions/enums/push_token_rollout_state_extension_test.dart @@ -10,23 +10,59 @@ void _testPushTokenRolloutstateExtension() { group('Push-Token Rolloutstate Extension', () { test('rollOutInProgress', () { expect(PushTokenRollOutState.rolloutNotStarted.rollOutInProgress, false); - expect(PushTokenRollOutState.generatingRSAKeyPair.rollOutInProgress, true); - expect(PushTokenRollOutState.generatingRSAKeyPairFailed.rollOutInProgress, false); + expect( + PushTokenRollOutState.generatingRSAKeyPair.rollOutInProgress, + true, + ); + expect( + PushTokenRollOutState.generatingRSAKeyPairFailed.rollOutInProgress, + false, + ); expect(PushTokenRollOutState.sendRSAPublicKey.rollOutInProgress, true); - expect(PushTokenRollOutState.sendRSAPublicKeyFailed.rollOutInProgress, false); + expect( + PushTokenRollOutState.sendRSAPublicKeyFailed.rollOutInProgress, + false, + ); expect(PushTokenRollOutState.parsingResponse.rollOutInProgress, true); - expect(PushTokenRollOutState.parsingResponseFailed.rollOutInProgress, false); + expect( + PushTokenRollOutState.parsingResponseFailed.rollOutInProgress, + false, + ); expect(PushTokenRollOutState.rolloutComplete.rollOutInProgress, false); }); test('getFailed', () { - expect(PushTokenRollOutState.rolloutNotStarted.getFailed(), PushTokenRollOutState.rolloutNotStarted); - expect(PushTokenRollOutState.generatingRSAKeyPair.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); - expect(PushTokenRollOutState.generatingRSAKeyPairFailed.getFailed(), PushTokenRollOutState.generatingRSAKeyPairFailed); - expect(PushTokenRollOutState.sendRSAPublicKey.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); - expect(PushTokenRollOutState.sendRSAPublicKeyFailed.getFailed(), PushTokenRollOutState.sendRSAPublicKeyFailed); - expect(PushTokenRollOutState.parsingResponse.getFailed(), PushTokenRollOutState.parsingResponseFailed); - expect(PushTokenRollOutState.parsingResponseFailed.getFailed(), PushTokenRollOutState.parsingResponseFailed); - expect(PushTokenRollOutState.rolloutComplete.getFailed(), PushTokenRollOutState.rolloutComplete); + expect( + PushTokenRollOutState.rolloutNotStarted.getFailed(), + PushTokenRollOutState.rolloutNotStarted, + ); + expect( + PushTokenRollOutState.generatingRSAKeyPair.getFailed(), + PushTokenRollOutState.generatingRSAKeyPairFailed, + ); + expect( + PushTokenRollOutState.generatingRSAKeyPairFailed.getFailed(), + PushTokenRollOutState.generatingRSAKeyPairFailed, + ); + expect( + PushTokenRollOutState.sendRSAPublicKey.getFailed(), + PushTokenRollOutState.sendRSAPublicKeyFailed, + ); + expect( + PushTokenRollOutState.sendRSAPublicKeyFailed.getFailed(), + PushTokenRollOutState.sendRSAPublicKeyFailed, + ); + expect( + PushTokenRollOutState.parsingResponse.getFailed(), + PushTokenRollOutState.parsingResponseFailed, + ); + expect( + PushTokenRollOutState.parsingResponseFailed.getFailed(), + PushTokenRollOutState.parsingResponseFailed, + ); + expect( + PushTokenRollOutState.rolloutComplete.getFailed(), + PushTokenRollOutState.rolloutComplete, + ); }); }); } diff --git a/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart index 1c69e894b..63ded90f3 100644 --- a/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart +++ b/test/unit_test/model/extensions/enums/token_origin_source_type_extension_test.dart @@ -19,21 +19,30 @@ void _testTokenOriginSourceTypeExtension() { isPrivacyIdeaToken: true, createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), ); - final TokenOriginData tokenOriginData = TokenOriginSourceType.qrScan.toTokenOrigin( - data: 'data', - originName: 'appName', - isPrivacyIdeaToken: true, - createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), - ); + final TokenOriginData tokenOriginData = TokenOriginSourceType.qrScan + .toTokenOrigin( + data: 'data', + originName: 'appName', + isPrivacyIdeaToken: true, + createdAt: DateTime.fromMicrosecondsSinceEpoch(1622160000000), + ); expect(tokenOriginData.source, tokenOriginDataMatch.source); expect(tokenOriginData.data, tokenOriginDataMatch.data); expect(tokenOriginData.appName, tokenOriginDataMatch.appName); - expect(tokenOriginData.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect( + tokenOriginData.isPrivacyIdeaToken, + tokenOriginDataMatch.isPrivacyIdeaToken, + ); expect(tokenOriginData.createdAt, tokenOriginDataMatch.createdAt); expect(tokenOriginData, tokenOriginDataMatch); }); test('addOriginToToken', () { - final token = HOTPToken(id: 'id', algorithm: Algorithms.SHA512, digits: 6, secret: 'secret'); + final token = HOTPToken( + id: 'id', + algorithm: Algorithms.SHA512, + digits: 6, + secret: 'secret', + ); final TokenOriginData tokenOriginDataMatch = TokenOriginData( source: TokenOriginSourceType.qrScan, data: 'data', @@ -53,7 +62,10 @@ void _testTokenOriginSourceTypeExtension() { expect(tokenWithOrigin.origin!.source, tokenOriginDataMatch.source); expect(tokenWithOrigin.origin!.data, tokenOriginDataMatch.data); expect(tokenWithOrigin.origin!.appName, tokenOriginDataMatch.appName); - expect(tokenWithOrigin.origin!.isPrivacyIdeaToken, tokenOriginDataMatch.isPrivacyIdeaToken); + expect( + tokenWithOrigin.origin!.isPrivacyIdeaToken, + tokenOriginDataMatch.isPrivacyIdeaToken, + ); expect(tokenWithOrigin.origin!.createdAt, tokenOriginDataMatch.createdAt); expect(tokenWithOrigin, tokenMatch); }); diff --git a/test/unit_test/model/extensions/int_extension_test.dart b/test/unit_test/model/extensions/int_extension_test.dart index 1cea777c8..0b87cf08b 100644 --- a/test/unit_test/model/extensions/int_extension_test.dart +++ b/test/unit_test/model/extensions/int_extension_test.dart @@ -8,30 +8,180 @@ void main() { void _testIntExtension() { group('int extension', () { group('bytes', () { - test('min int value', () => expect((-0x8000000000000000).bytes, [128, 0, 0, 0, 0, 0, 0, 0])); + test( + 'min int value', + () => expect((-0x8000000000000000).bytes, [128, 0, 0, 0, 0, 0, 0, 0]), + ); test('zero', () => expect(0.bytes, [0, 0, 0, 0, 0, 0, 0, 0])); - test('max int value', () => expect(0x7FFFFFFFFFFFFFFF.bytes, [127, 255, 255, 255, 255, 255, 255, 255])); + test( + 'max int value', + () => expect(0x7FFFFFFFFFFFFFFF.bytes, [ + 127, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + ]), + ); test('20 different int values', () { - expect(8254763140651989312.bytes, [114, 142, 207, 87, 63, 140, 185, 64]); + expect(8254763140651989312.bytes, [ + 114, + 142, + 207, + 87, + 63, + 140, + 185, + 64, + ]); expect(6867929122700968103.bytes, [95, 79, 201, 38, 53, 38, 192, 167]); expect(6658070822668124012.bytes, [92, 102, 56, 35, 34, 134, 191, 108]); - expect(6233195836444142436.bytes, [86, 128, 194, 150, 158, 178, 107, 100]); - expect(5665252064165200114.bytes, [78, 159, 4, 188, 143, 157, 128, 242]); + expect(6233195836444142436.bytes, [ + 86, + 128, + 194, + 150, + 158, + 178, + 107, + 100, + ]); + expect(5665252064165200114.bytes, [ + 78, + 159, + 4, + 188, + 143, + 157, + 128, + 242, + ]); expect(3836812696023088046.bytes, [53, 63, 24, 209, 152, 42, 59, 174]); - expect(4217815205603023728.bytes, [58, 136, 176, 149, 34, 53, 155, 112]); + expect(4217815205603023728.bytes, [ + 58, + 136, + 176, + 149, + 34, + 53, + 155, + 112, + ]); expect(1859730558376856620.bytes, [25, 207, 23, 22, 237, 253, 204, 44]); - expect((-1341891893474570224).bytes, [237, 96, 164, 110, 186, 120, 208, 16]); - expect((-1947790576164582988).bytes, [228, 248, 14, 202, 114, 219, 73, 180]); - expect((-5204853149876150168).bytes, [183, 196, 165, 162, 253, 143, 32, 104]); - expect((-5765512478999408848).bytes, [175, 252, 200, 242, 133, 40, 143, 48]); - expect((-6273311771369426144).bytes, [168, 240, 184, 46, 110, 63, 179, 32]); - expect((-6384342681465787276).bytes, [167, 102, 66, 40, 42, 221, 236, 116]); - expect((-6805707171905842232).bytes, [161, 141, 69, 98, 165, 57, 83, 200]); - expect((-7708924412950309696).bytes, [149, 4, 102, 23, 13, 194, 148, 192]); - expect((-7731444997339132318).bytes, [148, 180, 99, 188, 229, 33, 78, 98]); - expect((-7855692611255695686).bytes, [146, 250, 249, 48, 249, 113, 134, 186]); + expect((-1341891893474570224).bytes, [ + 237, + 96, + 164, + 110, + 186, + 120, + 208, + 16, + ]); + expect((-1947790576164582988).bytes, [ + 228, + 248, + 14, + 202, + 114, + 219, + 73, + 180, + ]); + expect((-5204853149876150168).bytes, [ + 183, + 196, + 165, + 162, + 253, + 143, + 32, + 104, + ]); + expect((-5765512478999408848).bytes, [ + 175, + 252, + 200, + 242, + 133, + 40, + 143, + 48, + ]); + expect((-6273311771369426144).bytes, [ + 168, + 240, + 184, + 46, + 110, + 63, + 179, + 32, + ]); + expect((-6384342681465787276).bytes, [ + 167, + 102, + 66, + 40, + 42, + 221, + 236, + 116, + ]); + expect((-6805707171905842232).bytes, [ + 161, + 141, + 69, + 98, + 165, + 57, + 83, + 200, + ]); + expect((-7708924412950309696).bytes, [ + 149, + 4, + 102, + 23, + 13, + 194, + 148, + 192, + ]); + expect((-7731444997339132318).bytes, [ + 148, + 180, + 99, + 188, + 229, + 33, + 78, + 98, + ]); + expect((-7855692611255695686).bytes, [ + 146, + 250, + 249, + 48, + 249, + 113, + 134, + 186, + ]); expect((-8557951589827587072).bytes, [137, 60, 12, 94, 251, 86, 84, 0]); - expect((-9153687162235632943).bytes, [128, 247, 146, 6, 53, 228, 66, 209]); + expect((-9153687162235632943).bytes, [ + 128, + 247, + 146, + 6, + 53, + 228, + 66, + 209, + ]); }); }); test('digits', () { diff --git a/test/unit_test/model/extensions/sortable_list_test.dart b/test/unit_test/model/extensions/sortable_list_test.dart index 74767527c..38bd9ba04 100644 --- a/test/unit_test/model/extensions/sortable_list_test.dart +++ b/test/unit_test/model/extensions/sortable_list_test.dart @@ -57,7 +57,7 @@ void _testSortableList() { _SortableTestClass(sortIndex: 1, name: '1'), _SortableTestClass(sortIndex: 12, name: '12'), _SortableTestClass(sortIndex: 5, name: '5'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 2, name: '2'), _SortableTestClass(sortIndex: 4, name: '4'), _SortableTestClass(sortIndex: 8, name: '8'), @@ -73,16 +73,16 @@ void _testSortableList() { _SortableTestClass(sortIndex: 5, name: '5'), _SortableTestClass(sortIndex: 8, name: '8'), _SortableTestClass(sortIndex: 12, name: '12'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), ]); }); test('1-5 and multible nulls', () { final list = [ - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 3, name: '3'), _SortableTestClass(sortIndex: 1, name: '1'), _SortableTestClass(sortIndex: 5, name: '5'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 2, name: '2'), _SortableTestClass(sortIndex: 4, name: '4'), ]; @@ -95,8 +95,8 @@ void _testSortableList() { _SortableTestClass(sortIndex: 3, name: '3'), _SortableTestClass(sortIndex: 4, name: '4'), _SortableTestClass(sortIndex: 5, name: '5'), - _SortableTestClass(sortIndex: null, name: 'null'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), + _SortableTestClass(name: 'null'), ]); }); }); @@ -106,7 +106,7 @@ void _testSortableList() { final result = [ _SortableTestClass(sortIndex: 3, name: '3'), _SortableTestClass(sortIndex: 1, name: '1'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 2, name: '2'), _SortableTestClass(sortIndex: 4, name: '4'), ].fillNullIndices(); @@ -124,7 +124,7 @@ void _testSortableList() { _SortableTestClass(sortIndex: 1, name: '1'), _SortableTestClass(sortIndex: 12, name: '12'), _SortableTestClass(sortIndex: 5, name: '5'), - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 2, name: '2'), _SortableTestClass(sortIndex: 4, name: '4'), _SortableTestClass(sortIndex: 8, name: '8'), @@ -176,7 +176,7 @@ void _testSortableList() { _SortableTestClass(sortIndex: 1, name: '1'), movedItem, moveBefore, - _SortableTestClass(sortIndex: null, name: 'null'), + _SortableTestClass(name: 'null'), _SortableTestClass(sortIndex: 2, name: '2'), _SortableTestClass(sortIndex: 4, name: '4'), _SortableTestClass(sortIndex: 8, name: '8'), diff --git a/test/unit_test/model/mixins/sortable_mixin_test.dart b/test/unit_test/model/mixins/sortable_mixin_test.dart index 711c961ab..c7dbdbd93 100644 --- a/test/unit_test/model/mixins/sortable_mixin_test.dart +++ b/test/unit_test/model/mixins/sortable_mixin_test.dart @@ -41,8 +41,8 @@ void _testSortableMixin() { }); test('null', () { // Arrange - final a = _SortableTestClass(sortIndex: null, name: 'null'); - final b = _SortableTestClass(sortIndex: null, name: 'null'); + final a = _SortableTestClass(name: 'null'); + final b = _SortableTestClass(name: 'null'); // Act final result = a.compareTo(b); // Assert @@ -62,7 +62,7 @@ void _testSortableMixin() { test('a = 1, b = null', () { // Arrange final a = _SortableTestClass(sortIndex: 1, name: '1'); - final b = _SortableTestClass(sortIndex: null, name: 'null'); + final b = _SortableTestClass(name: 'null'); // Act final result = a.compareTo(b); // Assert @@ -81,7 +81,7 @@ void _testSortableMixin() { }); test('a = null, b = 1', () { // Arrange - final a = _SortableTestClass(sortIndex: null, name: 'null'); + final a = _SortableTestClass(name: 'null'); final b = _SortableTestClass(sortIndex: 1, name: '1'); // Act final result = a.compareTo(b); diff --git a/test/unit_test/model/processor_result_test.dart b/test/unit_test/model/processor_result_test.dart index 08b4dd663..eeb001d83 100644 --- a/test/unit_test/model/processor_result_test.dart +++ b/test/unit_test/model/processor_result_test.dart @@ -27,7 +27,9 @@ void _testProcessorResult() { expect((result as ProcessorResultSuccess).resultData, 'data'); }); test('error', () { - final ProcessorResult result = ProcessorResult.failed((_) => 'error'); + final ProcessorResult result = ProcessorResult.failed( + (_) => 'error', + ); expect(result, isA()); expect((result as ProcessorResultFailed).message(l), 'error'); }); @@ -40,7 +42,9 @@ void _testProcessorResult() { expect(result.isFailed, isFalse); }); test('error', () { - final ProcessorResult result = ProcessorResultFailed((_) => 'error'); + final ProcessorResult result = ProcessorResultFailed( + (_) => 'error', + ); expect(result.isSuccess, isFalse); expect(result.isFailed, isTrue); }); @@ -53,7 +57,9 @@ void _testProcessorResult() { expect(result.asFailed, isNull); }); test('error', () { - final ProcessorResult result = ProcessorResultFailed((_) => 'error'); + final ProcessorResult result = ProcessorResultFailed( + (_) => 'error', + ); expect(result.asFailed?.message(l), 'error'); expect(result.asSuccess, isNull); }); diff --git a/test/unit_test/model/push_code_to_phone_request_test.dart b/test/unit_test/model/push_code_to_phone_request_test.dart index 02a602b81..f28f486f9 100644 --- a/test/unit_test/model/push_code_to_phone_request_test.dart +++ b/test/unit_test/model/push_code_to_phone_request_test.dart @@ -23,7 +23,7 @@ import 'package:privacyidea_authenticator/model/push_request/push_request.dart'; void main() { group('PushCodeToPhoneRequest Tests', () { - final testDate = DateTime(2025, 1, 1); + final testDate = DateTime(2025); test('Constructor and displayCode', () { final request = PushCodeToPhoneRequest( diff --git a/test/unit_test/model/push_default_request_test.dart b/test/unit_test/model/push_default_request_test.dart index a36b40d9e..5a12c6d74 100644 --- a/test/unit_test/model/push_default_request_test.dart +++ b/test/unit_test/model/push_default_request_test.dart @@ -23,7 +23,7 @@ import 'package:privacyidea_authenticator/model/push_request/push_request.dart'; void main() { group('PushMessageRequest Tests', () { - final testDate = DateTime(2025, 1, 1); + final testDate = DateTime(2025); test('Constructor and basic fields', () { final request = PushDefaultRequest( diff --git a/test/unit_test/model/states/introduction_state_test.dart b/test/unit_test/model/states/introduction_state_test.dart index 83a99eb91..7c0c83c2e 100644 --- a/test/unit_test/model/states/introduction_state_test.dart +++ b/test/unit_test/model/states/introduction_state_test.dart @@ -5,21 +5,45 @@ import 'package:privacyidea_authenticator/model/riverpod_states/introduction_sta void main() { group('IntroductionState', () { test('withCompletedIntroduction add introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); + const introductionState = IntroductionState( + completedIntroductions: {Introduction.addFolder}, + ); + final updatedState = introductionState.withCompletedIntroduction( + Introduction.addManually, + ); + expect(updatedState.completedIntroductions, { + Introduction.addFolder, + Introduction.addManually, + }); }); test('withoutCompletedIntroduction remove introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); - final updatedState = introductionState.withoutCompletedIntroduction(Introduction.addManually); + const introductionState = IntroductionState( + completedIntroductions: { + Introduction.addFolder, + Introduction.addManually, + }, + ); + final updatedState = introductionState.withoutCompletedIntroduction( + Introduction.addManually, + ); expect(updatedState.completedIntroductions, {Introduction.addFolder}); }); test('withoutCompletedIntroduction add duplicate introduction', () { - const introductionState = IntroductionState(completedIntroductions: {Introduction.addFolder, Introduction.addManually}); - final updatedState = introductionState.withCompletedIntroduction(Introduction.addManually); - expect(updatedState.completedIntroductions, {Introduction.addFolder, Introduction.addManually}); + const introductionState = IntroductionState( + completedIntroductions: { + Introduction.addFolder, + Introduction.addManually, + }, + ); + final updatedState = introductionState.withCompletedIntroduction( + Introduction.addManually, + ); + expect(updatedState.completedIntroductions, { + Introduction.addFolder, + Introduction.addManually, + }); }); }); } diff --git a/test/unit_test/model/states/settings_state_test.dart b/test/unit_test/model/states/settings_state_test.dart index fb07c1059..21859472c 100644 --- a/test/unit_test/model/states/settings_state_test.dart +++ b/test/unit_test/model/states/settings_state_test.dart @@ -24,7 +24,10 @@ void _testSettingsState() { expect(state.hideOpts, true); expect(state.enablePolling, true); expect(state.crashReportRecipients, {'test'}); - expect(state.localePreference.toLanguageTag(), const Locale('en').toLanguageTag()); + expect( + state.localePreference.toLanguageTag(), + const Locale('en').toLanguageTag(), + ); expect(state.useSystemLocale, true); expect(state.verboseLogging, true); }); @@ -44,7 +47,10 @@ void _testSettingsState() { expect(state.hideOpts, true); expect(state.enablePolling, true); expect(state.crashReportRecipients, {'test'}); - expect(state.localePreference.toLanguageTag(), const Locale('en').toLanguageTag()); + expect( + state.localePreference.toLanguageTag(), + const Locale('en').toLanguageTag(), + ); expect(state.useSystemLocale, true); expect(state.verboseLogging, true); expect(newState.isFirstRun, false); @@ -52,7 +58,10 @@ void _testSettingsState() { expect(newState.hideOpts, false); expect(newState.enablePolling, false); expect(newState.crashReportRecipients, {'test2'}); - expect(newState.localePreference.toLanguageTag(), const Locale('de').toLanguageTag()); + expect( + newState.localePreference.toLanguageTag(), + const Locale('de').toLanguageTag(), + ); expect(newState.useSystemLocale, false); expect(newState.verboseLogging, false); }); diff --git a/test/unit_test/model/states/token_folder_state_test.dart b/test/unit_test/model/states/token_folder_state_test.dart index eaad7f452..4fa998304 100644 --- a/test/unit_test/model/states/token_folder_state_test.dart +++ b/test/unit_test/model/states/token_folder_state_test.dart @@ -8,7 +8,9 @@ void main() { void _testTokenFolderState() { group('TokenFolderState', () { - const state = TokenFolderState(folders: [TokenFolder(label: 'label', folderId: 1)]); + const state = TokenFolderState( + folders: [TokenFolder(label: 'label', folderId: 1)], + ); test('constructor', () { expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); @@ -24,7 +26,9 @@ void _testTokenFolderState() { expect(newState.folders.last.folderId, 2); }); test('withUpdated', () { - final newState = state.addOrReplaceFolders([const TokenFolder(label: 'labelUpdated', folderId: 1)]); + final newState = state.addOrReplaceFolders([ + const TokenFolder(label: 'labelUpdated', folderId: 1), + ]); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 1); @@ -32,7 +36,9 @@ void _testTokenFolderState() { expect(newState.folders.first.folderId, 1); }); test('withoutFolder', () { - final newState = state.removeFolder(const TokenFolder(label: 'label', folderId: 1)); + final newState = state.removeFolder( + const TokenFolder(label: 'label', folderId: 1), + ); expect(state.folders.first.label, 'label'); expect(state.folders.first.folderId, 1); expect(newState.folders.length, 0); diff --git a/test/unit_test/model/states/token_state_test.dart b/test/unit_test/model/states/token_state_test.dart index f88139f9b..9f70d045d 100644 --- a/test/unit_test/model/states/token_state_test.dart +++ b/test/unit_test/model/states/token_state_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:privacyidea_authenticator/model/riverpod_states/token_state.dart'; import 'package:privacyidea_authenticator/model/tokens/token.dart'; -import 'package:mockito/mockito.dart'; // ignore: must_be_immutable class _TokenMock extends Mock implements Token { diff --git a/test/unit_test/model/token_folder_test.dart b/test/unit_test/model/token_folder_test.dart index a9a7a672f..5a281f3df 100644 --- a/test/unit_test/model/token_folder_test.dart +++ b/test/unit_test/model/token_folder_test.dart @@ -8,13 +8,23 @@ void main() { void _testTokenFolder() { group('TokenFolder', () { test('constructor', () { - const folder1 = TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: 0); + const folder1 = TokenFolder( + label: 'test', + folderId: 1, + isExpanded: true, + sortIndex: 0, + ); expect(folder1.label, 'test'); expect(folder1.folderId, 1); expect(folder1.isExpanded, true); expect(folder1.isLocked, false); expect(folder1.sortIndex, 0); - const folder2 = TokenFolder(label: 'test2', folderId: 2, isExpanded: false, isLocked: true, sortIndex: 1); + const folder2 = TokenFolder( + label: 'test2', + folderId: 2, + isLocked: true, + sortIndex: 1, + ); expect(folder2.label, 'test2'); expect(folder2.folderId, 2); expect(folder2.isExpanded, false); @@ -22,8 +32,19 @@ void _testTokenFolder() { expect(folder2.sortIndex, 1); }); test('copyWith', () { - const folder = TokenFolder(label: 'test', folderId: 1, isExpanded: true, isLocked: false, sortIndex: 0); - final folderCopy = folder.copyWith(label: 'test2', folderId: 2, isExpanded: false, isLocked: true, sortIndex: 1); + const folder = TokenFolder( + label: 'test', + folderId: 1, + isExpanded: true, + sortIndex: 0, + ); + final folderCopy = folder.copyWith( + label: 'test2', + folderId: 2, + isExpanded: false, + isLocked: true, + sortIndex: 1, + ); expect(folderCopy.label, 'test2'); expect(folderCopy.folderId, 2); expect(folderCopy.isExpanded, false); diff --git a/test/unit_test/model/tokens/day_password_test.dart b/test/unit_test/model/tokens/day_password_test.dart index 0d426cf06..a83fec186 100644 --- a/test/unit_test/model/tokens/day_password_test.dart +++ b/test/unit_test/model/tokens/day_password_test.dart @@ -187,7 +187,7 @@ void main() { }); test('equality check (==) includes viewMode and period', () { - final t1 = createTestToken(viewMode: DayPasswordTokenViewMode.VALIDFOR); + final t1 = createTestToken(); final t2 = createTestToken( viewMode: DayPasswordTokenViewMode.VALIDUNTIL, ); diff --git a/test/unit_test/model/tokens/hotp_token_test.dart b/test/unit_test/model/tokens/hotp_token_test.dart index 4b0c1e931..bab59fbd6 100644 --- a/test/unit_test/model/tokens/hotp_token_test.dart +++ b/test/unit_test/model/tokens/hotp_token_test.dart @@ -207,7 +207,6 @@ void _testHotpToken() { final t1 = HOTPToken( id: '1', secret: 's1', - counter: 0, serial: 'SER1', algorithm: Algorithms.SHA1, digits: 6, @@ -229,20 +228,15 @@ void _testHotpToken() { test('OTP for counter == 0', () { HOTPToken token0 = HOTPToken( id: '', - label: '', - issuer: '', algorithm: Algorithms.SHA1, digits: 6, secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 0, ); expect(token0.otpValue, '814628'); }); test('OTP for counter == 1', () { HOTPToken token1 = HOTPToken( id: '', - label: '', - issuer: '', algorithm: Algorithms.SHA1, digits: 6, secret: Encodings.base32.encode(utf8.encode('secret')), @@ -256,12 +250,9 @@ void _testHotpToken() { test('OTP for counter == 0', () { HOTPToken token0 = HOTPToken( id: '', - label: '', - issuer: '', algorithm: Algorithms.SHA1, digits: 8, secret: Encodings.base32.encode(utf8.encode('secret')), - counter: 0, ); expect(token0.otpValue, '31814628'); }); @@ -271,24 +262,18 @@ void _testHotpToken() { test('OTP for sha256', () { HOTPToken token1 = HOTPToken( id: '', - label: '', - issuer: '', algorithm: Algorithms.SHA256, digits: 6, secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, ); expect(token1.otpValue, '203782'); }); test('OTP for sha512', () { HOTPToken token2 = HOTPToken( id: '', - label: '', - issuer: '', algorithm: Algorithms.SHA512, digits: 6, secret: Encodings.base32.encode(utf8.encode('Secret')), - counter: 0, ); expect(token2.otpValue, '636350'); }); diff --git a/test/unit_test/model/tokens/push_token_test.dart b/test/unit_test/model/tokens/push_token_test.dart index ddb1647f9..1e7444b68 100644 --- a/test/unit_test/model/tokens/push_token_test.dart +++ b/test/unit_test/model/tokens/push_token_test.dart @@ -208,7 +208,7 @@ void main() { }); test('Equality (==) includes viewMode and period', () { - final t1 = createDay(viewMode: DayPasswordTokenViewMode.VALIDFOR); + final t1 = createDay(); final t2 = createDay(viewMode: DayPasswordTokenViewMode.VALIDUNTIL); final t3 = createDay(period: const Duration(hours: 1)); diff --git a/test/unit_test/model/tokens/token_test.dart b/test/unit_test/model/tokens/token_test.dart index ac1969a26..83158fc7f 100644 --- a/test/unit_test/model/tokens/token_test.dart +++ b/test/unit_test/model/tokens/token_test.dart @@ -139,13 +139,7 @@ void main() { test( 'isLocked returns false only if PIN, Biometrics, and _isLocked are all false', () { - const t = FakeToken( - id: '1', - type: 'T', - pin: false, - forceBiometricOption: ForceBiometricOption.none, - isLocked: false, - ); + const t = FakeToken(id: '1', type: 'T', pin: false, isLocked: false); expect(t.isLocked, isFalse); }, ); @@ -271,7 +265,7 @@ void main() { test('toTemplate captures correct state only when serial is present', () { const tWith = FakeToken(id: '1', type: 'T', serial: 'SN-123'); - const tWithout = FakeToken(id: '2', type: 'T', serial: null); + const tWithout = FakeToken(id: '2', type: 'T'); expect(tWith.toTemplate(), isNotNull); expect(tWith.toTemplate()?.serial, 'SN-123'); diff --git a/test/unit_test/processors/scheme_processors/token_container_scheme_processor_test.dart b/test/unit_test/processors/scheme_processors/token_container_scheme_processor_test.dart index 2e4c609b8..7b0993c5c 100644 --- a/test/unit_test/processors/scheme_processors/token_container_scheme_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_container_scheme_processor_test.dart @@ -34,7 +34,8 @@ void _testTokenContainerProcessor() { group('TokenContainerProcessor', () { group('processUri', () { test('valid uri', () async { - final uriString = "pia://container/SMPH00067A2F" + final uriString = + "pia://container/SMPH00067A2F" "?issuer=privacyIDEA" "&ttl=10" "&nonce=b33d3a11c8d1b45f19640035e27944ccf0b2383d" @@ -53,9 +54,15 @@ void _testTokenContainerProcessor() { final container = result[0].asSuccess!.resultData; expect(container, isA()); expect(container.issuer, "privacyIDEA"); - expect((container as TokenContainerUnfinalized).ttl, Duration(minutes: 10)); + expect( + (container as TokenContainerUnfinalized).ttl, + Duration(minutes: 10), + ); expect(container.nonce, "b33d3a11c8d1b45f19640035e27944ccf0b2383d"); - expect(container.timestamp, DateTime.parse("2024-12-06T11:14:26.885409+00:00")); + expect( + container.timestamp, + DateTime.parse("2024-12-06T11:14:26.885409+00:00"), + ); expect(container.serverUrl, Uri.parse("http://192.168.0.230:5000/")); expect(container.serial, "SMPH00067A2F"); expect(container.ecKeyAlgorithm, EcKeyAlgorithm.secp384r1); @@ -64,7 +71,8 @@ void _testTokenContainerProcessor() { expect(container.passphraseQuestion, ""); }); test('other values', () async { - final uriString = "pia://container/SMPH00067A2F2" + final uriString = + "pia://container/SMPH00067A2F2" "?issuer=privacyIDEA2" "&ttl=100" "&nonce=b33d3a11c8d1b45f19640035e27944ccf0b2383d22" @@ -83,9 +91,15 @@ void _testTokenContainerProcessor() { final container = result[0].asSuccess!.resultData; expect(container, isA()); expect(container.issuer, "privacyIDEA2"); - expect((container as TokenContainerUnfinalized).ttl, Duration(minutes: 100)); + expect( + (container as TokenContainerUnfinalized).ttl, + Duration(minutes: 100), + ); expect(container.nonce, "b33d3a11c8d1b45f19640035e27944ccf0b2383d22"); - expect(container.timestamp, DateTime.parse("2024-12-07T11:14:26.885409+00:00")); + expect( + container.timestamp, + DateTime.parse("2024-12-07T11:14:26.885409+00:00"), + ); expect(container.serverUrl, Uri.parse("http://192.168.0.231:5000/")); expect(container.serial, "SMPH00067A2F2"); expect(container.ecKeyAlgorithm, EcKeyAlgorithm.secp112r1); @@ -94,7 +108,8 @@ void _testTokenContainerProcessor() { expect(container.passphraseQuestion, "Enter your password"); }); test('missing nonce', () async { - final uriString = "pia://container/SMPH00067A2F2" + final uriString = + "pia://container/SMPH00067A2F2" "?issuer=privacyIDEA2" "&ttl=100" "&time=2024-12-07T11%3A14%3A26.885409%2B00%3A00" diff --git a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart index 29eb52fdb..2bef9f8e1 100644 --- a/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart +++ b/test/unit_test/processors/scheme_processors/token_import_scheme_processors/google_authenticator_qr_processor_test.dart @@ -29,7 +29,8 @@ void _testGooleAuthenticatorQrProcessor() { expect(token0.origin, isNotNull); final tokenOriginData0Matcher = TokenOriginData( source: TokenOriginSourceType.qrScanImport, - data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + data: + 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', appName: TokenImportOrigins.googleAuthenticator.appName, isPrivacyIdeaToken: false, createdAt: token0.origin!.createdAt, @@ -43,7 +44,8 @@ void _testGooleAuthenticatorQrProcessor() { expect(token1.origin, isNotNull); final tokenOriginData1Matcher = TokenOriginData( source: TokenOriginSourceType.qrScanImport, - data: 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', + data: + 'ChkKCpklNznImSU3OcgSBVRlc3QxIAEoATACChsKCpklNznamSU3OdoSBVRlc3QyIAEoATABOAAQARgBIAAo8enF1vr/////AQ==', appName: TokenImportOrigins.googleAuthenticator.appName, isPrivacyIdeaToken: false, createdAt: token1.origin!.createdAt, diff --git a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart index 2825eff32..53578c1e1 100644 --- a/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart +++ b/test/unit_test/processors/token_import_file_processor/aegis_import_file_processor_test.dart @@ -32,10 +32,17 @@ void _testAegisImportFileProcessor() { '32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 99, 111, 110, 34, 58, 32, 110, 117, 108, 108, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 105, 110, 102, 111, 34, 58, 32, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 115, 101, 99, 114, 101, 116, 34, 58, 32, 34, 65, 65, 65, 65, 65, 65, 65, 65, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32,' '32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 97, 108, 103, 111, 34, 58, 32, 34, 83, 72, 65, 49, 34, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 100, 105, 103, 105, 116, 115, 34, 58, 32, 54, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 99, 111, 117, 110, 116, 101, 114, 34, 58, 32, 48, 10, 32, 32, 32, 32, 32, 32, 32,' '32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 10, 32, 32, 32, 32, 32, 32, 32, 32, 93, 44, 10, 32, 32, 32, 32, 32, 32, 32, 32, 34, 103, 114, 111, 117, 112, 115, 34, 58, 32, 91, 93, 10, 32, 32, 32, 32, 125, 10, 125]'; - final byteData = ByteData.view(Uint8List.fromList((jsonDecode(byteDataString) as List).cast()).buffer); + final byteData = ByteData.view( + Uint8List.fromList( + (jsonDecode(byteDataString) as List).cast(), + ).buffer, + ); const aegisImportFileProcessor = AegisImportFileProcessor(); - final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_plain.json'); + final XFile file = XFile.fromData( + byteData.buffer.asUint8List(), + name: 'aegis_plain.json', + ); // Act final isValid = await aegisImportFileProcessor.fileIsValid(file); final results = await aegisImportFileProcessor.processFile(file); @@ -54,7 +61,12 @@ void _testAegisImportFileProcessor() { expect(totpToken.period, equals(30)); expect(totpToken.secret, equals('AAAAAAAA')); expect(totpToken.issuer, equals('Testing')); - expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713352639317)), equals('220975')); + expect( + totpToken.otpFromTime( + DateTime.fromMillisecondsSinceEpoch(1713352639317), + ), + equals('220975'), + ); final result1 = results[1]; expect(result1, isA()); final token1 = result1.asSuccess!.resultData; @@ -96,14 +108,25 @@ void _testAegisImportFileProcessor() { '115, 82, 102, 85, 76, 120, 112, 72, 116, 77, 70, 54, 86, 109, 117, 120, 52, 87, 51, 115, 43, 77, 88, 102, 67, 72, 66, 66, 71, 81, 112, 90, 105, 117, 48, 51, 98, 121, 86, 116, 101, 86, 111, 104, 68, 78, 97, 108, 52, 77, 70, 81, 117, 72, 52, 72, 66, 52, 106, 55, 71, 118, 99, 98, 113, 120, 56, 122, 56, 120, 73, 90, 113, 90, 104, 75, 73, 50, 111, 82, 97, 86, 81, 106, 53, 69, 48, 68, 56, 71, 85, 110, 74, 84, 49, 81, 100, 86, 65, 69,' '43, 77, 86, 75, 65, 78, 74, 68, 109, 104, 84, 56, 86, 111, 34, 10, 125]'; - final byteData = ByteData.view(Uint8List.fromList((jsonDecode(encryptedBytesString) as List).cast()).buffer); + final byteData = ByteData.view( + Uint8List.fromList( + (jsonDecode(encryptedBytesString) as List).cast(), + ).buffer, + ); const aegisImportFileProcessor = AegisImportFileProcessor(); - final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_encrypted.json'); + final XFile file = XFile.fromData( + byteData.buffer.asUint8List(), + name: 'aegis_encrypted.json', + ); // Act final isValid = await aegisImportFileProcessor.fileIsValid(file); - final fileNeedsPassword = await aegisImportFileProcessor.fileNeedsPassword(file); - final results = await aegisImportFileProcessor.processFile(file, password: 'test123'); + final fileNeedsPassword = await aegisImportFileProcessor + .fileNeedsPassword(file); + final results = await aegisImportFileProcessor.processFile( + file, + password: 'test123', + ); // Assert expect(isValid, isTrue); expect(fileNeedsPassword, isTrue); @@ -120,7 +143,12 @@ void _testAegisImportFileProcessor() { expect(totpToken.period, equals(30)); expect(totpToken.secret, equals('AAAAAAAA')); expect(totpToken.issuer, equals('Testing')); - expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713352639317)), equals('220975')); + expect( + totpToken.otpFromTime( + DateTime.fromMillisecondsSinceEpoch(1713352639317), + ), + equals('220975'), + ); final result1 = results[1]; expect(result1, isA()); final token1 = result1.asSuccess!.resultData; @@ -162,12 +190,25 @@ void _testAegisImportFileProcessor() { '115, 82, 102, 85, 76, 120, 112, 72, 116, 77, 70, 54, 86, 109, 117, 120, 52, 87, 51, 115, 43, 77, 88, 102, 67, 72, 66, 66, 71, 81, 112, 90, 105, 117, 48, 51, 98, 121, 86, 116, 101, 86, 111, 104, 68, 78, 97, 108, 52, 77, 70, 81, 117, 72, 52, 72, 66, 52, 106, 55, 71, 118, 99, 98, 113, 120, 56, 122, 56, 120, 73, 90, 113, 90, 104, 75, 73, 50, 111, 82, 97, 86, 81, 106, 53, 69, 48, 68, 56, 71, 85, 110, 74, 84, 49, 81, 100, 86, 65, 69,' '43, 77, 86, 75, 65, 78, 74, 68, 109, 104, 84, 56, 86, 111, 34, 10, 125]'; - final byteData = ByteData.view(Uint8List.fromList((jsonDecode(encryptedBytesString) as List).cast()).buffer); + final byteData = ByteData.view( + Uint8List.fromList( + (jsonDecode(encryptedBytesString) as List).cast(), + ).buffer, + ); const aegisImportFileProcessor = AegisImportFileProcessor(); - final XFile file = XFile.fromData(byteData.buffer.asUint8List(), name: 'aegis_encrypted.json'); + final XFile file = XFile.fromData( + byteData.buffer.asUint8List(), + name: 'aegis_encrypted.json', + ); // Act/Assert - expect(() async => await aegisImportFileProcessor.processFile(file, password: 'wrongPassword'), throwsA(isA())); + expect( + () async => await aegisImportFileProcessor.processFile( + file, + password: 'wrongPassword', + ), + throwsA(isA()), + ); }); }); group('import HTML', () { diff --git a/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart index bdedc5ef2..6c4c10b43 100644 --- a/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart +++ b/test/unit_test/processors/token_import_file_processor/authenticator_pro_import_file_processor_test.dart @@ -64,8 +64,13 @@ void _testAuthenticatorProImportFileProcessor() { '58, 110, 117, 108, 108, 44, 34, 73, 115, 115, 117, 101, 114, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 85, 115, 101, 114, 110, 97, 109, 101, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 83, 101, 99, 114, 101, 116, 34, 58, 34, 66, 66, 66, 66, 66, 66, 66, 66, 34, 44, 34, 80, 105, 110, 34, 58, 110, 117, 108, 108, 44, 34, 65, 108, 103, 111, 114, 105, 116, 104, 109, 34, 58, 48, 44, 34, 68, 105, 103, 105, 116, 115, 34, 58, 54, 44, 34, 80,' '101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 67, 111, 117, 110, 116, 101, 114, 34, 58, 48, 44, 34, 67, 111, 112, 121, 67, 111, 117, 110, 116, 34, 58, 48, 44, 34, 82, 97, 110, 107, 105, 110, 103, 34, 58, 48, 125, 93, 44, 34, 67, 97, 116, 101, 103, 111, 114, 105, 101, 115, 34, 58, 91, 93, 44, 34, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 111, 114, 67, 97, 116, 101, 103, 111, 114, 105, 101, 115, 34, 58, 91, 93, 44, 34, 67, 117, 115, 116,' '111, 109, 73, 99, 111, 110, 115, 34, 58, 91, 93, 125]'; - final byteData = Uint8List.fromList((jsonDecode(byteDataString) as List).cast()); - final XFile file = XFile.fromData(byteData, name: 'auth_pro_plain.json'); + final byteData = Uint8List.fromList( + (jsonDecode(byteDataString) as List).cast(), + ); + final XFile file = XFile.fromData( + byteData, + name: 'auth_pro_plain.json', + ); group('fileIsValid', () { test('isTrue', () async { // Act @@ -75,8 +80,13 @@ void _testAuthenticatorProImportFileProcessor() { }); test('isFalse', () async { // Arrange - final byteData = Uint8List.fromList((jsonDecode(byteDataString) as List).cast()..removeLast()); - final XFile file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.json'); + final byteData = Uint8List.fromList( + (jsonDecode(byteDataString) as List).cast()..removeLast(), + ); + final XFile file = XFile.fromData( + byteData, + name: 'auth_pro_plain_invalid.json', + ); // Act final result = await processor.fileIsValid(file); @@ -129,7 +139,9 @@ void _testAuthenticatorProImportFileProcessor() { '88, 122, 101, 48, 104, 108, 83, 55, 104, 77, 71, 104, 110, 66, 76, 112, 52, 111, 82, 82, 53, 49, 74, 57, 83, 54, 104, 122, 52, 53, 65, 47, 71, 107, 71, 121, 103, 88, 105, 51, 106, 83, 122, 107, 72, 83, 75, 98, 73, 78, 52, 65, 67, 55, 76, 65, 113, 72, 77, 77, 76, 108, 65, 99, 115, 82, 73, 111, 122, 105, 57, 97, 89, 109, 69, 76, 87, 47, 71, 73, 97, 116, 47, 106, 57, 105, 43, 100, 83, 98, 57, 97, 98, 107, 114, 53, 74, 105, 115, 57,' '89, 55, 80, 53, 103, 89, 120, 53, 80, 119, 52, 119, 102, 75, 47, 76, 98, 86, 65, 80, 65, 84, 84, 66, 56, 102, 121, 103, 116, 108, 114, 103, 47, 121, 56, 47, 115, 109, 102, 73, 70, 57, 110, 49, 118, 57, 117, 48, 80, 48, 100, 72, 119, 65, 65, 65, 65, 66, 74, 82, 85, 53, 69, 114, 107, 74, 103, 103, 103, 61, 61, 34, 62, 60, 47, 116, 100, 62, 10, 60, 47, 116, 114, 62, 10, 32, 32, 32, 32, 32, 32, 32, 32, 60, 47, 116, 97, 98, 108, 101,' '62, 10, 32, 32, 32, 32, 10, 32, 32, 32, 32, 60, 47, 98, 111, 100, 121, 62, 10, 60, 47, 104, 116, 109, 108, 62]'; - final byteData = Uint8List.fromList((jsonDecode(htmlFileByteString) as List).cast().toList()); + final byteData = Uint8List.fromList( + (jsonDecode(htmlFileByteString) as List).cast().toList(), + ); final file = XFile.fromData(byteData, name: 'auth_pro_plain.html'); @@ -142,9 +154,15 @@ void _testAuthenticatorProImportFileProcessor() { }); test('isFalse', () async { // Arrange - final byteData = Uint8List.fromList((jsonDecode(htmlFileByteString) as List).cast().toList()..removeAt(0)); + final byteData = Uint8List.fromList( + (jsonDecode(htmlFileByteString) as List).cast().toList() + ..removeAt(0), + ); - final file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.html'); + final file = XFile.fromData( + byteData, + name: 'auth_pro_plain_invalid.html', + ); // Act final fileIsValid = await processor.fileIsValid(file); // Assert @@ -170,7 +188,9 @@ void _testAuthenticatorProImportFileProcessor() { const uriListBytes = '[111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 116, 111, 116, 112, 47, 84, 101, 115, 116, 49, 37, 51, 65, 84, 101, 115, 116, 49, 63, 115, 101, 99, 114, 101, 116, 61, 65, 65, 65, 65, 65, 65, 65, 65, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 49, 10, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 104, 111, 116, 112, 47, 84, 101, 115, 116, 50, 37, 51, 65, 84, 101, 115, 116, 50, 63, 115, 101, 99, 114, 101, 116, 61, 66, 66, 66, 66, 66, 66,' '66, 66, 38, 105, 115, 115, 117, 101, 114, 61, 84, 101, 115, 116, 50, 38, 99, 111, 117, 110, 116, 101, 114, 61, 48, 10]'; - final byteData = Uint8List.fromList((jsonDecode(uriListBytes) as List).cast().toList()); + final byteData = Uint8List.fromList( + (jsonDecode(uriListBytes) as List).cast().toList(), + ); final file = XFile.fromData(byteData, name: 'auth_pro_plain.txt'); @@ -183,9 +203,15 @@ void _testAuthenticatorProImportFileProcessor() { }); test('isFalse', () async { // Act - final byteData = Uint8List.fromList((jsonDecode(uriListBytes) as List).cast().toList()..removeWhere((uint) => uint == 58)); // 58 is ':' + final byteData = Uint8List.fromList( + (jsonDecode(uriListBytes) as List).cast().toList() + ..removeWhere((uint) => uint == 58), + ); // 58 is ':' - final file = XFile.fromData(byteData, name: 'auth_pro_plain_invalid.txt'); + final file = XFile.fromData( + byteData, + name: 'auth_pro_plain_invalid.txt', + ); final fileIsValid = await processor.fileIsValid(file); // Assert expect(fileIsValid, isFalse); diff --git a/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart index c3e4a2582..ea4cc1288 100644 --- a/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart +++ b/test/unit_test/processors/token_import_file_processor/free_otp_plus_import_file_processor_test.dart @@ -51,7 +51,10 @@ void _assertSuccessResults(List> results) { expect(totpToken.algorithm, Algorithms.SHA256); expect(totpToken.digits, 8); expect(totpToken.period, 60); - expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), equals('46107496')); + expect( + totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), + equals('46107496'), + ); } void _testFreeOtpPlusImportFileProcessor() { @@ -65,8 +68,12 @@ void _testFreeOtpPlusImportFileProcessor() { '120, 116, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 34, 84, 101, 115, 116, 50, 34, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 51, 48, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 91, 56, 44, 54, 54, 44, 49, 54, 44, 45, 49, 50, 52, 44, 51, 51, 93, 44, 34, 116, 121, 112, 101, 34, 58, 34, 72, 79, 84, 80, 34, 125, 44, 123, 34, 97, 108, 103, 111, 34, 58, 34, 83, 72, 65, 50, 53, 54, 34, 44, 34,' '99, 111, 117, 110, 116, 101, 114, 34, 58, 48, 44, 34, 100, 105, 103, 105, 116, 115, 34, 58, 56, 44, 34, 105, 115, 115, 117, 101, 114, 69, 120, 116, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 34, 84, 101, 115, 116, 49, 34, 44, 34, 112, 101, 114, 105, 111, 100, 34, 58, 54, 48, 44, 34, 115, 101, 99, 114, 101, 116, 34, 58, 91, 48, 44, 48, 44, 48, 44, 48, 44, 48, 93, 44, 34, 116, 121, 112, 101, 34, 58, 34, 84,' '79, 84, 80, 34, 125, 93, 125]'; - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); - final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Free_OTP_Plus_plain.json'); + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List) + .cast(); + final jsonFile = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Free_OTP_Plus_plain.json', + ); group('fileIsValid', () { test('isTrue', () async { @@ -77,8 +84,13 @@ void _testFreeOtpPlusImportFileProcessor() { }); test('isFalse', () async { // Arrange - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); - final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Free_OTP_Plus_plain_invalid.json'); + final jsonFileBytes = + (jsonDecode(jsonFileBytesString) as List).cast() + ..removeLast(); + final jsonFileInvalid = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Free_OTP_Plus_plain_invalid.json', + ); // Act final fileIsValid = await processor.fileIsValid(jsonFileInvalid); // Assert @@ -103,7 +115,10 @@ void _testFreeOtpPlusImportFileProcessor() { '[111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 104, 111, 116, 112, 47, 84, 101, 115, 116, 50, 37, 51, 65, 84, 101, 115, 116, 50, 63, 115, 101, 99, 114, 101, 116, 61, 66, 66, 66, 66, 66, 66, 66, 66, 38, 97, 108, 103, 111, 114, 105, 116, 104, 109, 61, 83, 72, 65, 49, 38, 100, 105, 103, 105, 116, 115, 61, 56, 38, 112, 101, 114, 105, 111, 100, 61, 51, 48, 38, 99, 111, 117, 110, 116, 101, 114, 61, 53, 10, 111, 116, 112, 97, 117, 116, 104, 58, 47, 47, 116,' '111, 116, 112, 47, 84, 101, 115, 116, 49, 37, 51, 65, 84, 101, 115, 116, 49, 63, 115, 101, 99, 114, 101, 116, 61, 65, 65, 65, 65, 65, 65, 65, 65, 38, 97, 108, 103, 111, 114, 105, 116, 104, 109, 61, 83, 72, 65, 50, 53, 54, 38, 100, 105, 103, 105, 116, 115, 61, 56, 38, 112, 101, 114, 105, 111, 100, 61, 54, 48, 10]'; final uriListBytes = (jsonDecode(uriListByteString) as List).cast(); - final uriListFile = XFile.fromData(Uint8List.fromList(uriListBytes), name: 'Free_OTP_Plus_uri_list_plain.txt'); + final uriListFile = XFile.fromData( + Uint8List.fromList(uriListBytes), + name: 'Free_OTP_Plus_uri_list_plain.txt', + ); group('fileIsValid', () { test('isTrue', () async { // Act @@ -113,8 +128,12 @@ void _testFreeOtpPlusImportFileProcessor() { }); test('isFalse', () async { // Arrange - final uriListBytes = (jsonDecode(uriListByteString) as List).cast()..removeAt(0); - final uriListFileInvalid = XFile.fromData(Uint8List.fromList(uriListBytes), name: 'Free_OTP_Plus_uri_list_plain_invalid.txt'); + final uriListBytes = + (jsonDecode(uriListByteString) as List).cast()..removeAt(0); + final uriListFileInvalid = XFile.fromData( + Uint8List.fromList(uriListBytes), + name: 'Free_OTP_Plus_uri_list_plain_invalid.txt', + ); // Act final fileIsValid = await processor.fileIsValid(uriListFileInvalid); // Assert @@ -123,7 +142,9 @@ void _testFreeOtpPlusImportFileProcessor() { }); test('fileNeedsPassword', () async { // Act - final fileNeedsPassword = await processor.fileNeedsPassword(uriListFile); + final fileNeedsPassword = await processor.fileNeedsPassword( + uriListFile, + ); // Assert expect(fileNeedsPassword, isFalse); }); diff --git a/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart b/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart index aa8fe6077..66b2df546 100644 --- a/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart +++ b/test/unit_test/processors/token_import_file_processor/two_fas_authenticator_import_file_processor_test.dart @@ -29,14 +29,20 @@ void _assertSuccessResults(List> results) { expect(token0.type, TokenTypes.TOTP.name); expect(token0, isA()); expect(token0.origin, isNotNull); - expect(token0.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect( + token0.origin!.appName, + TokenImportOrigins.twoFasAuthenticator.appName, + ); expect(token0.origin!.source, TokenOriginSourceType.backupFile); final totpToken = token0 as TOTPToken; expect(totpToken.secret, equals('AAAAAAAA')); expect(totpToken.algorithm, Algorithms.SHA256); expect(totpToken.digits, 8); expect(totpToken.period, 60); - expect(totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), equals('46107496')); + expect( + totpToken.otpFromTime(DateTime.fromMillisecondsSinceEpoch(1713519600602)), + equals('46107496'), + ); final result1 = results[1]; expect(result1, isA()); final token1 = result1.asSuccess!.resultData; @@ -45,7 +51,10 @@ void _assertSuccessResults(List> results) { expect(token1.type, TokenTypes.HOTP.name); expect(token1, isA()); expect(token1.origin, isNotNull); - expect(token1.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect( + token1.origin!.appName, + TokenImportOrigins.twoFasAuthenticator.appName, + ); expect(token1.origin!.source, TokenOriginSourceType.backupFile); final hotpToken = token1 as HOTPToken; expect(hotpToken.secret, equals('BBBBBBBB')); @@ -61,7 +70,10 @@ void _assertSuccessResults(List> results) { expect(token2.type, TokenTypes.STEAM.name); expect(token2, isA()); expect(token2.origin, isNotNull); - expect(token2.origin!.appName, TokenImportOrigins.twoFasAuthenticator.appName); + expect( + token2.origin!.appName, + TokenImportOrigins.twoFasAuthenticator.appName, + ); expect(token2.origin!.source, TokenOriginSourceType.backupFile); final steamToken = token2 as SteamToken; expect(steamToken.secret, equals('CCCCCCCC')); @@ -85,8 +97,12 @@ void _testTwoFasImportFileProcessor() { '114, 34, 58, 123, 34, 112, 111, 115, 105, 116, 105, 111, 110, 34, 58, 50, 125, 44, 34, 105, 99, 111, 110, 34, 58, 123, 34, 115, 101, 108, 101, 99, 116, 101, 100, 34, 58, 34, 76, 97, 98, 101, 108, 34, 44, 34, 108, 97, 98, 101, 108, 34, 58, 123, 34, 116, 101, 120, 116, 34, 58, 34, 83, 84, 34, 44, 34, 98, 97, 99, 107, 103, 114, 111, 117, 110, 100, 67, 111, 108, 111, 114, 34, 58, 34, 76, 105, 103, 104, 116, 66, 108, 117, 101, 34, 125, 44, 34, 105, 99,' '111, 110, 67, 111, 108, 108, 101, 99, 116, 105, 111, 110, 34, 58, 123, 34, 105, 100, 34, 58, 34, 97, 53, 98, 51, 102, 98, 54, 53, 45, 52, 101, 99, 53, 45, 52, 51, 101, 54, 45, 56, 101, 99, 49, 45, 52, 57, 101, 50, 52, 99, 97, 57, 101, 55, 97, 100, 34, 125, 125, 125, 93, 44, 34, 103, 114, 111, 117, 112, 115, 34, 58, 91, 93, 44, 34, 117, 112, 100, 97, 116, 101, 100, 65, 116, 34, 58, 49, 55, 49, 51, 53, 50, 56, 56, 49, 57, 54, 54, 48,' '44, 34, 115, 99, 104, 101, 109, 97, 86, 101, 114, 115, 105, 111, 110, 34, 58, 52, 44, 34, 97, 112, 112, 86, 101, 114, 115, 105, 111, 110, 67, 111, 100, 101, 34, 58, 53, 48, 48, 48, 48, 49, 57, 44, 34, 97, 112, 112, 86, 101, 114, 115, 105, 111, 110, 78, 97, 109, 101, 34, 58, 34, 53, 46, 52, 46, 48, 34, 44, 34, 97, 112, 112, 79, 114, 105, 103, 105, 110, 34, 58, 34, 97, 110, 100, 114, 111, 105, 100, 34, 125]'; - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); - final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_plain.json'); + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List) + .cast(); + final jsonFile = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Two_Fas_plain.json', + ); group('plain', () { group('fileIsValid', () { test('isTrue', () async { @@ -97,8 +113,13 @@ void _testTwoFasImportFileProcessor() { }); test('isFalse', () async { // Arrange - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); - final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_plain_invalid.json'); + final jsonFileBytes = + (jsonDecode(jsonFileBytesString) as List).cast() + ..removeLast(); + final jsonFileInvalid = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Two_Fas_plain_invalid.json', + ); // Act final fileIsValid = await processor.fileIsValid(jsonFileInvalid); // Assert @@ -147,8 +168,12 @@ void _testTwoFasImportFileProcessor() { '102, 108, 105, 66, 81, 109, 119, 97, 115, 98, 76, 51, 77, 86, 65, 84, 79, 65, 88, 109, 57, 97, 84, 98, 111, 89, 119, 102, 86, 86, 105, 50, 71, 87, 98, 74, 119, 105, 120, 54, 47, 99, 116, 70, 48, 52, 101, 43, 102, 120, 90, 87, 119, 77, 80, 115, 82, 109, 53, 82, 51, 56, 102, 67, 122, 68, 54, 56, 82, 78, 67, 54, 86, 68, 51, 101, 122, 114, 54, 43, 52, 56, 85, 68, 120, 116, 112, 66, 72, 117, 69, 71, 47, 78, 78, 101, 77, 66, 70, 51,' '43, 122, 69, 86, 89, 74, 98, 112, 114, 116, 79, 79, 75, 113, 102, 101, 43, 101, 50, 84, 66, 85, 100, 106, 77, 84, 74, 72, 108, 52, 57, 70, 70, 83, 89, 83, 50, 73, 102, 68, 48, 105, 90, 73, 117, 77, 102, 105, 68, 53, 87, 76, 54, 67, 99, 69, 114, 76, 113, 54, 75, 98, 86, 73, 49, 65, 43, 74, 115, 98, 115, 80, 87, 108, 86, 109, 111, 79, 84, 112, 53, 43, 57, 116, 98, 67, 101, 83, 99, 56, 112, 52, 87, 110, 72, 49, 57, 70, 55, 81,' '56, 105, 84, 103, 72, 89, 77, 88, 52, 89, 83, 108, 111, 121, 47, 68, 90, 70, 108, 119, 52, 88, 79, 70, 100, 110, 102, 77, 98, 53, 70, 70, 57, 69, 79, 49, 115, 54, 48, 87, 77, 116, 76, 56, 65, 72, 109, 98, 107, 121, 53, 56, 57, 116, 97, 105, 43, 108, 78, 72, 88, 52, 77, 102, 55, 112, 80, 57, 107, 121, 89, 119, 61, 61, 58, 71, 103, 119, 105, 68, 99, 73, 79, 86, 84, 117, 68, 76, 80, 75, 75, 34, 125]'; - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast(); - final jsonFile = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_encrypted.json'); + final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List) + .cast(); + final jsonFile = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Two_Fas_encrypted.json', + ); group('fileIsValid', () { test('isTrue', () async { // Act @@ -158,8 +183,13 @@ void _testTwoFasImportFileProcessor() { }); test('isFalse', () async { // Arrange - final jsonFileBytes = (jsonDecode(jsonFileBytesString) as List).cast()..removeLast(); - final jsonFileInvalid = XFile.fromData(Uint8List.fromList(jsonFileBytes), name: 'Two_Fas_encrypted_invalid.json'); + final jsonFileBytes = + (jsonDecode(jsonFileBytesString) as List).cast() + ..removeLast(); + final jsonFileInvalid = XFile.fromData( + Uint8List.fromList(jsonFileBytes), + name: 'Two_Fas_encrypted_invalid.json', + ); // Act final fileIsValid = await processor.fileIsValid(jsonFileInvalid); // Assert @@ -176,7 +206,10 @@ void _testTwoFasImportFileProcessor() { // Arrange const password = 'test123'; // Act - final results = await processor.processFile(jsonFile, password: password); + final results = await processor.processFile( + jsonFile, + password: password, + ); // Assert _assertSuccessResults(results); }); diff --git a/test/unit_test/repo/secure_storage_test.dart b/test/unit_test/repo/secure_storage_test.dart index aeb4a49db..9ed146e92 100644 --- a/test/unit_test/repo/secure_storage_test.dart +++ b/test/unit_test/repo/secure_storage_test.dart @@ -11,7 +11,10 @@ void main() { setUp(() { mockStorage = MockFlutterSecureStorage(); - secureStorage = SecureStorage(storagePrefix: storagePrefix, storage: mockStorage); + secureStorage = SecureStorage( + storagePrefix: storagePrefix, + storage: mockStorage, + ); }); group('SecureStorage', () { @@ -20,33 +23,53 @@ void main() { }); test('write calls storage.write with correct key and value', () async { - when(mockStorage.write(key: anyNamed('key'), value: anyNamed('value'))).thenAnswer((_) async => {}); + when( + mockStorage.write(key: anyNamed('key'), value: anyNamed('value')), + ).thenAnswer((_) async => {}); await secureStorage.write(key: 'foo', value: 'bar'); verify(mockStorage.write(key: 'testprefix_foo', value: 'bar')).called(1); }); test('read calls storage.read with correct key', () async { - when(mockStorage.read(key: anyNamed('key'))).thenAnswer((_) async => 'value'); + when( + mockStorage.read(key: anyNamed('key')), + ).thenAnswer((_) async => 'value'); final result = await secureStorage.read(key: 'foo'); expect(result, 'value'); verify(mockStorage.read(key: 'testprefix_foo')).called(1); }); test('readAll returns only prefixed keys with prefix removed', () async { - when(mockStorage.readAll()).thenAnswer((_) async => {'testprefix_key1': 'val1', 'testprefix_key2': 'val2', 'otherprefix_key3': 'val3'}); + when(mockStorage.readAll()).thenAnswer( + (_) async => { + 'testprefix_key1': 'val1', + 'testprefix_key2': 'val2', + 'otherprefix_key3': 'val3', + }, + ); final result = await secureStorage.readAll(); expect(result, {'key1': 'val1', 'key2': 'val2'}); }); test('delete calls storage.delete with correct key', () async { - when(mockStorage.delete(key: anyNamed('key'))).thenAnswer((_) async => {}); + when( + mockStorage.delete(key: anyNamed('key')), + ).thenAnswer((_) async => {}); await secureStorage.delete(key: 'foo'); verify(mockStorage.delete(key: 'testprefix_foo')).called(1); }); test('deleteAll deletes only prefixed keys', () async { - when(mockStorage.readAll()).thenAnswer((_) async => {'testprefix_key1': 'val1', 'testprefix_key2': 'val2', 'otherprefix_key3': 'val3'}); - when(mockStorage.delete(key: anyNamed('key'))).thenAnswer((_) async => {}); + when(mockStorage.readAll()).thenAnswer( + (_) async => { + 'testprefix_key1': 'val1', + 'testprefix_key2': 'val2', + 'otherprefix_key3': 'val3', + }, + ); + when( + mockStorage.delete(key: anyNamed('key')), + ).thenAnswer((_) async => {}); await secureStorage.deleteAll(); verify(mockStorage.delete(key: 'testprefix_key1')).called(1); verify(mockStorage.delete(key: 'testprefix_key2')).called(1); diff --git a/test/unit_test/repo/secure_token_container_repository_test.dart b/test/unit_test/repo/secure_token_container_repository_test.dart index 065322ae7..1a107c969 100644 --- a/test/unit_test/repo/secure_token_container_repository_test.dart +++ b/test/unit_test/repo/secure_token_container_repository_test.dart @@ -25,9 +25,20 @@ void main() { mockStorage = MockFlutterSecureStorage(); mockLegacyStorage = MockFlutterSecureStorage(); // The repository uses these prefixes internally to create SecureStorage instances. - storage = SecureStorage(storagePrefix: SecureTokenContainerRepository.TOKEN_CONTAINER_PREFIX, storage: mockStorage); - legacyStorage = SecureStorage(storagePrefix: SecureTokenContainerRepository.TOKEN_CONTAINER_PREFIX_LEGACY, storage: mockLegacyStorage, seperator: '.'); - repository = SecureTokenContainerRepository(storage: storage, legacyStorage: legacyStorage); + storage = SecureStorage( + storagePrefix: SecureTokenContainerRepository.TOKEN_CONTAINER_PREFIX, + storage: mockStorage, + ); + legacyStorage = SecureStorage( + storagePrefix: + SecureTokenContainerRepository.TOKEN_CONTAINER_PREFIX_LEGACY, + storage: mockLegacyStorage, + seperator: '.', + ); + repository = SecureTokenContainerRepository( + storage: storage, + legacyStorage: legacyStorage, + ); }); TokenContainer createContainer(String serial) { @@ -46,14 +57,17 @@ void main() { } group('SecureTokenContainerRepository', () { - test('loadContainerState returns empty state if nothing is stored', () async { - when(mockStorage.readAll()).thenAnswer((_) async => {}); - when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); + test( + 'loadContainerState returns empty state if nothing is stored', + () async { + when(mockStorage.readAll()).thenAnswer((_) async => {}); + when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); - final state = await repository.loadContainerState(); + final state = await repository.loadContainerState(); - expect(state.containerList, isEmpty); - }); + expect(state.containerList, isEmpty); + }, + ); test('loadContainerState loads containers from storage', () async { final containerMap = { @@ -77,14 +91,20 @@ void main() { final expectedKey = '${newPrefix}_${container.serial}'; final expectedValue = jsonEncode(container.toJson()); - when(mockStorage.write(key: expectedKey, value: expectedValue)).thenAnswer((_) async {}); + when( + mockStorage.write(key: expectedKey, value: expectedValue), + ).thenAnswer((_) async {}); // The method under test reloads the state, so we need to mock readAll. - when(mockStorage.readAll()).thenAnswer((_) async => {expectedKey: expectedValue}); + when( + mockStorage.readAll(), + ).thenAnswer((_) async => {expectedKey: expectedValue}); when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); await repository.saveContainer(container); - verify(mockStorage.write(key: expectedKey, value: expectedValue)).called(1); + verify( + mockStorage.write(key: expectedKey, value: expectedValue), + ).called(1); }); test('deleteContainer removes a container from storage', () async { @@ -111,8 +131,12 @@ void main() { // 1. Mock readAll to return some values when(mockStorage.readAll()).thenAnswer((_) async => containerMap); // 2. Mock delete for the keys that should be deleted - when(mockStorage.delete(key: "${newPrefix}_serial1")).thenAnswer((_) async {}); - when(mockStorage.delete(key: "${newPrefix}_serial2")).thenAnswer((_) async {}); + when( + mockStorage.delete(key: "${newPrefix}_serial1"), + ).thenAnswer((_) async {}); + when( + mockStorage.delete(key: "${newPrefix}_serial2"), + ).thenAnswer((_) async {}); // 3. Mock legacy readAll for the subsequent loadContainerState call when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); @@ -129,42 +153,56 @@ void main() { }); group('Migration', () { - test('loadContainerState migrates legacy containers and returns them in state', () async { - final container1 = createContainer('serial1'); - final legacyKey = '$legacyPrefix.${container1.serial}'; - final newKey = '${newPrefix}_${container1.serial}'; - final value = jsonEncode(container1.toJson()); - - final legacyContainerMap = {legacyKey: value}; - final newContainerMap = {newKey: value}; // This is what readAll should find AFTER migration - - when(mockLegacyStorage.readAll()).thenAnswer((_) async => legacyContainerMap); - // When _migrate runs, it will write to the new storage. - when(mockStorage.write(key: newKey, value: value)).thenAnswer((_) async {}); - // When loadContainerState then reads from the new storage, it should find the migrated data. - when(mockStorage.readAll()).thenAnswer((_) async => newContainerMap); - // The migration also deletes the old data. - when(mockLegacyStorage.delete(key: legacyKey)).thenAnswer((_) async {}); - - final state = await repository.loadContainerState(); + test( + 'loadContainerState migrates legacy containers and returns them in state', + () async { + final container1 = createContainer('serial1'); + final legacyKey = '$legacyPrefix.${container1.serial}'; + final newKey = '${newPrefix}_${container1.serial}'; + final value = jsonEncode(container1.toJson()); + + final legacyContainerMap = {legacyKey: value}; + final newContainerMap = { + newKey: value, + }; // This is what readAll should find AFTER migration + + when( + mockLegacyStorage.readAll(), + ).thenAnswer((_) async => legacyContainerMap); + // When _migrate runs, it will write to the new storage. + when( + mockStorage.write(key: newKey, value: value), + ).thenAnswer((_) async {}); + // When loadContainerState then reads from the new storage, it should find the migrated data. + when(mockStorage.readAll()).thenAnswer((_) async => newContainerMap); + // The migration also deletes the old data. + when(mockLegacyStorage.delete(key: legacyKey)).thenAnswer((_) async {}); + + final state = await repository.loadContainerState(); + + // Verify migration calls happened + verify(mockStorage.write(key: newKey, value: value)).called(1); + verify(mockLegacyStorage.delete(key: legacyKey)).called(1); + + // Verify the final state contains the migrated container + expect(state.containerList.length, 1); + expect(state.containerList.first.serial, 'serial1'); + }, + ); - // Verify migration calls happened - verify(mockStorage.write(key: newKey, value: value)).called(1); - verify(mockLegacyStorage.delete(key: legacyKey)).called(1); + test( + 'loadContainerState does not migrate if no legacy containers exist', + () async { + when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); + when(mockStorage.readAll()).thenAnswer((_) async => {}); - // Verify the final state contains the migrated container - expect(state.containerList.length, 1); - expect(state.containerList.first.serial, 'serial1'); - }); + await repository.loadContainerState(); - test('loadContainerState does not migrate if no legacy containers exist', () async { - when(mockLegacyStorage.readAll()).thenAnswer((_) async => {}); - when(mockStorage.readAll()).thenAnswer((_) async => {}); - - await repository.loadContainerState(); - - verifyNever(mockStorage.write(key: anyNamed('key'), value: anyNamed('value'))); - verifyNever(mockLegacyStorage.delete(key: anyNamed('key'))); - }); + verifyNever( + mockStorage.write(key: anyNamed('key'), value: anyNamed('value')), + ); + verifyNever(mockLegacyStorage.delete(key: anyNamed('key'))); + }, + ); }); } diff --git a/test/unit_test/state_notifiers/allow_screenshot_notifier_test.dart b/test/unit_test/state_notifiers/allow_screenshot_notifier_test.dart index 402db9ff2..b62d35a04 100644 --- a/test/unit_test/state_notifiers/allow_screenshot_notifier_test.dart +++ b/test/unit_test/state_notifiers/allow_screenshot_notifier_test.dart @@ -43,19 +43,29 @@ void _testAllowScreenshotNotifier() { mockScreenshotUtils = MockAllowScreenshotUtils(); mockSettingsRepository = MockSettingsRepository(); settingsState = SettingsState(allowScreenshots: false); - container = ProviderContainer(overrides: [ - allowScreenshotProvider.overrideWith( - () => AllowScreenshotNotifier(screenshotUtilsOverride: mockScreenshotUtils), - ), - settingsProvider.overrideWith( - () => SettingsNotifier(repoOverride: mockSettingsRepository), - ), - ]); + container = ProviderContainer( + overrides: [ + allowScreenshotProvider.overrideWith( + () => AllowScreenshotNotifier( + screenshotUtilsOverride: mockScreenshotUtils, + ), + ), + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepository), + ), + ], + ); notifier = container.read(allowScreenshotProvider.notifier); when(mockScreenshotUtils.allowScreenshots()).thenAnswer((_) async => true); - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => false); - when(mockSettingsRepository.loadSettings()).thenAnswer((_) async => settingsState); - when(mockSettingsRepository.saveSettings(any)).thenAnswer((invocation) async { + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => false); + when( + mockSettingsRepository.loadSettings(), + ).thenAnswer((_) async => settingsState); + when(mockSettingsRepository.saveSettings(any)).thenAnswer(( + invocation, + ) async { final newState = invocation.positionalArguments[0] as SettingsState; settingsState = newState; return true; @@ -63,7 +73,9 @@ void _testAllowScreenshotNotifier() { }); test('Initial state is fetched correctly', () async { - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => true); + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => true); final result = await notifier.build(screenshotUtils: mockScreenshotUtils); @@ -73,7 +85,9 @@ void _testAllowScreenshotNotifier() { test('allowScreenshots enables screenshots and updates settings', () async { when(mockScreenshotUtils.allowScreenshots()).thenAnswer((_) async => true); - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => true); + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => true); final result = await notifier.allowScreenshots(); @@ -82,13 +96,17 @@ void _testAllowScreenshotNotifier() { verify(mockSettingsRepository.saveSettings(any)).called(1); final newSettingsState = await container.read(settingsProvider.future); expect(newSettingsState.allowScreenshots, true); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; expect(newAllowScreenshots, true); }); test('allowScreenshots failed to enable screenshots', () async { when(mockScreenshotUtils.allowScreenshots()).thenAnswer((_) async => false); - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => true); + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => true); final result = await notifier.allowScreenshots(); @@ -97,28 +115,43 @@ void _testAllowScreenshotNotifier() { verifyNever(mockSettingsRepository.saveSettings(any)); final newSettingsState = await container.read(settingsProvider.future); expect(newSettingsState.allowScreenshots, false); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; expect(newAllowScreenshots, false); }); - test('disallowScreenshots disables screenshots and updates settings', () async { - when(mockScreenshotUtils.allowScreenshots()).thenAnswer((_) async => true); // to set the initial state to true - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => true); - - await notifier.allowScreenshots(); // to set the initial state to true - final result = await notifier.disallowScreenshots(); - - expect(result, true); - verify(mockScreenshotUtils.disallowScreenshots()).called(2); - verify(mockSettingsRepository.saveSettings(any)).called(2); - final newSettingsState = await container.read(settingsProvider.future); - expect(newSettingsState.allowScreenshots, false); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; - expect(newAllowScreenshots, false); - }); + test( + 'disallowScreenshots disables screenshots and updates settings', + () async { + when( + mockScreenshotUtils.allowScreenshots(), + ).thenAnswer((_) async => true); // to set the initial state to true + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => true); + + await notifier.allowScreenshots(); // to set the initial state to true + final result = await notifier.disallowScreenshots(); + + expect(result, true); + verify(mockScreenshotUtils.disallowScreenshots()).called(2); + verify(mockSettingsRepository.saveSettings(any)).called(2); + final newSettingsState = await container.read(settingsProvider.future); + expect(newSettingsState.allowScreenshots, false); + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; + expect(newAllowScreenshots, false); + }, + ); test('disallowScreenshots failed to disable screenshots', () async { - when(mockScreenshotUtils.allowScreenshots()).thenAnswer((_) async => true); // to set the initial state to true - when(mockScreenshotUtils.disallowScreenshots()).thenAnswer((_) async => false); + when( + mockScreenshotUtils.allowScreenshots(), + ).thenAnswer((_) async => true); // to set the initial state to true + when( + mockScreenshotUtils.disallowScreenshots(), + ).thenAnswer((_) async => false); await notifier.allowScreenshots(); // to set the initial state to true final result = await notifier.disallowScreenshots(); @@ -128,36 +161,52 @@ void _testAllowScreenshotNotifier() { verify(mockSettingsRepository.saveSettings(any)).called(1); final newSettingsState = await container.read(settingsProvider.future); expect(newSettingsState.allowScreenshots, true); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; - expect(newAllowScreenshots, true); - }); - - test('toggleAllowScreenshots toggles the allowness of screenshots and updates settings', () async { - when(mockScreenshotUtils.toggleAllowScreenshots(any)).thenAnswer((_) async => true); - - final result = await notifier.toggleAllowScreenshots(); - - expect(result, true); - verify(mockScreenshotUtils.disallowScreenshots()).called(1); - verify(mockScreenshotUtils.toggleAllowScreenshots(false)).called(1); - verify(mockSettingsRepository.saveSettings(any)).called(1); - final newSettingsState = await container.read(settingsProvider.future); - expect(newSettingsState.allowScreenshots, true); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; expect(newAllowScreenshots, true); }); - test('toggleAllowScreenshots failed to toggle the allowness of screenshots', () async { - when(mockScreenshotUtils.toggleAllowScreenshots(any)).thenAnswer((_) async => false); - - final result = await notifier.toggleAllowScreenshots(); - expect(result, false); - verify(mockScreenshotUtils.disallowScreenshots()).called(1); - verify(mockScreenshotUtils.toggleAllowScreenshots(false)).called(1); - verifyNever(mockSettingsRepository.saveSettings(any)); - final newSettingsState = await container.read(settingsProvider.future); - expect(newSettingsState.allowScreenshots, false); - final newAllowScreenshots = (await container.read(settingsProvider.future)).allowScreenshots; - expect(newAllowScreenshots, false); - }); + test( + 'toggleAllowScreenshots toggles the allowness of screenshots and updates settings', + () async { + when( + mockScreenshotUtils.toggleAllowScreenshots(any), + ).thenAnswer((_) async => true); + + final result = await notifier.toggleAllowScreenshots(); + + expect(result, true); + verify(mockScreenshotUtils.disallowScreenshots()).called(1); + verify(mockScreenshotUtils.toggleAllowScreenshots(false)).called(1); + verify(mockSettingsRepository.saveSettings(any)).called(1); + final newSettingsState = await container.read(settingsProvider.future); + expect(newSettingsState.allowScreenshots, true); + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; + expect(newAllowScreenshots, true); + }, + ); + test( + 'toggleAllowScreenshots failed to toggle the allowness of screenshots', + () async { + when( + mockScreenshotUtils.toggleAllowScreenshots(any), + ).thenAnswer((_) async => false); + + final result = await notifier.toggleAllowScreenshots(); + + expect(result, false); + verify(mockScreenshotUtils.disallowScreenshots()).called(1); + verify(mockScreenshotUtils.toggleAllowScreenshots(false)).called(1); + verifyNever(mockSettingsRepository.saveSettings(any)); + final newSettingsState = await container.read(settingsProvider.future); + expect(newSettingsState.allowScreenshots, false); + final newAllowScreenshots = (await container.read( + settingsProvider.future, + )).allowScreenshots; + expect(newAllowScreenshots, false); + }, + ); } diff --git a/test/unit_test/state_notifiers/deeplink_notifier_test.dart b/test/unit_test/state_notifiers/deeplink_notifier_test.dart index 3da02d341..7a9763339 100644 --- a/test/unit_test/state_notifiers/deeplink_notifier_test.dart +++ b/test/unit_test/state_notifiers/deeplink_notifier_test.dart @@ -12,12 +12,21 @@ void _testDeeplinkNotifier() { group('Deeplink Notifier Test', () { test('initUri', () { final container = ProviderContainer(); - final initUri = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1'); - final deeplinkProvider = StateNotifierProvider( - (ref) => DeeplinkNotifier( - sources: [DeeplinkSource(name: 'test', stream: const Stream.empty(), initialUri: Future.value(initUri))], - ), + final initUri = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1', ); + final deeplinkProvider = + StateNotifierProvider( + (ref) => DeeplinkNotifier( + sources: [ + DeeplinkSource( + name: 'test', + stream: const Stream.empty(), + initialUri: Future.value(initUri), + ), + ], + ), + ); container.listen(deeplinkProvider, (prev, next) { expect(prev, isNull); expect(next, isNotNull); @@ -26,16 +35,29 @@ void _testDeeplinkNotifier() { }); test('initUri multible', () async { final container = ProviderContainer(); - final initUri = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1'); - final initUri2 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1'); - final deeplinkProvider = StateNotifierProvider( - (ref) => DeeplinkNotifier( - sources: [ - DeeplinkSource(name: 'test', stream: const Stream.empty(), initialUri: Future.value(initUri)), - DeeplinkSource(name: 'test2', stream: const Stream.empty(), initialUri: Future.value(initUri2)), - ], - ), + final initUri = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1', ); + final initUri2 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1', + ); + final deeplinkProvider = + StateNotifierProvider( + (ref) => DeeplinkNotifier( + sources: [ + DeeplinkSource( + name: 'test', + stream: const Stream.empty(), + initialUri: Future.value(initUri), + ), + DeeplinkSource( + name: 'test2', + stream: const Stream.empty(), + initialUri: Future.value(initUri2), + ), + ], + ), + ); container.listen(deeplinkProvider, (prev, next) { // There should be only one initial uri, others will be ignored expect(prev, isNull); @@ -45,17 +67,32 @@ void _testDeeplinkNotifier() { }); test('stream uri', () { final container = ProviderContainer(); - final uri1 = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1'); - final uri2 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1'); - final uri3 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=60&digits=6&algorithm=SHA1'); - final uri4 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=90&digits=6&algorithm=SHA1'); + final uri1 = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=0&digits=6&algorithm=SHA1', + ); + final uri2 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1', + ); + final uri3 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=60&digits=6&algorithm=SHA1', + ); + final uri4 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=90&digits=6&algorithm=SHA1', + ); final list = [uri1, uri2, uri3, uri4]; Stream stream = Stream.fromIterable([...list]); - final deeplinkProvider = StateNotifierProvider( - (ref) => DeeplinkNotifier( - sources: [DeeplinkSource(name: 'test', stream: stream, initialUri: Future.value(null))], - ), - ); + final deeplinkProvider = + StateNotifierProvider( + (ref) => DeeplinkNotifier( + sources: [ + DeeplinkSource( + name: 'test', + stream: stream, + initialUri: Future.value(), + ), + ], + ), + ); container.listen(deeplinkProvider, (prev, next) { expect(next?.uri, equals(list.removeAt(0))); expect(next?.fromInit, isFalse); @@ -63,27 +100,52 @@ void _testDeeplinkNotifier() { }); test('stream uri multible', () { final container = ProviderContainer(); - final hotp1 = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=1&digits=6&algorithm=SHA1'); - final hotp2 = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=2&digits=6&algorithm=SHA1'); - final hotp3 = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=3&digits=6&algorithm=SHA1'); - final hotp4 = Uri.parse('otpauth://hotp/issuer?secret=AAAAAAAA&counter=4&digits=6&algorithm=SHA1'); - final totp1 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=15&digits=6&algorithm=SHA1'); - final totp2 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1'); - final totp3 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=60&digits=6&algorithm=SHA1'); - final totp4 = Uri.parse('otpauth://totp/issuer?secret=AAAAAAAA&period=90&digits=6&algorithm=SHA1'); + final hotp1 = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=1&digits=6&algorithm=SHA1', + ); + final hotp2 = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=2&digits=6&algorithm=SHA1', + ); + final hotp3 = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=3&digits=6&algorithm=SHA1', + ); + final hotp4 = Uri.parse( + 'otpauth://hotp/issuer?secret=AAAAAAAA&counter=4&digits=6&algorithm=SHA1', + ); + final totp1 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=15&digits=6&algorithm=SHA1', + ); + final totp2 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=30&digits=6&algorithm=SHA1', + ); + final totp3 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=60&digits=6&algorithm=SHA1', + ); + final totp4 = Uri.parse( + 'otpauth://totp/issuer?secret=AAAAAAAA&period=90&digits=6&algorithm=SHA1', + ); final hotpList = [hotp1, hotp2, hotp3, hotp4]; final totpList = [totp1, totp2, totp3, totp4]; Stream hotpStream = Stream.fromIterable([...hotpList]); Stream totpStream = Stream.fromIterable([...totpList]); - final deeplinkProvider = StateNotifierProvider( - (ref) => DeeplinkNotifier( - sources: [ - DeeplinkSource(name: 'HOTPs', stream: hotpStream, initialUri: Future.value(null)), - DeeplinkSource(name: 'TOTPs', stream: totpStream, initialUri: Future.value(null)), - ], - ), - ); + final deeplinkProvider = + StateNotifierProvider( + (ref) => DeeplinkNotifier( + sources: [ + DeeplinkSource( + name: 'HOTPs', + stream: hotpStream, + initialUri: Future.value(), + ), + DeeplinkSource( + name: 'TOTPs', + stream: totpStream, + initialUri: Future.value(), + ), + ], + ), + ); container.listen(deeplinkProvider, (prev, next) { if (hotpList.length == totpList.length) { expect(next?.uri, equals(hotpList.removeAt(0))); diff --git a/test/unit_test/state_notifiers/push_request_notifier_test.dart b/test/unit_test/state_notifiers/push_request_notifier_test.dart index 87af3a257..074661f99 100644 --- a/test/unit_test/state_notifiers/push_request_notifier_test.dart +++ b/test/unit_test/state_notifiers/push_request_notifier_test.dart @@ -53,7 +53,6 @@ void _testPushRequestNotifier() { expirationDate: DateTime.now().add(const Duration(minutes: 5)), signature: 'signature', serial: 'serial', - accepted: null, ); final before = PushRequestState( @@ -128,7 +127,6 @@ void _testPushRequestNotifier() { expirationDate: DateTime.now().add(const Duration(minutes: 5)), signature: 'signature', serial: 'serial', - accepted: null, ); final before = PushRequestState( pushRequests: [pr], @@ -201,7 +199,6 @@ void _testPushRequestNotifier() { expirationDate: DateTime.now().add(const Duration(minutes: 5)), signature: 'signature', serial: 'serial', - accepted: null, ); final pr2 = pr.copyWith(serial: 'serial2', nonce: 'nonce2'); final before = PushRequestState( @@ -241,7 +238,6 @@ void _testPushRequestNotifier() { expirationDate: DateTime.now().add(const Duration(minutes: 5)), signature: 'signature', serial: 'serial', - accepted: null, ); final pr2 = pr.copyWith(serial: 'serial2'); final before = PushRequestState( diff --git a/test/unit_test/state_notifiers/sortable_notifier_test.dart b/test/unit_test/state_notifiers/sortable_notifier_test.dart index b59d67f05..a811b7cd6 100644 --- a/test/unit_test/state_notifiers/sortable_notifier_test.dart +++ b/test/unit_test/state_notifiers/sortable_notifier_test.dart @@ -27,30 +27,63 @@ void _testSortableNotifier() { test('handleNewList', () async { final mockSettingsRepo = MockSettingsRepository(); await GmsCheck().checkGmsAvailability(); - when(mockSettingsRepo.loadSettings()).thenAnswer((_) async => SettingsState()); - final MockTokenFolderRepository mockTokenFolderRepository = MockTokenFolderRepository(); + when( + mockSettingsRepo.loadSettings(), + ).thenAnswer((_) async => SettingsState()); + final MockTokenFolderRepository mockTokenFolderRepository = + MockTokenFolderRepository(); final MockTokenRepository mockTokenRepository = MockTokenRepository(); - TokenFolderState tokenFolderState = const TokenFolderState(folders: [ - TokenFolder(label: 'Folder 1', folderId: 1, sortIndex: null), - TokenFolder(label: 'Folder 2', folderId: 2, sortIndex: 2), - ]); + TokenFolderState tokenFolderState = const TokenFolderState( + folders: [ + TokenFolder(label: 'Folder 1', folderId: 1), + TokenFolder(label: 'Folder 2', folderId: 2, sortIndex: 2), + ], + ); when(mockTokenFolderRepository.loadState()).thenAnswer((_) async { Logger.debug('Loading token folder state'); return tokenFolderState; }); - when(mockTokenFolderRepository.saveState(any)).thenAnswer((newState) async { - tokenFolderState = newState.positionalArguments.first as TokenFolderState; + when(mockTokenFolderRepository.saveState(any)).thenAnswer(( + newState, + ) async { + tokenFolderState = + newState.positionalArguments.first as TokenFolderState; return true; }); List tokenState = [ - HOTPToken(id: 'Token 1', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret1', folderId: 1, sortIndex: null), - TOTPToken(period: 30, id: 'Token 2', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', folderId: 2, sortIndex: null), - HOTPToken(id: 'Token 3', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret3', folderId: null, sortIndex: 1), + HOTPToken( + id: 'Token 1', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret1', + folderId: 1, + ), + TOTPToken( + period: 30, + id: 'Token 2', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret2', + folderId: 2, + ), + HOTPToken( + id: 'Token 3', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret3', + sortIndex: 1, + ), ]; - when(mockTokenRepository.loadTokens()).thenAnswer((_) async => tokenState); - when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer((newState) async { + when( + mockTokenRepository.loadTokens(), + ).thenAnswer((_) async => tokenState); + when(mockTokenRepository.saveOrReplaceToken(any)).thenAnswer(( + newState, + ) async { final token = newState.positionalArguments.first as Token; - final index = tokenState.indexWhere((element) => element.id == token.id); + final index = tokenState.indexWhere( + (element) => element.id == token.id, + ); if (index != -1) { tokenState[index] = token; } else { @@ -59,16 +92,34 @@ void _testSortableNotifier() { return true; }); - final container = ProviderContainer(overrides: [ - settingsProvider.overrideWith(() => SettingsNotifier(repoOverride: mockSettingsRepo)), - tokenFolderProvider.overrideWith(() => TokenFolderNotifier(repoOverride: mockTokenFolderRepository)), - tokenProvider.overrideWith(() => TokenNotifier(repoOverride: mockTokenRepository)), - ]); + final container = ProviderContainer( + overrides: [ + settingsProvider.overrideWith( + () => SettingsNotifier(repoOverride: mockSettingsRepo), + ), + tokenFolderProvider.overrideWith( + () => TokenFolderNotifier(repoOverride: mockTokenFolderRepository), + ), + tokenProvider.overrideWith( + () => TokenNotifier(repoOverride: mockTokenRepository), + ), + ], + ); WidgetsFlutterBinding.ensureInitialized(); - final newToken = TOTPToken(period: 30, id: 'Token 4', algorithm: Algorithms.SHA1, digits: 6, secret: 'secret4', folderId: 1, sortIndex: 1); + final newToken = TOTPToken( + period: 30, + id: 'Token 4', + algorithm: Algorithms.SHA1, + digits: 6, + secret: 'secret4', + folderId: 1, + sortIndex: 1, + ); await container.read(tokenProvider.notifier).addNewToken(newToken); - await container.read(tokenFolderProvider.notifier).addNewFolder('Folder 3'); + await container + .read(tokenFolderProvider.notifier) + .addNewFolder('Folder 3'); final newSortableState = await container.read(sortablesProvider.future); expect(newSortableState.length, 7); diff --git a/test/unit_test/state_notifiers/token_container_notifier_test.dart b/test/unit_test/state_notifiers/token_container_notifier_test.dart index 7eb734a8e..78105cc73 100644 --- a/test/unit_test/state_notifiers/token_container_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_container_notifier_test.dart @@ -45,7 +45,6 @@ void main() { TokenContainerState buildFinalizedContainerState() => TokenContainerState( containerList: [ TokenContainerFinalized( - serverName: 'privacyIDEA', issuer: 'privacyIDEA', nonce: 'dbd2ab5aa9b539484fc3b78cd4bb08375d3eb30e', timestamp: DateTime.parse("2024-11-14 09:30:18.288530Z"), @@ -56,9 +55,6 @@ void main() { sslVerify: false, publicClientKey: 'publicClientKey', privateClientKey: 'privateClientKey', - finalizationState: FinalizationState.completed, - syncState: SyncState.notStarted, - passphraseQuestion: null, policies: ContainerPolicies( rolloverAllowed: false, initialTokenAssignment: false, @@ -79,7 +75,7 @@ void main() { ).thenAnswer((_) => Future.value(stateGetter())); when(mockContainerRepo.loadContainer(any)).thenAnswer((invocation) { final serial = invocation.positionalArguments[0] as String; - if (stateGetter().containerList.isEmpty) return Future.value(null); + if (stateGetter().containerList.isEmpty) return Future.value(); return Future.value( stateGetter().containerList.firstWhereOrNull( (element) => element.serial == serial, @@ -881,9 +877,6 @@ void main() { serial: "serial", ecKeyAlgorithm: EcKeyAlgorithm.secp521r1, hashAlgorithm: Algorithms.SHA512, - finalizationState: FinalizationState.completed, - syncState: SyncState.notStarted, - passphraseQuestion: null, sslVerify: true, privateClientKey: "random", publicClientKey: "random", diff --git a/test/unit_test/state_notifiers/token_folder_notifier_test.dart b/test/unit_test/state_notifiers/token_folder_notifier_test.dart index 89c5f7aa8..3e3730428 100644 --- a/test/unit_test/state_notifiers/token_folder_notifier_test.dart +++ b/test/unit_test/state_notifiers/token_folder_notifier_test.dart @@ -20,15 +20,7 @@ void _testTokenFolderNotifier() { final container = ProviderContainer(); const TokenFolderState before = TokenFolderState(folders: []); const TokenFolderState after = TokenFolderState( - folders: [ - TokenFolder( - label: 'test', - folderId: 1, - isExpanded: false, - isLocked: false, - sortIndex: null, - ), - ], + folders: [TokenFolder(label: 'test', folderId: 1)], ); when(mockRepo.loadState()).thenAnswer((_) async => before); when(mockRepo.saveState(after)).thenAnswer((_) async => true); @@ -54,15 +46,7 @@ void _testTokenFolderNotifier() { ], ); const before = TokenFolderState( - folders: [ - TokenFolder( - label: 'test', - folderId: 1, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), - ], + folders: [TokenFolder(label: 'test', folderId: 1, isExpanded: true)], ); const after = TokenFolderState(folders: []); when(mockRepo.loadState()).thenAnswer((_) async => before); @@ -81,25 +65,11 @@ void _testTokenFolderNotifier() { final mockRepo = MockTokenFolderRepository(); final container = ProviderContainer(); const before = TokenFolderState( - folders: [ - TokenFolder( - label: 'test', - folderId: 1, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), - ], + folders: [TokenFolder(label: 'test', folderId: 1, isExpanded: true)], ); const after = TokenFolderState( folders: [ - TokenFolder( - label: 'testUpdated', - folderId: 1, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), + TokenFolder(label: 'testUpdated', folderId: 1, isExpanded: true), ], ); when(mockRepo.loadState()).thenAnswer((_) async => before); @@ -120,38 +90,14 @@ void _testTokenFolderNotifier() { final container = ProviderContainer(); const before = TokenFolderState( folders: [ - TokenFolder( - label: 'test1', - folderId: 1, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), - TokenFolder( - label: 'test2', - folderId: 2, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), + TokenFolder(label: 'test1', folderId: 1, isExpanded: true), + TokenFolder(label: 'test2', folderId: 2, isExpanded: true), ], ); const after = TokenFolderState( folders: [ - TokenFolder( - label: 'test1Updated', - folderId: 1, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), - TokenFolder( - label: 'test2Updated', - folderId: 2, - isExpanded: true, - isLocked: false, - sortIndex: null, - ), + TokenFolder(label: 'test1Updated', folderId: 1, isExpanded: true), + TokenFolder(label: 'test2Updated', folderId: 2, isExpanded: true), ], ); when(mockRepo.loadState()).thenAnswer((_) async => before); diff --git a/test/unit_test/utils/crypto_utils_test.dart b/test/unit_test/utils/crypto_utils_test.dart index 3dfafa7d9..955932891 100644 --- a/test/unit_test/utils/crypto_utils_test.dart +++ b/test/unit_test/utils/crypto_utils_test.dart @@ -40,17 +40,66 @@ Future generateWrapper(List l) async { void _testGeneratePhoneChecksum() { group('generatePhoneChecksum', () { - test('1. SHA-1', () async => expect(await generateWrapper([0, 1, 2, 3, 4, 5, 6]), 'NXEG6EIAAEBAGBAFAY')); - test('2. SHA-1', () async => expect(await generateWrapper([9, 8, 7, 6, 5, 4, 3, 2, 1]), 'THKHQSYJBADQMBIEAMBAC')); - test('3. SHA-1', () async => expect(await generateWrapper([3, 5, 7, 2, 3, 4, 9, 1, 0, 4, 7, 3, 5, 6]), 'TGEEJ7QDAUDQEAYEBEAQABAHAMCQM')); - test('4. SHA-1', () async => expect(await generateWrapper([9, 5, 8, 1, 7, 3]), '2DO4TDAJAUEACBYD')); - test('5. SHA-1', () async => expect(await generateWrapper([1, 0, 2, 9, 3, 8, 4, 7, 5, 6]), 'ZOOALWIBAABASAYIAQDQKBQ')); + test( + '1. SHA-1', + () async => expect( + await generateWrapper([0, 1, 2, 3, 4, 5, 6]), + 'NXEG6EIAAEBAGBAFAY', + ), + ); + test( + '2. SHA-1', + () async => expect( + await generateWrapper([9, 8, 7, 6, 5, 4, 3, 2, 1]), + 'THKHQSYJBADQMBIEAMBAC', + ), + ); + test( + '3. SHA-1', + () async => expect( + await generateWrapper([3, 5, 7, 2, 3, 4, 9, 1, 0, 4, 7, 3, 5, 6]), + 'TGEEJ7QDAUDQEAYEBEAQABAHAMCQM', + ), + ); + test( + '4. SHA-1', + () async => + expect(await generateWrapper([9, 5, 8, 1, 7, 3]), '2DO4TDAJAUEACBYD'), + ); + test( + '5. SHA-1', + () async => expect( + await generateWrapper([1, 0, 2, 9, 3, 8, 4, 7, 5, 6]), + 'ZOOALWIBAABASAYIAQDQKBQ', + ), + ); }); } void _testPbkdf2() { group('pbkdf2', () { - Uint8List password = Uint8List.fromList([4, 142, 237, 243, 55, 58, 148, 100, 127, 56, 11, 99, 75, 217, 3, 59, 121, 167, 42, 164]); + Uint8List password = Uint8List.fromList([ + 4, + 142, + 237, + 243, + 55, + 58, + 148, + 100, + 127, + 56, + 11, + 99, + 75, + 217, + 3, + 59, + 121, + 167, + 42, + 164, + ]); Uint8List salt = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); int iterations = 10000; int keyLen = 20; @@ -59,52 +108,224 @@ void _testPbkdf2() { test( 'Pwd 1', () async => expect( - await pbkdf2( - password: Uint8List.fromList([204, 142, 237, 243, 154, 5, 48, 206, 127, 56, 11, 156, 75, 217, 116, 59, 121, 67, 152, 46]), - keyLength: keyLen, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([105, 176, 234, 116, 177, 125, 213, 148, 111, 87, 172, 184, 141, 16, 185, 208, 250, 127, 212, 64])), + await pbkdf2( + password: Uint8List.fromList([ + 204, + 142, + 237, + 243, + 154, + 5, + 48, + 206, + 127, + 56, + 11, + 156, + 75, + 217, + 116, + 59, + 121, + 67, + 152, + 46, + ]), + keyLength: keyLen, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 105, + 176, + 234, + 116, + 177, + 125, + 213, + 148, + 111, + 87, + 172, + 184, + 141, + 16, + 185, + 208, + 250, + 127, + 212, + 64, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Pwd 2', () async => expect( - await pbkdf2( - password: Uint8List.fromList([66, 142, 237, 243, 12, 5, 48, 206, 127, 56, 11, 99, 75, 217, 116, 59, 121, 167, 152, 4]), - keyLength: keyLen, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([11, 157, 107, 247, 204, 194, 23, 69, 211, 238, 200, 86, 38, 234, 99, 227, 247, 44, 220, 135])), + await pbkdf2( + password: Uint8List.fromList([ + 66, + 142, + 237, + 243, + 12, + 5, + 48, + 206, + 127, + 56, + 11, + 99, + 75, + 217, + 116, + 59, + 121, + 167, + 152, + 4, + ]), + keyLength: keyLen, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 11, + 157, + 107, + 247, + 204, + 194, + 23, + 69, + 211, + 238, + 200, + 86, + 38, + 234, + 99, + 227, + 247, + 44, + 220, + 135, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Pwd 3', () async => expect( - await pbkdf2( - password: Uint8List.fromList([222, 142, 237, 243, 55, 5, 48, 0, 127, 56, 11, 99, 75, 217, 3, 59, 121, 167, 152, 164]), - keyLength: keyLen, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([57, 88, 51, 7, 80, 51, 239, 58, 125, 6, 80, 79, 80, 62, 16, 0, 255, 245, 137, 168])), + await pbkdf2( + password: Uint8List.fromList([ + 222, + 142, + 237, + 243, + 55, + 5, + 48, + 0, + 127, + 56, + 11, + 99, + 75, + 217, + 3, + 59, + 121, + 167, + 152, + 164, + ]), + keyLength: keyLen, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 57, + 88, + 51, + 7, + 80, + 51, + 239, + 58, + 125, + 6, + 80, + 79, + 80, + 62, + 16, + 0, + 255, + 245, + 137, + 168, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Pwd 4', () async => expect( - await pbkdf2( - password: Uint8List.fromList([4, 142, 237, 243, 55, 58, 148, 100, 127, 56, 11, 99, 75, 217, 3, 59, 121, 167, 42, 164]), - keyLength: keyLen, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([135, 33, 148, 191, 86, 136, 13, 50, 14, 0, 188, 246, 48, 26, 209, 229, 68, 239, 111, 221])), + await pbkdf2( + password: Uint8List.fromList([ + 4, + 142, + 237, + 243, + 55, + 58, + 148, + 100, + 127, + 56, + 11, + 99, + 75, + 217, + 3, + 59, + 121, + 167, + 42, + 164, + ]), + keyLength: keyLen, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); }); @@ -113,283 +334,537 @@ void _testPbkdf2() { test( 'Salt 1', () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: iterations, - salt: Uint8List.fromList([0, 0, 0, 0, 0, 0, 0, 0]), - ), - Uint8List.fromList([0, 149, 53, 169, 140, 36, 152, 54, 213, 123, 214, 14, 11, 199, 89, 78, 180, 108, 104, 177])), + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: iterations, + salt: Uint8List.fromList([0, 0, 0, 0, 0, 0, 0, 0]), + ), + Uint8List.fromList([ + 0, + 149, + 53, + 169, + 140, + 36, + 152, + 54, + 213, + 123, + 214, + 14, + 11, + 199, + 89, + 78, + 180, + 108, + 104, + 177, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Salt 2', () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: iterations, - salt: Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]), - ), - Uint8List.fromList([135, 33, 148, 191, 86, 136, 13, 50, 14, 0, 188, 246, 48, 26, 209, 229, 68, 239, 111, 221])), + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: iterations, + salt: Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]), + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Salt 3', () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: iterations, - salt: Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5]), - ), - Uint8List.fromList([29, 98, 40, 192, 122, 52, 24, 18, 189, 124, 119, 99, 251, 64, 81, 75, 149, 176, 77, 210])), + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: iterations, + salt: Uint8List.fromList([ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 1, + 2, + 3, + 4, + 5, + ]), + ), + Uint8List.fromList([ + 29, + 98, + 40, + 192, + 122, + 52, + 24, + 18, + 189, + 124, + 119, + 99, + 251, + 64, + 81, + 75, + 149, + 176, + 77, + 210, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Salt 4', () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: iterations, - salt: Uint8List.fromList([42, 42, 42, 5, 6, 7, 8, 42]), - ), - Uint8List.fromList([196, 70, 123, 140, 14, 167, 102, 50, 223, 223, 120, 158, 35, 10, 215, 202, 117, 26, 85, 46])), + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: iterations, + salt: Uint8List.fromList([42, 42, 42, 5, 6, 7, 8, 42]), + ), + Uint8List.fromList([ + 196, + 70, + 123, + 140, + 14, + 167, + 102, + 50, + 223, + 223, + 120, + 158, + 35, + 10, + 215, + 202, + 117, + 26, + 85, + 46, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); }); - group( - 'Different iterations', - () { - test( - '100', - () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: 100, - salt: salt, - ), - Uint8List.fromList([126, 248, 52, 21, 94, 28, 200, 201, 165, 237, 0, 31, 10, 157, 59, 76, 63, 189, 247, 132])), - timeout: const Timeout(Duration(seconds: 60)), - ); - - test( - '1000', - () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: 1000, - salt: salt, - ), - Uint8List.fromList([70, 150, 241, 120, 152, 55, 135, 238, 232, 88, 94, 42, 245, 251, 156, 76, 165, 128, 102, 119])), - timeout: const Timeout(Duration(seconds: 60)), - ); - - test( - '10 000', - () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: 10000, - salt: salt, - ), - Uint8List.fromList([135, 33, 148, 191, 86, 136, 13, 50, 14, 0, 188, 246, 48, 26, 209, 229, 68, 239, 111, 221])), - timeout: const Timeout(Duration(seconds: 60)), - ); - - test( - '100 000', - () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: 100000, - salt: salt, - ), - Uint8List.fromList([60, 246, 237, 212, 183, 224, 78, 28, 204, 190, 27, 137, 164, 163, 80, 89, 21, 81, 244, 109])), - timeout: const Timeout(Duration(seconds: 60)), - ); - - test( - '1 000 000', - () async => expect( - await pbkdf2( - password: password, - keyLength: keyLen, - iterations: 1000000, - salt: salt, - ), - Uint8List.fromList([25, 39, 153, 115, 182, 177, 160, 241, 96, 198, 31, 79, 145, 109, 102, 47, 205, 167, 246, 253])), - timeout: const Timeout(Duration(seconds: 60)), - ); - }, - ); + group('Different iterations', () { + test( + '100', + () async => expect( + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: 100, + salt: salt, + ), + Uint8List.fromList([ + 126, + 248, + 52, + 21, + 94, + 28, + 200, + 201, + 165, + 237, + 0, + 31, + 10, + 157, + 59, + 76, + 63, + 189, + 247, + 132, + ]), + ), + timeout: const Timeout(Duration(seconds: 60)), + ); + + test( + '1000', + () async => expect( + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: 1000, + salt: salt, + ), + Uint8List.fromList([ + 70, + 150, + 241, + 120, + 152, + 55, + 135, + 238, + 232, + 88, + 94, + 42, + 245, + 251, + 156, + 76, + 165, + 128, + 102, + 119, + ]), + ), + timeout: const Timeout(Duration(seconds: 60)), + ); + + test( + '10 000', + () async => expect( + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: 10000, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + ]), + ), + timeout: const Timeout(Duration(seconds: 60)), + ); + + test( + '100 000', + () async => expect( + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: 100000, + salt: salt, + ), + Uint8List.fromList([ + 60, + 246, + 237, + 212, + 183, + 224, + 78, + 28, + 204, + 190, + 27, + 137, + 164, + 163, + 80, + 89, + 21, + 81, + 244, + 109, + ]), + ), + timeout: const Timeout(Duration(seconds: 60)), + ); + + test( + '1 000 000', + () async => expect( + await pbkdf2( + password: password, + keyLength: keyLen, + iterations: 1000000, + salt: salt, + ), + Uint8List.fromList([ + 25, + 39, + 153, + 115, + 182, + 177, + 160, + 241, + 96, + 198, + 31, + 79, + 145, + 109, + 102, + 47, + 205, + 167, + 246, + 253, + ]), + ), + timeout: const Timeout(Duration(seconds: 60)), + ); + }); group('Different output lengths', () { test( - 'Key lenght 1', - () async => expect( - await pbkdf2( - password: password, - keyLength: 1, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([135]))); + 'Key lenght 1', + () async => expect( + await pbkdf2( + password: password, + keyLength: 1, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([135]), + ), + ); test( - 'Key lenght 5', - () async => expect( - await pbkdf2( - password: password, - keyLength: 5, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([135, 33, 148, 191, 86]))); + 'Key lenght 5', + () async => expect( + await pbkdf2( + password: password, + keyLength: 5, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([135, 33, 148, 191, 86]), + ), + ); test( - 'Key lenght 12', - () async => expect( - await pbkdf2( - password: password, - keyLength: 12, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([135, 33, 148, 191, 86, 136, 13, 50, 14, 0, 188, 246]))); + 'Key lenght 12', + () async => expect( + await pbkdf2( + password: password, + keyLength: 12, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + ]), + ), + ); test( - 'Key lenght 20', - () async => expect( - await pbkdf2( - password: password, - keyLength: 20, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([135, 33, 148, 191, 86, 136, 13, 50, 14, 0, 188, 246, 48, 26, 209, 229, 68, 239, 111, 221]))); + 'Key lenght 20', + () async => expect( + await pbkdf2( + password: password, + keyLength: 20, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + ]), + ), + ); test( 'Key lenght 33', () async => expect( - await pbkdf2( - password: password, - keyLength: 33, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([ - 135, - 33, - 148, - 191, - 86, - 136, - 13, - 50, - 14, - 0, - 188, - 246, - 48, - 26, - 209, - 229, - 68, - 239, - 111, - 221, - 6, - 22, - 78, - 185, - 134, - 87, - 110, - 131, - 183, - 7, - 5, - 208, - 219 - ])), + await pbkdf2( + password: password, + keyLength: 33, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + 6, + 22, + 78, + 185, + 134, + 87, + 110, + 131, + 183, + 7, + 5, + 208, + 219, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); test( 'Key lenght 55', () async => expect( - await pbkdf2( - password: password, - keyLength: 55, - iterations: iterations, - salt: salt, - ), - Uint8List.fromList([ - 135, - 33, - 148, - 191, - 86, - 136, - 13, - 50, - 14, - 0, - 188, - 246, - 48, - 26, - 209, - 229, - 68, - 239, - 111, - 221, - 6, - 22, - 78, - 185, - 134, - 87, - 110, - 131, - 183, - 7, - 5, - 208, - 219, - 82, - 16, - 35, - 40, - 99, - 223, - 134, - 45, - 102, - 101, - 59, - 19, - 20, - 47, - 119, - 212, - 164, - 58, - 255, - 137, - 22, - 83 - ])), + await pbkdf2( + password: password, + keyLength: 55, + iterations: iterations, + salt: salt, + ), + Uint8List.fromList([ + 135, + 33, + 148, + 191, + 86, + 136, + 13, + 50, + 14, + 0, + 188, + 246, + 48, + 26, + 209, + 229, + 68, + 239, + 111, + 221, + 6, + 22, + 78, + 185, + 134, + 87, + 110, + 131, + 183, + 7, + 5, + 208, + 219, + 82, + 16, + 35, + 40, + 99, + 223, + 134, + 45, + 102, + 101, + 59, + 19, + 20, + 47, + 119, + 212, + 164, + 58, + 255, + 137, + 22, + 83, + ]), + ), timeout: const Timeout(Duration(seconds: 60)), ); }); @@ -414,13 +889,25 @@ void _testDecodeSecretToUint8() { }); test('Test base32 secret', () { - expect(Encodings.base32.decode('OBZGS5TBMN4Q===='), Uint8List.fromList([112, 114, 105, 118, 97, 99, 121])); - expect(Encodings.base32.decode('JFCEKQI='), Uint8List.fromList([73, 68, 69, 65])); + expect( + Encodings.base32.decode('OBZGS5TBMN4Q===='), + Uint8List.fromList([112, 114, 105, 118, 97, 99, 121]), + ); + expect( + Encodings.base32.decode('JFCEKQI='), + Uint8List.fromList([73, 68, 69, 65]), + ); }); test('Test utf-8 secret', () { - expect(Encodings.none.decode('ABCD'), Uint8List.fromList([65, 66, 67, 68])); - expect(Encodings.none.decode('DEG3'), Uint8List.fromList([68, 69, 71, 51])); + expect( + Encodings.none.decode('ABCD'), + Uint8List.fromList([65, 66, 67, 68]), + ); + expect( + Encodings.none.decode('DEG3'), + Uint8List.fromList([68, 69, 71, 51]), + ); }); }); } @@ -433,13 +920,27 @@ void _testEncodeSecretAs() { }); test('Test base32 secret', () { - expect(Encodings.base32.encode(Uint8List.fromList([112, 114, 105, 118, 97, 99, 121])), 'OBZGS5TBMN4Q===='); - expect(Encodings.base32.encode(Uint8List.fromList([73, 68, 69, 65])), 'JFCEKQI='); + expect( + Encodings.base32.encode( + Uint8List.fromList([112, 114, 105, 118, 97, 99, 121]), + ), + 'OBZGS5TBMN4Q====', + ); + expect( + Encodings.base32.encode(Uint8List.fromList([73, 68, 69, 65])), + 'JFCEKQI=', + ); }); test('Test utf-8 secret', () { - expect(Encodings.none.encode(Uint8List.fromList([65, 66, 67, 68])), 'ABCD'); - expect(Encodings.none.encode(Uint8List.fromList([68, 69, 71, 51])), 'DEG3'); + expect( + Encodings.none.encode(Uint8List.fromList([65, 66, 67, 68])), + 'ABCD', + ); + expect( + Encodings.none.encode(Uint8List.fromList([68, 69, 71, 51])), + 'DEG3', + ); }); }); } @@ -447,13 +948,26 @@ void _testEncodeSecretAs() { void _testIsValidEncoding() { group('isValidEncoding', () { group('valid encodings', () { - test('valid hex', () => expect(Encodings.hex.isValidEncoding('abcd'), true)); - test('valid base32', () => expect(Encodings.base32.isValidEncoding('OBZGS5TBMN4Q===='), true)); + test( + 'valid hex', + () => expect(Encodings.hex.isValidEncoding('abcd'), true), + ); + test( + 'valid base32', + () => + expect(Encodings.base32.isValidEncoding('OBZGS5TBMN4Q===='), true), + ); }); group('invalid encodings', () { - test('invalid hex', () => expect(Encodings.hex.isValidEncoding('RXYZ'), false)); - test('invalid base32', () => expect(Encodings.base32.isValidEncoding('????'), false)); + test( + 'invalid hex', + () => expect(Encodings.hex.isValidEncoding('RXYZ'), false), + ); + test( + 'invalid base32', + () => expect(Encodings.base32.isValidEncoding('????'), false), + ); }); }); } diff --git a/test/unit_test/utils/custom_int_buffer_test.dart b/test/unit_test/utils/custom_int_buffer_test.dart index 18504bcb0..c8435d30c 100644 --- a/test/unit_test/utils/custom_int_buffer_test.dart +++ b/test/unit_test/utils/custom_int_buffer_test.dart @@ -42,7 +42,10 @@ void verifyCustomStringBufferWorks() { expect(buffer3_30.contains(3), true); expect(buffer3_30.contains(4), false); - final values = List.generate(buffer3_30.maxSize - buffer3_30.length, (index) => 0 - index); // 27 elements + final values = List.generate( + buffer3_30.maxSize - buffer3_30.length, + (index) => 0 - index, + ); // 27 elements final buffer30_30 = buffer3_30.putList(values); // full buffer 30/30 @@ -61,14 +64,14 @@ void verifyCustomStringBufferWorks() { final json = buffer.toJson(); expect(json, { 'maxSize': 30, - 'list': [1, 2, 3] + 'list': [1, 2, 3], }); }); test('fromJson', () { final buffer = CustomIntBuffer.fromJson({ 'maxSize': 30, - 'list': [1, 2, 3] + 'list': [1, 2, 3], }); expect(buffer.maxSize, 30); expect(buffer.length, 3); diff --git a/test/unit_test/utils/firebase_utils_test.dart b/test/unit_test/utils/firebase_utils_test.dart index 7646973ca..61e9691c1 100644 --- a/test/unit_test/utils/firebase_utils_test.dart +++ b/test/unit_test/utils/firebase_utils_test.dart @@ -26,15 +26,26 @@ void main() { setUp(() { mockStorage = MockFlutterSecureStorage(); mockLegacyStorage = MockFlutterSecureStorage(); - final storage = SecureStorage(storagePrefix: FirebaseUtils.FIREBASE_TOKEN_KEY_PREFIX, storage: mockStorage); - final legacyStorage = SecureStorage(storagePrefix: FirebaseUtils.FIREBASE_TOKEN_KEY_PREFIX_LEGACY, storage: mockLegacyStorage); - firebaseUtils = FirebaseUtils(storage: storage, legacyStorage: legacyStorage); + final storage = SecureStorage( + storagePrefix: FirebaseUtils.FIREBASE_TOKEN_KEY_PREFIX, + storage: mockStorage, + ); + final legacyStorage = SecureStorage( + storagePrefix: FirebaseUtils.FIREBASE_TOKEN_KEY_PREFIX_LEGACY, + storage: mockLegacyStorage, + ); + firebaseUtils = FirebaseUtils( + storage: storage, + legacyStorage: legacyStorage, + ); }); group('FirebaseUtils SecureStorage', () { group('getCurrentFirebaseToken', () { test('returns token from new storage if available', () async { - when(mockStorage.read(key: fullCurrentNewKey)).thenAnswer((_) async => 'new_token'); + when( + mockStorage.read(key: fullCurrentNewKey), + ).thenAnswer((_) async => 'new_token'); final token = await firebaseUtils.getCurrentFirebaseToken(); @@ -43,23 +54,40 @@ void main() { verifyNever(mockLegacyStorage.read(key: anyNamed('key'))); }); - test('migrates token from legacy storage if new one is not available', () async { - when(mockStorage.read(key: fullCurrentNewKey)).thenAnswer((_) async => null); - when(mockLegacyStorage.read(key: fullCurrentLegacyKey)).thenAnswer((_) async => 'legacy_token'); - when(mockStorage.write(key: fullCurrentNewKey, value: 'legacy_token')).thenAnswer((_) async {}); - when(mockLegacyStorage.delete(key: fullCurrentLegacyKey)).thenAnswer((_) async {}); - - final token = await firebaseUtils.getCurrentFirebaseToken(); - - expect(token, 'legacy_token'); - verify(mockLegacyStorage.read(key: fullCurrentLegacyKey)).called(1); - verify(mockStorage.write(key: fullCurrentNewKey, value: 'legacy_token')).called(1); - verify(mockLegacyStorage.delete(key: fullCurrentLegacyKey)).called(1); - }); + test( + 'migrates token from legacy storage if new one is not available', + () async { + when( + mockStorage.read(key: fullCurrentNewKey), + ).thenAnswer((_) async => null); + when( + mockLegacyStorage.read(key: fullCurrentLegacyKey), + ).thenAnswer((_) async => 'legacy_token'); + when( + mockStorage.write(key: fullCurrentNewKey, value: 'legacy_token'), + ).thenAnswer((_) async {}); + when( + mockLegacyStorage.delete(key: fullCurrentLegacyKey), + ).thenAnswer((_) async {}); + + final token = await firebaseUtils.getCurrentFirebaseToken(); + + expect(token, 'legacy_token'); + verify(mockLegacyStorage.read(key: fullCurrentLegacyKey)).called(1); + verify( + mockStorage.write(key: fullCurrentNewKey, value: 'legacy_token'), + ).called(1); + verify(mockLegacyStorage.delete(key: fullCurrentLegacyKey)).called(1); + }, + ); test('returns null if no token is available', () async { - when(mockStorage.read(key: fullCurrentNewKey)).thenAnswer((_) async => null); - when(mockLegacyStorage.read(key: fullCurrentLegacyKey)).thenAnswer((_) async => null); + when( + mockStorage.read(key: fullCurrentNewKey), + ).thenAnswer((_) async => null); + when( + mockLegacyStorage.read(key: fullCurrentLegacyKey), + ).thenAnswer((_) async => null); final token = await firebaseUtils.getCurrentFirebaseToken(); @@ -69,34 +97,52 @@ void main() { group('getNewFirebaseToken', () { test('migrates token from legacy storage', () async { - when(mockStorage.read(key: fullNewNewKey)).thenAnswer((_) async => null); - when(mockLegacyStorage.read(key: fullNewLegacyKey)).thenAnswer((_) async => 'legacy_new_token'); - when(mockStorage.write(key: fullNewNewKey, value: 'legacy_new_token')).thenAnswer((_) async {}); - when(mockLegacyStorage.delete(key: fullNewLegacyKey)).thenAnswer((_) async {}); + when( + mockStorage.read(key: fullNewNewKey), + ).thenAnswer((_) async => null); + when( + mockLegacyStorage.read(key: fullNewLegacyKey), + ).thenAnswer((_) async => 'legacy_new_token'); + when( + mockStorage.write(key: fullNewNewKey, value: 'legacy_new_token'), + ).thenAnswer((_) async {}); + when( + mockLegacyStorage.delete(key: fullNewLegacyKey), + ).thenAnswer((_) async {}); final token = await firebaseUtils.getNewFirebaseToken(); expect(token, 'legacy_new_token'); verify(mockLegacyStorage.read(key: fullNewLegacyKey)).called(1); - verify(mockStorage.write(key: fullNewNewKey, value: 'legacy_new_token')).called(1); + verify( + mockStorage.write(key: fullNewNewKey, value: 'legacy_new_token'), + ).called(1); verify(mockLegacyStorage.delete(key: fullNewLegacyKey)).called(1); }); }); test('setCurrentFirebaseToken writes to new storage', () async { - when(mockStorage.write(key: fullCurrentNewKey, value: 'test_token')).thenAnswer((_) async {}); + when( + mockStorage.write(key: fullCurrentNewKey, value: 'test_token'), + ).thenAnswer((_) async {}); await firebaseUtils.setCurrentFirebaseToken('test_token'); - verify(mockStorage.write(key: fullCurrentNewKey, value: 'test_token')).called(1); + verify( + mockStorage.write(key: fullCurrentNewKey, value: 'test_token'), + ).called(1); }); test('setNewFirebaseToken writes to new storage', () async { - when(mockStorage.write(key: fullNewNewKey, value: 'test_token')).thenAnswer((_) async {}); + when( + mockStorage.write(key: fullNewNewKey, value: 'test_token'), + ).thenAnswer((_) async {}); await firebaseUtils.setNewFirebaseToken('test_token'); - verify(mockStorage.write(key: fullNewNewKey, value: 'test_token')).called(1); + verify( + mockStorage.write(key: fullNewNewKey, value: 'test_token'), + ).called(1); }); }); } diff --git a/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart index 16ae69cde..18a2a848a 100644 --- a/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart +++ b/test/unit_test/utils/riverpod/riverpod_providers/generated_providers/token_notifier_test.dart @@ -135,7 +135,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; final after = before; @@ -238,7 +237,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), HOTPToken( label: 'label2', @@ -247,7 +245,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', - counter: 0, ), ]; final after = [ @@ -258,7 +255,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -306,7 +302,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; final after = [ @@ -317,7 +312,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), HOTPToken( label: 'label2', @@ -326,7 +320,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', - counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -375,7 +368,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), HOTPToken( label: 'label2', @@ -384,7 +376,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret2', - counter: 0, ), ]; final after = [ @@ -395,7 +386,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), HOTPToken( label: 'labelUpdated', @@ -404,7 +394,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA256, digits: 8, secret: 'secret2Updated', - counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -455,7 +444,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; final after = [ @@ -466,7 +454,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), HOTPToken( label: 'label2', @@ -475,7 +462,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA256, digits: 6, secret: 'secret2', - counter: 0, ), HOTPToken( label: 'label3', @@ -484,7 +470,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA512, digits: 8, secret: 'secret3', - counter: 0, ), ]; when(mockRepo.loadTokens()).thenAnswer((_) async => before); @@ -524,7 +509,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; final after = [ @@ -535,7 +519,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), TOTPToken( label: 'label2', @@ -615,7 +598,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; final pushTokenShouldBe = PushToken( @@ -623,9 +605,6 @@ void _testTokenNotifier() { issuer: 'privacyIDEA', id: '20663f77-a26e-41c3-8946-d0efb8b386d3', pin: false, - tokenImage: null, - sortIndex: null, - folderId: null, serial: 'PIPU0006BF18', sslVerify: false, enrollmentCredentials: 'ae60d4744ac5384515574b85f538c6a4e0c7bc82', @@ -645,7 +624,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), pushTokenShouldBe, ]; @@ -828,7 +806,6 @@ void _testTokenNotifier() { algorithm: Algorithms.SHA1, digits: 6, secret: 'secret', - counter: 0, ), ]; when(mockRepo.saveOrReplaceTokens(any)).thenAnswer((_) async => []); diff --git a/test/unit_test/utils/rsa_utils_test.dart b/test/unit_test/utils/rsa_utils_test.dart index 3cfcab02a..afdcd05ad 100644 --- a/test/unit_test/utils/rsa_utils_test.dart +++ b/test/unit_test/utils/rsa_utils_test.dart @@ -12,10 +12,15 @@ void _testSerializingRSAKeys() { group('PKCS#1 format', () { const rsaUtils = RsaUtils(); test('Converting key', () async { - RSAPublicKey publicKey = RSAPublicKey(BigInt.from(431254), BigInt.from(32545)); + RSAPublicKey publicKey = RSAPublicKey( + BigInt.from(431254), + BigInt.from(32545), + ); String base64String = rsaUtils.serializeRSAPublicKeyPKCS1(publicKey); - RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS1(base64String); + RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS1( + base64String, + ); expect(publicKey.modulus, convertedKey.modulus); expect(publicKey.exponent, convertedKey.exponent); @@ -26,14 +31,17 @@ void _testSerializingRSAKeys() { RSAPublicKey publicKey = asymmetricKeyPair.publicKey; String base64String = rsaUtils.serializeRSAPublicKeyPKCS1(publicKey); - RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS1(base64String); + RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS1( + base64String, + ); expect(publicKey.modulus, convertedKey.modulus); expect(publicKey.exponent, convertedKey.exponent); }, timeout: const Timeout(Duration(seconds: 60))); test('Parsing existing key', () async { - String serializedPublicKey = 'MIICCgKCAgEAtOE6hDrwB+9Quk5Ibp9DduUMAmQ' + String serializedPublicKey = + 'MIICCgKCAgEAtOE6hDrwB+9Quk5Ibp9DduUMAmQ' 'i3KSn4pSZPrj4vhx9COenh+K6NtWFDwSPZcEOMk/s7GXsgAzdQvUVp4KpmBSAL3C' 'XgwZrhG4DZWRvXhB4P0Toxz1McVnPvabriWqU1L3Jorca1bnlvaaYh9rywbBrxes' 'IA4VUmfFoWHpn+HMdYp4g2UG1UeBIqBsgI4syPiwlEDW6sWTeSDcvQWTYGBsHMXf' @@ -46,17 +54,27 @@ void _testSerializingRSAKeys() { 'WBf1mlwUNh1Vuu+ZGdTQKisxI4G8k2dZrlTWkQqOmLebCE3L38jnh0Oek+Jl9fNm' 'TcMl8sPWxB8lgGpUCAwEAAQ=='; - expect(rsaUtils.serializeRSAPublicKeyPKCS1(rsaUtils.deserializeRSAPublicKeyPKCS1(serializedPublicKey)), serializedPublicKey); + expect( + rsaUtils.serializeRSAPublicKeyPKCS1( + rsaUtils.deserializeRSAPublicKeyPKCS1(serializedPublicKey), + ), + serializedPublicKey, + ); }, timeout: const Timeout(Duration(seconds: 60))); }); group('PKCS#8 format', () { const rsaUtils = RsaUtils(); test('Converting key', () async { - RSAPublicKey publicKey = RSAPublicKey(BigInt.from(431254), BigInt.from(32545)); + RSAPublicKey publicKey = RSAPublicKey( + BigInt.from(431254), + BigInt.from(32545), + ); String base64String = rsaUtils.serializeRSAPublicKeyPKCS8(publicKey); - RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS8(base64String); + RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS8( + base64String, + ); expect(publicKey.modulus, convertedKey.modulus); expect(publicKey.exponent, convertedKey.exponent); @@ -67,14 +85,17 @@ void _testSerializingRSAKeys() { RSAPublicKey publicKey = asymmetricKeyPair.publicKey; String base64String = rsaUtils.serializeRSAPublicKeyPKCS8(publicKey); - RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS8(base64String); + RSAPublicKey convertedKey = rsaUtils.deserializeRSAPublicKeyPKCS8( + base64String, + ); expect(publicKey.modulus, convertedKey.modulus); expect(publicKey.exponent, convertedKey.exponent); }, timeout: const Timeout(Duration(seconds: 60))); test('Parse existing key', () async { - String serializedPublicKey = 'MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCA' + String serializedPublicKey = + 'MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCA' 'gEAwdxugfnlsrd3rwZsEvI8GzEF4BtGEK3+vXRWVv43Z0Itn9NAtN5TWYgUkI/1RdI' 'ahWSZ8xM8vqza3Vb6SzI/vzw4O22TvFwNGDQcwIpxf/I0Iow+U/0uA0VFH2nPdyeJw' 'eNjEFaPkIZEHSyJ0CUtNS2umXpx4IyUN2R9Xve4OddbUpfTFPDYdcOiqPn1IkVLan/' @@ -87,16 +108,24 @@ void _testSerializingRSAKeys() { 'nuwUCqJvPlKJHd/ikm2OfQS+BsPH8HDvrQGQyHyzBzV20oRfNGPIXVOXc9AEIJAPxB' 'QYQE2aoTR+l7N4On4x59z8qU1UCAwEAAQ=='; - expect(rsaUtils.serializeRSAPublicKeyPKCS8(rsaUtils.deserializeRSAPublicKeyPKCS8(serializedPublicKey)), serializedPublicKey); + expect( + rsaUtils.serializeRSAPublicKeyPKCS8( + rsaUtils.deserializeRSAPublicKeyPKCS8(serializedPublicKey), + ), + serializedPublicKey, + ); }, timeout: const Timeout(Duration(seconds: 60))); }); group('Serialize RSA private keys', () { const rsaUtils = RsaUtils(); test('Converting key', () async { - RSAPrivateKey privateKey = (await rsaUtils.generateRSAKeyPair()).privateKey; + RSAPrivateKey privateKey = + (await rsaUtils.generateRSAKeyPair()).privateKey; String base64String = rsaUtils.serializeRSAPrivateKeyPKCS1(privateKey); - RSAPrivateKey convertedKey = rsaUtils.deserializeRSAPrivateKeyPKCS1(base64String); + RSAPrivateKey convertedKey = rsaUtils.deserializeRSAPrivateKeyPKCS1( + base64String, + ); expect(privateKey.modulus, convertedKey.modulus); expect(privateKey.exponent, convertedKey.exponent); @@ -114,9 +143,15 @@ void _testSerializingRSAKeys() { String message = 'I am a signature.'; - var signature = rsaUtils.createRSASignature(privateKey, utf8.encode(message)); + var signature = rsaUtils.createRSASignature( + privateKey, + utf8.encode(message), + ); - expect(true, rsaUtils.verifyRSASignature(publicKey, utf8.encode(message), signature)); + expect( + true, + rsaUtils.verifyRSASignature(publicKey, utf8.encode(message), signature), + ); }, timeout: const Timeout(Duration(minutes: 5))); test('Signature is invalid', () async { @@ -126,9 +161,19 @@ void _testSerializingRSAKeys() { String message = 'I am a signature.'; - var signature = rsaUtils.createRSASignature(privateKey, utf8.encode(message)); - - expect(false, rsaUtils.verifyRSASignature(publicKey, utf8.encode('I am not the signature you are looking for.'), signature)); + var signature = rsaUtils.createRSASignature( + privateKey, + utf8.encode(message), + ); + + expect( + false, + rsaUtils.verifyRSASignature( + publicKey, + utf8.encode('I am not the signature you are looking for.'), + signature, + ), + ); }, timeout: const Timeout(Duration(minutes: 5))); }); } diff --git a/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart b/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart index 6b3643252..c2dd26881 100644 --- a/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart +++ b/test/unit_test/views/container_view/container_widgets/buttons/rollover_container_tokens_button_test.dart @@ -29,7 +29,7 @@ import 'package:privacyidea_authenticator/model/token_container.dart'; import 'package:privacyidea_authenticator/utils/ecc_utils.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import 'package:privacyidea_authenticator/views/container_view/container_widgets/buttons/rollover_container_tokens_button.dart'; -import 'package:privacyidea_authenticator/widgets/button_widgets/time_guarded_button.dart'; +import 'package:privacyidea_authenticator/widgets/button_widgets/intent_button.dart'; import '../../../../../tests_app_wrapper.mocks.dart'; @@ -67,30 +67,22 @@ void main() { group('RolloverContainerTokensButton - SyncState Corners', () { testWidgets('should be enabled when state is notStarted', (tester) async { await pumpButton(tester, state: SyncState.notStarted); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNotNull); }); testWidgets('should be disabled when state is syncing', (tester) async { await pumpButton(tester, state: SyncState.syncing); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNull); }); testWidgets('should be enabled when state is completed', (tester) async { await pumpButton(tester, state: SyncState.completed); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNotNull); }); testWidgets('should be enabled when state is failed', (tester) async { await pumpButton(tester, state: SyncState.failed); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNotNull); }); testWidgets( @@ -101,9 +93,7 @@ void main() { state: SyncState.notStarted, containerExists: false, ); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNull); }, ); diff --git a/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart index e1b278c02..b16e94698 100644 --- a/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart +++ b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart @@ -16,7 +16,7 @@ import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/gene import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/rsa_utils.dart'; import 'package:privacyidea_authenticator/views/container_view/container_widgets/buttons/sync_container_button.dart'; -import 'package:privacyidea_authenticator/widgets/button_widgets/time_guarded_button.dart'; +import 'package:privacyidea_authenticator/widgets/button_widgets/intent_button.dart'; import '../../../../../tests_app_wrapper.dart'; import '../../../../../tests_app_wrapper.mocks.dart'; @@ -102,9 +102,7 @@ void main() { debugPrint(tester.allWidgets.toString()); - debugPrint( - 'Looking for TimeGuardedButton: ${find.byType(TimeGuardedButton)}', - ); + debugPrint('Looking for IntentButton: ${find.byType(IntentButton)}'); // Warte kurz auf das Layout await tester.pumpAndSettle(); } @@ -117,7 +115,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(TimeGuardedButton), findsNothing); + expect(find.byType(IntentButton), findsNothing); expect(find.byIcon(Icons.sync), findsOneWidget); }); @@ -126,27 +124,21 @@ void main() { ) async { await pumpButton(tester, state: SyncState.notStarted); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNotNull); }); testWidgets('should be disabled when SyncState is syncing', (tester) async { await pumpButton(tester, state: SyncState.syncing); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNull); }); testWidgets('should be enabled when SyncState is failed', (tester) async { await pumpButton(tester, state: SyncState.failed); - final button = tester.widget( - find.byType(TimeGuardedButton), - ); + final button = tester.widget(find.byType(IntentButton)); expect(button.onPressed, isNotNull); }); @@ -162,7 +154,7 @@ void main() { ), ).thenAnswer((_) async => {}); - await tester.tap(find.byType(TimeGuardedButton)); + await tester.tap(find.byType(IntentButton)); await tester.pump(); verify( From 89a74fabeaaaab3f4b9faa569664629eba4c5373 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:07:43 +0200 Subject: [PATCH 09/13] feat: enhance DayPasswordTokenWidgetTile and CustomTrailing for improved state management and UI responsiveness --- .../day_password_token_widget_tile.dart | 281 +++++++++--------- lib/widgets/custom_trailing.dart | 19 +- 2 files changed, 148 insertions(+), 152 deletions(-) diff --git a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart index ea93936e3..2dd23efd4 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/day_password_token_widgets/day_password_token_widget_tile.dart @@ -27,9 +27,7 @@ import 'package:intl/intl.dart'; import '../../../../../../../utils/view_utils.dart'; import '../../../../../l10n/app_localizations.dart'; import '../../../../../model/enums/day_password_token_view_mode.dart'; -import '../../../../../model/riverpod_states/settings_state.dart'; import '../../../../../model/tokens/day_password_token.dart'; -import '../../../../../utils/riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import '../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../../../../utils/utils.dart'; import '../../../../../widgets/custom_trailing.dart'; @@ -39,6 +37,7 @@ import '../token_widget_tile.dart'; class DayPasswordTokenWidgetTile extends ConsumerStatefulWidget { final DayPasswordToken token; final bool isPreview; + const DayPasswordTokenWidgetTile( this.token, { this.isPreview = false, @@ -52,36 +51,36 @@ class DayPasswordTokenWidgetTile extends ConsumerStatefulWidget { class _DayPasswordTokenWidgetTileState extends ConsumerState { - double secondsLeft = 0; - late DateTime lastCount; + double _secondsLeft = 0; + late DateTime _lastCount; @override void initState() { super.initState(); - secondsLeft = widget.token.durationUntilNextOTP.inMilliseconds / 1000; - lastCount = DateTime.now(); + _secondsLeft = widget.token.durationUntilNextOTP.inMilliseconds / 1000; + _lastCount = DateTime.now(); _startCountDown(); } + // --- Logic --- + void _startCountDown() { - final now = DateTime.now(); - final msSinceLastCount = now.difference(lastCount).inMilliseconds; - lastCount = now; if (!mounted) return; - if (secondsLeft - (msSinceLastCount / 1000) > 0) { - setState(() => secondsLeft -= msSinceLastCount / 1000); - } else { - setState( - () => secondsLeft = - widget.token.durationUntilNextOTP.inMilliseconds / 1000, - ); - } - final msUntilNextSecond = - (secondsLeft * 1000).toInt() % 1000 + 1; // +1 to avoid 0 - Future.delayed( - Duration(milliseconds: msUntilNextSecond), - () => _startCountDown(), - ); + + final now = DateTime.now(); + final msSinceLastCount = now.difference(_lastCount).inMilliseconds; + _lastCount = now; + + setState(() { + final reduction = msSinceLastCount / 1000; + _secondsLeft = (_secondsLeft - reduction > 0) + ? _secondsLeft - reduction + : widget.token.durationUntilNextOTP.inMilliseconds / 1000; + }); + + // +1 ms to avoid 0 + final msUntilNextSecond = (_secondsLeft * 1000).toInt() % 1000 + 1; + Future.delayed(Duration(milliseconds: msUntilNextSecond), _startCountDown); } void _copyOtpValue() { @@ -89,146 +88,136 @@ class _DayPasswordTokenWidgetTileState ref.read(disableCopyOtpProvider.notifier).state = true; Clipboard.setData(ClipboardData(text: widget.token.otpValue)); + showSnackBar( AppLocalizations.of( context, )!.otpValueCopiedMessage(widget.token.otpValue), ); + Future.delayed(const Duration(seconds: 5), () { - ref.read(disableCopyOtpProvider.notifier).state = false; + if (mounted) ref.read(disableCopyOtpProvider.notifier).state = false; }); } + void _toggleViewMode() { + final newMode = widget.token.viewMode == DayPasswordTokenViewMode.VALIDFOR + ? DayPasswordTokenViewMode.VALIDUNTIL + : DayPasswordTokenViewMode.VALIDFOR; + + ref + .read(tokenProvider.notifier) + .updateToken(widget.token, (t) => t.copyWith(viewMode: newMode)); + } + + // --- UI Getters --- + + String get _semanticsLabel => widget.token.isHidden + ? AppLocalizations.of(context)!.authenticateToShowOtp + : AppLocalizations.of(context)!.copyOTPToClipboard; + + String get _title => insertCharAt( + widget.token.otpValue, + ' ', + (widget.token.digits / 2).ceil(), + ); + + VoidCallback? get _titleOnTap { + if (widget.isPreview) return null; + if (widget.token.isLocked && widget.token.isHidden) { + return () => ref.read(tokenProvider.notifier).showToken(widget.token); + } + return _copyOtpValue; + } + + List get _additionalSubtitles => widget.isPreview + ? [ + 'Algorithm: ${widget.token.algorithm.name}', + 'Period: ${widget.token.period.toString().split('.').first}', + ] + : []; + + String get _durationString => + Duration(seconds: _secondsLeft.ceil()).toString().split('.').first; + + String get _validUntilString { + final locale = Localizations.localeOf(context).languageCode; + final end = widget.token.nextOTPTimeStart; + return '${DateFormat.yMMMd(locale).format(end)}\n${DateFormat.E(locale).add_jm().format(end)}'; + } + + // --- Build --- + @override Widget build(BuildContext context) { - final currentLocale = - ref - .watch(settingsProvider) - .whenOrNull(data: (data) => data.currentLocale) ?? - SettingsState.localeDefault; - final dateTimeTokenEnd = widget.token.nextOTPTimeStart; - final yMdFormat = DateFormat.yMMMd(currentLocale.languageCode); - final yMdString = yMdFormat.format(dateTimeTokenEnd); - final ejmFormat = DateFormat.E(currentLocale.languageCode).add_jm(); - final ejmString = ejmFormat.format(dateTimeTokenEnd); - final duration = Duration(seconds: secondsLeft.ceil()); - final durationString = duration.toString().split('.').first; return TokenWidgetTile( key: Key('${widget.token.hashCode}TokenWidgetTile'), token: widget.token, - semanticsLabel: widget.token.isHidden - ? AppLocalizations.of(context)!.authenticateToShowOtp - : AppLocalizations.of(context)!.copyOTPToClipboard, - titleOnTap: widget.isPreview - ? null - : widget.token.isLocked && widget.token.isHidden - ? () async => - await ref.read(tokenProvider.notifier).showToken(widget.token) - : _copyOtpValue, - title: insertCharAt( - widget.token.otpValue, - ' ', - (widget.token.digits / 2).ceil(), - ), - additionalSubtitles: widget.isPreview - ? [ - 'Algorithm: ${widget.token.algorithm.name}', - 'Period: ${widget.token.period.toString().split('.').first}', - ] - : [], - trailing: SizedBox( - height: double.maxFinite, - child: CustomTrailing( - padding: const EdgeInsets.all(0), - fit: BoxFit.none, - child: HideableWidget( - isHidden: widget.token.isHidden && !widget.isPreview, - token: widget.token, - child: GestureDetector( - behavior: HitTestBehavior.deferToChild, - onTap: widget.isPreview - ? null - : () { - if (widget.token.viewMode == - DayPasswordTokenViewMode.VALIDFOR) { - ref - .read(tokenProvider.notifier) - .updateToken( - widget.token, - (p0) => p0.copyWith( - viewMode: DayPasswordTokenViewMode.VALIDUNTIL, - ), - ); - return; - } - if (widget.token.viewMode == - DayPasswordTokenViewMode.VALIDUNTIL) { - ref - .read(tokenProvider.notifier) - .updateToken( - widget.token, - (p0) => p0.copyWith( - viewMode: DayPasswordTokenViewMode.VALIDFOR, - ), - ); - return; - } - }, - child: Container( - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: - Theme.of( - context, - ).listTileTheme.subtitleTextStyle!.fontSize! * - 1.5, - child: Center( - child: Text( - switch (widget.token.viewMode) { - DayPasswordTokenViewMode.VALIDFOR => - '${AppLocalizations.of(context)!.dayPasswordValidFor}:', - DayPasswordTokenViewMode.VALIDUNTIL => - '${AppLocalizations.of(context)!.dayPasswordValidUntil}:', - }, - style: Theme.of( - context, - ).listTileTheme.subtitleTextStyle, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), + semanticsLabel: _semanticsLabel, + titleOnTap: _titleOnTap, + title: _title, + additionalSubtitles: _additionalSubtitles, + trailing: _buildTrailing(), + ); + } + + Widget _buildTrailing() { + final l10n = AppLocalizations.of(context)!; + final titleSyle = Theme.of(context).listTileTheme.subtitleTextStyle; + final titleString = switch (widget.token.viewMode) { + DayPasswordTokenViewMode.VALIDFOR => l10n.dayPasswordValidFor, + DayPasswordTokenViewMode.VALIDUNTIL => l10n.dayPasswordValidUntil, + }; + final timeHeight = Theme.of(context).textTheme.bodyMedium!.fontSize! * 2.5; + final timeString = switch (widget.token.viewMode) { + DayPasswordTokenViewMode.VALIDFOR => _durationString, + DayPasswordTokenViewMode.VALIDUNTIL => _validUntilString, + }; + + return SizedBox( + height: double.maxFinite, + child: CustomTrailing( + fit: BoxFit.none, + child: HideableWidget( + isHidden: widget.token.isHidden && !widget.isPreview, + token: widget.token, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.isPreview ? null : _toggleViewMode, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: (titleSyle?.fontSize ?? 12) * 1.5, + child: Center( + child: Text( + '$titleString:', + style: titleSyle, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.fade, + softWrap: false, ), - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: - Theme.of(context).textTheme.bodyLarge!.fontSize! * - 3, - minHeight: - Theme.of(context).textTheme.bodyLarge!.fontSize! * - 3, - ), - child: Center( - child: Text( - switch (widget.token.viewMode) { - DayPasswordTokenViewMode.VALIDFOR => durationString, - DayPasswordTokenViewMode.VALIDUNTIL => - '$yMdString\n$ejmString', - }, - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 2, - ), - ), + ), + ), + const SizedBox(height: 2), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: timeHeight, + minHeight: timeHeight, + ), + child: Center( + child: Text( + timeString, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.fade, + softWrap: false, ), - ], + ), ), - ), + ], ), ), ), diff --git a/lib/widgets/custom_trailing.dart b/lib/widgets/custom_trailing.dart index d26f3b2a3..a036cbfaa 100644 --- a/lib/widgets/custom_trailing.dart +++ b/lib/widgets/custom_trailing.dart @@ -2,11 +2,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import '../utils/customization/theme_extentions/app_dimensions.dart'; + class CustomTrailing extends StatelessWidget { final Widget child; final double maxPercentWidth; final double maxPixelsWidth; - final EdgeInsetsGeometry? padding; final BoxFit fit; /// Creates a widget that limits the width of [child] to [maxPercentWidth] of @@ -17,7 +18,7 @@ class CustomTrailing extends StatelessWidget { super.key, double? maxPercentWidth, double? maxPixelsWidth, - this.padding, + this.fit = BoxFit.contain, }) : maxPercentWidth = maxPercentWidth ?? 27.5, maxPixelsWidth = maxPixelsWidth ?? 85; @@ -30,10 +31,16 @@ class CustomTrailing extends StatelessWidget { maxPixelsWidth, constraints.maxWidth * maxPercentWidth / 100, ); - return SizedBox( - width: boxSize, - height: boxSize, - child: FittedBox(fit: fit, child: child), + final dimensions = + Theme.of(context).extension() ?? + const AppDimensions(); + return Padding( + padding: EdgeInsets.only(right: dimensions.spacingSmall), + child: SizedBox( + width: boxSize, + height: boxSize, + child: FittedBox(fit: fit, child: child), + ), ); }, ); From d3c9be7d49c489ffa6cc58f94f1722c57c32908f Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:55:46 +0200 Subject: [PATCH 10/13] feat: update EnumExtension and Token logic for improved case sensitivity handling; enhance logging in ForceBiometricOption extension --- lib/model/extensions/enum_extension.dart | 4 +- .../force_biometric_option_extension.dart | 15 ++- lib/model/tokens/token.dart | 25 ++--- .../token_folder_expandable_header.dart | 7 +- .../folder_widgets/token_folder_widget.dart | 105 +++++++++--------- lib/widgets/custom_trailing.dart | 4 +- .../model/extensions/enum_extension_test.dart | 42 +++---- 7 files changed, 103 insertions(+), 99 deletions(-) diff --git a/lib/model/extensions/enum_extension.dart b/lib/model/extensions/enum_extension.dart index 3529c7d8f..585113c9c 100644 --- a/lib/model/extensions/enum_extension.dart +++ b/lib/model/extensions/enum_extension.dart @@ -18,5 +18,7 @@ * limitations under the License. */ extension EnumExtension on Enum { - bool isName(String enumName, {bool caseSensitive = true}) => caseSensitive ? name == enumName : name.toLowerCase() == enumName.toLowerCase(); + bool isName(String enumName, {bool caseSensitive = false}) => caseSensitive + ? name == enumName + : name.toLowerCase() == enumName.toLowerCase(); } diff --git a/lib/model/extensions/enums/force_biometric_option_extension.dart b/lib/model/extensions/enums/force_biometric_option_extension.dart index a8e477523..66f1e3c61 100644 --- a/lib/model/extensions/enums/force_biometric_option_extension.dart +++ b/lib/model/extensions/enums/force_biometric_option_extension.dart @@ -18,6 +18,7 @@ * limitations under the License. */ +import '../../../utils/logger.dart'; import '../../../utils/object_validator/object_validators.dart'; import '../../enums/force_biometric_option.dart'; @@ -25,10 +26,14 @@ extension ForceBiometricOptionX on ForceBiometricOption { static final validator = DefaultObjectValidator( defaultValue: ForceBiometricOption.none, transformer: (v) { + Logger.info('Transforming value to ForceBiometricOption: $v'); if (v is ForceBiometricOption) return v; if (v is String) { return ForceBiometricOptionX.fromString(v)!; } + Logger.warning( + 'Invalid type for ForceBiometricOption: ${v.runtimeType}, value: $v', + ); throw ArgumentError( 'Invalid type for ForceBiometricOption: ${v.runtimeType}, value: $v', ); @@ -37,10 +42,14 @@ extension ForceBiometricOptionX on ForceBiometricOption { static ForceBiometricOption? fromString(String? value) { if (value == null) return null; + // cut "ForceBiometricOption." prefix if present + final enumValue = value.contains('.') ? value.split('.').last : value; return ForceBiometricOption.values.firstWhere( - (e) => e.name.toLowerCase() == value.toLowerCase(), - orElse: () => - throw ArgumentError('Invalid ForceBiometricOption value: $value'), + (e) => e.name.toLowerCase() == enumValue.toLowerCase(), + orElse: () { + Logger.warning('Unknown ForceBiometricOption value: $value'); + throw ArgumentError('Invalid ForceBiometricOption value: $enumValue'); + }, ); } } diff --git a/lib/model/tokens/token.dart b/lib/model/tokens/token.dart index 4805c22d0..98342dd63 100644 --- a/lib/model/tokens/token.dart +++ b/lib/model/tokens/token.dart @@ -175,20 +175,19 @@ abstract class Token with SortableMixin { 'Token type is not defined in the json', ); } - if (TokenTypes.HOTP.isName(type, caseSensitive: false)) { + if (TokenTypes.HOTP.isName(type)) { return HOTPToken.fromJson(json); } - if (TokenTypes.TOTP.isName(type, caseSensitive: false)) { + if (TokenTypes.TOTP.isName(type)) { return TOTPToken.fromJson(json); } - if (TokenTypes.PIPUSH.isName(type, caseSensitive: false) || - TokenTypes.PUSH.isName(type, caseSensitive: false)) { + if (TokenTypes.PIPUSH.isName(type) || TokenTypes.PUSH.isName(type)) { return PushToken.fromJson(json); } - if (TokenTypes.DAYPASSWORD.isName(type, caseSensitive: false)) { + if (TokenTypes.DAYPASSWORD.isName(type)) { return DayPasswordToken.fromJson(json); } - if (TokenTypes.STEAM.isName(type, caseSensitive: false)) { + if (TokenTypes.STEAM.isName(type)) { return SteamToken.fromJson(json); } @@ -213,32 +212,31 @@ abstract class Token with SortableMixin { 'Token type is not defined in the uri map', ); } - if (TokenTypes.HOTP.isName(type, caseSensitive: false)) { + if (TokenTypes.HOTP.isName(type)) { return HOTPToken.fromOtpAuthMap( otpAuthMap, additionalData: additionalData, ); } - if (TokenTypes.TOTP.isName(type, caseSensitive: false)) { + if (TokenTypes.TOTP.isName(type)) { return TOTPToken.fromOtpAuthMap( otpAuthMap, additionalData: additionalData, ); } - if (TokenTypes.PIPUSH.isName(type, caseSensitive: false) || - TokenTypes.PUSH.isName(type, caseSensitive: false)) { + if (TokenTypes.PIPUSH.isName(type) || TokenTypes.PUSH.isName(type)) { return PushToken.fromOtpAuthMap( otpAuthMap, additionalData: additionalData, ); } - if (TokenTypes.DAYPASSWORD.isName(type, caseSensitive: false)) { + if (TokenTypes.DAYPASSWORD.isName(type)) { return DayPasswordToken.fromOtpAuthMap( otpAuthMap, additionalData: additionalData, ); } - if (TokenTypes.STEAM.isName(type, caseSensitive: false)) { + if (TokenTypes.STEAM.isName(type)) { return SteamToken.fromOtpAuthMap( otpAuthMap, additionalData: additionalData, @@ -315,6 +313,7 @@ abstract class Token with SortableMixin { PIN: pin ? PIN_VALUE_TRUE : PIN_VALUE_FALSE, OFFLINE: isOffline, IMAGE: ?tokenImage, + FORCE_BIOMETRIC_OPTION: forceBiometricOption.name, }; } @@ -323,7 +322,7 @@ abstract class Token with SortableMixin { ORIGIN: origin, SORT_INDEX: sortIndex, FOLDER_ID: folderId, - IS_HIDDEN: isHidden, + // IS_HIDDEN: isHidden, // isHidden is derived from isLocked and pin, so we don't store it directly to avoid confusion CHECKED_CONTAINERS: checkedContainer, CONTAINER_SERIAL: containerSerial, }; diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart index 539faea2d..c9fd35f8e 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_expandable_widgets/token_folder_expandable_header.dart @@ -31,6 +31,7 @@ import '../../../../../model/tokens/token.dart'; import '../../../../../utils/lock_auth.dart'; import '../../../../../utils/riverpod/riverpod_providers/state_providers/dragging_sortable_provider.dart'; import '../../../../../utils/utils.dart'; +import '../../../../../widgets/custom_trailing.dart'; import '../../token_widgets/token_widget.dart'; import '../token_folder_actions.dart/delete_token_folder_action.dart'; import '../token_folder_actions.dart/lock_token_folder_action.dart'; @@ -70,7 +71,6 @@ class _TokenFolderExpandableHeaderState super.dispose(); } - // TODO: FIx expanding after minimizing the app @override Widget build(BuildContext context) { final isExpanded = widget.expandableController.value; @@ -90,7 +90,7 @@ class _TokenFolderExpandableHeaderState LockTokenFolderAction(folder: widget.folder), ], child: Padding( - padding: EdgeInsets.only(left: 8, right: 10), + padding: EdgeInsets.only(left: 8), child: DragTarget( onWillAcceptWithDetails: (details) { if (details.data.folderId != widget.folder.folderId) { @@ -171,8 +171,7 @@ class _TokenFolderExpandableHeaderState softWrap: false, ), ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 3), + CustomTrailing( child: TokenFolderExpandableHeaderIcon( showEmptyFolderIcon: (widget.tokens.isEmpty || diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart index 7c7f98c20..683042d8e 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/token_folder_widget.dart @@ -48,63 +48,60 @@ class TokenFolderWidget extends ConsumerWidget { final TokenFolder? draggingFolder = draggingSortable is TokenFolder ? draggingSortable : null; - return draggingSortable == null - ? LongPressDraggable( - maxSimultaneousDrags: 1, - dragAnchorStrategy: (draggable, context, position) { - final textSize = textSizeOf( - text: folder.label, - style: Theme.of(context).textTheme.titleMedium!, - textScaler: MediaQuery.of(context).textScaler, - maxLines: 1, - ); - return Offset( - max(textSize.width / 2, 30), - textSize.height / 2 + 30, - ); - }, - onDragStarted: () => - ref.read(draggingSortableProvider.notifier).state = folder, - onDragCompleted: () { - Logger.info('Draggable completed'); - // Will be handled by the sortableNotifier - }, - onDraggableCanceled: (velocity, offset) { - Logger.info('Draggable canceled'); - ref.read(draggingSortableProvider.notifier).state = null; - }, - data: folder, - childWhenDragging: const SizedBox(), - feedback: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.folder, size: 60), - Material( - color: Colors.transparent, - child: Text( - folder.label, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.only(top: 4), - child: TokenFolderExpandable( - folder: folder, - folderTokens: folderTokens, - key: Key('TokenFolderExpandable#${folder.folderId}'), + return switch (true) { + _ when draggingSortable == null => LongPressDraggable( + maxSimultaneousDrags: 1, + dragAnchorStrategy: (draggable, context, position) { + final textSize = textSizeOf( + text: folder.label, + style: Theme.of(context).textTheme.titleMedium!, + textScaler: MediaQuery.of(context).textScaler, + maxLines: 1, + ); + return Offset(max(textSize.width / 2, 30), textSize.height / 2 + 30); + }, + onDragStarted: () => + ref.read(draggingSortableProvider.notifier).state = folder, + onDragCompleted: () { + Logger.info('Draggable completed'); + // Will be handled by the sortableNotifier + }, + onDraggableCanceled: (velocity, offset) { + Logger.info('Draggable canceled'); + ref.read(draggingSortableProvider.notifier).state = null; + }, + data: folder, + childWhenDragging: const SizedBox(), + feedback: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.folder, size: 60), + Material( + color: Colors.transparent, + child: Text( + folder.label, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, ), ), - ) - : (draggingFolder == folder) - ? const SizedBox() - : TokenFolderExpandable( + ], + ), + child: Padding( + padding: const EdgeInsets.only(top: 4), + child: TokenFolderExpandable( folder: folder, folderTokens: folderTokens, - filter: filter, - ); + key: Key('TokenFolderExpandable#${folder.folderId}'), + ), + ), + ), + _ when draggingFolder == folder => const SizedBox(), + _ => TokenFolderExpandable( + folder: folder, + folderTokens: folderTokens, + filter: filter, + ), + }; } } diff --git a/lib/widgets/custom_trailing.dart b/lib/widgets/custom_trailing.dart index a036cbfaa..77544ebd0 100644 --- a/lib/widgets/custom_trailing.dart +++ b/lib/widgets/custom_trailing.dart @@ -12,15 +12,13 @@ class CustomTrailing extends StatelessWidget { /// Creates a widget that limits the width of [child] to [maxPercentWidth] of /// the parent width or [maxPixelsWidth] if the parent width is too small. - /// Defaults: [maxPercentWidth] = 27.5, [maxPixelsWidth] = 85 const CustomTrailing({ required this.child, super.key, double? maxPercentWidth, double? maxPixelsWidth, - this.fit = BoxFit.contain, - }) : maxPercentWidth = maxPercentWidth ?? 27.5, + }) : maxPercentWidth = maxPercentWidth ?? 20, maxPixelsWidth = maxPixelsWidth ?? 85; @override diff --git a/test/unit_test/model/extensions/enum_extension_test.dart b/test/unit_test/model/extensions/enum_extension_test.dart index b9139e456..1aeb2314b 100644 --- a/test/unit_test/model/extensions/enum_extension_test.dart +++ b/test/unit_test/model/extensions/enum_extension_test.dart @@ -11,49 +11,49 @@ void _testEnumExtension() { group('Enum Extension', () { group('isName', () { test('caseSensitive', () { - expect(_TestEnum.entryOne.isName('entryOne'), true); - expect(_TestEnum.entryOne.isName('entryone'), false); - expect(_TestEnum.entryOne.isName('entryTwo'), false); - expect(_TestEnum.entryTwo.isName('entryTwo'), true); - expect(_TestEnum.entryTwo.isName('entrytwo'), false); - expect(_TestEnum.entryTwo.isName('entryThree'), false); - expect(_TestEnum.entryThree.isName('entryThree'), true); - expect(_TestEnum.entryThree.isName('entrythree'), false); - }); - test('caseInsensitive', () { expect( - _TestEnum.entryOne.isName('entryone', caseSensitive: false), + _TestEnum.entryOne.isName('entryOne', caseSensitive: true), true, ); expect( - _TestEnum.entryOne.isName('ENTRYONE', caseSensitive: false), - true, + _TestEnum.entryOne.isName('entryone', caseSensitive: true), + false, ); expect( - _TestEnum.entryOne.isName('entryTwo', caseSensitive: false), + _TestEnum.entryOne.isName('entryTwo', caseSensitive: true), false, ); expect( - _TestEnum.entryTwo.isName('entrytwo', caseSensitive: false), + _TestEnum.entryTwo.isName('entryTwo', caseSensitive: true), true, ); expect( - _TestEnum.entryTwo.isName('ENTRYTWO', caseSensitive: false), - true, + _TestEnum.entryTwo.isName('entrytwo', caseSensitive: true), + false, ); expect( - _TestEnum.entryTwo.isName('entryThree', caseSensitive: false), + _TestEnum.entryTwo.isName('entryThree', caseSensitive: true), false, ); expect( - _TestEnum.entryThree.isName('entrythree', caseSensitive: false), + _TestEnum.entryThree.isName('entryThree', caseSensitive: true), true, ); expect( - _TestEnum.entryThree.isName('ENTRYTHREE', caseSensitive: false), - true, + _TestEnum.entryThree.isName('entrythree', caseSensitive: true), + false, ); }); + test('caseInsensitive', () { + expect(_TestEnum.entryOne.isName('entryone'), true); + expect(_TestEnum.entryOne.isName('ENTRYONE'), true); + expect(_TestEnum.entryOne.isName('entryTwo'), false); + expect(_TestEnum.entryTwo.isName('entrytwo'), true); + expect(_TestEnum.entryTwo.isName('ENTRYTWO'), true); + expect(_TestEnum.entryTwo.isName('entryThree'), false); + expect(_TestEnum.entryThree.isName('entrythree'), true); + expect(_TestEnum.entryThree.isName('ENTRYTHREE'), true); + }); }); }); } From c78b07403f4659cfede3cf32c3da1ae50631b1de Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:28:13 +0200 Subject: [PATCH 11/13] Refactor status message handling and improve UI responsiveness - Removed direct usage of statusMessageProvider in favor of showErrorStatusMessage utility for better error handling in export tokens dialog and push request dialog. - Introduced StatusBar widget to manage status messages more effectively across settings view. - Updated IntentButton to support loading states and improved button behavior during sync operations. - Enhanced splash screen logic to ensure proper context checks. - Adjusted unit tests for sync container button to validate loading states and button behavior under different sync conditions. - Updated pubspec.lock and pubspec.yaml to include new dependencies and version adjustments. --- analysis_options.yaml | 3 +- lib/api/impl/privacy_idea_container_api.dart | 7 +- lib/model/pi_server_response.dart | 18 +- lib/model/processor_result.dart | 4 +- lib/utils/push_provider.dart | 16 +- .../push_request_provider.dart | 30 ++-- .../token_container_notifier.dart | 29 ++-- .../generated_providers/token_notifier.dart | 71 ++++---- .../status_message_provider.dart | 64 +++++-- .../connectivity_provider.dart | 32 ++-- lib/utils/utils.dart | 1 + lib/utils/view_utils.dart | 20 +-- .../add_token_manually_view.dart | 43 ++--- .../link_input_field.dart | 20 ++- lib/views/container_view/container_view.dart | 21 ++- .../buttons/sync_container_button.dart | 12 +- .../delete_container_dialog.dart | 1 + .../transfer_container_action_dialog.dart | 1 + .../transfer_delete_dontainer_dialog.dart | 1 + lib/views/feedback_view/feedback_view.dart | 158 +++++++++--------- .../import_tokens_view.dart | 67 ++++---- lib/views/license_view/license_view.dart | 25 +-- .../link_home_widget_view.dart | 69 ++++---- .../connectivity_listener.dart | 11 +- .../add_token_folder_dialog.dart | 1 + .../push_token_view/push_tokens_view.dart | 23 +-- .../qr_scanner_view/qr_scanner_view.dart | 3 +- .../dialogs/export_tokens_to_file_dialog.dart | 9 +- lib/views/settings_view/settings_view.dart | 37 ++-- .../update_firebase_token_dialog.dart | 1 + lib/views/splash_screen/splash_screen.dart | 2 + lib/widgets/button_widgets/intent_button.dart | 18 +- .../push_request_dialog.dart | 5 +- lib/widgets/status_bar.dart | 94 +++-------- pubspec.lock | 60 +++---- .../api/privacy_idea_container_api_test.dart | 107 ++++++------ .../buttons/sync_container_button_test.dart | 78 +++++---- 37 files changed, 621 insertions(+), 541 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index a0af6e5a6..88d0ddec3 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,12 +1,13 @@ analyzer: errors: constant_identifier_names: ignore + include: package:flutter_lints/flutter.yaml linter: rules: + use_build_context_synchronously: true unnecessary_string_escapes: false unnecessary_string_interpolations: false - # unawaited_futures: true avoid_redundant_argument_values: true avoid_void_async: true diff --git a/lib/api/impl/privacy_idea_container_api.dart b/lib/api/impl/privacy_idea_container_api.dart index e1eaf1942..f7eb266a3 100644 --- a/lib/api/impl/privacy_idea_container_api.dart +++ b/lib/api/impl/privacy_idea_container_api.dart @@ -259,8 +259,11 @@ class PiContainerApi implements TokenContainerApi { EmptyResultDetail >(); } catch (e) { - Logger.error('Failed to parse response', error: e); - rethrow; + Logger.debug( + "Failed to parse container finalization response: Respone is not from privacyIDEA server", + error: e, + ); + throw ResponseError(response); } if (piResponse.isError) { diff --git a/lib/model/pi_server_response.dart b/lib/model/pi_server_response.dart index c31cb2af5..b0016a198 100644 --- a/lib/model/pi_server_response.dart +++ b/lib/model/pi_server_response.dart @@ -82,7 +82,7 @@ sealed class PiServerResponse< static PiServerResponse fromJson< V extends PiServerResultValue, D extends PiServerResultDetail - >(Map json, {int statisCode = 200}) { + >(Map json, {int statusCode = 200}) { Logger.debug('Received container sync response: $json'); final map = validateMap( map: json, @@ -101,7 +101,7 @@ sealed class PiServerResponse< name: 'PiServerResponse#fromJson', ); return PiServerResponse.success( - statusCode: statisCode, + statusCode: statusCode, id: map[ID] as int, jsonrpc: map[JSONRPC] as String, result: PiServerResult.fromResultMap( @@ -121,9 +121,19 @@ sealed class PiServerResponse< V extends PiServerResultValue, D extends PiServerResultDetail >(Response response) { + late final Map json; + try { + json = jsonDecode(response.body); + } catch (e) { + throw FormatException( + 'Failed to parse response body as JSON: ${response.body}', + e, + ); + } + return PiServerResponse.fromJson( - jsonDecode(response.body), - statisCode: response.statusCode, + json, + statusCode: response.statusCode, ); } } diff --git a/lib/model/processor_result.dart b/lib/model/processor_result.dart index dcdf4140e..ce33f2051 100644 --- a/lib/model/processor_result.dart +++ b/lib/model/processor_result.dart @@ -21,10 +21,8 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import '../utils/globals.dart'; import '../utils/logger.dart'; import '../utils/object_validator/object_validators.dart'; -import '../utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import '../utils/view_utils.dart'; part 'processor_result.freezed.dart'; @@ -70,7 +68,7 @@ extension ListProcessorResult on List> { WidgetsBinding.instance.addPostFrameCallback((_) async { for (var failedResult in failedResults) { - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.malformedData, details: failedResult.message, ); diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index 54057c1fd..e735fa15e 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -27,6 +27,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:http/http.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations_en.dart'; +import 'package:privacyidea_authenticator/utils/view_utils.dart'; import '../../../../../../../repo/secure_push_request_repository.dart'; import '../../../../../../../utils/pi_notifications.dart'; @@ -39,7 +40,6 @@ import 'logger.dart'; import 'privacyidea_io_client.dart'; import 'riverpod/riverpod_providers/generated_providers/settings_notifier.dart'; import 'riverpod/riverpod_providers/generated_providers/token_notifier.dart'; -import 'riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import 'rsa_utils.dart'; import 'utils.dart'; @@ -345,7 +345,7 @@ class PushProvider { if (connectivityResult.contains(ConnectivityResult.none)) { if (isManually) { Logger.info('Tried to poll without any internet connection available.'); - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.pollingFailed, details: (localization) => localization.noNetworkConnection, ); @@ -381,7 +381,7 @@ class PushProvider { Logger.info(rsaUtils.runtimeType.toString()); String? signature = await rsaUtils.trySignWithToken(token, message); if (signature == null) { - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.pollingFailedFor(token.serial), details: (localization) => localization.couldNotSignMessage, ); @@ -412,7 +412,7 @@ class PushProvider { ); } catch (_) { if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.errorWhenPullingChallenges(token.serial), details: (localization) => localization.couldNotConnectToServer, @@ -428,9 +428,7 @@ class PushProvider { challengeList = _getAndValidateDataFromResponse(response); } catch (_) { if (isManually) { - globalRef - ?.read(statusMessageProvider.notifier) - .state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.errorWhenPullingChallenges(token.serial), details: (localization) => localization.pushRequestParseError, @@ -444,7 +442,7 @@ class PushProvider { case 403: final error = getErrorMessageFromResponse(response); if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.pollingFailedFor(token.serial), details: error != null @@ -462,7 +460,7 @@ class PushProvider { default: final error = getErrorMessageFromResponse(response); if (isManually) { - globalRef?.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.pollingFailedFor(token.serial), details: error != null diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart index dcd43c946..8a004f5f4 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/push_request_provider.dart @@ -391,9 +391,7 @@ class PushRequestNotifier extends _$PushRequestNotifier { ); } catch (e) { await _addOrReplacePushRequest(oldRequest); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (l) => l.connectionFailed, - ); + ref.read(statusProvider.notifier).show((l) => l.connectionFailed); return null; } } @@ -402,23 +400,27 @@ class PushRequestNotifier extends _$PushRequestNotifier { try { piResponse = response.asPiServerResponse(); } catch (e) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (l) => - '${l.sendPushRequestResponseFailed}\n${l.statusCode(response.statusCode)}', - details: (_) => 'Failed to parse response: $e', - ); + ref + .read(statusProvider.notifier) + .show( + (l) => + '${l.sendPushRequestResponseFailed}\n${l.statusCode(response.statusCode)}', + details: (_) => 'Failed to parse response: $e', + ); await _addOrReplacePushRequest(oldRequest); return null; } if (piResponse.isError) { await _addOrReplacePushRequest(oldRequest); final errorResponse = piResponse.asError!; - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (l) => - '${l.sendPushRequestResponseFailed}\n${l.statusCode(errorResponse.statusCode)}', - details: (_) => - '${errorResponse.piServerResultError.code}: ${errorResponse.piServerResultError.message}', - ); + ref + .read(statusProvider.notifier) + .show( + (l) => + '${l.sendPushRequestResponseFailed}\n${l.statusCode(errorResponse.statusCode)}', + details: (_) => + '${errorResponse.piServerResultError.code}: ${errorResponse.piServerResultError.message}', + ); return null; } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart index ed6681f70..06f4cc6e2 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart @@ -35,7 +35,6 @@ import '../../../../../../../model/processor_result.dart'; import '../../../../../../../model/tokens/token.dart'; import '../../../../../../../utils/privacyidea_io_client.dart'; import '../../../../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; -import '../../../../../../../utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import '../../../../../../../utils/view_utils.dart'; import '../../../../api/impl/privacy_idea_container_api.dart'; import '../../../../api/interfaces/container_api.dart'; @@ -694,14 +693,13 @@ class TokenContainerNotifier extends _$TokenContainerNotifier await _curentOf(container), response, ); - _finalizationMutex.release(); return finalizedContainer; } on StateError catch (e) { if (isManually) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => container.finalizationState.asFailed .rolloutMsgLocalized(localization), - details: (localization) => e.toString(), + details: (_) => e.toString(), ); } } on LocalizedArgumentError catch (e) { @@ -730,13 +728,14 @@ class TokenContainerNotifier extends _$TokenContainerNotifier 'Failed to finalize container ${container.serial}', error: e, ); + } finally { + await updateContainer( + container, + (TokenContainerUnfinalized c) => + c.copyWith(finalizationState: c.finalizationState.asFailed), + ); + _finalizationMutex.release(); } - await updateContainer( - container, - (TokenContainerUnfinalized c) => - c.copyWith(finalizationState: c.finalizationState.asFailed), - ); - _finalizationMutex.release(); return null; } @@ -811,12 +810,10 @@ class TokenContainerNotifier extends _$TokenContainerNotifier if (container == null) throw StateError('Container was removed'); try { response = (await _containerApi.finalizeContainer(container, eccUtils)); - } catch (_) { - container = await updateContainer( - container, - (TokenContainerUnfinalized c) => c.copyWith( - finalizationState: FinalizationState.sendingPublicKeyFailed, - ), + } on ResponseError catch (e) { + Logger.debug( + "Failed to parse container finalization response: Respone is not from privacyIDEA server", + error: e, ); rethrow; } diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart index 9275f70ef..1462e7388 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart @@ -29,7 +29,6 @@ import 'package:mutex/mutex.dart'; import 'package:pointycastle/asymmetric/api.dart'; import 'package:privacyidea_authenticator/model/extensions/token_list_extension.dart'; import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/generated_providers/localization_notifier.dart'; -import 'package:privacyidea_authenticator/utils/view_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../../../../model/extensions/enums/push_token_rollout_state_extension.dart'; @@ -504,11 +503,12 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { await firebaseUtils.deleteFirebaseToken(); } on SocketException { Logger.warning('Could not delete firebase token.'); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => - localization.errorUnlinkingPushToken(token.label), - details: (localization) => localization.checkYourNetwork, - ); + ref + .read(statusProvider.notifier) + .show( + (localization) => localization.errorUnlinkingPushToken(token.label), + details: (localization) => localization.checkYourNetwork, + ); return false; } final deleted = await _removeToken(token); @@ -524,12 +524,14 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { Logger.warning( 'Could not update firebase token because no firebase token is available.', ); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => - localization.errorSynchronizationNoNetworkConnection, - details: (localization) => - localization.syncFbTokenManuallyWhenNetworkIsAvailable, - ); + ref + .read(statusProvider.notifier) + .show( + (localization) => + localization.errorSynchronizationNoNetworkConnection, + details: (localization) => + localization.syncFbTokenManuallyWhenNetworkIsAvailable, + ); return deleted; } @@ -544,12 +546,14 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { Logger.warning( 'Could not update firebase token for ${notUpdated.length} tokens.', ); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => - localization.errorSynchronizationNoNetworkConnection, - details: (localization) => - localization.syncFbTokenManuallyWhenNetworkIsAvailable, - ); + ref + .read(statusProvider.notifier) + .show( + (localization) => + localization.errorSynchronizationNoNetworkConnection, + details: (localization) => + localization.syncFbTokenManuallyWhenNetworkIsAvailable, + ); } return deleted; } @@ -595,10 +599,12 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { if (token.expirationDate?.isBefore(DateTime.now()) == true) { Logger.info('Token "${token.id}" is expired.'); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (loc) => loc.errorRollOutNotPossibleAnymore, - details: (loc) => loc.errorTokenExpired(token.label), - ); + ref + .read(statusProvider.notifier) + .show( + (loc) => loc.errorRollOutNotPossibleAnymore, + details: (loc) => loc.errorTokenExpired(token.label), + ); await _removeToken(token); return false; } @@ -638,10 +644,12 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { return await firebaseUtils.getFBToken(); } catch (e, s) { Logger.warning('Could not get firebase token.', error: e, stackTrace: s); - showErrorStatusMessage( - message: (l) => l.errorRollOutFailed(token.label), - details: (l) => l.noFbToken, - ); + ref + .read(statusProvider.notifier) + .show( + (l) => l.errorRollOutFailed(token.label), + details: (l) => l.checkYourNetwork, + ); await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKeyFailed); return null; } @@ -713,9 +721,9 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { return true; } catch (e, s) { Logger.error('Roll out failed.', error: e, stackTrace: s); - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (loc) => loc.errorRollOutUnknownError(token.label), - ); + ref + .read(statusProvider.notifier) + .show((loc) => loc.errorRollOutUnknownError(token.label)); await _updateStatus(token, PushTokenRollOutState.sendRSAPublicKeyFailed); return false; } @@ -1096,6 +1104,9 @@ class TokenNotifier extends _$TokenNotifier with ResultHandler { ); } } - ref.read(statusMessageProvider.notifier).state = statusMessage; + + ref + .read(statusProvider.notifier) + .show(statusMessage.message, details: statusMessage.details); } } diff --git a/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart b/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart index f08bb4d14..ee1dd62b0 100644 --- a/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart +++ b/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart @@ -3,7 +3,7 @@ * * Author: Frank Merkel * - * Copyright (c) 2025 NetKnights GmbH + * Copyright (c) 2026 NetKnights GmbH * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -17,26 +17,62 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import 'dart:collection'; + import 'package:flutter_riverpod/legacy.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; -import 'package:privacyidea_authenticator/l10n/app_localizations_en.dart'; - -import '../../../logger.dart'; -final statusMessageProvider = StateProvider((ref) { - Logger.info("New statusMessageProvider created"); - return null; -}); +final statusProvider = StateNotifierProvider( + (ref) => StatusNotifier(), +); class StatusMessage { - String Function(AppLocalizations localization) message; - String Function(AppLocalizations localization)? details; - bool isError; + final String Function(AppLocalizations localization) message; + final String Function(AppLocalizations localization)? details; + final bool isError; StatusMessage({required this.message, this.details, this.isError = true}); +} + +class StatusState { + final StatusMessage? current; + final Queue queue; + StatusState({this.current, required this.queue}); +} + +class StatusNotifier extends StateNotifier { + StatusNotifier() : super(StatusState(queue: Queue())); + + void show( + String Function(AppLocalizations l) message, { + String Function(AppLocalizations l)? details, + bool isError = true, + }) { + final statusMessage = StatusMessage( + message: message, + details: details, + isError: isError, + ); + + if (state.current == statusMessage || state.queue.contains(statusMessage)) { + return; + } + + state.queue.add(statusMessage); + _tryNext(); + } + + void dismiss() { + state = StatusState(queue: state.queue); + _tryNext(); + } - @override - String toString() { - return 'StatusMessage{message: ${message(AppLocalizationsEn())}, details: ${details?.call(AppLocalizationsEn())}}'; + void _tryNext() { + if (state.current == null && state.queue.isNotEmpty) { + state = StatusState( + current: state.queue.removeFirst(), + queue: state.queue, + ); + } } } diff --git a/lib/utils/riverpod/riverpod_providers/stream_providers/connectivity_provider.dart b/lib/utils/riverpod/riverpod_providers/stream_providers/connectivity_provider.dart index c26fce2f5..cfc0864f3 100644 --- a/lib/utils/riverpod/riverpod_providers/stream_providers/connectivity_provider.dart +++ b/lib/utils/riverpod/riverpod_providers/stream_providers/connectivity_provider.dart @@ -24,20 +24,18 @@ import '../../../logger.dart'; import '../generated_providers/token_notifier.dart'; import '../state_providers/status_message_provider.dart'; -final connectivityProvider = StreamProvider>( - (ref) { - Logger.info("New connectivityProvider created"); - ref.read(tokenProvider.future).then( - (newState) { - Connectivity().checkConnectivity().then((connectivity) { - Logger.info("First connectivity check: $connectivity"); - final hasNoConnection = connectivity.contains(ConnectivityResult.none); - if (hasNoConnection && newState.hasPushTokens) { - ref.read(statusMessageProvider.notifier).state = StatusMessage(message: (localization) => localization.noNetworkConnection); - } - }); - }, - ); - return Connectivity().onConnectivityChanged; - }, -); +final connectivityProvider = StreamProvider>((ref) { + Logger.info("New connectivityProvider created"); + ref.read(tokenProvider.future).then((newState) { + Connectivity().checkConnectivity().then((connectivity) { + Logger.info("First connectivity check: $connectivity"); + final hasNoConnection = connectivity.contains(ConnectivityResult.none); + if (hasNoConnection && newState.hasPushTokens) { + ref + .read(statusProvider.notifier) + .show((localization) => localization.noNetworkConnection); + } + }); + }); + return Connectivity().onConnectivityChanged; +}); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 4c7c84265..554e41e90 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -163,6 +163,7 @@ Future dragSortableOnAccept({ }) async { var allSortables = await ref.read(sortablesProvider.future); if (dragedSortable is TokenFolder) { + if (!ref.context.mounted) return; final tokensInFolder = (await ref.read(tokenProvider.future)).tokens .where((element) => element.folderId == dragedSortable.folderId) .toList(); diff --git a/lib/utils/view_utils.dart b/lib/utils/view_utils.dart index 80b9bceb6..0b637a3aa 100644 --- a/lib/utils/view_utils.dart +++ b/lib/utils/view_utils.dart @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import '../l10n/app_localizations.dart'; +import '../views/view_interface.dart'; import 'globals.dart'; import 'logger.dart'; import 'riverpod/riverpod_providers/state_providers/status_message_provider.dart'; @@ -88,34 +89,31 @@ ScaffoldFeatureController? _showSnackBar( void showErrorStatusMessage({ required String Function(AppLocalizations) message, String Function(AppLocalizations)? details, + WidgetRef? ref, }) { - final ref = globalRef; + ref ??= globalRef; Logger.warning('$message : $details'); if (ref == null) { Logger.error('Could not show status message: globalRef is null'); return; } - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: message, - details: details, - ); + ref.read(statusProvider.notifier).show(message, details: details); } void showSuccessStatusMessage({ required String Function(AppLocalizations) message, String Function(AppLocalizations)? details, + WidgetRef? ref, }) { - final ref = globalRef; + ref ??= globalRef; Logger.warning('$message : $details'); if (ref == null) { Logger.error('Could not show status message: globalRef is null'); return; } - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: message, - details: details, - isError: false, - ); + ref + .read(statusProvider.notifier) + .show(message, details: details, isError: false); } Future showAsyncDialog({ diff --git a/lib/views/add_token_manually_view/add_token_manually_view.dart b/lib/views/add_token_manually_view/add_token_manually_view.dart index 7fd7216b2..5a4138ffd 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view.dart @@ -25,6 +25,7 @@ import '../../l10n/app_localizations.dart'; import '../../model/enums/algorithms.dart'; import '../../model/enums/encodings.dart'; import '../../model/enums/token_types.dart'; +import '../../widgets/status_bar.dart'; import 'add_token_manually_view_widgets/add_tokens_manually/add_daypassword_manually.dart'; import 'add_token_manually_view_widgets/add_tokens_manually/add_hotp_manually.dart'; import 'add_token_manually_view_widgets/add_tokens_manually/add_steam_manually.dart'; @@ -152,28 +153,30 @@ class _AddTokenManuallyViewState extends ConsumerState { ), ), body: SafeArea( - child: Column( - children: [ - PageViewIndicator( - controller: pageController, - icons: [Icon(Icons.edit), Icon(Icons.link)], - ), - Expanded( - child: PageView( + child: StatusBar( + child: Column( + children: [ + PageViewIndicator( controller: pageController, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), - child: page, - ), - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), - child: LinkInputView(), - ), - ], + icons: [Icon(Icons.edit), Icon(Icons.link)], ), - ), - ], + Expanded( + child: PageView( + controller: pageController, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), + child: page, + ), + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 16.0), + child: LinkInputView(), + ), + ], + ), + ), + ], + ), ), ), ); diff --git a/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart b/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart index 3d427bd21..e09171a85 100644 --- a/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart +++ b/lib/views/add_token_manually_view/add_token_manually_view_widgets/link_input_field.dart @@ -23,7 +23,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../l10n/app_localizations.dart'; import '../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; -import '../../../utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; +import '../../../utils/view_utils.dart'; class LinkInputView extends ConsumerStatefulWidget { const LinkInputView({super.key}); @@ -37,9 +37,11 @@ class _LinkInputViewState extends ConsumerState { Future addToken(Uri link) async { final linkHandled = await ref.read(tokenProvider.notifier).handleLink(link); + if (!ref.context.mounted) return; if (!linkHandled) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.linkMustOtpAuth, + ref: ref, ); return; } @@ -64,10 +66,9 @@ class _LinkInputViewState extends ConsumerState { try { addToken(Uri.parse(text)); } catch (e) { - ref - .read(statusMessageProvider.notifier) - .state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.invalidUrl, + ref: ref, ); } }, @@ -83,12 +84,12 @@ class _LinkInputViewState extends ConsumerState { icon: Icon(Icons.paste), onPressed: () async { ClipboardData? data = await Clipboard.getData('text/plain'); + if (!ref.context.mounted) return; if (data == null || data.text == null || data.text!.isEmpty) { if (context.mounted) { - ref - .read(statusMessageProvider.notifier) - .state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.clipboardEmpty, + ref: ref, ); } return; @@ -112,8 +113,9 @@ class _LinkInputViewState extends ConsumerState { try { addToken(Uri.parse(textController.text)); } catch (e) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (localization) => localization.invalidUrl, + ref: ref, ); } }, diff --git a/lib/views/container_view/container_view.dart b/lib/views/container_view/container_view.dart index d2946c2e7..749f73b5e 100644 --- a/lib/views/container_view/container_view.dart +++ b/lib/views/container_view/container_view.dart @@ -26,6 +26,7 @@ import '../../../../../../../views/main_view/main_view_widgets/drag_target_divid import '../../../../../../../views/main_view/main_view_widgets/main_view_navigation_buttons/qr_scanner_button.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; import 'container_widgets/container_widget.dart'; @@ -56,16 +57,18 @@ class ContainerView extends ConsumerView { ), floatingActionButton: const QrScannerButton(), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - body: Center( - child: SlidableAutoCloseBehavior( - child: Column( - children: [ - for (var container in containerList) ...[ - if (containerList.indexOf(container) != 0) - const DefaultDivider(), - ContainerWidget(container: container), + body: StatusBar( + child: Center( + child: SlidableAutoCloseBehavior( + child: Column( + children: [ + for (var container in containerList) ...[ + if (containerList.indexOf(container) != 0) + const DefaultDivider(), + ContainerWidget(container: container), + ], ], - ], + ), ), ), ), diff --git a/lib/views/container_view/container_widgets/buttons/sync_container_button.dart b/lib/views/container_view/container_widgets/buttons/sync_container_button.dart index b42816f63..9b2066130 100644 --- a/lib/views/container_view/container_widgets/buttons/sync_container_button.dart +++ b/lib/views/container_view/container_widgets/buttons/sync_container_button.dart @@ -17,10 +17,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../model/enums/sync_state.dart'; import '../../../../model/token_container.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart'; import '../../../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; @@ -40,11 +40,18 @@ class SyncContainerButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { if (isPreview) return const Icon(Icons.sync, size: 40); + final currentContainer = ref + .watch(tokenContainerProvider) + .whenOrNull( + data: (state) => state.currentOf(container), + ); + return IntentButton( intent: DialogActionIntent.neutral, - // The button is disabled (null) if the container is already syncing + isLoading: currentContainer?.syncState == SyncState.syncing, onPressed: () async { final tokenState = await ref.read(tokenProvider.future); + if (!context.mounted) return; await ref .read(tokenContainerProvider.notifier) .syncContainers( @@ -52,7 +59,6 @@ class SyncContainerButton extends ConsumerWidget { containersToSync: [container], isManually: true, ); - return; }, child: const Icon(Icons.sync, size: 40), ); diff --git a/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart b/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart index e336d45a5..f4edeac7d 100644 --- a/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart +++ b/lib/views/container_view/container_widgets/dialogs/delete_container_dialogs.dart/delete_container_dialog.dart @@ -97,6 +97,7 @@ class DeleteContainerDialog extends ConsumerWidget { wasContainerDeleted = (await ForceDeleteContainerDialog.showDialog(container)) == true; } + if (!ref.context.mounted) return; final containerTokens = (await ref.read( tokenProvider.future, )).containerTokens(container.serial); diff --git a/lib/views/container_view/container_widgets/dialogs/transfer_container_action_dialog.dart b/lib/views/container_view/container_widgets/dialogs/transfer_container_action_dialog.dart index feb00d8b0..b15f8436c 100644 --- a/lib/views/container_view/container_widgets/dialogs/transfer_container_action_dialog.dart +++ b/lib/views/container_view/container_widgets/dialogs/transfer_container_action_dialog.dart @@ -91,6 +91,7 @@ class _TransferContainerDialogState return showErrorStatusMessage( message: (localization) => localization.transferContainerFailed, details: (_) => e.toString(), + ref: ref, ); } if (!mounted) return; diff --git a/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart b/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart index f52e0fad3..670e81de8 100644 --- a/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart +++ b/lib/views/container_view/container_widgets/dialogs/transfer_dialogs/transfer_delete_dontainer_dialog.dart @@ -53,6 +53,7 @@ class _TransferDeleteContainerDialogState super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { final tokenState = await ref.read(tokenProvider.future); + if (!ref.context.mounted) return; final failedContainers = await ref .read(tokenContainerProvider.notifier) .syncContainers( diff --git a/lib/views/feedback_view/feedback_view.dart b/lib/views/feedback_view/feedback_view.dart index 3829fe5a5..37b17e783 100644 --- a/lib/views/feedback_view/feedback_view.dart +++ b/lib/views/feedback_view/feedback_view.dart @@ -24,6 +24,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../../../../../views/feedback_view/widgets/feedback_send_row.dart'; import '../../l10n/app_localizations.dart'; import '../../utils/globals.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; class FeedbackView extends StatefulView { @@ -70,92 +71,93 @@ class _FeedbackViewState extends State { maxLines: 2, // Title can be shown on small screens too. ), ), - body: Padding( - padding: const EdgeInsets.all(14.0), - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Text( - AppLocalizations.of(context)!.feedbackTitle, - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, + body: StatusBar( + child: Padding( + padding: const EdgeInsets.all(14.0), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + AppLocalizations.of(context)!.feedbackTitle, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - AppLocalizations.of(context)!.feedbackDescription, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.justify, - ), - const SizedBox(height: 16), - TextField( - onTapOutside: (event) { - _focusNode.unfocus(); - }, - focusNode: _focusNode, - controller: _feedbackController, - decoration: InputDecoration( - border: const OutlineInputBorder( - borderSide: BorderSide(width: 1.5), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(width: 1.5), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(width: 1.5), - ), - labelText: AppLocalizations.of(context)!.feedback, + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + AppLocalizations.of(context)!.feedbackDescription, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.justify, ), - maxLines: 5, - ), - const SizedBox(height: 16), - RichText( - textAlign: TextAlign.justify, - text: TextSpan( - children: [ - TextSpan( - text: - '${AppLocalizations.of(context)!.feedbackHint} ', - style: Theme.of(context).textTheme.bodySmall, + const SizedBox(height: 16), + TextField( + onTapOutside: (event) { + _focusNode.unfocus(); + }, + focusNode: _focusNode, + controller: _feedbackController, + decoration: InputDecoration( + border: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - TextSpan( - text: AppLocalizations.of( - context, - )!.feedbackPrivacyPolicy1, - style: Theme.of(context).textTheme.bodySmall, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - TextSpan( - text: AppLocalizations.of( - context, - )!.feedbackPrivacyPolicy2, - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith(color: Colors.blue), - recognizer: TapGestureRecognizer() - ..onTap = () => launchUrl(policyStatementUri), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(width: 1.5), ), - TextSpan( - text: AppLocalizations.of( - context, - )!.feedbackPrivacyPolicy3, - style: Theme.of(context).textTheme.bodySmall, - ), - ], + labelText: AppLocalizations.of(context)!.feedback, + ), + maxLines: 5, + ), + const SizedBox(height: 16), + RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + TextSpan( + text: + '${AppLocalizations.of(context)!.feedbackHint} ', + style: Theme.of(context).textTheme.bodySmall, + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy1, + style: Theme.of(context).textTheme.bodySmall, + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy2, + style: Theme.of(context).textTheme.bodySmall + ?.copyWith(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrl(policyStatementUri), + ), + TextSpan( + text: AppLocalizations.of( + context, + )!.feedbackPrivacyPolicy3, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), - ), - FeedbackSendRow(feedbackController: _feedbackController), - ], + FeedbackSendRow(feedbackController: _feedbackController), + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/views/import_tokens_view/import_tokens_view.dart b/lib/views/import_tokens_view/import_tokens_view.dart index 337811fb3..8d9184e3d 100644 --- a/lib/views/import_tokens_view/import_tokens_view.dart +++ b/lib/views/import_tokens_view/import_tokens_view.dart @@ -25,6 +25,7 @@ import '../../model/token_import/token_import_origin.dart'; import '../../model/tokens/token.dart'; import '../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../utils/token_import_origins.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; import 'pages/import_start_page.dart'; import 'pages/select_import_type_page.dart'; @@ -85,39 +86,47 @@ class _ImportTokensViewState extends ConsumerState { maxLines: 2, // Title can be shown on small screens too. ), ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - children: [ - for (final item in TokenImportOrigins.appList) - ListTile( - // leading: Image.asset(appList[index].iconPath!), - title: TextButton( - onPressed: () => _onPressed(item), - child: Text(item.appName), - ), - trailing: Theme( - data: ThemeData( - iconTheme: const IconThemeData(color: Colors.red), - primaryIconTheme: const IconThemeData(color: Colors.blue), - iconButtonTheme: const IconButtonThemeData( - style: ButtonStyle( - foregroundColor: WidgetStatePropertyAll(Colors.green), - backgroundColor: WidgetStatePropertyAll(Colors.green), - iconColor: WidgetStatePropertyAll(Colors.green), - iconSize: WidgetStatePropertyAll(50), + body: StatusBar( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + for (final item in TokenImportOrigins.appList) + ListTile( + // leading: Image.asset(appList[index].iconPath!), + title: TextButton( + onPressed: () => _onPressed(item), + child: Text(item.appName), + ), + trailing: Theme( + data: ThemeData( + iconTheme: const IconThemeData(color: Colors.red), + primaryIconTheme: const IconThemeData( + color: Colors.blue, + ), + iconButtonTheme: const IconButtonThemeData( + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll( + Colors.green, + ), + backgroundColor: WidgetStatePropertyAll( + Colors.green, + ), + iconColor: WidgetStatePropertyAll(Colors.green), + iconSize: WidgetStatePropertyAll(50), + ), ), ), - ), - child: IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: () => _onPressed(item), + child: IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: () => _onPressed(item), + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/views/license_view/license_view.dart b/lib/views/license_view/license_view.dart index d3e35d3cf..e696b6fae 100644 --- a/lib/views/license_view/license_view.dart +++ b/lib/views/license_view/license_view.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../../widgets/push_request_listener.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; class LicenseView extends StatelessView { @@ -40,18 +41,20 @@ class LicenseView extends StatelessView { @override Widget build(BuildContext context) => PushRequestListener( - child: FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, platformInfo) => LicensePage( - applicationName: appName, - applicationIcon: Padding( - padding: const EdgeInsets.all(32), - child: appImage, + child: StatusBar( + child: FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, platformInfo) => LicensePage( + applicationName: appName, + applicationIcon: Padding( + padding: const EdgeInsets.all(32), + child: appImage, + ), + applicationLegalese: '© $websiteLink', + applicationVersion: platformInfo.data == null + ? '' + : '${platformInfo.data?.version}+${platformInfo.data?.buildNumber}', ), - applicationLegalese: '© $websiteLink', - applicationVersion: platformInfo.data == null - ? '' - : '${platformInfo.data?.version}+${platformInfo.data?.buildNumber}', ), ), ); diff --git a/lib/views/link_home_widget_view/link_home_widget_view.dart b/lib/views/link_home_widget_view/link_home_widget_view.dart index d035917bc..9f1812818 100644 --- a/lib/views/link_home_widget_view/link_home_widget_view.dart +++ b/lib/views/link_home_widget_view/link_home_widget_view.dart @@ -27,6 +27,7 @@ import '../../utils/home_widget_utils.dart'; import '../../utils/riverpod/riverpod_providers/generated_providers/token_folder_notifier.dart'; import '../../utils/riverpod/riverpod_providers/generated_providers/token_notifier.dart'; import '../../utils/utils.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; class LinkHomeWidgetView extends ConsumerStatefulView { @@ -59,39 +60,41 @@ class _LinkHomeWidgetViewState extends ConsumerState { maxLines: 2, // Title can be shown on small screens too. ), ), - body: ListView.builder( - itemBuilder: (context, index) { - final otpToken = otpTokens![index]; - final folderIsLocked = - ref - .watch(tokenFolderProvider) - .currentOfId(otpToken.folderId) - ?.isLocked ?? - false; - final otpString = otpToken.isLocked || folderIsLocked - ? veilingCharacter * otpToken.otpValue.length - : otpToken.otpValue; - return ListTile( - title: Text(otpToken.label), - subtitle: Text( - insertCharAt(otpString, ' ', (otpString.length / 2).ceil()), - ), - onTap: alreadyTapped - ? () {} - : () async { - if (alreadyTapped) return; - setState(() => alreadyTapped = true); - await HomeWidgetUtils().link( - widget.homeWidgetId, - otpToken.id, - ); - await SystemNavigator.pop(); - await Future.delayed(const Duration(milliseconds: 500)); - if (context.mounted) Navigator.pop(context); - }, - ); - }, - itemCount: otpTokens?.length ?? 0, + body: StatusBar( + child: ListView.builder( + itemBuilder: (context, index) { + final otpToken = otpTokens![index]; + final folderIsLocked = + ref + .watch(tokenFolderProvider) + .currentOfId(otpToken.folderId) + ?.isLocked ?? + false; + final otpString = otpToken.isLocked || folderIsLocked + ? veilingCharacter * otpToken.otpValue.length + : otpToken.otpValue; + return ListTile( + title: Text(otpToken.label), + subtitle: Text( + insertCharAt(otpString, ' ', (otpString.length / 2).ceil()), + ), + onTap: alreadyTapped + ? () {} + : () async { + if (alreadyTapped) return; + setState(() => alreadyTapped = true); + await HomeWidgetUtils().link( + widget.homeWidgetId, + otpToken.id, + ); + await SystemNavigator.pop(); + await Future.delayed(const Duration(milliseconds: 500)); + if (context.mounted) Navigator.pop(context); + }, + ); + }, + itemCount: otpTokens?.length ?? 0, + ), ), ); } diff --git a/lib/views/main_view/main_view_widgets/connectivity_listener.dart b/lib/views/main_view/main_view_widgets/connectivity_listener.dart index ce1d7adb0..a11564a79 100644 --- a/lib/views/main_view/main_view_widgets/connectivity_listener.dart +++ b/lib/views/main_view/main_view_widgets/connectivity_listener.dart @@ -40,10 +40,13 @@ class ConnectivityListener extends ConsumerWidget { if (newState.hasPushTokens) { Logger.info("Connectivity changed: $connectivity"); if (!context.mounted) return; - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (localization) => - AppLocalizations.of(context)!.noNetworkConnection, - ); + + ref + .read(statusProvider.notifier) + .show( + (localization) => + AppLocalizations.of(context)!.noNetworkConnection, + ); } }); } diff --git a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart index fc72c3ec9..4a4f58fc7 100644 --- a/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart +++ b/lib/views/main_view/main_view_widgets/folder_widgets/add_token_folder_dialog.dart @@ -95,6 +95,7 @@ class _AddTokenFolderDialogState extends ConsumerState { Future _handleCreate() async { final intro = await ref.read(introductionNotifierProvider.future); + if (!ref.context.mounted) return; if (!intro.isCompleted(Introduction.addFolder)) { await ref .read(introductionNotifierProvider.notifier) diff --git a/lib/views/push_token_view/push_tokens_view.dart b/lib/views/push_token_view/push_tokens_view.dart index 21076939e..97ecd99e3 100644 --- a/lib/views/push_token_view/push_tokens_view.dart +++ b/lib/views/push_token_view/push_tokens_view.dart @@ -19,6 +19,7 @@ */ import 'package:flutter/material.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/widgets/status_bar.dart'; import '../../widgets/push_request_listener.dart'; import '../view_interface.dart'; @@ -42,17 +43,19 @@ class PushTokensView extends StatelessView { ), ), body: PushRequestListener( - child: Stack( - children: [ - Center( - child: Icon( - Icons.notifications_none, - size: 300, - color: Colors.grey.withValues(alpha: 0.2), + child: StatusBar( + child: Stack( + children: [ + Center( + child: Icon( + Icons.notifications_none, + size: 300, + color: Colors.grey.withValues(alpha: 0.2), + ), ), - ), - const PushTokensViwList(), - ], + const PushTokensViwList(), + ], + ), ), ), ); diff --git a/lib/views/qr_scanner_view/qr_scanner_view.dart b/lib/views/qr_scanner_view/qr_scanner_view.dart index 73774d0d3..b43be7cf4 100644 --- a/lib/views/qr_scanner_view/qr_scanner_view.dart +++ b/lib/views/qr_scanner_view/qr_scanner_view.dart @@ -25,6 +25,7 @@ import '../../l10n/app_localizations.dart'; import '../../utils/logger.dart'; import '../../views/view_interface.dart'; import '../../widgets/dialog_widgets/default_dialog.dart'; +import '../../widgets/status_bar.dart'; import 'qr_scanner_view_widgets/qr_scanner_widget.dart'; class QRScannerView extends StatefulView { @@ -96,7 +97,7 @@ class _QRScannerViewState extends State { elevation: 0, ), extendBodyBehindAppBar: true, - body: const QRScannerWidget(), + body: StatusBar(child: const QRScannerWidget()), ), _ => DefaultDialog( title: Text( diff --git a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart index a20518f3e..552a77815 100644 --- a/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart +++ b/lib/views/settings_view/settings_groups/import_export_tokens_widgets/dialogs/export_tokens_to_file_dialog.dart @@ -31,7 +31,6 @@ import '../../../../../model/enums/force_biometric_option.dart'; import '../../../../../model/tokens/token.dart'; import '../../../../../utils/encryption/token_encryption.dart'; import '../../../../../utils/lock_auth.dart'; -import '../../../../../utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import '../../../../../utils/validators.dart'; import '../../../../../widgets/dialog_widgets/default_dialog.dart'; @@ -220,9 +219,7 @@ class _ExportTokensToFileDialogState return true; } catch (e) { if (context.mounted) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (l) => l.errorSavingFile, - ); + showErrorStatusMessage(message: (l) => l.errorSavingFile, ref: ref); } setState(() => _exportPressed = false); return false; @@ -242,9 +239,7 @@ class _ExportTokensToFileDialogState return true; } catch (e) { if (context.mounted) { - ref.read(statusMessageProvider.notifier).state = StatusMessage( - message: (l) => l.errorSavingFile, - ); + showErrorStatusMessage(message: (l) => l.errorSavingFile, ref: ref); } setState(() => _exportPressed = false); return false; diff --git a/lib/views/settings_view/settings_view.dart b/lib/views/settings_view/settings_view.dart index 46cbf2650..78535bc70 100644 --- a/lib/views/settings_view/settings_view.dart +++ b/lib/views/settings_view/settings_view.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import '../../l10n/app_localizations.dart'; import '../../widgets/push_request_listener.dart'; +import '../../widgets/status_bar.dart'; import '../view_interface.dart'; import 'settings_groups/settings_group_allow_screenshot/settings_group_allow_screenshot.dart'; import 'settings_groups/settings_group_background_image.dart'; @@ -50,23 +51,25 @@ class SettingsView extends ConsumerView { maxLines: 2, // Title can be shown on small screens too. ), ), - body: SafeArea( - child: const SingleChildScrollView( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsGroupFeedback(), - SettingsGroupImportExportTokens(), - SettingsGroupPushToken(), - SettingsGroupContainer(), - SettingsGroupLanguage(), - SettingsGroupTheme(), - SettingsGroupBackroundImage(), - SettingsGroupAllowScreenshot(), - SettingsGroupErrorLog(), - SettingsGroupGeneral(), - ], + body: StatusBar( + child: SafeArea( + child: const SingleChildScrollView( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsGroupFeedback(), + SettingsGroupImportExportTokens(), + SettingsGroupPushToken(), + SettingsGroupContainer(), + SettingsGroupLanguage(), + SettingsGroupTheme(), + SettingsGroupBackroundImage(), + SettingsGroupAllowScreenshot(), + SettingsGroupErrorLog(), + SettingsGroupGeneral(), + ], + ), ), ), ), diff --git a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart index a92a28778..fdd899c01 100644 --- a/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart +++ b/lib/views/settings_view/settings_view_widgets/update_firebase_token_dialog.dart @@ -81,6 +81,7 @@ class _UpdateFirebaseTokenDialogState showErrorStatusMessage( message: (l) => l.firebaseToken, details: (l) => l.errorSynchronizationNoNetworkConnection, + ref: ref, ); return; } diff --git a/lib/views/splash_screen/splash_screen.dart b/lib/views/splash_screen/splash_screen.dart index 6024f36b5..d04c64c41 100644 --- a/lib/views/splash_screen/splash_screen.dart +++ b/lib/views/splash_screen/splash_screen.dart @@ -83,6 +83,7 @@ class _SplashScreenState extends ConsumerState { if (!mounted) return []; final tokenState = await ref.read(tokenProvider.future); + if (!ref.context.mounted) return []; ref .read(tokenContainerProvider.notifier) .syncContainers(tokenState: tokenState, isManually: false); @@ -92,6 +93,7 @@ class _SplashScreenState extends ConsumerState { .then((values) async { if (!mounted) return; final tokenState = await ref.read(tokenProvider.future); + if (!ref.context.mounted) return; ref .read(tokenContainerProvider.notifier) .syncContainers(tokenState: tokenState, isManually: false); diff --git a/lib/widgets/button_widgets/intent_button.dart b/lib/widgets/button_widgets/intent_button.dart index 078f3083e..65bb4c4e8 100644 --- a/lib/widgets/button_widgets/intent_button.dart +++ b/lib/widgets/button_widgets/intent_button.dart @@ -50,6 +50,7 @@ class IntentButton extends StatefulWidget { final Widget child; final int delaySeconds; final int cooldownMs; + final bool isLoading; const IntentButton({ super.key, @@ -58,6 +59,7 @@ class IntentButton extends StatefulWidget { required this.child, this.delaySeconds = 0, this.cooldownMs = 0, + this.isLoading = false, }); @override @@ -66,11 +68,13 @@ class IntentButton extends StatefulWidget { class _IntentButtonState extends State with SingleTickerProviderStateMixin { - bool _isLoading = false; + bool _isInternalLoading = false; bool _isCooldown = false; late int _currentDelay; late AnimationController _animation; + bool get _effectiveLoading => widget.isLoading || _isInternalLoading; + @override void initState() { super.initState(); @@ -99,7 +103,7 @@ class _IntentButtonState extends State Future _handlePress() async { if (widget.onPressed == null || _isCooldown || - _isLoading || + _effectiveLoading || _currentDelay > 0) { return; } @@ -111,7 +115,7 @@ class _IntentButtonState extends State if (mounted) { setState(() { - if (isFuture) _isLoading = true; + if (isFuture) _isInternalLoading = true; if (widget.cooldownMs > 0) _isCooldown = true; }); } @@ -128,14 +132,16 @@ class _IntentButtonState extends State if (mounted) { setState(() { - _isLoading = false; + _isInternalLoading = false; _isCooldown = false; }); } } VoidCallback? get _effectiveOnPressed { - if (widget.onPressed == null || _isCooldown || _isLoading) return null; + if (widget.onPressed == null || _isCooldown || _effectiveLoading) { + return null; + } if (_currentDelay > 0) return () {}; return _handlePress; } @@ -242,7 +248,7 @@ class _IntentButtonState extends State Widget _buildChildWithStatus(AppDimensions dimensions) { if (_currentDelay > 0) return _buildCountdownStack(dimensions); - if (!_isLoading) return widget.child; + if (!_effectiveLoading) return widget.child; return Stack( alignment: Alignment.center, diff --git a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart index 328bbd08f..cbb62525c 100644 --- a/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart +++ b/lib/widgets/dialog_widgets/push_request_dialog/push_request_dialog.dart @@ -24,7 +24,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; -import 'package:privacyidea_authenticator/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import 'package:privacyidea_authenticator/widgets/button_widgets/push_action_button.dart'; import '../../../../model/push_request/push_requests.dart'; @@ -108,8 +107,9 @@ mixin PushDialogMixin { .accept(token, pushRequest, selectedAnswer: answer); } catch (e) { Logger.error('Error accepting push request: $e'); - ref.read(statusMessageProvider.notifier).state = StatusMessage( + showErrorStatusMessage( message: (l10n) => "Error accepting push request: $e", + ref: ref, ); return; } @@ -158,6 +158,7 @@ mixin PushDialogMixin { } if (!ref.context.mounted) return; await ref.read(pushRequestProvider.notifier).remove(pushRequest); + if (!ref.context.mounted) return; if (context.mounted) { _onHandled(context: context, ref: ref); } diff --git a/lib/widgets/status_bar.dart b/lib/widgets/status_bar.dart index f52fd026c..2fd0743a2 100644 --- a/lib/widgets/status_bar.dart +++ b/lib/widgets/status_bar.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,83 +7,41 @@ import '../utils/customization/theme_extentions/status_colors.dart'; import '../utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart'; import '../utils/utils.dart'; -class StatusBar extends ConsumerStatefulWidget { +class StatusBar extends ConsumerWidget { final Widget child; const StatusBar({super.key, required this.child}); @override - ConsumerState createState() => _StatusBarState(); -} - -class _StatusBarState extends ConsumerState { - StatusMessage? previousStatusMessage; - StatusMessage? currentStatusMessage; - Queue statusbarQueue = Queue(); - - late Function(DismissDirection) onDismissed; - - OverlayEntry? statusbarOverlay; - ScaffoldFeatureController? snackbarController; - - @override - Widget build(BuildContext context) { - final newStatusMessage = ref.watch(statusMessageProvider); - _addToQueueIfNotInQueue(newStatusMessage); - return widget.child; - } - - @override - void initState() { - onDismissed = (direction) { - if (!mounted) return; - setState(() { - currentStatusMessage = null; - statusbarOverlay!.remove(); - statusbarOverlay = null; - ref.read(statusMessageProvider.notifier).state = null; - _tryPop(); - }); - }; - - super.initState(); - } - - void _addToQueueIfNotInQueue(StatusMessage? statusMessage) { - if (statusMessage == null) return; - if (!statusbarQueue.contains(statusMessage) && - currentStatusMessage != statusMessage) { - statusbarQueue.add(statusMessage); - } - _tryPop(); - } - - void _tryPop() { - if (currentStatusMessage == null && statusbarQueue.isNotEmpty) { - currentStatusMessage = statusbarQueue.removeFirst(); - _showStatusbarOverlay(currentStatusMessage!); - } + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(statusProvider, (previous, next) { + if (next.current != null && previous?.current != next.current) { + _showOverlay(context, ref, next.current!); + } + }); + return child; } - void _showStatusbarOverlay(StatusMessage statusMessage) { - final localizations = AppLocalizations.of(context)!; - final statusText = statusMessage.message(localizations); - final statusSubText = statusMessage.details?.call(localizations); - if (statusbarOverlay != null) { - statusbarOverlay?.remove(); - statusbarOverlay = null; - } + void _showOverlay( + BuildContext context, + WidgetRef ref, + StatusMessage message, + ) { + final l10n = AppLocalizations.of(context)!; + OverlayEntry? entry; - statusbarOverlay = OverlayEntry( + entry = OverlayEntry( builder: (context) => StatusBarOverlayEntry( - onDismissed: onDismissed, - statusText: statusText, - statusSubText: statusSubText, - isError: statusMessage.isError, + statusText: message.message(l10n), + statusSubText: message.details?.call(l10n), + isError: message.isError, + onDismissed: (_) { + entry?.remove(); + ref.read(statusProvider.notifier).dismiss(); + }, ), ); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Overlay.of(context).insert(statusbarOverlay!); - }); + + Overlay.of(context).insert(entry); } } diff --git a/pubspec.lock b/pubspec.lock index 7fad125a9..71f40634f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "92.0.0" + version: "91.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,34 +21,34 @@ packages: dependency: transitive description: name: analysis_server_plugin - sha256: "44adba4d74a2541173bad4c11531d2a4d22810c29c5ddb458a38e9f4d0e5eac7" + sha256: "26844e7f977087567135d62532b67d5639fe206c5194c3f410ba75e1a04a2747" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "8.4.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: ff4bd291778c7417fe53fe24ee0d0a1f1ffe281a2d4ea887e7094f16e36eace7 + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.11" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "6645a029da947ffd823d98118f385d4bd26b54eb069c006b22e0b94e451814b5" + sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" url: "https://pub.dev" source: hosted - version: "0.13.11" + version: "0.13.10" app_links: dependency: "direct main" description: @@ -668,10 +668,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.1.0" flutter_secure_storage: dependency: "direct main" description: @@ -766,10 +766,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: f23ea33b3863f119b58ed1b586e881a46bd28715ddcc4dbc33104524e3434131 + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: @@ -1000,18 +1000,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.dev" source: hosted - version: "6.13.0" + version: "6.11.2" leak_tracker: dependency: transitive description: @@ -1432,42 +1432,42 @@ packages: dependency: transitive description: name: riverpod - sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.dev" source: hosted - version: "1.0.0-dev.9" + version: "1.0.0-dev.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "16471a1260b94e939394d78f1c63a9350936ac4a68c9fbdab40be47268c0b04f" + sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46 url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.0" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "6f9220534d7a353b53c875ea191a84d28cb4e52ac420a66a1bd7318329d977b0" + sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.0+1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "64e8debf5b719a37d48b9785dd595d34133fdcd84b8fd07157a621c54ab2156f" + sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.0" shared_preferences: dependency: "direct main" description: @@ -1581,10 +1581,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "4a85e90b50694e652075cbe4575665539d253e6ec10e46e76b45368ab5e3caae" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.10" + version: "1.3.8" source_map_stack_trace: dependency: transitive description: diff --git a/test/unit_test/api/privacy_idea_container_api_test.dart b/test/unit_test/api/privacy_idea_container_api_test.dart index 87a0696ba..72a8b0444 100644 --- a/test/unit_test/api/privacy_idea_container_api_test.dart +++ b/test/unit_test/api/privacy_idea_container_api_test.dart @@ -450,7 +450,7 @@ void _testPrivacyIdeaContainerApi() { } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = - '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"tokentype":"HOTP","label":"label1","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["435986","964213"],"counter":"5"}]}'; + '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"tokentype":"HOTP","label":"label1","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","otp":["435986","964213"],"counter":"5"}]}'; final signMessage2 = '$containerChallengeNonce|' @@ -460,51 +460,62 @@ void _testPrivacyIdeaContainerApi() { '$publicEncKeyClientB64|' '$containerDictClient'; - if (invocationUrl.toString() == - 'http://example.com/container/synchronize' && - invocationBody['container_dict_client'] == containerDictClient && - EccUtils().validateSignature( - tokenContainer.ecPublicClientKey!, - invocationBody['signature']!, - signMessage2, - )) { - return Response( - jsonEncode({ - 'id': 5, - 'jsonrpc': '2.0', - 'result': { - 'status': true, - 'value': { - 'container_dict_server': - 'oAI8Mq9tdIffSbuX62L8bSN3ZZ7wIGrip_pDCGY59Qz32DziorR4BMZ9vf_Vu5aUeDophX4Hdq_DdK2OJpzlYywSe5mysluVpCepGqr8xsGpTHwU5lPjIAN-OFCNO1_u2QH3dgwbZDVoHyPKfwICkULXqoIHmtb6BOtWzpMtC2Sd8v0ytA5HY7dgpO5MldidXrhI6RBdqfjJoknwnC7girLqZj0otDjZvfQrz3RFtCBdTqa6e4gxsnIEE8c1UTohvEN_LNvkP4sx7xpO3uI048ZXg8YxJMZThdAK5ai36cEGKmRuQVbM01DVeJHQHXwgacHIGKTTNgoUJb-g-7Qf-TJdhsrAH9S6eEiJLv8N_S2eRNfXubPlO9trK9HRsEra6I3f1Epw8-GRHB9CW5Yd6U_e9-ndaNKMGXkbUUaGF_wLDTqPnLSzgiNgDlAZtfaTHJRYqm-pBqDXHUZe7mgvLUqOhTHKIuBrQxW2t8TZ5VWU1psVT70D4J4RR9MInmZvUDcO__AqnAXQsHiaoWF-z2QSmX9I5Em8jpF5fQSgHNJBM3UEzBHJEE_D6dxnoz5M1O314qz3YIy5JphBssRjfBYhxdRPB5nYBp2EpSvBvAYHrbu5Iv431JG05nLbb_4AgOsJDm1RhGjqCcjbvDYKpyN6ewoUP--q0L-eZPxUlRGeITuqMmNpIVXlCiPiJ9u2L1zQNdPv0OEvhkOtd1vFm492sZ7Mr9nyzJ13f5Ca86smHK1sDDYczjqHlxpevqtoDxlpgCQqTv_XzF6DjXRHPtNB6hiAuP-jIzCTL5RYokj5nv28Ih3dCRCVRfU8CVcz8KiRVBSo6WTQ_Q7DzL_ZqcmK2xnlwGdmoWVhTXUp75ed1YjSwGaVQ6D5w50ZgWNP5YYiJIUpN27WS3akEIDrwpm7h0ZctP8Kvlj46VRxd2Urhi_6NafWBnX4M8Jo6AWsp2hoFl6i2WoSa9c2kOi-OvVg8PzHFknNiFSjHTvytm-Q6zxpm--6kia1xbdRdEDoCD4YW901GJXoZOKtfUAA7soODoI5mNJT9D_ZXBuAJcDqjEkKpZWMWyYkfOwrTqlsrSKtYue4tQ4INBnhfwTZk2ppTwlRFp6jOjHTgsSdcfCKOzresp75u8My30dbIhzRIYgFp6jwcRikAlBbwXBVBT7iR_6Nrw==', - 'encryption_algorithm': 'AES', - 'encryption_params': { - 'algorithm': 'AES', - 'mode': 'GCM', - 'init_vector': 'Q3f4NMAuDFrdI9R3uZ7DHA==', - 'tag': 'iThLozfTiK4twnPXnvF0nA==', - }, - 'policies': { - 'disable_client_container_unregister': true, - 'disable_client_token_deletion': true, - 'container_client_rollover': false, - 'initially_add_tokens_to_container': false, - }, - 'public_server_key': - 'AMc1nbpqrEOgQLe1-nR2ExnqE1IM8qMDETYw65IU6wQ=', - 'server_url': 'http://example.com/container/synchronize', - }, - }, - 'time': 1.0, - 'version': 'privacyIDEA 3.6.2', - 'versionnumber': '3.6.2', - 'detail': null, - 'signature': 'signature', - }), - 200, + if (invocationUrl.toString() != + 'http://example.com/container/synchronize') { + Logger.warning( + 'invocationUrl.toString() did not match expected value: ${invocationUrl.toString()}', ); + return Response(jsonEncode(exampleError), 400); } - return Response(jsonEncode(exampleError), 400); + if (invocationBody['container_dict_client'] != containerDictClient) { + Logger.warning( + 'invocationBody[\'container_dict_client\'] did not match expected value: ${invocationBody['container_dict_client']}', + ); + return Response(jsonEncode(exampleError), 400); + } + if (!EccUtils().validateSignature( + tokenContainer.ecPublicClientKey!, + invocationBody['signature']!, + signMessage2, + )) { + Logger.warning('Signature validation failed'); + return Response(jsonEncode(exampleError), 400); + } + return Response( + jsonEncode({ + 'id': 5, + 'jsonrpc': '2.0', + 'result': { + 'status': true, + 'value': { + 'container_dict_server': + 'oAI8Mq9tdIffSbuX62L8bSN3ZZ7wIGrip_pDCGY59Qz32DziorR4BMZ9vf_Vu5aUeDophX4Hdq_DdK2OJpzlYywSe5mysluVpCepGqr8xsGpTHwU5lPjIAN-OFCNO1_u2QH3dgwbZDVoHyPKfwICkULXqoIHmtb6BOtWzpMtC2Sd8v0ytA5HY7dgpO5MldidXrhI6RBdqfjJoknwnC7girLqZj0otDjZvfQrz3RFtCBdTqa6e4gxsnIEE8c1UTohvEN_LNvkP4sx7xpO3uI048ZXg8YxJMZThdAK5ai36cEGKmRuQVbM01DVeJHQHXwgacHIGKTTNgoUJb-g-7Qf-TJdhsrAH9S6eEiJLv8N_S2eRNfXubPlO9trK9HRsEra6I3f1Epw8-GRHB9CW5Yd6U_e9-ndaNKMGXkbUUaGF_wLDTqPnLSzgiNgDlAZtfaTHJRYqm-pBqDXHUZe7mgvLUqOhTHKIuBrQxW2t8TZ5VWU1psVT70D4J4RR9MInmZvUDcO__AqnAXQsHiaoWF-z2QSmX9I5Em8jpF5fQSgHNJBM3UEzBHJEE_D6dxnoz5M1O314qz3YIy5JphBssRjfBYhxdRPB5nYBp2EpSvBvAYHrbu5Iv431JG05nLbb_4AgOsJDm1RhGjqCcjbvDYKpyN6ewoUP--q0L-eZPxUlRGeITuqMmNpIVXlCiPiJ9u2L1zQNdPv0OEvhkOtd1vFm492sZ7Mr9nyzJ13f5Ca86smHK1sDDYczjqHlxpevqtoDxlpgCQqTv_XzF6DjXRHPtNB6hiAuP-jIzCTL5RYokj5nv28Ih3dCRCVRfU8CVcz8KiRVBSo6WTQ_Q7DzL_ZqcmK2xnlwGdmoWVhTXUp75ed1YjSwGaVQ6D5w50ZgWNP5YYiJIUpN27WS3akEIDrwpm7h0ZctP8Kvlj46VRxd2Urhi_6NafWBnX4M8Jo6AWsp2hoFl6i2WoSa9c2kOi-OvVg8PzHFknNiFSjHTvytm-Q6zxpm--6kia1xbdRdEDoCD4YW901GJXoZOKtfUAA7soODoI5mNJT9D_ZXBuAJcDqjEkKpZWMWyYkfOwrTqlsrSKtYue4tQ4INBnhfwTZk2ppTwlRFp6jOjHTgsSdcfCKOzresp75u8My30dbIhzRIYgFp6jwcRikAlBbwXBVBT7iR_6Nrw==', + 'encryption_algorithm': 'AES', + 'encryption_params': { + 'algorithm': 'AES', + 'mode': 'GCM', + 'init_vector': 'Q3f4NMAuDFrdI9R3uZ7DHA==', + 'tag': 'iThLozfTiK4twnPXnvF0nA==', + }, + 'policies': { + 'disable_client_container_unregister': true, + 'disable_client_token_deletion': true, + 'container_client_rollover': false, + 'initially_add_tokens_to_container': false, + }, + 'public_server_key': + 'AMc1nbpqrEOgQLe1-nR2ExnqE1IM8qMDETYw65IU6wQ=', + 'server_url': 'http://example.com/container/synchronize', + }, + }, + 'time': 1.0, + 'version': 'privacyIDEA 3.6.2', + 'versionnumber': '3.6.2', + 'detail': null, + 'signature': 'signature', + }), + 200, + ); }); final type = X25519().keyPairType; @@ -592,7 +603,7 @@ void _testPrivacyIdeaContainerApi() { } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = - '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"OATH00068B93","tokentype":"HOTP","label":"OATH00068B93","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","counter":"1"}]}'; + '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"OATH00068B93","tokentype":"HOTP","label":"OATH00068B93","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","counter":"1"}]}'; final signMessage2 = '$containerChallengeNonce|' @@ -745,7 +756,7 @@ void _testPrivacyIdeaContainerApi() { } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = - '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; + '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; final signMessage2 = '$containerChallengeNonce|' @@ -896,7 +907,7 @@ void _testPrivacyIdeaContainerApi() { } final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = - '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"serial":"OATH00166051","tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","counter":"1"}]}'; + '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","period":"30"},{"serial":"OATH00166051","tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","counter":"1"}]}'; final signMessage2 = '$containerChallengeNonce|' @@ -1041,7 +1052,7 @@ void _testPrivacyIdeaContainerApi() { final publicEncKeyClientB64 = invocationBody['public_enc_key_client']; final containerDictClient = - '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; + '{"container_serial":"SMPH00067A2F","type":"smartphone","tokens":[{"serial":"TOTP00011B1F","tokentype":"TOTP","label":"TOTP00011B1F","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","period":"30"},{"tokentype":"HOTP","label":"OATH00166051","issuer":"privacyIDEA","pin":"False","offline":false,"app_force_unlock":"none","algorithm":"SHA1","digits":"6","otp":["079447","501895"],"counter":"1"}]}'; final signMessage2 = '$containerChallengeNonce|' diff --git a/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart index b16e94698..4be76ca78 100644 --- a/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart +++ b/test/unit_test/views/container_view/container_widgets/buttons/sync_container_button_test.dart @@ -33,14 +33,15 @@ class FakeTokenNotifier extends TokenNotifier { class FakeTokenContainerNotifier extends TokenContainerNotifier { final MockTokenContainerNotifier mock; - FakeTokenContainerNotifier(this.mock); + final List initialContainers; + FakeTokenContainerNotifier(this.mock, {this.initialContainers = const []}); @override Future build({ required TokenContainerApi containerApi, required EccUtils eccUtils, required TokenContainerRepository repo, - }) async => TokenContainerState(containerList: []); + }) async => TokenContainerState(containerList: initialContainers); @override Future> syncContainers({ @@ -65,6 +66,7 @@ void main() { setUp(() { mockContainer = MockTokenContainerFinalized(); mockInternalNotifier = MockTokenContainerNotifier(); + when(mockContainer.serial).thenReturn('SERIAL_1'); }); Future pumpButton( @@ -79,32 +81,24 @@ void main() { overrides: [ tokenProvider.overrideWith(() => FakeTokenNotifier()), tokenContainerProvider.overrideWith( - () => FakeTokenContainerNotifier(mockInternalNotifier), + () => FakeTokenContainerNotifier( + mockInternalNotifier, + initialContainers: [mockContainer], + ), ), hasFirebaseProvider.overrideWith((ref) => Future.value(true)), ], child: MaterialApp( home: Scaffold( - body: SizedBox.expand( - child: Center( - child: SyncContainerButton( - isPreview: isPreview, - container: mockContainer, - ), - ), + body: SyncContainerButton( + isPreview: isPreview, + container: mockContainer, ), ), ), ), ); - - debugPrint('Pump complete for state: $state, isPreview: $isPreview'); - - debugPrint(tester.allWidgets.toString()); - - debugPrint('Looking for IntentButton: ${find.byType(IntentButton)}'); - // Warte kurz auf das Layout - await tester.pumpAndSettle(); + await tester.pump(); } group('SyncContainerButton - Logic and States', () { @@ -112,34 +106,50 @@ void main() { tester, ) async { await pumpButton(tester, state: SyncState.notStarted, isPreview: true); - - await tester.pumpAndSettle(); - expect(find.byType(IntentButton), findsNothing); expect(find.byIcon(Icons.sync), findsOneWidget); }); - testWidgets('should be enabled when SyncState is notStarted', ( + testWidgets( + 'should be enabled and NOT loading when SyncState is completed', + (tester) async { + await pumpButton(tester, state: SyncState.completed); + final button = tester.widget(find.byType(IntentButton)); + expect(button.onPressed, isNotNull); + expect(button.isLoading, isFalse); + }, + ); + + testWidgets('should show loading state when SyncState is syncing', ( tester, ) async { - await pumpButton(tester, state: SyncState.notStarted); - + await pumpButton(tester, state: SyncState.syncing); final button = tester.widget(find.byType(IntentButton)); - expect(button.onPressed, isNotNull); + expect(button.isLoading, isTrue); + expect(find.byType(CircularProgressIndicator), findsOneWidget); }); - testWidgets('should be disabled when SyncState is syncing', (tester) async { - await pumpButton(tester, state: SyncState.syncing); + testWidgets('should show loading state when pressed', (tester) async { + await pumpButton(tester, state: SyncState.completed); - final button = tester.widget(find.byType(IntentButton)); - expect(button.onPressed, isNull); - }); + when( + mockInternalNotifier.syncContainers( + tokenState: anyNamed('tokenState'), + containersToSync: anyNamed('containersToSync'), + isManually: anyNamed('isManually'), + isInitSync: anyNamed('isInitSync'), + ), + ).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 100)); + return {}; + }); - testWidgets('should be enabled when SyncState is failed', (tester) async { - await pumpButton(tester, state: SyncState.failed); + await tester.tap(find.byType(IntentButton)); + await tester.pump(); + expect(find.byType(CircularProgressIndicator), findsOneWidget); - final button = tester.widget(find.byType(IntentButton)); - expect(button.onPressed, isNotNull); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(); }); testWidgets('should call syncContainers when pressed', (tester) async { From b1931365164b49c95af2391cbb77eaca21c3868a Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:25:17 +0200 Subject: [PATCH 12/13] feat: add controlMinWidth to AppDimensions and update IntentButton minimum size for improved layout --- .../theme_extentions/app_dimensions.dart | 13 +++++++++++++ lib/widgets/button_widgets/intent_button.dart | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/utils/customization/theme_extentions/app_dimensions.dart b/lib/utils/customization/theme_extentions/app_dimensions.dart index cbc156f20..81eb08096 100644 --- a/lib/utils/customization/theme_extentions/app_dimensions.dart +++ b/lib/utils/customization/theme_extentions/app_dimensions.dart @@ -66,6 +66,10 @@ class AppDimensions extends ThemeExtension { final double controlHeight; static const defaultControlHeight = 48.0; + /// The minimum width for interactive elements like [ElevatedButton] or [TextField]. + final double controlMinWidth; + static const defaultControlMinWidth = 120.0; + /// The maximum width for content on large screens (tablets/desktop) to maintain readability. final double maxContentWidth; static const defaultMaxContentWidth = 600.0; @@ -81,6 +85,7 @@ class AppDimensions extends ThemeExtension { double? iconSizeLarge, double? strokeWidth, double? controlHeight, + double? controlMinWidth, double? maxContentWidth, }) : spacingSmall = spacingSmall ?? defaultSpacingSmall, spacingMedium = spacingMedium ?? defaultSpacingMedium, @@ -92,6 +97,7 @@ class AppDimensions extends ThemeExtension { iconSizeLarge = iconSizeLarge ?? defaultIconSizeLarge, strokeWidth = strokeWidth ?? defaultStrokeWidth, controlHeight = controlHeight ?? defaultControlHeight, + controlMinWidth = controlMinWidth ?? defaultControlMinWidth, maxContentWidth = maxContentWidth ?? defaultMaxContentWidth; @override @@ -106,6 +112,7 @@ class AppDimensions extends ThemeExtension { iconSizeMedium: lerpDouble(iconSizeMedium, other.iconSizeMedium, t)!, strokeWidth: lerpDouble(strokeWidth, other.strokeWidth, t)!, controlHeight: lerpDouble(controlHeight, other.controlHeight, t)!, + controlMinWidth: lerpDouble(controlMinWidth, other.controlMinWidth, t)!, maxContentWidth: lerpDouble(maxContentWidth, other.maxContentWidth, t)!, ); } @@ -122,6 +129,7 @@ class AppDimensions extends ThemeExtension { double? iconSizeLarge, double? strokeWidth, double? controlHeight, + double? controlMinWidth, double? maxContentWidth, }) { return AppDimensions( @@ -135,6 +143,7 @@ class AppDimensions extends ThemeExtension { iconSizeLarge: iconSizeLarge ?? this.iconSizeLarge, strokeWidth: strokeWidth ?? this.strokeWidth, controlHeight: controlHeight ?? this.controlHeight, + controlMinWidth: controlMinWidth ?? this.controlMinWidth, maxContentWidth: maxContentWidth ?? this.maxContentWidth, ); } @@ -153,6 +162,7 @@ class AppDimensions extends ThemeExtension { spacingLarge: (json['spacingLarge'] as num?)?.toDouble(), strokeWidth: (json['strokeWidth'] as num?)?.toDouble(), screenPadding: (json['screenPadding'] as num?)?.toDouble(), + controlMinWidth: (json['controlMinWidth'] as num?)?.toDouble(), maxContentWidth: (json['maxContentWidth'] as num?)?.toDouble(), ); } @@ -169,6 +179,9 @@ class AppDimensions extends ThemeExtension { 'spacingMedium': spacingMedium, 'spacingLarge': spacingLarge, 'strokeWidth': strokeWidth, + 'screenPadding': screenPadding, + 'controlMinWidth': controlMinWidth, + 'maxContentWidth': maxContentWidth, }; } } diff --git a/lib/widgets/button_widgets/intent_button.dart b/lib/widgets/button_widgets/intent_button.dart index 65bb4c4e8..c1c270ffb 100644 --- a/lib/widgets/button_widgets/intent_button.dart +++ b/lib/widgets/button_widgets/intent_button.dart @@ -177,7 +177,7 @@ class _IntentButtonState extends State disabledForegroundColor: theme.colorScheme.onSurface.withValues( alpha: 0.38, ), - minimumSize: Size(0, dimensions.controlHeight), + minimumSize: Size(dimensions.controlMinWidth, dimensions.controlHeight), padding: const EdgeInsets.symmetric(horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(dimensions.borderRadius), @@ -193,7 +193,7 @@ class _IntentButtonState extends State return OutlinedButton( onPressed: _effectiveOnPressed, style: OutlinedButton.styleFrom( - minimumSize: Size(0, dimensions.controlHeight), + minimumSize: Size(dimensions.controlMinWidth, dimensions.controlHeight), padding: const EdgeInsets.symmetric(horizontal: 16), disabledForegroundColor: theme.colorScheme.onSurface.withValues( alpha: 0.38, @@ -218,7 +218,7 @@ class _IntentButtonState extends State onPressed: _effectiveOnPressed, style: TextButton.styleFrom( foregroundColor: theme.colorScheme.onSurface, - minimumSize: Size(0, dimensions.controlHeight), + minimumSize: Size(dimensions.controlMinWidth, dimensions.controlHeight), padding: const EdgeInsets.symmetric(horizontal: 12), disabledForegroundColor: theme.colorScheme.onSurface.withValues( alpha: 0.38, From cbe90d69069a7375c0106576dedcb1559901c7e3 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:15:04 +0200 Subject: [PATCH 13/13] feat: enhance logging in OptionalObjectValidator and improve equality checks in StatusMessage --- .../optional_object_validator.dart | 1 + .../status_message_provider.dart | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/utils/object_validator/optional_object_validator.dart b/lib/utils/object_validator/optional_object_validator.dart index a69753d07..8042ac830 100644 --- a/lib/utils/object_validator/optional_object_validator.dart +++ b/lib/utils/object_validator/optional_object_validator.dart @@ -28,6 +28,7 @@ class OptionalObjectValidator extends BaseValidator { try { return _executeTransform(value, name); } catch (e) { + Logger.debug('Failed to transform value "$value" for $name: $e'); return null; } } diff --git a/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart b/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart index ee1dd62b0..1d1e27dcf 100644 --- a/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart +++ b/lib/utils/riverpod/riverpod_providers/state_providers/status_message_provider.dart @@ -21,6 +21,7 @@ import 'dart:collection'; import 'package:flutter_riverpod/legacy.dart'; import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations_en.dart'; final statusProvider = StateNotifierProvider( (ref) => StatusNotifier(), @@ -32,6 +33,24 @@ class StatusMessage { final bool isError; StatusMessage({required this.message, this.details, this.isError = true}); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is StatusMessage && + other.message(AppLocalizationsEn()) == message(AppLocalizationsEn()) && + other.details?.call(AppLocalizationsEn()) == + details?.call(AppLocalizationsEn()) && + other.isError == isError; + } + + @override + int get hashCode => Object.hashAll([ + message(AppLocalizationsEn()), + details?.call(AppLocalizationsEn()), + isError, + ]); } class StatusState {