diff --git a/.github/workflows/flutter_build.yml b/.github/workflows/flutter_build.yml index 219d45712..b5d892e76 100644 --- a/.github/workflows/flutter_build.yml +++ b/.github/workflows/flutter_build.yml @@ -19,14 +19,15 @@ jobs: api-level: [21,31] # [minSdk, most used, newest (30 is not working :(] 19 would be minSDK but does not support x86_64 target: [default] # [default, google_apis] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: - java-version: '17.0.7' + distribution: 'zulu' + java-version: '11' - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.16.0' + flutter-version: '3.16.5' - run: "flutter upgrade" - run: "flutter --version" - run: "flutter pub get" diff --git a/.github/workflows/flutter_test.yml b/.github/workflows/flutter_test.yml index 10eaaf279..a10d35310 100644 --- a/.github/workflows/flutter_test.yml +++ b/.github/workflows/flutter_test.yml @@ -11,14 +11,15 @@ jobs: timeout-minutes: 40 steps: - - uses: actions/checkout@v1 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: - java-version: '17.0.7' - - uses: subosito/flutter-action@v1 + distribution: 'zulu' + java-version: '11' + - uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.16.0' + flutter-version: '3.16.5' - run: "flutter --version" - run: flutter pub get - run: flutter test diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 57cceb1b5..b46db6b52 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -10,24 +10,22 @@ jobs: strategy: matrix: api-level: [29] - target: [playstore] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: - distribution: 'temurin' + distribution: 'zulu' java-version: '11' - - uses: subosito/flutter-action@v1 + - uses: subosito/flutter-action@v2 with: - flutter-version: '3.16.0' + flutter-version: '3.16.5' channel: 'stable' # Run integration test - name: Run Flutter Driver tests uses: reactivecircus/android-emulator-runner@v2 with: - target: ${{ matrix.target }} api-level: ${{ matrix.api-level }} arch: x86_64 profile: Nexus 6 diff --git a/integration_test/two_step_rollout_test.dart b/integration_test/two_step_rollout_test.dart index 98a305d41..35f4904cc 100644 --- a/integration_test/two_step_rollout_test.dart +++ b/integration_test/two_step_rollout_test.dart @@ -9,8 +9,11 @@ import 'package:privacyidea_authenticator/state_notifiers/settings_notifier.dart import 'package:privacyidea_authenticator/state_notifiers/token_folder_notifier.dart'; import 'package:privacyidea_authenticator/state_notifiers/token_notifier.dart'; import 'package:privacyidea_authenticator/utils/app_customizer.dart'; +import 'package:privacyidea_authenticator/utils/logger.dart'; import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; import 'package:privacyidea_authenticator/views/main_view/main_view.dart'; +import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/hotp_token_widgets/hotp_token_widget_tile.dart'; +import 'package:privacyidea_authenticator/views/main_view/main_view_widgets/token_widgets/totp_token_widgets/totp_token_widget_tile.dart'; import 'package:privacyidea_authenticator/widgets/widget_keys.dart'; import '../test/tests_app_wrapper.dart'; @@ -74,40 +77,49 @@ void main() { Future _addTwoStepHotpTokenTest(WidgetTester tester) async { await pumpUntilFindNWidgets(tester, find.byType(MainView), 1, const Duration(seconds: 10)); + Logger.info('Adding HOTP Token'); globalRef!.read(tokenProvider.notifier).addTokenFromOtpAuth( otpAuth: 'otpauth://hotp/OATH0001DBD0?secret=AALIBQJMOGEE7SAVEZ5D3K2ADO7MVFQD&counter=1&digits=6&issuer=privacyIDEA&2step_salt=8&2step_output=20&2step_difficulty=10000'); + Logger.info('Finding phone part dialog'); await pumpUntilFindNWidgets(tester, find.text(AppLocalizationsEn().phonePart), 1, const Duration(seconds: 20)); expect(find.text(AppLocalizationsEn().phonePart), findsOneWidget); final finder = find.byKey(twoStepDialogContent); expect(finder, findsOneWidget); final text = finder.evaluate().single.widget as Text; final phonePart = text.data; + Logger.info('Checking phone part exists and has correct length'); expect(phonePart, isNotNull); expect(phonePart, isNotEmpty); // step_output=20 expect(phonePart!.replaceAll(' ', '').length, 20); expect(find.text(AppLocalizationsEn().dismiss), findsOneWidget); + Logger.info('Dismissing dialog'); await tester.tap(find.text(AppLocalizationsEn().dismiss)); - await tester.pumpAndSettle(); + await pumpUntilFindNWidgets(tester, find.byType(HOTPTokenWidgetTile), 1, const Duration(seconds: 10)); } Future _addTwoStepTotpTokenTest(WidgetTester tester) async { await pumpUntilFindNWidgets(tester, find.byType(MainView), 1, const Duration(seconds: 10)); + Logger.info('Adding TOTP Token'); globalRef!.read(tokenProvider.notifier).addTokenFromOtpAuth( otpAuth: 'otpauth://totp/TOTP00009D5F?secret=NZ4OPONKAAGDFN2QHV26ZWYVTLFER4C6&period=30&digits=6&issuer=privacyIDEA&2step_salt=8&2step_output=20&2step_difficulty=10000'); + Logger.info('Finding phone part dialog'); await pumpUntilFindNWidgets(tester, find.text(AppLocalizationsEn().phonePart), 1, const Duration(seconds: 20)); expect(find.text(AppLocalizationsEn().phonePart), findsOneWidget); final finder = find.byKey(twoStepDialogContent); expect(finder, findsOneWidget); final text = finder.evaluate().single.widget as Text; final phonePart = text.data; + Logger.info('Checking phone part exists and has correct length'); expect(phonePart, isNotNull); expect(phonePart, isNotEmpty); // step_output=20 expect(phonePart!.replaceAll(' ', '').length, 20); expect(find.text(AppLocalizationsEn().dismiss), findsOneWidget); + Logger.info('Dismissing dialog'); await tester.tap(find.text(AppLocalizationsEn().dismiss)); - await tester.pumpAndSettle(); + await pumpUntilFindNWidgets(tester, find.byType(TOTPTokenWidgetTile), 1, 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 5b1df1962..f1bee958c 100644 --- a/integration_test/views_test.dart +++ b/integration_test/views_test.dart @@ -84,7 +84,7 @@ Future _popUntilMainView(WidgetTester tester) async { } Future _licensesViewTest(WidgetTester tester) async { - await pumpUntilFindNWidgets(tester, find.byIcon(Icons.info_outline), 1, const Duration(seconds: 10)); + await pumpUntilFindNWidgets(tester, find.byIcon(Icons.info_outline), 1, const Duration(seconds: 20)); await tester.tap(find.byIcon(Icons.info_outline)); await tester.pumpAndSettle(); expect(find.text(ApplicationCustomization.defaultCustomization.appName), findsOneWidget); diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 86ef658e7..113f82d50 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -476,5 +476,18 @@ "hidePushTokensDescription": "Skrýt push tokeny ze seznamu tokenů. Tím se tokeny neodstraní a budou stále viditelné na samostatné obrazovce.", "settingsGroupGeneral": "Obecné informace", "licensesAndVersion": "Licence a verze", - "privacyPolicy": "Zásady ochrany osobních údajů" + "privacyPolicy": "Zásady ochrany osobních údajů", + "legacySigningErrorTitle": "Při použití zastaralého tokenu došlo k chybě: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index af6b8c0b4..ab4a3b9d5 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -455,5 +455,18 @@ "hidePushTokensDescription": "Push-Token aus der Token-Liste ausblenden. Dadurch werden die Token nicht gelöscht und sind weiterhin auf einem separaten Bildschirm sichtbar.", "settingsGroupGeneral": "Allgemeines", "licensesAndVersion": "Lizenzen und Version", - "privacyPolicy": "Datenschutzerklärung" + "privacyPolicy": "Datenschutzerklärung", + "legacySigningErrorTitle": "Bei der Verwendung des veralteten Tokens ist ein Fehler aufgetreten: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "Der Token wurde in einer veralteten Version der App erstellt, was zu Problemen bei der Verwendung führen kann. Es wird empfohlen, einen neuen Push-Token zu erstellen, wenn das Problem weiterhin besteht!", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4d06cf173..687c6eee1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -467,5 +467,18 @@ "hidePushTokensDescription": "Hide push tokens from the token list. This will not delete the tokens and they will still be visible on a separate screen.", "settingsGroupGeneral": "General", "licensesAndVersion": "Licenses and version", - "privacyPolicy": "Privacy policy" + "privacyPolicy": "Privacy policy", + "legacySigningErrorTitle": "An error occured while using the legacy token: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "The token was enrolled in a old version of this app, which may cause trouble using it.\nIt is suggested to enroll a new push token if the problem persist!", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e2d10073f..8b51f7607 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -472,5 +472,18 @@ "hidePushTokensDescription": "Ocultar tokens push de la lista de tokens. Esto no borrará los tokens y seguirán siendo visibles en una pantalla aparte", "settingsGroupGeneral": "Información general", "licensesAndVersion": "Licencias y versión", - "privacyPolicy": "Política de privacidad" + "privacyPolicy": "Política de privacidad", + "legacySigningErrorTitle": "Se ha producido un error al utilizar el token obsoleto: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "El token se creó en una versión obsoleta de la aplicación, lo que puede provocar problemas al utilizarlo.\nSe recomienda crear un nuevo token push si el problema persiste.", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2fb8fac7b..3307299bf 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -477,5 +477,18 @@ "hidePushTokensDescription": "Masquer les jetons de poussée de la liste des jetons. Cela ne supprimera pas les jetons et ils seront toujours visibles sur un écran séparé", "settingsGroupGeneral": "Généralités", "licensesAndVersion": "Licences et version", - "privacyPolicy": "Politique de confidentialité" + "privacyPolicy": "Politique de confidentialité", + "legacySigningErrorTitle": "Une erreur s'est produite lors de l'utilisation du jeton obsolète : {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "Le token a été créé dans une version obsolète de l'application, ce qui peut entraîner des problèmes d'utilisation.\nIl est recommandé de créer un nouveau token push si le problème persiste !", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bd9a26c08..0a0515bef 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -990,6 +990,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Privacy policy'** String get privacyPolicy; + + /// Title of the error dialog that is shown when an error occurs while using a legacy token. + /// + /// In en, this message translates to: + /// **'An error occured while using the legacy token: {tokenLabel}'** + String legacySigningErrorTitle(Object tokenLabel); + + /// Message of the error dialog that is shown when an error occurs while using a legacy token. + /// + /// In en, this message translates to: + /// **'The token was enrolled in a old version of this app, which may cause trouble using it.\nIt is suggested to enroll a new push token if the problem persist!'** + String get legacySigningErrorMessage; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 828244447..42555bcd6 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -467,4 +467,12 @@ class AppLocalizationsCs extends AppLocalizations { @override String get privacyPolicy => 'Zásady ochrany osobních údajů'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Při použití zastaralého tokenu došlo k chybě: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'Token byl vytvořen v zastaralé verzi aplikace, což může vést k problémům při jeho používání.\nPokud problém přetrvává, doporučujeme vytvořit nový push token!'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f6ab86544..8bb2e2c50 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -467,4 +467,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get privacyPolicy => 'Datenschutzerklärung'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Bei der Verwendung des veralteten Tokens ist ein Fehler aufgetreten: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'Der Token wurde in einer veralteten Version der App erstellt, was zu Problemen bei der Verwendung führen kann. Es wird empfohlen, einen neuen Push-Token zu erstellen, wenn das Problem weiterhin besteht!'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b67450712..ecb07b7c7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -467,4 +467,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get privacyPolicy => 'Privacy policy'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'An error occured while using the legacy token: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'The token was enrolled in a old version of this app, which may cause trouble using it.\nIt is suggested to enroll a new push token if the problem persist!'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index b1d40f6ad..35ee0472a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -467,4 +467,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get privacyPolicy => 'Política de privacidad'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Se ha producido un error al utilizar el token obsoleto: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'El token se creó en una versión obsoleta de la aplicación, lo que puede provocar problemas al utilizarlo.\nSe recomienda crear un nuevo token push si el problema persiste.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d8eb95e45..b3a0edf8e 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -467,4 +467,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get privacyPolicy => 'Politique de confidentialité'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Une erreur s\'est produite lors de l\'utilisation du jeton obsolète : $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'Le token a été créé dans une version obsolète de l\'application, ce qui peut entraîner des problèmes d\'utilisation.\nIl est recommandé de créer un nouveau token push si le problème persiste !'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ca9085218..040a99b46 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -467,4 +467,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get privacyPolicy => 'Privacybeleid'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Er is een fout opgetreden bij het gebruik van het verouderde token: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'Het token is aangemaakt in een verouderde versie van de app, wat kan leiden tot problemen bij het gebruik ervan.\nHet wordt aanbevolen om een nieuw push token aan te maken als het probleem zich blijft voordoen!'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index fd8588ff3..70be5ddb3 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -467,4 +467,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get privacyPolicy => 'Polityka prywatności'; + + @override + String legacySigningErrorTitle(Object tokenLabel) { + return 'Wystąpił błąd podczas korzystania z przestarzałego tokena: $tokenLabel'; + } + + @override + String get legacySigningErrorMessage => 'Token został utworzony w nieaktualnej wersji aplikacji, co może prowadzić do problemów podczas korzystania z niego.\nZaleca się utworzenie nowego tokena push, jeśli problem nadal występuje!'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 05af7e593..5d95b74c7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -466,5 +466,18 @@ "hidePushTokensDescription": "Verberg push tokens uit de token lijst. Hierdoor worden de tokens niet verwijderd en blijven ze zichtbaar op een apart scherm.", "settingsGroupGeneral": "Algemene informatie", "licensesAndVersion": "Licenties en versie", - "privacyPolicy": "Privacybeleid" + "privacyPolicy": "Privacybeleid", + "legacySigningErrorTitle": "Er is een fout opgetreden bij het gebruik van het verouderde token: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "Het token is aangemaakt in een verouderde versie van de app, wat kan leiden tot problemen bij het gebruik ervan.\nHet wordt aanbevolen om een nieuw push token aan te maken als het probleem zich blijft voordoen!", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index ba86eba40..878aa24f9 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -470,5 +470,18 @@ "hidePushTokensDescription": "Ukryj tokeny push z listy tokenów. Nie spowoduje to usunięcia tokenów i będą one nadal widoczne na osobnym ekranie", "settingsGroupGeneral": "Informacje ogólne", "licensesAndVersion": "Licencje i wersja", - "privacyPolicy": "Polityka prywatności" + "privacyPolicy": "Polityka prywatności", + "legacySigningErrorTitle": "Wystąpił błąd podczas korzystania z przestarzałego tokena: {tokenLabel}", + "@legacySigningErrorTitle": { + "description": "Title of the error dialog that is shown when an error occurs while using a legacy token.", + "placeholders": { + "tokenLabel": { + "example": "PUSH1234A" + } + } + }, + "legacySigningErrorMessage": "Token został utworzony w nieaktualnej wersji aplikacji, co może prowadzić do problemów podczas korzystania z niego.\nZaleca się utworzenie nowego tokena push, jeśli problem nadal występuje!", + "@legacySigningErrorMessage": { + "description": "Message of the error dialog that is shown when an error occurs while using a legacy token." + } } \ No newline at end of file diff --git a/lib/model/push_request_queue.dart b/lib/model/push_request_queue.dart index eb1ddbccc..95e92452f 100644 --- a/lib/model/push_request_queue.dart +++ b/lib/model/push_request_queue.dart @@ -51,7 +51,7 @@ class PushRequestQueue { PushRequest? peek() => list.isNotEmpty ? list.first : null; - PushRequest pop() => list.removeAt(0); + PushRequest? tryPop() => list.isNotEmpty ? list.removeAt(0) : null; @override String toString() { diff --git a/lib/repo/secure_token_repository.dart b/lib/repo/secure_token_repository.dart index b3b9a42e8..7a61a0a17 100644 --- a/lib/repo/secure_token_repository.dart +++ b/lib/repo/secure_token_repository.dart @@ -111,6 +111,7 @@ class SecureTokenRepository implements TokenRepository { if (key == _CURRENT_APP_TOKEN_KEY || key == _NEW_APP_TOKEN_KEY) { continue; } + _storage.delete(key: key); Logger.warning( 'Could not deserialize token from secure storage. Value: $value, key: $key', name: 'storage_utils.dart#loadAllTokens', diff --git a/lib/state_notifiers/push_request_notifier.dart b/lib/state_notifiers/push_request_notifier.dart index 22c053299..ce8643997 100644 --- a/lib/state_notifiers/push_request_notifier.dart +++ b/lib/state_notifiers/push_request_notifier.dart @@ -53,10 +53,8 @@ class PushRequestNotifier extends StateNotifier { // ACTIONS Future acceptPop(PushToken pushToken) async { - if (pushToken.pushRequests.peek() == null) { - return false; - } - final pushRequest = pushToken.pushRequests.pop(); + final pushRequest = pushToken.pushRequests.tryPop(); + if (pushRequest == null) return false; Logger.info('Approving push request.', name: 'push_request_notifier.dart#approve'); final updatedPushRequest = pushRequest.copyWith(accepted: true); final successfullyApproved = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); @@ -69,10 +67,8 @@ class PushRequestNotifier extends StateNotifier { } Future declinePop(PushToken pushToken) async { - if (pushToken.pushRequests.peek() == null) { - return false; - } - final pushRequest = pushToken.pushRequests.pop(); + final pushRequest = pushToken.pushRequests.tryPop(); + if (pushRequest == null) return false; Logger.info('Decline push request.', name: 'push_request_notifier.dart#decline'); final updatedPushRequest = pushRequest.copyWith(accepted: false); final successfullyDeclined = await _handleReaction(pushRequest: updatedPushRequest, token: pushToken); diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index 6df313ee6..df48cb469 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -68,7 +68,7 @@ class PrivacyIdeaIOClient { /// Custom POST request allows to not verify certificates. Future doPost({required Uri url, required Map body, bool sslVerify = true}) async { - if (kIsWeb) return Response('', 405); + if (kIsWeb) return Response('Platform not supported', 405); Logger.info('Sending post request (SSLVerify: $sslVerify)', name: 'utils.dart#doPost'); List entries = body.entries.where((element) => element.value == null).toList(); @@ -94,6 +94,8 @@ class PrivacyIdeaIOClient { response = await ioClient.post(url, body: body); } on SocketException catch (e, s) { response = Response('${e.runtimeType} : $s', 404); + } on HandshakeException catch (e, s) { + response = Response('${e.runtimeType} : $s', 525); } if (response.statusCode != 200) { diff --git a/lib/utils/push_provider.dart b/lib/utils/push_provider.dart index a051e87b9..e4a82008f 100644 --- a/lib/utils/push_provider.dart +++ b/lib/utils/push_provider.dart @@ -57,6 +57,7 @@ class PushProvider { Future initialize({required PushRequestNotifier pushSubscriber, required FirebaseUtils firebaseUtils}) async { if (_initialized) return; + Logger.warning('PushProvider is already initialized', name: 'push_provider.dart#initializePushProvider'); _initialized = true; this.firebaseUtils = firebaseUtils; this.pushSubscriber = pushSubscriber; diff --git a/lib/utils/rsa_utils.dart b/lib/utils/rsa_utils.dart index 35e5ba3c0..c94464465 100644 --- a/lib/utils/rsa_utils.dart +++ b/lib/utils/rsa_utils.dart @@ -25,10 +25,13 @@ import 'package:base32/base32.dart'; import 'package:flutter/foundation.dart'; import 'package:pi_authenticator_legacy/pi_authenticator_legacy.dart'; import 'package:pointycastle/export.dart'; +import 'package:privacyidea_authenticator/l10n/app_localizations.dart'; import 'package:privacyidea_authenticator/model/tokens/push_token.dart'; import 'package:privacyidea_authenticator/utils/crypto_utils.dart'; +import 'package:privacyidea_authenticator/utils/customizations.dart'; import 'package:privacyidea_authenticator/utils/identifiers.dart'; import 'package:privacyidea_authenticator/utils/logger.dart'; +import 'package:privacyidea_authenticator/utils/riverpod_providers.dart'; class RsaUtils { const RsaUtils(); @@ -213,25 +216,19 @@ class RsaUtils { /// push token so that the app can directly access the private key. /// Returns the signature on success and null on failure. Future trySignWithToken(PushToken token, String message) async { - String? signature; - if (token.privateTokenKey == null) { - // It is a legacy token so the operation could cause an exception - try { - signature = await const LegacyUtils().sign(token.serial, message); - } catch (error, stackTrace) { - Logger.error("Error", - error: "An error occured while using the legacy token ${token.label}. " - "The token was enrolled in a old version of this app, which may cause trouble" - " using it. It is suggested to enroll a new push token if the problems persist!", - name: 'crypto_utils.dart#trySignWithToken', - stackTrace: stackTrace); - return null; - } - } else { - signature = createBase32Signature(token.rsaPrivateTokenKey!, utf8.encode(message) as Uint8List); + if (token.privateTokenKey != null) { + return createBase32Signature(token.rsaPrivateTokenKey!, utf8.encode(message) as Uint8List); } + // It is a legacy token so the operation could cause an exception + try { + return await const LegacyUtils().sign(token.serial, message); + } catch (error) { + final legacySigningErrorTitle = AppLocalizations.of(globalNavigatorKey.currentContext!)!.legacySigningErrorTitle(token.label); + final legacySigningErrorMessage = AppLocalizations.of(globalNavigatorKey.currentContext!)!.legacySigningErrorMessage; + globalRef?.read(statusMessageProvider.notifier).state = (legacySigningErrorTitle, legacySigningErrorMessage); - return signature; + return null; + } } Future> generateRSAKeyPair() async { diff --git a/lib/views/license_view/license_view.dart b/lib/views/license_view/license_view.dart index 909a97359..f2578b5c0 100644 --- a/lib/views/license_view/license_view.dart +++ b/lib/views/license_view/license_view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import '../../widgets/push_request_listener.dart'; + class LicenseView extends StatelessWidget { static const String routeName = '/license'; final String appName; @@ -10,16 +12,18 @@ class LicenseView extends StatelessWidget { const LicenseView({required this.appName, required this.websiteLink, required this.appImage, super.key}); @override - Widget build(BuildContext context) => FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, platformInfo) => LicensePage( - applicationName: appName, - applicationIcon: Padding( - padding: const EdgeInsets.all(32), - child: appImage, + 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}', ), - applicationLegalese: websiteLink, - applicationVersion: platformInfo.data == null ? '' : '${platformInfo.data?.version}+${platformInfo.data?.buildNumber}', ), ); } diff --git a/lib/views/main_view/main_view.dart b/lib/views/main_view/main_view.dart index 870568a5c..0904483f9 100644 --- a/lib/views/main_view/main_view.dart +++ b/lib/views/main_view/main_view.dart @@ -9,7 +9,7 @@ import '../../widgets/status_bar.dart'; import 'main_view_widgets/connectivity_listener.dart'; import 'main_view_widgets/main_view_navigation_bar.dart'; import 'main_view_widgets/main_view_tokens_list.dart'; -import 'main_view_widgets/push_request_listener.dart'; +import '../../widgets/push_request_listener.dart'; export 'package:privacyidea_authenticator/views/main_view/main_view.dart'; @@ -42,22 +42,22 @@ class _MainViewState extends ConsumerState with LifecycleMixin { } @override - Widget build(BuildContext context) => Scaffold( - resizeToAvoidBottomInset: false, - 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: Padding( - padding: const EdgeInsets.all(4), - child: widget.appIcon, + Widget build(BuildContext context) => PushRequestListener( + child: Scaffold( + resizeToAvoidBottomInset: false, + 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: Padding( + padding: const EdgeInsets.all(4), + child: widget.appIcon, + ), ), - ), - body: PushRequestListener( - child: ConnectivityListener( + body: ConnectivityListener( child: StatusBar( child: Stack( clipBehavior: Clip.antiAliasWithSaveLayer, diff --git a/lib/views/push_token_view/push_tokens_view.dart b/lib/views/push_token_view/push_tokens_view.dart index d57ec8b1c..c747dbdee 100644 --- a/lib/views/push_token_view/push_tokens_view.dart +++ b/lib/views/push_token_view/push_tokens_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../../widgets/push_request_listener.dart'; import 'widgets/push_tokens_view_list.dart'; class PushTokensView extends StatelessWidget { @@ -12,13 +13,15 @@ class PushTokensView extends StatelessWidget { appBar: AppBar( title: const Text('Push Tokens'), ), - body: Stack( - children: [ - Center( - child: Icon(Icons.notifications_none, size: 300, color: Colors.grey.withOpacity(0.2)), - ), - const PushTokensViwList(), - ], + body: PushRequestListener( + child: Stack( + children: [ + Center( + child: Icon(Icons.notifications_none, size: 300, color: Colors.grey.withOpacity(0.2)), + ), + const PushTokensViwList(), + ], + ), ), ); } diff --git a/lib/views/settings_view/settings_view.dart b/lib/views/settings_view/settings_view.dart index 6dd58aabb..b39d26dc7 100644 --- a/lib/views/settings_view/settings_view.dart +++ b/lib/views/settings_view/settings_view.dart @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../l10n/app_localizations.dart'; import '../../model/tokens/push_token.dart'; import '../../utils/riverpod_providers.dart'; +import '../../widgets/push_request_listener.dart'; import '../license_view/license_view.dart'; import 'settings_view_widgets/logging_menu.dart'; import 'settings_view_widgets/settings_groups.dart'; @@ -23,249 +24,231 @@ class SettingsView extends ConsumerWidget { final unsupported = enrolledPushTokenList.where((e) => e.url == null).toList(); final showPushSettingsGroup = enrolledPushTokenList.isNotEmpty; - return Scaffold( - appBar: AppBar( - title: Text( - AppLocalizations.of(context)!.settings, + return 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. + overflow: TextOverflow.ellipsis, // maxLines: 2 only works like this. + maxLines: 2, // Title can be shown on small screens too. + ), ), - ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsGroup( - title: AppLocalizations.of(context)!.settingsGroupGeneral, - children: [ - GestureDetector( - onTap: () async { - Uri uri = Uri.parse("https://netknights.it/en/privacy-statement/"); - if (!await launchUrl(uri)) { - throw Exception('Could not launch $uri'); - } - }, - child: ListTile( - title: Text( - AppLocalizations.of(context)!.privacyPolicy, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsGroup( + title: AppLocalizations.of(context)!.settingsGroupGeneral, + children: [ + GestureDetector( + onTap: () async { + Uri uri = Uri.parse("https://netknights.it/en/privacy-statement/"); + if (!await launchUrl(uri)) { + throw Exception('Could not launch $uri'); + } + }, + child: ListTile( + title: Text( + AppLocalizations.of(context)!.privacyPolicy, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), ), ), - ), - GestureDetector( - onTap: () { - Navigator.pushNamed(context, LicenseView.routeName); - }, - child: ListTile( + GestureDetector( + onTap: () { + Navigator.pushNamed(context, LicenseView.routeName); + }, + child: ListTile( + title: Text( + AppLocalizations.of(context)!.licensesAndVersion, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.fade, + softWrap: false, + ), + ), + ) + ], + ), + const Divider(), + SettingsGroup( + title: AppLocalizations.of(context)!.theme, + children: [ + RadioListTile( title: Text( - AppLocalizations.of(context)!.licensesAndVersion, + AppLocalizations.of(context)!.lightTheme, style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.fade, softWrap: false, ), + value: ThemeMode.light, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false), ), - ) - ], - ), - const Divider(), - SettingsGroup( - title: AppLocalizations.of(context)!.theme, - children: [ - RadioListTile( - title: Text( - AppLocalizations.of(context)!.lightTheme, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - value: ThemeMode.light, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: false), - ), - RadioListTile( - title: Text( - AppLocalizations.of(context)!.darkTheme, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, - ), - value: ThemeMode.dark, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: true), - ), - RadioListTile( - title: Text( - AppLocalizations.of(context)!.systemTheme, - style: Theme.of(context).textTheme.titleMedium, - ), - value: ThemeMode.system, - groupValue: EasyDynamicTheme.of(context).themeMode, - controlAffinity: ListTileControlAffinity.trailing, - onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: true, dark: false), - ), - ], - ), - const Divider(), - SettingsGroup( - title: AppLocalizations.of(context)!.language, - children: [ - SwitchListTile( + RadioListTile( title: Text( - AppLocalizations.of(context)!.useDeviceLocaleTitle, + AppLocalizations.of(context)!.darkTheme, style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - AppLocalizations.of(context)!.useDeviceLocaleDescription, - overflow: TextOverflow.fade, - ), - value: ref.watch(settingsProvider).useSystemLocale, - onChanged: (value) => ref.read(settingsProvider.notifier).setUseSystemLocale(value)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: DropdownButton( - disabledHint: Text( - '${ref.watch(settingsProvider).currentLocale}', - style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.grey), overflow: TextOverflow.fade, softWrap: false, ), - isExpanded: true, - value: ref.watch(settingsProvider).currentLocale, - items: AppLocalizations.supportedLocales.map>((Locale itemLocale) { - return DropdownMenuItem( - value: itemLocale, - child: Text( - '$itemLocale', - overflow: TextOverflow.fade, - softWrap: false, - ), - ); - }).toList(), - onChanged: ref.watch(settingsProvider).useSystemLocale ? null : (value) => ref.read(settingsProvider.notifier).setLocalePreference(value!), + value: ThemeMode.dark, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: false, dark: true), ), - ), - ], - ), - Visibility( - visible: showPushSettingsGroup, - child: SettingsGroup( - title: AppLocalizations.of(context)!.pushToken, - children: [ - ListTile( + RadioListTile( title: Text( - AppLocalizations.of(context)!.synchronizePushTokens, + AppLocalizations.of(context)!.systemTheme, style: Theme.of(context).textTheme.titleMedium, ), - subtitle: Text( - AppLocalizations.of(context)!.synchronizesTokensWithServer, - overflow: TextOverflow.fade, - ), - trailing: ElevatedButton( - child: Text( - AppLocalizations.of(context)!.sync, + value: ThemeMode.system, + groupValue: EasyDynamicTheme.of(context).themeMode, + controlAffinity: ListTileControlAffinity.trailing, + onChanged: (dynamic value) => EasyDynamicTheme.of(context).changeTheme(dynamic: true, dark: false), + ), + ], + ), + const Divider(), + SettingsGroup( + title: AppLocalizations.of(context)!.language, + children: [ + SwitchListTile( + title: Text( + AppLocalizations.of(context)!.useDeviceLocaleTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + AppLocalizations.of(context)!.useDeviceLocaleDescription, + overflow: TextOverflow.fade, + ), + value: ref.watch(settingsProvider).useSystemLocale, + onChanged: (value) => ref.read(settingsProvider.notifier).setUseSystemLocale(value)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: DropdownButton( + disabledHint: Text( + '${ref.watch(settingsProvider).currentLocale}', + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.grey), overflow: TextOverflow.fade, softWrap: false, ), - onPressed: () { - showDialog( - useRootNavigator: false, - context: context, - barrierDismissible: false, - builder: (context) => const UpdateFirebaseTokenDialog(), + isExpanded: true, + value: ref.watch(settingsProvider).currentLocale, + items: AppLocalizations.supportedLocales.map>((Locale itemLocale) { + return DropdownMenuItem( + value: itemLocale, + child: Text( + '$itemLocale', + overflow: TextOverflow.fade, + softWrap: false, + ), ); - }, + }).toList(), + onChanged: + ref.watch(settingsProvider).useSystemLocale ? null : (value) => ref.read(settingsProvider.notifier).setLocalePreference(value!), ), ), - ListTile( - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: AppLocalizations.of(context)!.enablePolling, - style: Theme.of(context).textTheme.titleMedium, - ), - // Add clickable icon to inform user of unsupported push tokens (for polling) - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: unsupported.isNotEmpty && enrolledPushTokenList.isNotEmpty - ? GestureDetector( - onTap: () {}, // () => _showPollingInfo(unsupported), - child: const Icon( - Icons.info_outline, - color: Colors.red, - ), - ) - : null, - ), - ), - ], + ], + ), + Visibility( + visible: showPushSettingsGroup, + child: SettingsGroup( + title: AppLocalizations.of(context)!.pushToken, + children: [ + ListTile( + title: Text( + AppLocalizations.of(context)!.synchronizePushTokens, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + AppLocalizations.of(context)!.synchronizesTokensWithServer, + overflow: TextOverflow.fade, + ), + trailing: ElevatedButton( + child: Text( + AppLocalizations.of(context)!.sync, + overflow: TextOverflow.fade, + softWrap: false, + ), + onPressed: () { + showDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + builder: (context) => const UpdateFirebaseTokenDialog(), + ); + }, ), ), - subtitle: Text( - AppLocalizations.of(context)!.requestPushChallengesPeriodically, - overflow: TextOverflow.fade, - ), - trailing: Switch( - value: ref.watch(settingsProvider).enablePolling, - onChanged: (value) => ref.read(settingsProvider.notifier).setPolling(value), + ListTile( + title: RichText( + text: TextSpan( + children: [ + TextSpan( + text: AppLocalizations.of(context)!.enablePolling, + style: Theme.of(context).textTheme.titleMedium, + ), + // Add clickable icon to inform user of unsupported push tokens (for polling) + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: unsupported.isNotEmpty && enrolledPushTokenList.isNotEmpty + ? GestureDetector( + onTap: () {}, // () => _showPollingInfo(unsupported), + child: const Icon( + Icons.info_outline, + color: Colors.red, + ), + ) + : null, + ), + ), + ], + ), + ), + subtitle: Text( + AppLocalizations.of(context)!.requestPushChallengesPeriodically, + overflow: TextOverflow.fade, + ), + trailing: Switch( + value: ref.watch(settingsProvider).enablePolling, + onChanged: (value) => ref.read(settingsProvider.notifier).setPolling(value), + ), ), - ), - // if (ref.watch(tokenProvider).hasHOTPTokens) - // ListTile( - // title: RichText( - // text: TextSpan( - // children: [ - // TextSpan( - // text: AppLocalizations.of(context)!.hidePushTokens, - // style: Theme.of(context).textTheme.titleMedium, - // ), - // ], - // ), - // ), - // subtitle: Text( - // AppLocalizations.of(context)!.hidePushTokensDescription, - // overflow: TextOverflow.fade, - // ), - // trailing: Switch( - // value: ref.watch(settingsProvider).hidePushTokensState != HidePushTokens.notHidden, - // onChanged: (value) => ref.read(settingsProvider.notifier).setHidePushTokens(isHidden: value), - // ), - // ) - ], - ), - ), - const Divider(), - SettingsGroup(title: AppLocalizations.of(context)!.errorLogTitle, children: [ - ListTile( - title: Text( - AppLocalizations.of(context)!.logMenu, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.fade, - softWrap: false, + ], ), - style: ListTileStyle.list, - trailing: ElevatedButton( - child: Text( - AppLocalizations.of(context)!.open, + ), + const Divider(), + SettingsGroup(title: AppLocalizations.of(context)!.errorLogTitle, children: [ + ListTile( + title: Text( + AppLocalizations.of(context)!.logMenu, + style: Theme.of(context).textTheme.titleMedium, overflow: TextOverflow.fade, softWrap: false, ), - onPressed: () => showDialog( - context: context, - builder: (context) => const LoggingMenu(), - useRootNavigator: false, + style: ListTileStyle.list, + trailing: ElevatedButton( + child: Text( + AppLocalizations.of(context)!.open, + overflow: TextOverflow.fade, + softWrap: false, + ), + onPressed: () => showDialog( + context: context, + builder: (context) => const LoggingMenu(), + useRootNavigator: false, + ), ), ), - ), - ]), - ], + ]), + ], + ), ), ), ); diff --git a/lib/widgets/app_wrapper.dart b/lib/widgets/app_wrapper.dart index 2ea8b9fd8..50103e527 100644 --- a/lib/widgets/app_wrapper.dart +++ b/lib/widgets/app_wrapper.dart @@ -2,6 +2,8 @@ import 'package:easy_dynamic_theme/easy_dynamic_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'single_touch_recognizer.dart'; + class AppWrapper extends StatelessWidget { final Widget child; @@ -10,7 +12,9 @@ class AppWrapper extends StatelessWidget { @override Widget build(BuildContext context) { return ProviderScope( - child: EasyDynamicThemeWidget(child: child), + child: SingleTouchRecognizer( + child: EasyDynamicThemeWidget(child: child), + ), ); } } diff --git a/lib/views/main_view/main_view_widgets/push_request_listener.dart b/lib/widgets/push_request_listener.dart similarity index 62% rename from lib/views/main_view/main_view_widgets/push_request_listener.dart rename to lib/widgets/push_request_listener.dart index 2b76f83ee..7fe7e85c6 100644 --- a/lib/views/main_view/main_view_widgets/push_request_listener.dart +++ b/lib/widgets/push_request_listener.dart @@ -2,14 +2,15 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacyidea_authenticator/utils/push_provider.dart'; -import '../../../l10n/app_localizations.dart'; -import '../../../model/tokens/push_token.dart'; -import '../../../utils/customizations.dart'; -import '../../../utils/lock_auth.dart'; -import '../../../utils/riverpod_providers.dart'; -import '../../../widgets/default_dialog.dart'; -import '../../../widgets/press_button.dart'; +import '../l10n/app_localizations.dart'; +import '../model/tokens/push_token.dart'; +import '../utils/customizations.dart'; +import '../utils/lock_auth.dart'; +import '../utils/riverpod_providers.dart'; +import 'default_dialog.dart'; +import 'press_button.dart'; class PushRequestListener extends ConsumerStatefulWidget { final Widget child; @@ -20,6 +21,14 @@ class PushRequestListener extends ConsumerStatefulWidget { } class _PushRequestListenerState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + PushProvider().pollForChallenges(isManually: false); + }); + } + @override Widget build(BuildContext context) { final tokensWithPushRequest = ref.watch(tokenProvider).pushTokens.where((token) => token.pushRequests.isNotEmpty); @@ -52,85 +61,90 @@ class _PushRequestDialogState extends State { @override Widget build(BuildContext context) => isHandled ? const SizedBox() - : DefaultDialog( - title: Center(child: Text(' ${widget.tokenWithPushRequest.label}', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge!)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.tokenWithPushRequest.pushRequests.peek()?.title ?? '', - style: Theme.of(context).textTheme.bodyMedium, - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: Text( - widget.tokenWithPushRequest.pushRequests.peek()?.question ?? '', - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - Row( - mainAxisSize: MainAxisSize.max, + : Container( + color: Colors.transparent, + child: Center( + child: DefaultDialog( + title: Center(child: Text(' ${widget.tokenWithPushRequest.label}', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleLarge!)), + content: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - flex: 6, - child: PressButton( - onPressed: () async { - if (widget.tokenWithPushRequest.isLocked && - await lockAuth(context: context, localizedReason: AppLocalizations.of(context)!.authToAcceptPushRequest) == false) return; - globalRef?.read(pushRequestProvider.notifier).acceptPop(widget.tokenWithPushRequest); - if (mounted) setState(() => isHandled = true); - }, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.accept.padRight(AppLocalizations.of(context)!.decline.length), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - const Flexible( - child: Icon(Icons.check_outlined), + Text( + widget.tokenWithPushRequest.pushRequests.peek()?.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + widget.tokenWithPushRequest.pushRequests.peek()?.question ?? '', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + flex: 6, + child: PressButton( + onPressed: () async { + if (widget.tokenWithPushRequest.isLocked && + await lockAuth(context: context, localizedReason: AppLocalizations.of(context)!.authToAcceptPushRequest) == false) return; + globalRef?.read(pushRequestProvider.notifier).acceptPop(widget.tokenWithPushRequest); + if (mounted) setState(() => isHandled = true); + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.accept.padRight(AppLocalizations.of(context)!.decline.length), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const Flexible( + child: Icon(Icons.check_outlined), + ), + ], ), - ], + ), ), ), - ), - ), - const Expanded(child: SizedBox()), - Expanded( - flex: 6, - child: PressButton( - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), - onPressed: () async { - if (widget.tokenWithPushRequest.isLocked && - await lockAuth(context: context, localizedReason: AppLocalizations.of(context)!.authToDeclinePushRequest) == false) { - return; - } - _showConfirmationDialog(widget.tokenWithPushRequest); - }, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.decline.padRight(AppLocalizations.of(context)!.accept.length), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - const Flexible( - flex: 1, - child: Icon(Icons.close_outlined), + const Expanded(child: SizedBox()), + Expanded( + flex: 6, + child: PressButton( + style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Theme.of(context).colorScheme.errorContainer)), + onPressed: () async { + if (widget.tokenWithPushRequest.isLocked && + await lockAuth(context: context, localizedReason: AppLocalizations.of(context)!.authToDeclinePushRequest) == false) { + return; + } + _showConfirmationDialog(widget.tokenWithPushRequest); + }, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + AppLocalizations.of(context)!.decline.padRight(AppLocalizations.of(context)!.accept.length), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const Flexible( + flex: 1, + child: Icon(Icons.close_outlined), + ), + ], ), - ], - ), - )), + )), + ), + ], ), ], ), - ], + ), ), ); diff --git a/lib/widgets/single_touch_recognizer.dart b/lib/widgets/single_touch_recognizer.dart new file mode 100644 index 000000000..c5de625cd --- /dev/null +++ b/lib/widgets/single_touch_recognizer.dart @@ -0,0 +1,50 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class SingleTouchRecognizer extends StatelessWidget { + final Widget child; + const SingleTouchRecognizer({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + gestures: { + _SingleTouchRecognizer: GestureRecognizerFactoryWithHandlers<_SingleTouchRecognizer>( + () => _SingleTouchRecognizer(), + (_SingleTouchRecognizer instance) {}, + ), + }, + child: child, + ); + } +} + +class _SingleTouchRecognizer extends OneSequenceGestureRecognizer { + int _p = 0; + + @override + void addAllowedPointer(PointerDownEvent event) { + //first register the current pointer so that related events will be handled by this recognizer + startTrackingPointer(event.pointer); + //ignore event if another event is already in progress + if (_p == 0) { + resolve(GestureDisposition.rejected); + _p = event.pointer; + } else { + resolve(GestureDisposition.accepted); + } + } + + @override + String get debugDescription => 'only one pointer recognizer'; + + @override + void didStopTrackingLastPointer(int pointer) {} + + @override + void handleEvent(PointerEvent event) { + if (!event.down && event.pointer == _p) { + _p = 0; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 0e9987bce..20b493173 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: eb0ac20f704799b986049fbb3c1c16421eca319a1b872378d669513e12452ba5 + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 url: "https://pub.dev" source: hosted - version: "1.3.14" + version: "1.3.16" analyzer: dependency: transitive description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.8.0" + version: "8.8.1" characters: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: code_builder - sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" collection: dependency: "direct main" description: @@ -293,10 +293,10 @@ packages: dependency: "direct main" description: name: extended_nested_scroll_view - sha256: "444a6f883e6e07effc7639e69a309e1fb491b6c19b095e9281714a51ace2b384" + sha256: "835580d40c2c62b448bd14adecd316acba469ba61f1510ef559d17668a85e777" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.2.1" fake_async: dependency: transitive description: @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: d301561d614487688d797717bef013a264c517d1d09e4c5c1325c3a64c835efb + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" url: "https://pub.dev" source: hosted - version: "2.24.0" + version: "2.24.2" firebase_core_platform_interface: dependency: transitive description: @@ -341,34 +341,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "10159d9ee42c79f4548971d92f3f0fcd5791f6738cda3583a4e3b2c8b244c018" + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "260064e1b512a9e1970b5964d645eba888208ca3de42459c38e484c8ecdc37a9" + sha256: "199fe8186a5370d1cf5ce0819191079afc305914e8f38715f5e23943940dfe2d" url: "https://pub.dev" source: hosted - version: "14.7.6" + version: "14.7.9" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "81fb8c983356dd75ee660f276c918380325df7a1ab1e981ede911809e9ddff30" + sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" url: "https://pub.dev" source: hosted - version: "4.5.15" + version: "4.5.18" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "1c5d9b6cf929ab471300143059d1641a26b73c9c24adb5266e241aea23c090aa" + sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" url: "https://pub.dev" source: hosted - version: "3.5.15" + version: "3.5.18" fixnum: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e + sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535" url: "https://pub.dev" source: hosted - version: "16.2.0" + version: "16.3.0" flutter_local_notifications_linux: dependency: transitive description: @@ -680,10 +680,10 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: df4ccb3193525b8a60c78a5ca7bf188a47705bcf77bcc837a6b2cf6da64ae0e2 + sha256: "54e9c35ce52c06333355ab0d0f41e4c06dbca354b23426765ba41dfb1de27598" url: "https://pub.dev" source: hosted - version: "1.0.35" + version: "1.0.36" local_auth_ios: dependency: "direct main" description: @@ -784,10 +784,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "4b693867cee1853c9d1d7ecc1871f27f39b2ef2c13c0d8d8507dfe5bebd8aaf1" + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.3" + version: "5.4.4" mutex: dependency: "direct main" description: @@ -864,10 +864,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: @@ -1124,10 +1124,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1300,10 +1300,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.2.2" url_launcher_android: dependency: transitive description: @@ -1324,10 +1324,10 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" url_launcher_macos: dependency: transitive description: @@ -1348,26 +1348,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "138bd45b3a456dcfafc46d1a146787424f8d2edfbf2809c9324361e58f851cf7" + sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d875dbfc..47645896b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ publish_to: none # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.2.3+402303 # TODO Set the right version number +version: 4.2.3+402305 # TODO Set the right version number # version: major.minor.build + 2x major|2x minor|3x build # version: version number + build number (optional) # android: build-name + versionCode diff --git a/test/unit_test/utils/push_request_queue_test.dart b/test/unit_test/utils/push_request_queue_test.dart index 23a2dd4d2..50c1090c5 100644 --- a/test/unit_test/utils/push_request_queue_test.dart +++ b/test/unit_test/utils/push_request_queue_test.dart @@ -61,8 +61,8 @@ void verifyCustomListBehavesLikeQueue() { expect(fifo.isNotEmpty, true); expect(fifo.isEmpty, false); - var pop = fifo.pop(); - expect(pop, pushRequest); + var tryPop = fifo.tryPop(); + expect(tryPop, pushRequest); expect(fifo.isNotEmpty, false); expect(fifo.isEmpty, true); @@ -126,7 +126,7 @@ void verifyCustomListBehavesLikeQueue() { fifo.add(three); expect(fifo.peek(), queue.first); - fifo.pop(); + fifo.tryPop(); queue.removeFirst(); expect(fifo.peek(), queue.first); @@ -134,10 +134,10 @@ void verifyCustomListBehavesLikeQueue() { fifo.add(four); queue.addLast(five); fifo.add(five); - expect(fifo.pop(), queue.removeFirst()); - expect(fifo.pop(), queue.removeFirst()); - expect(fifo.pop(), queue.removeFirst()); - expect(fifo.pop(), queue.removeFirst()); + expect(fifo.tryPop(), queue.removeFirst()); + expect(fifo.tryPop(), queue.removeFirst()); + expect(fifo.tryPop(), queue.removeFirst()); + expect(fifo.tryPop(), queue.removeFirst()); expect(fifo.isEmpty, true); expect(queue.isEmpty, true);