diff --git a/README.md b/README.md index 590d3ac0..c4ba2a2a 100644 --- a/README.md +++ b/README.md @@ -61,36 +61,9 @@ If you discover a security issue in twonly, please adhere to the coordinated vul us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not guarantee a bounty currently :/ - - -## Development - -
-Setup Instructions (macOS) - -## Building - -Some dependencies are downloaded directly from the source as there are some new changes which are not yet published on -pub.dev or because they require some special installation. - -```bash -git submodule update --init --recursive - -cd dependencies/flutter_zxing -git submodule update --init --recursive -./scripts/update_ios_macos_src.s -``` - -## Debugging files - -```bash -run-as eu.twonly.testing ls /data/user/0/eu.twonly.testing/ -``` - -
+## Contribution +Currently there are still some core features and rewrites open I want to do like the switch to the MLS protocol involving a huge rewrite in Rust. Because of this, contributions are currently not wanted. You can still view the code (if you find any security issues please contact me!). Also, issues are currently closed. If you find a bug, please use the in-app option as there you can upload your debug log which helps a lot. ## Signing Keys diff --git a/lib/app.dart b/lib/app.dart index c327d972..5d77c850 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,9 +5,15 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; +import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart' + show PasswordlessRecoveryService; +import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/visual/components/app_outdated.comp.dart'; @@ -18,7 +24,7 @@ import 'package:twonly/src/visual/views/home.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; -import 'package:twonly/src/visual/views/recovery.view.dart'; +import 'package:twonly/src/visual/views/recovery_from_secure_storage.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; class App extends StatefulWidget { @@ -151,6 +157,10 @@ class _AppMainWidgetState extends State { Future initAsync() async { Log.info('AppWidgetState: initAsync started'); + final onboardingState = await KeyValueStore.getModel( + KeyValueKeys.onboardingState, + ); + _showOnboarding = !onboardingState.hasOnboardingFinished; if (userService.isUserCreated) { if (_initialPage != 0) { final count = await twonlyDB.contactsDao.getContactsCount(); @@ -180,8 +190,18 @@ class _AppMainWidgetState extends State { } else { _proofOfWork = (null, disabled); } + + if (onboardingState.hasStartedPasswordlessRecovery) { + WidgetsBinding.instance.addPostFrameCallback((_) { + routerProvider.push(Routes.recoverPasswordless); + }); + } } + await PasswordlessRecoveryService.handleRecoveryLink( + 'https://me.twonly.eu/r/#d0e204fa-3d1e-4e53-a6a5-53aadf1e2ee5/oHeAEPp_CivU3QO67L0AkSlM94E5klbP8XDLbNgsGmA', + ); + setState(() { _isLoaded = true; }); @@ -223,9 +243,13 @@ class _AppMainWidgetState extends State { } } else if (_showOnboarding) { child = OnboardingView( - callbackOnSuccess: () => setState(() { - _showOnboarding = false; - }), + callbackOnSuccess: () async { + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (state) => state.hasOnboardingFinished = true, + ); + if (mounted) setState(() => _showOnboarding = false); + }, ); } else { child = RegisterView( diff --git a/lib/src/constants/keyvalue.keys.dart b/lib/src/constants/keyvalue.keys.dart index 57db9890..9737cb9d 100644 --- a/lib/src/constants/keyvalue.keys.dart +++ b/lib/src/constants/keyvalue.keys.dart @@ -3,4 +3,5 @@ class KeyValueKeys { 'last_periodic_task_execution'; static const String currentBackupState = 'current_backup_state'; static const String backupRecoveryState = 'backup_recovery_state'; + static const String onboardingState = 'onboarding_state'; } diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index 017745f0..8a0156c7 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -16,6 +16,7 @@ class Routes { static String profileContact(int contactId) => '/profile/contact/$contactId'; static const String cameraQRScanner = '/camera/qr_scanner'; + static const String recoverPasswordless = '/recover/passwordless'; static const String settings = '/settings'; static const String settingsProfile = '/settings/profile'; diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index f6f878a1..73bca631 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -149,7 +149,7 @@ abstract class AppLocalizations { /// No description provided for @onboardingNotProductBody. /// /// In en, this message translates to: - /// **'twonly is financed by donations and an optional subscription. Your data will never be sold.'** + /// **'twonly is financed by an optional subscription. Your data will never be sold.'** String get onboardingNotProductBody; /// No description provided for @registerUsernameSlogan. @@ -3852,6 +3852,12 @@ abstract class AppLocalizations { /// **'Enable Passwordless Recovery'** String get passwordlessRecoveryEnableBtn; + /// No description provided for @passwordlessRecoveryRecoverBtn. + /// + /// In en, this message translates to: + /// **'Recover passwordless'** + String get passwordlessRecoveryRecoverBtn; + /// No description provided for @passwordlessRecoveryModifyBtn. /// /// In en, this message translates to: @@ -3959,6 +3965,150 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Recover your identity without a password.'** String get passwordlessRecoverySubtitle; + + /// No description provided for @recoverPasswordlessExplanation. + /// + /// In en, this message translates to: + /// **'If enabled, you can recover your account by asking your selected friends to help you.'** + String get recoverPasswordlessExplanation; + + /// No description provided for @recoverPasswordlessNotificationCardTitle. + /// + /// In en, this message translates to: + /// **'Enable notifications'** + String get recoverPasswordlessNotificationCardTitle; + + /// No description provided for @recoverPasswordlessNotificationCardSubtitle. + /// + /// In en, this message translates to: + /// **'Get notified when a friend has helped you recover your account.'** + String get recoverPasswordlessNotificationCardSubtitle; + + /// No description provided for @recoverPasswordlessNotificationCardBtn. + /// + /// In en, this message translates to: + /// **'Enable push notifications'** + String get recoverPasswordlessNotificationCardBtn; + + /// No description provided for @recoverPasswordlessQrInstructions. + /// + /// In en, this message translates to: + /// **'Let friends scan the QR code or share the link.'** + String get recoverPasswordlessQrInstructions; + + /// No description provided for @recoverPasswordlessShareBtn. + /// + /// In en, this message translates to: + /// **'Share Recovery Link'** + String get recoverPasswordlessShareBtn; + + /// No description provided for @recoverPasswordlessCopyBtn. + /// + /// In en, this message translates to: + /// **'Copy'** + String get recoverPasswordlessCopyBtn; + + /// No description provided for @recoverPasswordlessCopiedSnackbar. + /// + /// In en, this message translates to: + /// **'Link copied to clipboard!'** + String get recoverPasswordlessCopiedSnackbar; + + /// No description provided for @passwordlessRecoveryNoShareStored. + /// + /// In en, this message translates to: + /// **'No recovery share stored for this contact.'** + String get passwordlessRecoveryNoShareStored; + + /// No description provided for @passwordlessRecoveryShareSent. + /// + /// In en, this message translates to: + /// **'Recovery share successfully sent!'** + String get passwordlessRecoveryShareSent; + + /// No description provided for @passwordlessRecoveryNetworkError. + /// + /// In en, this message translates to: + /// **'Network error, please ensure you have internet'** + String get passwordlessRecoveryNetworkError; + + /// No description provided for @passwordlessRecoveryHelpAFriend. + /// + /// In en, this message translates to: + /// **'Help a Friend'** + String get passwordlessRecoveryHelpAFriend; + + /// No description provided for @passwordlessRecoverySelectContactDesc. + /// + /// In en, this message translates to: + /// **'Please select the contact who requested recovery below to send them their recovery key.'** + String get passwordlessRecoverySelectContactDesc; + + /// No description provided for @passwordlessRecoverySearchContacts. + /// + /// In en, this message translates to: + /// **'Search contacts...'** + String get passwordlessRecoverySearchContacts; + + /// No description provided for @passwordlessRecoveryNoContactsFound. + /// + /// In en, this message translates to: + /// **'No contacts found'** + String get passwordlessRecoveryNoContactsFound; + + /// No description provided for @passwordlessRecoveryCantHelpHim. + /// + /// In en, this message translates to: + /// **'You can\'t help him'** + String get passwordlessRecoveryCantHelpHim; + + /// No description provided for @passwordlessRecoveryDoesAskedYou. + /// + /// In en, this message translates to: + /// **'Does {username} has asked you?'** + String passwordlessRecoveryDoesAskedYou(Object username); + + /// No description provided for @passwordlessRecoveryVerifySourceDesc. + /// + /// In en, this message translates to: + /// **'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'** + String get passwordlessRecoveryVerifySourceDesc; + + /// No description provided for @passwordlessRecoveryNo. + /// + /// In en, this message translates to: + /// **'No'** + String get passwordlessRecoveryNo; + + /// No description provided for @passwordlessRecoveryYes. + /// + /// In en, this message translates to: + /// **'Yes'** + String get passwordlessRecoveryYes; + + /// No description provided for @passwordlessRecoveryYesWithTimer. + /// + /// In en, this message translates to: + /// **'Yes ({seconds}s)'** + String passwordlessRecoveryYesWithTimer(Object seconds); + + /// No description provided for @recoverPasswordlessBeingRecoveredLabel. + /// + /// In en, this message translates to: + /// **'Account being recovered'** + String get recoverPasswordlessBeingRecoveredLabel; + + /// No description provided for @recoverPasswordlessSharesReceived. + /// + /// In en, this message translates to: + /// **'{received} of {threshold} friends have shared'** + String recoverPasswordlessSharesReceived(Object received, Object threshold); + + /// No description provided for @recoverPasswordlessRecoverNowBtn. + /// + /// In en, this message translates to: + /// **'Recover now'** + String get recoverPasswordlessRecoverNowBtn; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index c8a6f1af..c9f25363 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -38,7 +38,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get onboardingNotProductBody => - 'twonly wird durch Spenden und ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.'; + 'twonly wird durch ein optionales Abonnement finanziert. Deine Daten werden niemals verkauft.'; @override String get registerUsernameSlogan => 'Konto erstellen'; @@ -404,7 +404,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactUsLastWarning => - 'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfen Sie sie und klicke dann auf „Abschicken“.'; + 'Dies sind die Informationen, die an uns gesendet werden. Bitte prüfe sie und klicke dann auf „Abschicken“.'; @override String get contactUsSuccess => 'Feedback erfolgreich übermittelt!'; @@ -2211,6 +2211,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoveryEnableBtn => 'Passwortloses Backup aktivieren'; + @override + String get passwordlessRecoveryRecoverBtn => 'Passwortlos wiederherstellen'; + @override String get passwordlessRecoveryModifyBtn => 'Passwortloses Backup bearbeiten'; @@ -2279,4 +2282,94 @@ class AppLocalizationsDe extends AppLocalizations { @override String get passwordlessRecoverySubtitle => 'Stelle deine Identität ohne Passwort wieder her.'; + + @override + String get recoverPasswordlessExplanation => + 'Wenn du die passwortlose Wiederherstellung aktiviert hast, kannst du dein Konto mithilfe deiner Freunde wiederherstellen.'; + + @override + String get recoverPasswordlessNotificationCardTitle => + 'Benachrichtigungen aktivieren'; + + @override + String get recoverPasswordlessNotificationCardSubtitle => + 'Lass dich benachrichtigen, wenn ein Freund dir geholfen hat, dein Konto wiederherzustellen.'; + + @override + String get recoverPasswordlessNotificationCardBtn => + 'Push-Benachrichtigungen aktivieren'; + + @override + String get recoverPasswordlessQrInstructions => + 'Lass deine Freunde den QR-Code scannen oder teile den Link.'; + + @override + String get recoverPasswordlessShareBtn => 'Wiederherstellungs-Link teilen'; + + @override + String get recoverPasswordlessCopyBtn => 'Kopieren'; + + @override + String get recoverPasswordlessCopiedSnackbar => + 'Link in die Zwischenablage kopiert!'; + + @override + String get passwordlessRecoveryNoShareStored => + 'Kein Wiederherstellungs-Teil für diesen Kontakt gespeichert.'; + + @override + String get passwordlessRecoveryShareSent => + 'Wiederherstellungs-Teil erfolgreich gesendet!'; + + @override + String get passwordlessRecoveryNetworkError => + 'Netzwerkfehler, bitte stelle sicher, dass du Internet hast'; + + @override + String get passwordlessRecoveryHelpAFriend => 'Einem Freund helfen'; + + @override + String get passwordlessRecoverySelectContactDesc => + 'Wähle den Kontakt aus, dem du helfen möchtest.'; + + @override + String get passwordlessRecoverySearchContacts => 'Kontakte suchen...'; + + @override + String get passwordlessRecoveryNoContactsFound => 'Keine Kontakte gefunden'; + + @override + String get passwordlessRecoveryCantHelpHim => 'Du kannst ihm nicht helfen'; + + @override + String passwordlessRecoveryDoesAskedYou(Object username) { + return 'Hat $username dich gefragt?'; + } + + @override + String get passwordlessRecoveryVerifySourceDesc => + 'Bitte stelle sicher, dass du den Link/QR-Code tatsächlich von deinem Freund erhalten hast! Wenn du dir unsicher bist, verifiziere dies bitte erneut und klicke dann noch einmal auf den Link oder kehre zurück.'; + + @override + String get passwordlessRecoveryNo => 'Nein'; + + @override + String get passwordlessRecoveryYes => 'Ja'; + + @override + String passwordlessRecoveryYesWithTimer(Object seconds) { + return 'Ja (${seconds}s)'; + } + + @override + String get recoverPasswordlessBeingRecoveredLabel => + 'Konto wird wiederhergestellt'; + + @override + String recoverPasswordlessSharesReceived(Object received, Object threshold) { + return '$received von $threshold Freunden haben geteilt'; + } + + @override + String get recoverPasswordlessRecoverNowBtn => 'Jetzt wiederherstellen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 4ddd8a68..ced3438a 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -37,7 +37,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get onboardingNotProductBody => - 'twonly is financed by donations and an optional subscription. Your data will never be sold.'; + 'twonly is financed by an optional subscription. Your data will never be sold.'; @override String get registerUsernameSlogan => 'Create your account'; @@ -2195,6 +2195,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoveryEnableBtn => 'Enable Passwordless Recovery'; + @override + String get passwordlessRecoveryRecoverBtn => 'Recover passwordless'; + @override String get passwordlessRecoveryModifyBtn => 'Modify Passwordless Recovery'; @@ -2262,4 +2265,92 @@ class AppLocalizationsEn extends AppLocalizations { @override String get passwordlessRecoverySubtitle => 'Recover your identity without a password.'; + + @override + String get recoverPasswordlessExplanation => + 'If enabled, you can recover your account by asking your selected friends to help you.'; + + @override + String get recoverPasswordlessNotificationCardTitle => 'Enable notifications'; + + @override + String get recoverPasswordlessNotificationCardSubtitle => + 'Get notified when a friend has helped you recover your account.'; + + @override + String get recoverPasswordlessNotificationCardBtn => + 'Enable push notifications'; + + @override + String get recoverPasswordlessQrInstructions => + 'Let friends scan the QR code or share the link.'; + + @override + String get recoverPasswordlessShareBtn => 'Share Recovery Link'; + + @override + String get recoverPasswordlessCopyBtn => 'Copy'; + + @override + String get recoverPasswordlessCopiedSnackbar => 'Link copied to clipboard!'; + + @override + String get passwordlessRecoveryNoShareStored => + 'No recovery share stored for this contact.'; + + @override + String get passwordlessRecoveryShareSent => + 'Recovery share successfully sent!'; + + @override + String get passwordlessRecoveryNetworkError => + 'Network error, please ensure you have internet'; + + @override + String get passwordlessRecoveryHelpAFriend => 'Help a Friend'; + + @override + String get passwordlessRecoverySelectContactDesc => + 'Please select the contact who requested recovery below to send them their recovery key.'; + + @override + String get passwordlessRecoverySearchContacts => 'Search contacts...'; + + @override + String get passwordlessRecoveryNoContactsFound => 'No contacts found'; + + @override + String get passwordlessRecoveryCantHelpHim => 'You can\'t help him'; + + @override + String passwordlessRecoveryDoesAskedYou(Object username) { + return 'Does $username has asked you?'; + } + + @override + String get passwordlessRecoveryVerifySourceDesc => + 'Please ensure that you actualy received the link/qr code from your friend! If you are unsure, please verify again and then click again on the link or come back.'; + + @override + String get passwordlessRecoveryNo => 'No'; + + @override + String get passwordlessRecoveryYes => 'Yes'; + + @override + String passwordlessRecoveryYesWithTimer(Object seconds) { + return 'Yes (${seconds}s)'; + } + + @override + String get recoverPasswordlessBeingRecoveredLabel => + 'Account being recovered'; + + @override + String recoverPasswordlessSharesReceived(Object received, Object threshold) { + return '$received of $threshold friends have shared'; + } + + @override + String get recoverPasswordlessRecoverNowBtn => 'Recover now'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 4d8e1373..bd641ce3 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 4d8e1373bf3fea617157a86791be7c65edd879d3 +Subproject commit bd641ce3a3f3b9c709dc97a279d9fe8b9b6f07e1 diff --git a/lib/src/model/json/onboarding_state.model.dart b/lib/src/model/json/onboarding_state.model.dart new file mode 100644 index 00000000..25a6d6ab --- /dev/null +++ b/lib/src/model/json/onboarding_state.model.dart @@ -0,0 +1,62 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'onboarding_state.model.g.dart'; + +/// Holds information about a single trusted friend who has already shared their +/// recovery part. +@JsonSerializable() +class ReceivedRecoveryShare { + ReceivedRecoveryShare({ + required this.messageId, + required this.trustedFriendDisplayName, + required this.myDisplayName, + required this.myUserId, + required this.myAvatarSvg, + required this.threshold, + required this.sharedSecretDataBytes, + }); + + factory ReceivedRecoveryShare.fromJson(Map json) => + _$ReceivedRecoveryShareFromJson(json); + + final int messageId; + + final String trustedFriendDisplayName; + + final String myDisplayName; + final int myUserId; + final List? myAvatarSvg; + + final int threshold; + + final List sharedSecretDataBytes; + + Map toJson() => _$ReceivedRecoveryShareToJson(this); +} + +@JsonSerializable() +class OnboardingState { + OnboardingState({ + this.hasOnboardingFinished = false, + this.hasStartedPasswordlessRecovery = false, + this.notificationId, + this.downloadAuthToken, + this.serverRegistered = false, + this.encryptionKey, + }); + + factory OnboardingState.fromJson(Map json) => + _$OnboardingStateFromJson(json); + + bool hasOnboardingFinished; + bool hasStartedPasswordlessRecovery; + + String? notificationId; + List? downloadAuthToken; + bool serverRegistered; + List? encryptionKey; + + List receivedShares = []; + + Map toJson() => _$OnboardingStateToJson(this); +} diff --git a/lib/src/model/json/onboarding_state.model.g.dart b/lib/src/model/json/onboarding_state.model.g.dart new file mode 100644 index 00000000..d6fc82d6 --- /dev/null +++ b/lib/src/model/json/onboarding_state.model.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'onboarding_state.model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson( + Map json, +) => ReceivedRecoveryShare( + messageId: (json['messageId'] as num).toInt(), + trustedFriendDisplayName: json['trustedFriendDisplayName'] as String, + myDisplayName: json['myDisplayName'] as String, + myUserId: (json['myUserId'] as num).toInt(), + myAvatarSvg: (json['myAvatarSvg'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), + threshold: (json['threshold'] as num).toInt(), + sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List) + .map((e) => (e as num).toInt()) + .toList(), +); + +Map _$ReceivedRecoveryShareToJson( + ReceivedRecoveryShare instance, +) => { + 'messageId': instance.messageId, + 'trustedFriendDisplayName': instance.trustedFriendDisplayName, + 'myDisplayName': instance.myDisplayName, + 'myUserId': instance.myUserId, + 'myAvatarSvg': instance.myAvatarSvg, + 'threshold': instance.threshold, + 'sharedSecretDataBytes': instance.sharedSecretDataBytes, +}; + +OnboardingState _$OnboardingStateFromJson(Map json) => + OnboardingState( + hasOnboardingFinished: json['hasOnboardingFinished'] as bool? ?? false, + hasStartedPasswordlessRecovery: + json['hasStartedPasswordlessRecovery'] as bool? ?? false, + notificationId: json['notificationId'] as String?, + downloadAuthToken: (json['downloadAuthToken'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), + serverRegistered: json['serverRegistered'] as bool? ?? false, + encryptionKey: (json['encryptionKey'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), + ) + ..receivedShares = (json['receivedShares'] as List) + .map((e) => ReceivedRecoveryShare.fromJson(e as Map)) + .toList(); + +Map _$OnboardingStateToJson(OnboardingState instance) => + { + 'hasOnboardingFinished': instance.hasOnboardingFinished, + 'hasStartedPasswordlessRecovery': instance.hasStartedPasswordlessRecovery, + 'notificationId': instance.notificationId, + 'downloadAuthToken': instance.downloadAuthToken, + 'serverRegistered': instance.serverRegistered, + 'encryptionKey': instance.encryptionKey, + 'receivedShares': instance.receivedShares, + }; diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 1ef870be..b592eea1 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -813,7 +813,6 @@ class Handshake_GetServerKeyForPasswordLessRecovery extends $pb.GeneratedMessage { factory Handshake_GetServerKeyForPasswordLessRecovery({ $fixnum.Int64? userId, - $core.List<$core.int>? encryptedServerKeyMac, $core.List<$core.int>? encryptedServerKeyNone, $core.List<$core.int>? pinUnlockToken, $core.List<$core.int>? pinProtectionKey, @@ -821,8 +820,6 @@ class Handshake_GetServerKeyForPasswordLessRecovery }) { final result = create(); if (userId != null) result.userId = userId; - if (encryptedServerKeyMac != null) - result.encryptedServerKeyMac = encryptedServerKeyMac; if (encryptedServerKeyNone != null) result.encryptedServerKeyNone = encryptedServerKeyNone; if (pinUnlockToken != null) result.pinUnlockToken = pinUnlockToken; @@ -849,14 +846,12 @@ class Handshake_GetServerKeyForPasswordLessRecovery createEmptyInstance: create) ..aInt64(1, _omitFieldNames ? '' : 'userId') ..a<$core.List<$core.int>>( - 2, _omitFieldNames ? '' : 'encryptedServerKeyMac', $pb.PbFieldType.OY) + 2, _omitFieldNames ? '' : 'encryptedServerKeyNone', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( - 3, _omitFieldNames ? '' : 'encryptedServerKeyNone', $pb.PbFieldType.OY) + 3, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( - 4, _omitFieldNames ? '' : 'pinUnlockToken', $pb.PbFieldType.OY) - ..a<$core.List<$core.int>>( - 5, _omitFieldNames ? '' : 'pinProtectionKey', $pb.PbFieldType.OY) - ..aOS(6, _omitFieldNames ? '' : 'email') + 4, _omitFieldNames ? '' : 'pinProtectionKey', $pb.PbFieldType.OY) + ..aOS(5, _omitFieldNames ? '' : 'email') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -894,51 +889,219 @@ class Handshake_GetServerKeyForPasswordLessRecovery void clearUserId() => $_clearField(1); @$pb.TagNumber(2) - $core.List<$core.int> get encryptedServerKeyMac => $_getN(1); + $core.List<$core.int> get encryptedServerKeyNone => $_getN(1); @$pb.TagNumber(2) - set encryptedServerKeyMac($core.List<$core.int> value) => + set encryptedServerKeyNone($core.List<$core.int> value) => $_setBytes(1, value); @$pb.TagNumber(2) - $core.bool hasEncryptedServerKeyMac() => $_has(1); + $core.bool hasEncryptedServerKeyNone() => $_has(1); @$pb.TagNumber(2) - void clearEncryptedServerKeyMac() => $_clearField(2); + void clearEncryptedServerKeyNone() => $_clearField(2); @$pb.TagNumber(3) - $core.List<$core.int> get encryptedServerKeyNone => $_getN(2); + $core.List<$core.int> get pinUnlockToken => $_getN(2); @$pb.TagNumber(3) - set encryptedServerKeyNone($core.List<$core.int> value) => - $_setBytes(2, value); + set pinUnlockToken($core.List<$core.int> value) => $_setBytes(2, value); @$pb.TagNumber(3) - $core.bool hasEncryptedServerKeyNone() => $_has(2); + $core.bool hasPinUnlockToken() => $_has(2); @$pb.TagNumber(3) - void clearEncryptedServerKeyNone() => $_clearField(3); + void clearPinUnlockToken() => $_clearField(3); @$pb.TagNumber(4) - $core.List<$core.int> get pinUnlockToken => $_getN(3); + $core.List<$core.int> get pinProtectionKey => $_getN(3); @$pb.TagNumber(4) - set pinUnlockToken($core.List<$core.int> value) => $_setBytes(3, value); + set pinProtectionKey($core.List<$core.int> value) => $_setBytes(3, value); @$pb.TagNumber(4) - $core.bool hasPinUnlockToken() => $_has(3); + $core.bool hasPinProtectionKey() => $_has(3); @$pb.TagNumber(4) - void clearPinUnlockToken() => $_clearField(4); + void clearPinProtectionKey() => $_clearField(4); @$pb.TagNumber(5) - $core.List<$core.int> get pinProtectionKey => $_getN(4); + $core.String get email => $_getSZ(4); @$pb.TagNumber(5) - set pinProtectionKey($core.List<$core.int> value) => $_setBytes(4, value); + set email($core.String value) => $_setString(4, value); @$pb.TagNumber(5) - $core.bool hasPinProtectionKey() => $_has(4); + $core.bool hasEmail() => $_has(4); @$pb.TagNumber(5) - void clearPinProtectionKey() => $_clearField(5); + void clearEmail() => $_clearField(5); +} - @$pb.TagNumber(6) - $core.String get email => $_getSZ(5); - @$pb.TagNumber(6) - set email($core.String value) => $_setString(5, value); - @$pb.TagNumber(6) - $core.bool hasEmail() => $_has(5); - @$pb.TagNumber(6) - void clearEmail() => $_clearField(6); +class Handshake_RegisterPasswordlessNotification extends $pb.GeneratedMessage { + factory Handshake_RegisterPasswordlessNotification({ + $core.String? notificationId, + $core.List<$core.int>? downloadAuthToken, + $core.String? langCode, + $core.String? googleFcm, + }) { + final result = create(); + if (notificationId != null) result.notificationId = notificationId; + if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken; + if (langCode != null) result.langCode = langCode; + if (googleFcm != null) result.googleFcm = googleFcm; + return result; + } + + Handshake_RegisterPasswordlessNotification._(); + + factory Handshake_RegisterPasswordlessNotification.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_RegisterPasswordlessNotification.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.RegisterPasswordlessNotification', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'notificationId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY) + ..aOS(3, _omitFieldNames ? '' : 'langCode') + ..aOS(4, _omitFieldNames ? '' : 'googleFcm') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_RegisterPasswordlessNotification clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_RegisterPasswordlessNotification copyWith( + void Function(Handshake_RegisterPasswordlessNotification) updates) => + super.copyWith((message) => + updates(message as Handshake_RegisterPasswordlessNotification)) + as Handshake_RegisterPasswordlessNotification; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_RegisterPasswordlessNotification create() => + Handshake_RegisterPasswordlessNotification._(); + @$core.override + Handshake_RegisterPasswordlessNotification createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_RegisterPasswordlessNotification getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Handshake_RegisterPasswordlessNotification>(create); + static Handshake_RegisterPasswordlessNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get notificationId => $_getSZ(0); + @$pb.TagNumber(1) + set notificationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasNotificationId() => $_has(0); + @$pb.TagNumber(1) + void clearNotificationId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get downloadAuthToken => $_getN(1); + @$pb.TagNumber(2) + set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasDownloadAuthToken() => $_has(1); + @$pb.TagNumber(2) + void clearDownloadAuthToken() => $_clearField(2); + + @$pb.TagNumber(3) + $core.String get langCode => $_getSZ(2); + @$pb.TagNumber(3) + set langCode($core.String value) => $_setString(2, value); + @$pb.TagNumber(3) + $core.bool hasLangCode() => $_has(2); + @$pb.TagNumber(3) + void clearLangCode() => $_clearField(3); + + @$pb.TagNumber(4) + $core.String get googleFcm => $_getSZ(3); + @$pb.TagNumber(4) + set googleFcm($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasGoogleFcm() => $_has(3); + @$pb.TagNumber(4) + void clearGoogleFcm() => $_clearField(4); +} + +class Handshake_CheckForPasswordlessNotification extends $pb.GeneratedMessage { + factory Handshake_CheckForPasswordlessNotification({ + $core.String? notificationId, + $core.List<$core.int>? downloadAuthToken, + $core.Iterable<$fixnum.Int64>? alreadyReceivedMessageIds, + }) { + final result = create(); + if (notificationId != null) result.notificationId = notificationId; + if (downloadAuthToken != null) result.downloadAuthToken = downloadAuthToken; + if (alreadyReceivedMessageIds != null) + result.alreadyReceivedMessageIds.addAll(alreadyReceivedMessageIds); + return result; + } + + Handshake_CheckForPasswordlessNotification._(); + + factory Handshake_CheckForPasswordlessNotification.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_CheckForPasswordlessNotification.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.CheckForPasswordlessNotification', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'notificationId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'downloadAuthToken', $pb.PbFieldType.OY) + ..p<$fixnum.Int64>(3, _omitFieldNames ? '' : 'alreadyReceivedMessageIds', + $pb.PbFieldType.K6) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_CheckForPasswordlessNotification clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_CheckForPasswordlessNotification copyWith( + void Function(Handshake_CheckForPasswordlessNotification) updates) => + super.copyWith((message) => + updates(message as Handshake_CheckForPasswordlessNotification)) + as Handshake_CheckForPasswordlessNotification; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_CheckForPasswordlessNotification create() => + Handshake_CheckForPasswordlessNotification._(); + @$core.override + Handshake_CheckForPasswordlessNotification createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_CheckForPasswordlessNotification getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Handshake_CheckForPasswordlessNotification>(create); + static Handshake_CheckForPasswordlessNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get notificationId => $_getSZ(0); + @$pb.TagNumber(1) + set notificationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasNotificationId() => $_has(0); + @$pb.TagNumber(1) + void clearNotificationId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get downloadAuthToken => $_getN(1); + @$pb.TagNumber(2) + set downloadAuthToken($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasDownloadAuthToken() => $_has(1); + @$pb.TagNumber(2) + void clearDownloadAuthToken() => $_clearField(2); + + @$pb.TagNumber(3) + $pb.PbList<$fixnum.Int64> get alreadyReceivedMessageIds => $_getList(2); } enum Handshake_Handshake { @@ -950,6 +1113,8 @@ enum Handshake_Handshake { authenticateWithLoginToken, getUseridByUsername, getServerKeyForPasswordlessRecovery, + registerPasswordlessNotification, + checkForPasswordlessNotification, notSet } @@ -964,6 +1129,10 @@ class Handshake extends $pb.GeneratedMessage { Handshake_GetUserIdByUsername? getUseridByUsername, Handshake_GetServerKeyForPasswordLessRecovery? getServerKeyForPasswordlessRecovery, + Handshake_RegisterPasswordlessNotification? + registerPasswordlessNotification, + Handshake_CheckForPasswordlessNotification? + checkForPasswordlessNotification, }) { final result = create(); if (register != null) result.register = register; @@ -978,6 +1147,12 @@ class Handshake extends $pb.GeneratedMessage { if (getServerKeyForPasswordlessRecovery != null) result.getServerKeyForPasswordlessRecovery = getServerKeyForPasswordlessRecovery; + if (registerPasswordlessNotification != null) + result.registerPasswordlessNotification = + registerPasswordlessNotification; + if (checkForPasswordlessNotification != null) + result.checkForPasswordlessNotification = + checkForPasswordlessNotification; return result; } @@ -1000,6 +1175,8 @@ class Handshake extends $pb.GeneratedMessage { 6: Handshake_Handshake.authenticateWithLoginToken, 7: Handshake_Handshake.getUseridByUsername, 8: Handshake_Handshake.getServerKeyForPasswordlessRecovery, + 9: Handshake_Handshake.registerPasswordlessNotification, + 10: Handshake_Handshake.checkForPasswordlessNotification, 0: Handshake_Handshake.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -1007,7 +1184,7 @@ class Handshake extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) ..aOM(1, _omitFieldNames ? '' : 'register', subBuilder: Handshake_Register.create) ..aOM( @@ -1029,6 +1206,12 @@ class Handshake extends $pb.GeneratedMessage { ..aOM( 8, _omitFieldNames ? '' : 'getServerKeyForPasswordlessRecovery', subBuilder: Handshake_GetServerKeyForPasswordLessRecovery.create) + ..aOM( + 9, _omitFieldNames ? '' : 'registerPasswordlessNotification', + subBuilder: Handshake_RegisterPasswordlessNotification.create) + ..aOM( + 10, _omitFieldNames ? '' : 'checkForPasswordlessNotification', + subBuilder: Handshake_CheckForPasswordlessNotification.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1057,6 +1240,8 @@ class Handshake extends $pb.GeneratedMessage { @$pb.TagNumber(6) @$pb.TagNumber(7) @$pb.TagNumber(8) + @$pb.TagNumber(9) + @$pb.TagNumber(10) Handshake_Handshake whichHandshake() => _Handshake_HandshakeByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -1067,6 +1252,8 @@ class Handshake extends $pb.GeneratedMessage { @$pb.TagNumber(6) @$pb.TagNumber(7) @$pb.TagNumber(8) + @$pb.TagNumber(9) + @$pb.TagNumber(10) void clearHandshake() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -1165,6 +1352,36 @@ class Handshake extends $pb.GeneratedMessage { @$pb.TagNumber(8) Handshake_GetServerKeyForPasswordLessRecovery ensureGetServerKeyForPasswordlessRecovery() => $_ensure(7); + + @$pb.TagNumber(9) + Handshake_RegisterPasswordlessNotification + get registerPasswordlessNotification => $_getN(8); + @$pb.TagNumber(9) + set registerPasswordlessNotification( + Handshake_RegisterPasswordlessNotification value) => + $_setField(9, value); + @$pb.TagNumber(9) + $core.bool hasRegisterPasswordlessNotification() => $_has(8); + @$pb.TagNumber(9) + void clearRegisterPasswordlessNotification() => $_clearField(9); + @$pb.TagNumber(9) + Handshake_RegisterPasswordlessNotification + ensureRegisterPasswordlessNotification() => $_ensure(8); + + @$pb.TagNumber(10) + Handshake_CheckForPasswordlessNotification + get checkForPasswordlessNotification => $_getN(9); + @$pb.TagNumber(10) + set checkForPasswordlessNotification( + Handshake_CheckForPasswordlessNotification value) => + $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasCheckForPasswordlessNotification() => $_has(9); + @$pb.TagNumber(10) + void clearCheckForPasswordlessNotification() => $_clearField(10); + @$pb.TagNumber(10) + Handshake_CheckForPasswordlessNotification + ensureCheckForPasswordlessNotification() => $_ensure(9); } class ApplicationData_TextMessage extends $pb.GeneratedMessage { @@ -2422,6 +2639,79 @@ class ApplicationData_RegisterPasswordLessRecovery void clearPinUnlockToken() => $_clearField(2); } +class ApplicationData_PasswordlessNotification extends $pb.GeneratedMessage { + factory ApplicationData_PasswordlessNotification({ + $core.String? notificationId, + $core.List<$core.int>? encryptedMessage, + }) { + final result = create(); + if (notificationId != null) result.notificationId = notificationId; + if (encryptedMessage != null) result.encryptedMessage = encryptedMessage; + return result; + } + + ApplicationData_PasswordlessNotification._(); + + factory ApplicationData_PasswordlessNotification.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_PasswordlessNotification.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.PasswordlessNotification', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'notificationId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_PasswordlessNotification clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_PasswordlessNotification copyWith( + void Function(ApplicationData_PasswordlessNotification) updates) => + super.copyWith((message) => + updates(message as ApplicationData_PasswordlessNotification)) + as ApplicationData_PasswordlessNotification; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_PasswordlessNotification create() => + ApplicationData_PasswordlessNotification._(); + @$core.override + ApplicationData_PasswordlessNotification createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_PasswordlessNotification getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ApplicationData_PasswordlessNotification>(create); + static ApplicationData_PasswordlessNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get notificationId => $_getSZ(0); + @$pb.TagNumber(1) + set notificationId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasNotificationId() => $_has(0); + @$pb.TagNumber(1) + void clearNotificationId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedMessage => $_getN(1); + @$pb.TagNumber(2) + set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasEncryptedMessage() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedMessage() => $_clearField(2); +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, @@ -2450,6 +2740,7 @@ enum ApplicationData_ApplicationData { addAdditionalUser, setLoginToken, registerPasswordlessRecovery, + passwordlessNotification, notSet } @@ -2482,6 +2773,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_AddAdditionalUser? addAdditionalUser, ApplicationData_SetLoginToken? setLoginToken, ApplicationData_RegisterPasswordLessRecovery? registerPasswordlessRecovery, + ApplicationData_PasswordlessNotification? passwordlessNotification, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -2518,6 +2810,8 @@ class ApplicationData extends $pb.GeneratedMessage { if (setLoginToken != null) result.setLoginToken = setLoginToken; if (registerPasswordlessRecovery != null) result.registerPasswordlessRecovery = registerPasswordlessRecovery; + if (passwordlessNotification != null) + result.passwordlessNotification = passwordlessNotification; return result; } @@ -2559,6 +2853,7 @@ class ApplicationData extends $pb.GeneratedMessage { 29: ApplicationData_ApplicationData.addAdditionalUser, 30: ApplicationData_ApplicationData.setLoginToken, 31: ApplicationData_ApplicationData.registerPasswordlessRecovery, + 32: ApplicationData_ApplicationData.passwordlessNotification, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -2593,7 +2888,8 @@ class ApplicationData extends $pb.GeneratedMessage { 28, 29, 30, - 31 + 31, + 32 ]) ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -2688,6 +2984,9 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM( 31, _omitFieldNames ? '' : 'registerPasswordlessRecovery', subBuilder: ApplicationData_RegisterPasswordLessRecovery.create) + ..aOM( + 32, _omitFieldNames ? '' : 'passwordlessNotification', + subBuilder: ApplicationData_PasswordlessNotification.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2736,6 +3035,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(29) @$pb.TagNumber(30) @$pb.TagNumber(31) + @$pb.TagNumber(32) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -2765,6 +3065,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(29) @$pb.TagNumber(30) @$pb.TagNumber(31) + @$pb.TagNumber(32) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -3085,6 +3386,21 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(31) ApplicationData_RegisterPasswordLessRecovery ensureRegisterPasswordlessRecovery() => $_ensure(26); + + @$pb.TagNumber(32) + ApplicationData_PasswordlessNotification get passwordlessNotification => + $_getN(27); + @$pb.TagNumber(32) + set passwordlessNotification( + ApplicationData_PasswordlessNotification value) => + $_setField(32, value); + @$pb.TagNumber(32) + $core.bool hasPasswordlessNotification() => $_has(27); + @$pb.TagNumber(32) + void clearPasswordlessNotification() => $_clearField(32); + @$pb.TagNumber(32) + ApplicationData_PasswordlessNotification ensurePasswordlessNotification() => + $_ensure(27); } class Response_PreKey extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index b908a69b..56e88824 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -161,6 +161,24 @@ const Handshake$json = { '9': 0, '10': 'getServerKeyForPasswordlessRecovery' }, + { + '1': 'register_passwordless_notification', + '3': 9, + '4': 1, + '5': 11, + '6': '.client_to_server.Handshake.RegisterPasswordlessNotification', + '9': 0, + '10': 'registerPasswordlessNotification' + }, + { + '1': 'check_for_passwordless_notification', + '3': 10, + '4': 1, + '5': 11, + '6': '.client_to_server.Handshake.CheckForPasswordlessNotification', + '9': 0, + '10': 'checkForPasswordlessNotification' + }, ], '3': [ Handshake_RequestPOW$json, @@ -170,7 +188,9 @@ const Handshake$json = { Handshake_GetAuthToken$json, Handshake_Authenticate$json, Handshake_AuthenticateWithLoginToken$json, - Handshake_GetServerKeyForPasswordLessRecovery$json + Handshake_GetServerKeyForPasswordLessRecovery$json, + Handshake_RegisterPasswordlessNotification$json, + Handshake_CheckForPasswordlessNotification$json ], '8': [ {'1': 'Handshake'}, @@ -318,23 +338,16 @@ const Handshake_GetServerKeyForPasswordLessRecovery$json = { '1': 'GetServerKeyForPasswordLessRecovery', '2': [ {'1': 'user_id', '3': 1, '4': 1, '5': 3, '10': 'userId'}, - { - '1': 'encrypted_server_key_mac', - '3': 2, - '4': 1, - '5': 12, - '10': 'encryptedServerKeyMac' - }, { '1': 'encrypted_server_key_none', - '3': 3, + '3': 2, '4': 1, '5': 12, '10': 'encryptedServerKeyNone' }, { '1': 'pin_unlock_token', - '3': 4, + '3': 3, '4': 1, '5': 12, '9': 0, @@ -343,14 +356,14 @@ const Handshake_GetServerKeyForPasswordLessRecovery$json = { }, { '1': 'pin_protection_key', - '3': 5, + '3': 4, '4': 1, '5': 12, '9': 1, '10': 'pinProtectionKey', '17': true }, - {'1': 'email', '3': 6, '4': 1, '5': 9, '9': 2, '10': 'email', '17': true}, + {'1': 'email', '3': 5, '4': 1, '5': 9, '9': 2, '10': 'email', '17': true}, ], '8': [ {'1': '_pin_unlock_token'}, @@ -359,6 +372,56 @@ const Handshake_GetServerKeyForPasswordLessRecovery$json = { ], }; +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_RegisterPasswordlessNotification$json = { + '1': 'RegisterPasswordlessNotification', + '2': [ + {'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'}, + { + '1': 'download_auth_token', + '3': 2, + '4': 1, + '5': 12, + '10': 'downloadAuthToken' + }, + {'1': 'lang_code', '3': 3, '4': 1, '5': 9, '10': 'langCode'}, + { + '1': 'google_fcm', + '3': 4, + '4': 1, + '5': 9, + '9': 0, + '10': 'googleFcm', + '17': true + }, + ], + '8': [ + {'1': '_google_fcm'}, + ], +}; + +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_CheckForPasswordlessNotification$json = { + '1': 'CheckForPasswordlessNotification', + '2': [ + {'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'}, + { + '1': 'download_auth_token', + '3': 2, + '4': 1, + '5': 12, + '10': 'downloadAuthToken' + }, + { + '1': 'already_received_message_ids', + '3': 3, + '4': 3, + '5': 3, + '10': 'alreadyReceivedMessageIds' + }, + ], +}; + /// Descriptor for `Handshake`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'CglIYW5kc2hha2USQgoIcmVnaXN0ZXIYASABKAsyJC5jbGllbnRfdG9fc2VydmVyLkhhbmRzaG' @@ -375,32 +438,44 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'VIAFITZ2V0VXNlcmlkQnlVc2VybmFtZRKYAQooZ2V0X3NlcnZlcl9rZXlfZm9yX3Bhc3N3b3Jk' 'bGVzc19yZWNvdmVyeRgIIAEoCzI/LmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkdldFNlcn' 'ZlcktleUZvclBhc3N3b3JkTGVzc1JlY292ZXJ5SABSI2dldFNlcnZlcktleUZvclBhc3N3b3Jk' - 'bGVzc1JlY292ZXJ5GgwKClJlcXVlc3RQT1caygMKCFJlZ2lzdGVyEhoKCHVzZXJuYW1lGAEgAS' - 'gJUgh1c2VybmFtZRIkCgtpbnZpdGVfY29kZRgCIAEoCUgAUgppbnZpdGVDb2RliAEBEi4KE3B1' - 'YmxpY19pZGVudGl0eV9rZXkYAyABKAxSEXB1YmxpY0lkZW50aXR5S2V5EiMKDXNpZ25lZF9wcm' - 'VrZXkYBCABKAxSDHNpZ25lZFByZWtleRI2ChdzaWduZWRfcHJla2V5X3NpZ25hdHVyZRgFIAEo' - 'DFIVc2lnbmVkUHJla2V5U2lnbmF0dXJlEigKEHNpZ25lZF9wcmVrZXlfaWQYBiABKANSDnNpZ2' - '5lZFByZWtleUlkEicKD3JlZ2lzdHJhdGlvbl9pZBgHIAEoA1IOcmVnaXN0cmF0aW9uSWQSFQoG' - 'aXNfaW9zGAggASgIUgVpc0lvcxIbCglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIKDXByb2' - '9mX29mX3dvcmsYCiABKANSC3Byb29mT2ZXb3JrEiQKC2xvZ2luX3Rva2VuGAsgASgMSAFSCmxv' - 'Z2luVG9rZW6IAQFCDgoMX2ludml0ZV9jb2RlQg4KDF9sb2dpbl90b2tlbhoSChBHZXRBdXRoQ2' - 'hhbGxlbmdlGjEKE0dldFVzZXJJZEJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJu' - 'YW1lGkMKDEdldEF1dGhUb2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSGgoIcmVzcG9uc2' - 'UYAiABKAxSCHJlc3BvbnNlGugBCgxBdXRoZW50aWNhdGUSFwoHdXNlcl9pZBgBIAEoA1IGdXNl' - 'cklkEh0KCmF1dGhfdG9rZW4YAiABKAxSCWF1dGhUb2tlbhIkCgthcHBfdmVyc2lvbhgDIAEoCU' - 'gAUgphcHBWZXJzaW9uiAEBEiAKCWRldmljZV9pZBgEIAEoA0gBUghkZXZpY2VJZIgBARIoCg1p' - 'bl9iYWNrZ3JvdW5kGAUgASgISAJSDGluQmFja2dyb3VuZIgBAUIOCgxfYXBwX3ZlcnNpb25CDA' - 'oKX2RldmljZV9pZEIQCg5faW5fYmFja2dyb3VuZBrGAQoaQXV0aGVudGljYXRlV2l0aExvZ2lu' - 'VG9rZW4SFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEiwKEnNlY3JldF9sb2dpbl90b2tlbhgCIA' - 'EoDFIQc2VjcmV0TG9naW5Ub2tlbhIfCgthcHBfdmVyc2lvbhgDIAEoCVIKYXBwVmVyc2lvbhIb' - 'CglkZXZpY2VfaWQYBCABKANSCGRldmljZUlkEiMKDWluX2JhY2tncm91bmQYBSABKAhSDGluQm' - 'Fja2dyb3VuZBrlAgojR2V0U2VydmVyS2V5Rm9yUGFzc3dvcmRMZXNzUmVjb3ZlcnkSFwoHdXNl' - 'cl9pZBgBIAEoA1IGdXNlcklkEjcKGGVuY3J5cHRlZF9zZXJ2ZXJfa2V5X21hYxgCIAEoDFIVZW' - '5jcnlwdGVkU2VydmVyS2V5TWFjEjkKGWVuY3J5cHRlZF9zZXJ2ZXJfa2V5X25vbmUYAyABKAxS' - 'FmVuY3J5cHRlZFNlcnZlcktleU5vbmUSLQoQcGluX3VubG9ja190b2tlbhgEIAEoDEgAUg5waW' - '5VbmxvY2tUb2tlbogBARIxChJwaW5fcHJvdGVjdGlvbl9rZXkYBSABKAxIAVIQcGluUHJvdGVj' - 'dGlvbktleYgBARIZCgVlbWFpbBgGIAEoCUgCUgVlbWFpbIgBAUITChFfcGluX3VubG9ja190b2' - 'tlbkIVChNfcGluX3Byb3RlY3Rpb25fa2V5QggKBl9lbWFpbEILCglIYW5kc2hha2U='); + 'bGVzc1JlY292ZXJ5EowBCiJyZWdpc3Rlcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAkgAS' + 'gLMjwuY2xpZW50X3RvX3NlcnZlci5IYW5kc2hha2UuUmVnaXN0ZXJQYXNzd29yZGxlc3NOb3Rp' + 'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2' + 'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I' + 'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX' + 'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0ZXISGgoIdXNl' + 'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG' + 'WIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRpdHlLZXkSIwoN' + 'c2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbm' + 'F0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3ByZWtleV9pZBgG' + 'IAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg5yZWdpc3RyYX' + 'Rpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEoCVIIbGFuZ0Nv' + 'ZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW5fdG9rZW4YCy' + 'ABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2luX3Rva2VuGhIK' + 'EEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIA' + 'EoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIa' + 'CghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGA' + 'EgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJz' + 'aW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZU' + 'lkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBQg4KDF9hcHBf' + 'dmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYBChpBdXRoZW50aWNhdG' + 'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu' + 'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH' + 'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF' + 'IAEoCFIMaW5CYWNrZ3JvdW5kGqwCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm' + 'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSOQoZZW5jcnlwdGVkX3NlcnZlcl9rZXlfbm9u' + 'ZRgCIAEoDFIWZW5jcnlwdGVkU2VydmVyS2V5Tm9uZRItChBwaW5fdW5sb2NrX3Rva2VuGAMgAS' + 'gMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tleRgEIAEoDEgBUhBw' + 'aW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiAEBQhMKEV9waW5fdW' + '5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWlsGssBCiBSZWdpc3Rl' + 'clBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faWQYASABKAlSDm5vdG' + 'lmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRvd25sb2FkQXV0aFRv' + 'a2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2ZjbRgEIAEoCUgAUg' + 'lnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFzc3dvcmRsZXNzTm90' + 'aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSLgoTZG' + '93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SPwocYWxyZWFkeV9y' + 'ZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTWVzc2FnZUlkc0ILCg' + 'lIYW5kc2hha2U='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { @@ -649,6 +724,15 @@ const ApplicationData$json = { '9': 0, '10': 'registerPasswordlessRecovery' }, + { + '1': 'passwordless_notification', + '3': 32, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.PasswordlessNotification', + '9': 0, + '10': 'passwordlessNotification' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -671,7 +755,8 @@ const ApplicationData$json = { ApplicationData_AddAdditionalUser$json, ApplicationData_SetLoginToken$json, ApplicationData_Deprecated$json, - ApplicationData_RegisterPasswordLessRecovery$json + ApplicationData_RegisterPasswordLessRecovery$json, + ApplicationData_PasswordlessNotification$json ], '8': [ {'1': 'ApplicationData'}, @@ -876,6 +961,21 @@ const ApplicationData_RegisterPasswordLessRecovery$json = { ], }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_PasswordlessNotification$json = { + '1': 'PasswordlessNotification', + '2': [ + {'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'}, + { + '1': 'encrypted_message', + '3': 2, + '4': 1, + '5': 12, + '10': 'encryptedMessage' + }, + ], +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -924,29 +1024,33 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'ljYXRpb25EYXRhLlNldExvZ2luVG9rZW5IAFINc2V0TG9naW5Ub2tlbhKGAQoecmVnaXN0ZXJf' 'cGFzc3dvcmRsZXNzX3JlY292ZXJ5GB8gASgLMj4uY2xpZW50X3RvX3NlcnZlci5BcHBsaWNhdG' 'lvbkRhdGEuUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeUgAUhxyZWdpc3RlclBhc3N3b3Jk' - 'bGVzc1JlY292ZXJ5GmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBISCg' - 'Rib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNoRGF0YYgBAUIMCgpf' - 'cHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vybm' - 'FtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaNQoUVXBk' - 'YXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2xlRmNtGiYKC0dldF' - 'VzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFpbGFibGVQbGFucxoX' - 'ChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5JbmZvcxovChRSZW1vdm' - 'VBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2V0UHJla2V5c0J5' - 'VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQcmVLZXlCeVVzZX' - 'JJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZFByZUtleRIoChBz' - 'aWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWduZWRfcHJla2V5GA' - 'IgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUYAyABKAxSFXNp' - 'Z25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRfdG9rZW4YAS' - 'ABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2VyX2lkGAEg' - 'ASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJUEFQdXJjaG' - 'FzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgASgJUgZzb3Vy' - 'Y2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEaDwoNSVBBRm' - '9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEhcKB3VzZXJf' - 'aWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3Rva2VuGAEgASgMUg' - 'psb2dpblRva2VuGgwKCkRlcHJlY2F0ZWQajgEKHFJlZ2lzdGVyUGFzc3dvcmRMZXNzUmVjb3Zl' - 'cnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeXB0ZWRTZXJ2ZXJLZXkSKwoOcG' - 'luVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6IAQFCEQoPX3BpblVubG9ja1Rv' - 'a2VuQhEKD0FwcGxpY2F0aW9uRGF0YQ=='); + 'bGVzc1JlY292ZXJ5EnkKGXBhc3N3b3JkbGVzc19ub3RpZmljYXRpb24YICABKAsyOi5jbGllbn' + 'RfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIY' + 'cGFzc3dvcmRsZXNzTm90aWZpY2F0aW9uGmoKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKA' + 'NSBnVzZXJJZBISCgRib2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNo' + 'RGF0YYgBAUIMCgpfcHVzaF9kYXRhGi8KEUdldFVzZXJCeVVzZXJuYW1lEhoKCHVzZXJuYW1lGA' + 'EgASgJUgh1c2VybmFtZRosCg5DaGFuZ2VVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNl' + 'cm5hbWUaNQoUVXBkYXRlR29vZ2xlRmNtVG9rZW4SHQoKZ29vZ2xlX2ZjbRgBIAEoCVIJZ29vZ2' + 'xlRmNtGiYKC0dldFVzZXJCeUlkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoTChFHZXRBdmFp' + 'bGFibGVQbGFucxoXChVHZXRBZGRBY2NvdW50c0ludml0ZXMaFQoTR2V0Q3VycmVudFBsYW5Jbm' + 'ZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoS' + 'R2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZW' + 'RQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25l' + 'ZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaW' + 'duZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1' + 'cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRo1CgxEb3dubG9hZERvbmUSJQoOZG93bm' + 'xvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRl' + 'ZF91c2VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbh' + 'pxCgtJUEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNl' + 'GAIgASgJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbk' + 'RhdGEaDwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxV' + 'c2VyEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3' + 'Rva2VuGAEgASgMUgpsb2dpblRva2VuGgwKCkRlcHJlY2F0ZWQajgEKHFJlZ2lzdGVyUGFzc3dv' + 'cmRMZXNzUmVjb3ZlcnkSLgoSZW5jcnlwdGVkU2VydmVyS2V5GAEgASgMUhJlbmNyeXB0ZWRTZX' + 'J2ZXJLZXkSKwoOcGluVW5sb2NrVG9rZW4YAiABKAxIAFIOcGluVW5sb2NrVG9rZW6IAQFCEQoP' + 'X3BpblVubG9ja1Rva2VuGnAKGFBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYX' + 'Rpb25faWQYASABKAlSDm5vdGlmaWNhdGlvbklkEisKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgM' + 'UhBlbmNyeXB0ZWRNZXNzYWdlQhEKD0FwcGxpY2F0aW9uRGF0YQ=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 393ee769..fdeb45a6 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -1505,6 +1505,136 @@ class Response_ProofOfWork extends $pb.GeneratedMessage { void clearDifficulty() => $_clearField(2); } +class Response_PasswordlessNotificationMessage extends $pb.GeneratedMessage { + factory Response_PasswordlessNotificationMessage({ + $fixnum.Int64? id, + $core.List<$core.int>? encryptedMessage, + }) { + final result = create(); + if (id != null) result.id = id; + if (encryptedMessage != null) result.encryptedMessage = encryptedMessage; + return result; + } + + Response_PasswordlessNotificationMessage._(); + + factory Response_PasswordlessNotificationMessage.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_PasswordlessNotificationMessage.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.PasswordlessNotificationMessage', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'id') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'encryptedMessage', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PasswordlessNotificationMessage clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PasswordlessNotificationMessage copyWith( + void Function(Response_PasswordlessNotificationMessage) updates) => + super.copyWith((message) => + updates(message as Response_PasswordlessNotificationMessage)) + as Response_PasswordlessNotificationMessage; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_PasswordlessNotificationMessage create() => + Response_PasswordlessNotificationMessage._(); + @$core.override + Response_PasswordlessNotificationMessage createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_PasswordlessNotificationMessage getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Response_PasswordlessNotificationMessage>(create); + static Response_PasswordlessNotificationMessage? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get id => $_getI64(0); + @$pb.TagNumber(1) + set id($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get encryptedMessage => $_getN(1); + @$pb.TagNumber(2) + set encryptedMessage($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasEncryptedMessage() => $_has(1); + @$pb.TagNumber(2) + void clearEncryptedMessage() => $_clearField(2); +} + +class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage { + factory Response_PasswordlessNotificationMessages({ + $core.Iterable? messages, + }) { + final result = create(); + if (messages != null) result.messages.addAll(messages); + return result; + } + + Response_PasswordlessNotificationMessages._(); + + factory Response_PasswordlessNotificationMessages.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_PasswordlessNotificationMessages.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.PasswordlessNotificationMessages', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), + createEmptyInstance: create) + ..pPM( + 1, _omitFieldNames ? '' : 'messages', + subBuilder: Response_PasswordlessNotificationMessage.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PasswordlessNotificationMessages clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_PasswordlessNotificationMessages copyWith( + void Function(Response_PasswordlessNotificationMessages) updates) => + super.copyWith((message) => + updates(message as Response_PasswordlessNotificationMessages)) + as Response_PasswordlessNotificationMessages; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_PasswordlessNotificationMessages create() => + Response_PasswordlessNotificationMessages._(); + @$core.override + Response_PasswordlessNotificationMessages createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_PasswordlessNotificationMessages getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Response_PasswordlessNotificationMessages>(create); + static Response_PasswordlessNotificationMessages? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList get messages => + $_getList(0); +} + enum Response_Ok_Ok { none, userid, @@ -1522,6 +1652,7 @@ enum Response_Ok_Ok { signedprekey, proofOfWork, passwordlessRecoveryServerKey, + passwordlessNotificationMessages, notSet } @@ -1543,6 +1674,7 @@ class Response_Ok extends $pb.GeneratedMessage { Response_SignedPreKey? signedprekey, Response_ProofOfWork? proofOfWork, $core.List<$core.int>? passwordlessRecoveryServerKey, + Response_PasswordlessNotificationMessages? passwordlessNotificationMessages, }) { final result = create(); if (none != null) result.none = none; @@ -1563,6 +1695,9 @@ class Response_Ok extends $pb.GeneratedMessage { if (proofOfWork != null) result.proofOfWork = proofOfWork; if (passwordlessRecoveryServerKey != null) result.passwordlessRecoveryServerKey = passwordlessRecoveryServerKey; + if (passwordlessNotificationMessages != null) + result.passwordlessNotificationMessages = + passwordlessNotificationMessages; return result; } @@ -1592,6 +1727,7 @@ class Response_Ok extends $pb.GeneratedMessage { 14: Response_Ok_Ok.signedprekey, 15: Response_Ok_Ok.proofOfWork, 16: Response_Ok_Ok.passwordlessRecoveryServerKey, + 17: Response_Ok_Ok.passwordlessNotificationMessages, 0: Response_Ok_Ok.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -1599,7 +1735,7 @@ class Response_Ok extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]) + ..oo(0, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]) ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aInt64(2, _omitFieldNames ? '' : 'userid') ..a<$core.List<$core.int>>( @@ -1633,6 +1769,9 @@ class Response_Ok extends $pb.GeneratedMessage { 16, _omitFieldNames ? '' : 'passwordlessRecoveryServerKey', $pb.PbFieldType.OY) + ..aOM( + 17, _omitFieldNames ? '' : 'passwordlessNotificationMessages', + subBuilder: Response_PasswordlessNotificationMessages.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1670,6 +1809,7 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(14) @$pb.TagNumber(15) @$pb.TagNumber(16) + @$pb.TagNumber(17) Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) @@ -1687,6 +1827,7 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(14) @$pb.TagNumber(15) @$pb.TagNumber(16) + @$pb.TagNumber(17) void clearOk() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -1856,6 +1997,21 @@ class Response_Ok extends $pb.GeneratedMessage { $core.bool hasPasswordlessRecoveryServerKey() => $_has(15); @$pb.TagNumber(16) void clearPasswordlessRecoveryServerKey() => $_clearField(16); + + @$pb.TagNumber(17) + Response_PasswordlessNotificationMessages + get passwordlessNotificationMessages => $_getN(16); + @$pb.TagNumber(17) + set passwordlessNotificationMessages( + Response_PasswordlessNotificationMessages value) => + $_setField(17, value); + @$pb.TagNumber(17) + $core.bool hasPasswordlessNotificationMessages() => $_has(16); + @$pb.TagNumber(17) + void clearPasswordlessNotificationMessages() => $_clearField(17); + @$pb.TagNumber(17) + Response_PasswordlessNotificationMessages + ensurePasswordlessNotificationMessages() => $_ensure(16); } enum Response_Response { ok, error, notSet } diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index 5c7b3a4d..5a400286 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -176,6 +176,8 @@ const Response$json = { Response_UploadToken$json, Response_DownloadTokens$json, Response_ProofOfWork$json, + Response_PasswordlessNotificationMessage$json, + Response_PasswordlessNotificationMessages$json, Response_Ok$json ], '8': [ @@ -509,6 +511,36 @@ const Response_ProofOfWork$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_PasswordlessNotificationMessage$json = { + '1': 'PasswordlessNotificationMessage', + '2': [ + {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'}, + { + '1': 'encrypted_message', + '3': 2, + '4': 1, + '5': 12, + '10': 'encryptedMessage' + }, + ], +}; + +@$core.Deprecated('Use responseDescriptor instead') +const Response_PasswordlessNotificationMessages$json = { + '1': 'PasswordlessNotificationMessages', + '2': [ + { + '1': 'messages', + '3': 1, + '4': 3, + '5': 11, + '6': '.server_to_client.Response.PasswordlessNotificationMessage', + '10': 'messages' + }, + ], +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_Ok$json = { '1': 'Ok', @@ -631,6 +663,15 @@ const Response_Ok$json = { '9': 0, '10': 'passwordlessRecoveryServerKey' }, + { + '1': 'passwordless_notification_messages', + '3': 17, + '4': 1, + '5': 11, + '6': '.server_to_client.Response.PasswordlessNotificationMessages', + '9': 0, + '10': 'passwordlessNotificationMessages' + }, ], '8': [ {'1': 'Ok'}, @@ -685,23 +726,29 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'cKD2Rvd25sb2FkX3Rva2VucxgCIAMoDFIOZG93bmxvYWRUb2tlbnMaOQoORG93bmxvYWRUb2tl' 'bnMSJwoPZG93bmxvYWRfdG9rZW5zGAEgAygMUg5kb3dubG9hZFRva2VucxpFCgtQcm9vZk9mV2' '9yaxIWCgZwcmVmaXgYASABKAlSBnByZWZpeBIeCgpkaWZmaWN1bHR5GAIgASgDUgpkaWZmaWN1' - 'bHR5GqIICgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBnVzZX' - 'JpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9r' - 'ZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG' - '9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5V' - 'c2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEkoKDG' - 'RlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVjYXRl' - 'ZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2NsaW' - 'VudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSAB' - 'KAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW5iYW' - 'xsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNlSABS' - 'DHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaWVudC' - '5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2ludml0' - 'ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aXRlc0' - 'gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJf' - 'dG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk0KDH' - 'NpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVkUHJl' - 'S2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b19jbG' - 'llbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmsSSQogcGFzc3dvcmRsZXNz' - 'X3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlTZXJ2ZX' - 'JLZXlCBAoCT2tCCgoIUmVzcG9uc2U='); + 'bHR5Gl4KH1Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEi' + 'sKEWVuY3J5cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3Jk' + 'bGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2' + 'NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdl' + 'cxqwCQoCT2sSFAoETm9uZRgBIAEoCEgAUgROb25lEhgKBnVzZXJpZBgCIAEoA0gAUgZ1c2VyaW' + 'QSJgoNYXV0aGNoYWxsZW5nZRgDIAEoDEgAUg1hdXRoY2hhbGxlbmdlEkoKC3VwbG9hZHRva2Vu' + 'GAQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5VcGxvYWRUb2tlbkgAUgt1cGxvYW' + 'R0b2tlbhJBCgh1c2VyZGF0YRgFIAEoCzIjLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuVXNl' + 'ckRhdGFIAFIIdXNlcmRhdGESHgoJYXV0aHRva2VuGAYgASgMSABSCWF1dGh0b2tlbhJKCgxkZX' + 'ByZWNhdGVkXzcYByABKAsyJS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRlcHJlY2F0ZWRI' + 'AFILZGVwcmVjYXRlZDcSUAoNYXV0aGVudGljYXRlZBgIIAEoCzIoLnNlcnZlcl90b19jbGllbn' + 'QuUmVzcG9uc2UuQXV0aGVudGljYXRlZEgAUg1hdXRoZW50aWNhdGVkEjgKBXBsYW5zGAkgASgL' + 'MiAuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5QbGFuc0gAUgVwbGFucxJNCgxwbGFuYmFsbG' + 'FuY2UYCiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5CYWxsYW5jZUgAUgxw' + 'bGFuYmFsbGFuY2USTAoNZGVwcmVjYXRlZF8xMRgLIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUm' + 'VzcG9uc2UuRGVwcmVjYXRlZEgAUgxkZXByZWNhdGVkMTESXwoSYWRkYWNjb3VudHNpbnZpdGVz' + 'GAwgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BZGRBY2NvdW50c0ludml0ZXNIAF' + 'ISYWRkYWNjb3VudHNpbnZpdGVzElMKDmRvd25sb2FkdG9rZW5zGA0gASgLMikuc2VydmVyX3Rv' + 'X2NsaWVudC5SZXNwb25zZS5Eb3dubG9hZFRva2Vuc0gAUg5kb3dubG9hZHRva2VucxJNCgxzaW' + 'duZWRwcmVrZXkYDiABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlNpZ25lZFByZUtl' + 'eUgAUgxzaWduZWRwcmVrZXkSSgoLcHJvb2ZPZldvcmsYDyABKAsyJi5zZXJ2ZXJfdG9fY2xpZW' + '50LlJlc3BvbnNlLlByb29mT2ZXb3JrSABSC3Byb29mT2ZXb3JrEkkKIHBhc3N3b3JkbGVzc19y' + 'ZWNvdmVyeV9zZXJ2ZXJfa2V5GBAgASgMSABSHXBhc3N3b3JkbGVzc1JlY292ZXJ5U2VydmVyS2' + 'V5EosBCiJwYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uX21lc3NhZ2VzGBEgASgLMjsuc2VydmVy' + 'X3RvX2NsaWVudC5SZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0gAUi' + 'BwYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlc0IECgJPa0IKCghSZXNwb25zZQ=='); diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart index 2d27f285..c9512262 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pb.dart @@ -24,11 +24,11 @@ export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions; /// -> The user who wants to recover his account does not need to remember her old username class RecoveryRequest extends $pb.GeneratedMessage { factory RecoveryRequest({ - $fixnum.Int64? tempId, + $core.String? notificationId, $core.List<$core.int>? publicKey, }) { final result = create(); - if (tempId != null) result.tempId = tempId; + if (notificationId != null) result.notificationId = notificationId; if (publicKey != null) result.publicKey = publicKey; return result; } @@ -47,7 +47,7 @@ class RecoveryRequest extends $pb.GeneratedMessage { package: const $pb.PackageName( _omitMessageNames ? '' : 'passwordless_recovery'), createEmptyInstance: create) - ..aInt64(1, _omitFieldNames ? '' : 'tempId') + ..aOS(1, _omitFieldNames ? '' : 'notificationId') ..a<$core.List<$core.int>>( 2, _omitFieldNames ? '' : 'publicKey', $pb.PbFieldType.OY) ..hasRequiredFields = false; @@ -72,13 +72,13 @@ class RecoveryRequest extends $pb.GeneratedMessage { static RecoveryRequest? _defaultInstance; @$pb.TagNumber(1) - $fixnum.Int64 get tempId => $_getI64(0); + $core.String get notificationId => $_getSZ(0); @$pb.TagNumber(1) - set tempId($fixnum.Int64 value) => $_setInt64(0, value); + set notificationId($core.String value) => $_setString(0, value); @$pb.TagNumber(1) - $core.bool hasTempId() => $_has(0); + $core.bool hasNotificationId() => $_has(0); @$pb.TagNumber(1) - void clearTempId() => $_clearField(1); + void clearNotificationId() => $_clearField(1); @$pb.TagNumber(2) $core.List<$core.int> get publicKey => $_getN(1); diff --git a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart index 85db75a6..6d66b0b6 100644 --- a/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/passwordless_recovery.pbjson.dart @@ -19,15 +19,15 @@ import 'dart:typed_data' as $typed_data; const RecoveryRequest$json = { '1': 'RecoveryRequest', '2': [ - {'1': 'temp_id', '3': 1, '4': 1, '5': 3, '10': 'tempId'}, + {'1': 'notification_id', '3': 1, '4': 1, '5': 9, '10': 'notificationId'}, {'1': 'public_key', '3': 2, '4': 1, '5': 12, '10': 'publicKey'}, ], }; /// Descriptor for `RecoveryRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List recoveryRequestDescriptor = $convert.base64Decode( - 'Cg9SZWNvdmVyeVJlcXVlc3QSFwoHdGVtcF9pZBgBIAEoA1IGdGVtcElkEh0KCnB1YmxpY19rZX' - 'kYAiABKAxSCXB1YmxpY0tleQ=='); + 'Cg9SZWNvdmVyeVJlcXVlc3QSJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb2' + '5JZBIdCgpwdWJsaWNfa2V5GAIgASgMUglwdWJsaWNLZXk='); @$core.Deprecated('Use encryptedEnvelopeDescriptor instead') const EncryptedEnvelope$json = { diff --git a/lib/src/model/protobuf/client/passwordless_recovery.proto b/lib/src/model/protobuf/client/passwordless_recovery.proto index e89e8c12..9080605f 100644 --- a/lib/src/model/protobuf/client/passwordless_recovery.proto +++ b/lib/src/model/protobuf/client/passwordless_recovery.proto @@ -12,7 +12,7 @@ package passwordless_recovery; // -> Using this phishing is harder, as the user has to manualy select the user to recovery // -> The user who wants to recover his account does not need to remember her old username message RecoveryRequest { - int64 temp_id = 1; + string notification_id = 1; bytes public_key = 2; } diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 55b3ef1e..691b9a49 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -13,7 +13,8 @@ import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart'; -import 'package:twonly/src/visual/views/onboarding/recover.view.dart'; +import 'package:twonly/src/visual/views/onboarding/recover_password.view.dart'; +import 'package:twonly/src/visual/views/onboarding/recover_passwordless.view.dart'; import 'package:twonly/src/visual/views/public_profile.view.dart'; import 'package:twonly/src/visual/views/settings/account.view.dart'; import 'package:twonly/src/visual/views/settings/appearance.view.dart'; @@ -58,6 +59,10 @@ final routerProvider = GoRouter( path: Routes.home, builder: (context, state) => const AppMainWidget(initialPage: 1), ), + GoRoute( + path: Routes.recoverPasswordless, + builder: (context, state) => const RecoverPasswordless(), + ), // Chats GoRoute( diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 5e557c8f..30e131ce 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -776,6 +776,41 @@ class ApiService { return sendRequestSync(req); } + Future submitRecoveryShare({ + required String notificationId, + required List encryptedMessage, + }) async { + final req = createClientToServerFromApplicationData( + ApplicationData( + passwordlessNotification: ApplicationData_PasswordlessNotification( + notificationId: notificationId, + encryptedMessage: encryptedMessage, + ), + ), + ); + return sendRequestSync(req); + } + + Future registerPasswordlessNotification({ + required String notificationId, + required List downloadAuthToken, + required String langCode, + required String? googleFcm, + }) async { + final registerNotif = Handshake_RegisterPasswordlessNotification() + ..notificationId = notificationId + ..downloadAuthToken = downloadAuthToken + ..langCode = langCode; + if (googleFcm != null) { + registerNotif.googleFcm = googleFcm; + } + + final handshake = Handshake() + ..registerPasswordlessNotification = registerNotif; + final req = createClientToServerFromHandshake(handshake); + return sendRequestSync(req, authenticated: false); + } + Future addAdditionalUser(Int64 userId) async { final get = ApplicationData_AddAdditionalUser()..userId = userId; final appData = ApplicationData()..addAdditionalUser = get; @@ -888,4 +923,30 @@ class ApiService { final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req, contactId: target); } + + /// Polls the server for new passwordless recovery notification messages. + /// [alreadyReceivedIds] prevents the server from sending duplicates. + Future checkForPasswordlessNotification({ + required String notificationId, + required List downloadAuthToken, + List? alreadyReceivedIds, + }) async { + final check = Handshake_CheckForPasswordlessNotification() + ..notificationId = notificationId + ..downloadAuthToken = downloadAuthToken; + if (alreadyReceivedIds != null && alreadyReceivedIds.isNotEmpty) { + check.alreadyReceivedMessageIds.addAll(alreadyReceivedIds); + } + + final handshake = Handshake()..checkForPasswordlessNotification = check; + final req = createClientToServerFromHandshake(handshake); + final res = await sendRequestSync(req, authenticated: false); + if (res.isSuccess) { + final ok = res.value as server.Response_Ok; + if (ok.hasPasswordlessNotificationMessages()) { + return ok.passwordlessNotificationMessages; + } + } + return null; + } } diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index 39fa5a65..6de3623e 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -13,6 +13,8 @@ import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/services/api/mediafiles/upload.api.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart' + show PasswordlessRecoveryService; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -30,6 +32,11 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { // Check if this is the QR code link which was // therefore scanned with the system camera + if (uri.toString().startsWith(PasswordlessRecoveryService.linkPrefix)) { + await PasswordlessRecoveryService.handleRecoveryLink(uri.toString()); + return true; + } + if (uri.toString().startsWith(QrCodeUtils.linkPrefix)) { final result = await QrCodeUtils.handleQrCodeLink(uri.toString()); diff --git a/lib/src/services/passwordless_recovery.service.dart b/lib/src/services/passwordless_recovery.service.dart index bf86236c..4504effb 100644 --- a/lib/src/services/passwordless_recovery.service.dart +++ b/lib/src/services/passwordless_recovery.service.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'dart:convert' show utf8; +import 'dart:convert' show base64Url, utf8; import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; @@ -11,20 +11,124 @@ import 'package:fixnum/fixnum.dart'; import 'package:twonly/core/bridge/wrapper.dart'; import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart' + show getContactDisplayName; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/model/json/userdata.model.dart' show PasswordLessRecovery; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; import 'package:twonly/src/model/protobuf/client/generated/passwordless_recovery.pb.dart'; +import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/avatars.dart' show getAvatarSvg; +import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart'; -enum SecondFactorType { none, pin, email } +enum SecondFactorType { email, pin, none } class PasswordlessRecoveryService { + static String linkPrefix = 'https://me.twonly.eu/r/#'; + + static final Set _handledNotificationIds = {}; + + static Future handleRecoveryLink(String link) async { + final hashIndex = link.indexOf('#'); + if (hashIndex == -1) return; + + final fragment = link.substring(hashIndex + 1); + final parts = fragment.split('/'); + if (parts.length < 2) return; + + final notificationId = parts[0]; + final base64Key = parts[1]; + + if (_handledNotificationIds.contains(notificationId)) { + Log.info( + 'Notification ID $notificationId was already handled, skipping.', + ); + return; + } + _handledNotificationIds.add(notificationId); + + final encryptionKey = base64Url.decode(base64Url.normalize(base64Key)); + + final context = rootNavigatorKey.currentContext; + if (context != null && context.mounted) { + unawaited( + context.navPush( + HelpAFriendPasswordlessRecoveryView( + notificationId: notificationId, + encryptionKey: encryptionKey, + ), + ), + ); + } + } + + static Future submitRecoveryShare( + String notificationId, + List encryptionKey, + Contact contact, + ) async { + try { + final share = contact.recoveryContactsSecretShare; + if (share == null) { + return false; + } + + final trustedFriend = TrustedFriendShare_User( + userId: Int64(userService.currentUser.userId), + displayName: userService.currentUser.displayName, + avatar: userService.currentUser.avatarSvg != null + ? utf8.encode(userService.currentUser.avatarSvg!) + : null, + ); + + final shareUser = TrustedFriendShare_User( + userId: Int64(contact.userId), + displayName: getContactDisplayName(contact), + avatar: contact.avatarSvgCompressed != null + ? utf8.encode(getAvatarSvg(contact.avatarSvgCompressed!)) + : null, + ); + + final trustedFriendShare = TrustedFriendShare( + trustedFriend: trustedFriend, + shareUser: shareUser, + threshold: 0, + sharedSecretData: share, + ); + + final xchacha20 = Xchacha20.poly1305Aead(); + final secretBox = await xchacha20.encrypt( + trustedFriendShare.writeToBuffer(), + secretKey: SecretKey(encryptionKey), + nonce: xchacha20.newNonce(), + ); + + final envelope = EncryptedEnvelope( + encryptedData: secretBox.cipherText, + iv: secretBox.nonce, + mac: secretBox.mac.bytes, + ); + + final res = await apiService.submitRecoveryShare( + notificationId: notificationId, + encryptedMessage: envelope.writeToBuffer(), + ); + return res.isSuccess; + } catch (e) { + Log.error('Failed to submit recovery share', error: e); + return false; + } + } + static Future enablePasswordlessRecovery({ required List trustedFriendIds, required SecondFactorType secondFactorType, @@ -434,4 +538,85 @@ class PasswordlessRecoveryService { ), ); } + + static Future checkAndStorePasswordlessMessages( + OnboardingState state, + ) async { + if (!state.serverRegistered || + state.notificationId == null || + state.downloadAuthToken == null || + state.encryptionKey == null) { + return false; + } + + final alreadyReceivedIds = state.receivedShares + .map((s) => Int64(s.messageId)) + .toList(); + + final response = await apiService.checkForPasswordlessNotification( + notificationId: state.notificationId!, + downloadAuthToken: state.downloadAuthToken!, + alreadyReceivedIds: alreadyReceivedIds, + ); + + if (response == null || response.messages.isEmpty) { + return false; + } + + final xchacha20 = Xchacha20.poly1305Aead(); + final secretKey = SecretKey(state.encryptionKey!); + var didUpdate = false; + + for (final msg in response.messages) { + final msgId = msg.id.toInt(); + + try { + final envelope = EncryptedEnvelope.fromBuffer(msg.encryptedMessage); + final secretBox = SecretBox( + envelope.encryptedData, + nonce: envelope.iv, + mac: Mac(envelope.mac), + ); + final plaintext = await xchacha20.decrypt( + secretBox, + secretKey: secretKey, + ); + + final share = TrustedFriendShare.fromBuffer(plaintext); + + final receivedShare = ReceivedRecoveryShare( + messageId: msgId, + trustedFriendDisplayName: share.trustedFriend.displayName, + myDisplayName: share.shareUser.displayName, + myUserId: share.shareUser.userId.toInt(), + myAvatarSvg: share.shareUser.hasAvatar() + ? share.shareUser.avatar + : null, + threshold: share.threshold, + sharedSecretDataBytes: share.sharedSecretData, + ); + + state.receivedShares.add(receivedShare); + didUpdate = true; + + Log.info( + 'Received recovery share from ${share.trustedFriend.displayName} ' + 'for user ${share.shareUser.displayName}', + ); + } catch (e) { + Log.error( + 'Failed to decrypt/parse passwordless notification message $msgId: $e', + ); + } + } + + if (didUpdate) { + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (s) => s.receivedShares = state.receivedShares, + ); + } + + return didUpdate; + } } diff --git a/lib/src/utils/keyvalue.dart b/lib/src/utils/keyvalue.dart index f879a208..b1ce6202 100644 --- a/lib/src/utils/keyvalue.dart +++ b/lib/src/utils/keyvalue.dart @@ -1,12 +1,39 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:mutex/mutex.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/model/json/backup.model.dart'; +import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/utils/exclusive_access.utils.dart'; import 'package:twonly/src/utils/log.dart'; +typedef ModelFactory = ({ + Object Function(Map json) fromJson, + Map Function(Object value) toJson, + Object Function() defaultValue, +}); + class KeyValueStore { + static final Map _registry = { + OnboardingState: ( + fromJson: OnboardingState.fromJson, + toJson: (val) => (val as OnboardingState).toJson(), + defaultValue: OnboardingState.new, + ), + CurrentBackupStatus: ( + fromJson: CurrentBackupStatus.fromJson, + toJson: (val) => (val as CurrentBackupStatus).toJson(), + defaultValue: CurrentBackupStatus.new, + ), + BackupRecovery: ( + fromJson: BackupRecovery.fromJson, + toJson: (val) => (val as BackupRecovery).toJson(), + defaultValue: () => BackupRecovery(username: '', password: '', userId: 0), + ), + }; + static final Map _mutexes = {}; static Mutex _getMutex(String key) { @@ -65,4 +92,77 @@ class KeyValueStore { } }); } + + static Future update({ + required String key, + required FutureOr Function(T value) update, + }) async { + final factory = _registry[T]; + if (factory == null) { + throw ArgumentError('Type $T is not registered in KeyValueStore.'); + } + return _exclusive(key, () async { + T val; + final file = await _getFilePath(key); + try { + if (file.existsSync()) { + final contents = await file.readAsString(); + val = factory.fromJson(jsonDecode(contents) as Map) as T; + } else { + val = factory.defaultValue() as T; + } + } catch (e) { + Log.warn('Error reading file. Resetting to default.: $e'); + val = factory.defaultValue() as T; + } + + await update(val); + + try { + await file.parent.create(recursive: true); + await file.writeAsString(jsonEncode(factory.toJson(val as Object))); + } catch (e) { + Log.error('Error writing file: $e'); + } + return val; + }); + } + + static Future getModel(String key) async { + final factory = _registry[T]; + if (factory == null) { + throw ArgumentError('Type $T is not registered in KeyValueStore.'); + } + return _exclusive(key, () async { + final file = await _getFilePath(key); + try { + if (file.existsSync()) { + final contents = await file.readAsString(); + return factory.fromJson(jsonDecode(contents) as Map) as T; + } + } catch (e) { + Log.warn('Error reading file. Returning default.: $e'); + } + return factory.defaultValue() as T; + }); + } + + static Future getModelOrNull(String key) async { + final factory = _registry[T]; + if (factory == null) { + throw ArgumentError('Type $T is not registered in KeyValueStore.'); + } + return _exclusive(key, () async { + final file = await _getFilePath(key); + try { + if (file.existsSync()) { + final contents = await file.readAsString(); + return factory.fromJson(jsonDecode(contents) as Map) as T; + } + } catch (e) { + Log.warn('Error reading file.: $e'); + } + return null; + }); + } } diff --git a/lib/src/visual/components/avatar_icon.comp.dart b/lib/src/visual/components/avatar_icon.comp.dart index eb23a144..a11f675b 100644 --- a/lib/src/visual/components/avatar_icon.comp.dart +++ b/lib/src/visual/components/avatar_icon.comp.dart @@ -16,12 +16,14 @@ class AvatarIcon extends StatefulWidget { this.contactId, this.myAvatar = false, this.fontSize = 20, + this.svg, this.color, }); final Group? group; final int? contactId; final bool myAvatar; final double? fontSize; + final String? svg; final Color? color; @override @@ -145,7 +147,12 @@ class _AvatarIconState extends State { Widget avatars = Container(); - if (widget.myAvatar) { + if (widget.svg != null) { + avatars = SvgPicture.string( + widget.svg!, + errorBuilder: errorBuilder, + ); + } else if (widget.myAvatar) { if (_myAvatarPath != null) { avatars = Image.file( File(_myAvatarPath!), diff --git a/lib/src/visual/elements/my_input.element.dart b/lib/src/visual/elements/my_input.element.dart index df671987..9362cb12 100644 --- a/lib/src/visual/elements/my_input.element.dart +++ b/lib/src/visual/elements/my_input.element.dart @@ -18,6 +18,7 @@ class MyInput extends StatefulWidget { this.errorText, this.obscureText = false, this.dense = false, + this.readOnly = false, super.key, }); @@ -33,6 +34,7 @@ class MyInput extends StatefulWidget { final String? errorText; final bool obscureText; final bool dense; + final bool readOnly; @override State createState() => _MyInputState(); @@ -64,6 +66,7 @@ class _MyInputState extends State { alwaysAnimate: true, child: TextField( controller: widget.controller, + readOnly: widget.readOnly, onChanged: widget.onChanged, onSubmitted: widget.onSubmitted, onTapOutside: (event) { diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 2b33309d..53238250 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -14,6 +14,8 @@ import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart' + show PasswordlessRecoveryService; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/qr.utils.dart'; @@ -504,6 +506,11 @@ class MainCameraController { if (barcode.displayValue == null) continue; final link = barcode.displayValue!; + if (link.startsWith(PasswordlessRecoveryService.linkPrefix)) { + await PasswordlessRecoveryService.handleRecoveryLink(link); + continue; + } + if (link.startsWith(QrCodeUtils.linkPrefix)) { if (_handledProfileLinks.contains(link)) continue; _handledProfileLinks.add(link); diff --git a/lib/src/visual/views/critical_error.view.dart b/lib/src/visual/views/critical_error.view.dart index c39a7108..c847514f 100644 --- a/lib/src/visual/views/critical_error.view.dart +++ b/lib/src/visual/views/critical_error.view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:restart_app/restart_app.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/views/onboarding/recover.view.dart'; +import 'package:twonly/src/visual/views/onboarding/recover_password.view.dart'; import 'package:twonly/src/visual/views/settings/help/contact_us.view.dart'; class CriticalErrorView extends StatelessWidget { diff --git a/lib/src/visual/views/onboarding/components/animated_bell_icon.comp.dart b/lib/src/visual/views/onboarding/components/animated_bell_icon.comp.dart new file mode 100644 index 00000000..be2b2b9a --- /dev/null +++ b/lib/src/visual/views/onboarding/components/animated_bell_icon.comp.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class AnimatedBellIcon extends StatefulWidget { + const AnimatedBellIcon({super.key, this.color}); + final Color? color; + + @override + State createState() => _AnimatedBellIconState(); +} + +class _AnimatedBellIconState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + _animation = Tween( + begin: -0.02, + end: 0.02, + ).animate(_controller); + + _controller.repeat(reverse: true); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RotationTransition( + turns: _animation, + alignment: Alignment.topCenter, + child: Icon( + Icons.notifications_active_rounded, + size: 32, + color: widget.color ?? context.color.primary, + ), + ); + } +} diff --git a/lib/src/visual/views/onboarding/onboarding.view.dart b/lib/src/visual/views/onboarding/onboarding.view.dart index f0df4f08..bfc1106a 100644 --- a/lib/src/visual/views/onboarding/onboarding.view.dart +++ b/lib/src/visual/views/onboarding/onboarding.view.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:lottie/lottie.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; @@ -56,6 +58,7 @@ class OnboardingView extends StatelessWidget { ), PageViewModel( title: context.lang.onboardingNotProductTitle, + useScrollView: false, bodyWidget: Column( children: [ Text( @@ -67,20 +70,41 @@ class OnboardingView extends StatelessWidget { padding: const EdgeInsets.only( left: 50, right: 50, - top: 20, + top: 10, ), child: MyButton( + variant: MyButtonVariant.primaryMiddle, onPressed: callbackOnSuccess, child: Text(context.lang.registerSubmitButton), ), ), + Padding( + padding: const EdgeInsets.only( + left: 50, + right: 50, + top: 10, + ), + child: MyButton( + onPressed: () { + callbackOnSuccess(); + context.push(Routes.settingsBackupRecovery); + }, + variant: MyButtonVariant.secondaryDense, + child: Text( + context.lang.twonlySafeRecoverBtn, + ), + ), + ), ], ), image: Center( child: Padding( - padding: const EdgeInsets.only(top: 60), - child: Lottie.asset( - 'assets/animations/donation.lottie', + padding: const EdgeInsets.only(top: 40), + child: SizedBox( + height: 200, + child: Lottie.asset( + 'assets/animations/donation.lottie', + ), ), ), ), diff --git a/lib/src/visual/views/onboarding/passwordless_recovery.view.dart b/lib/src/visual/views/onboarding/passwordless_recovery.view.dart deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/src/visual/views/onboarding/recover.view.dart b/lib/src/visual/views/onboarding/recover_password.view.dart similarity index 87% rename from lib/src/visual/views/onboarding/recover.view.dart rename to lib/src/visual/views/onboarding/recover_password.view.dart index f659c972..5f17a3ed 100644 --- a/lib/src/visual/views/onboarding/recover.view.dart +++ b/lib/src/visual/views/onboarding/recover_password.view.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:restart_app/restart_app.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/model/json/onboarding_state.model.dart'; import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart'; @@ -64,7 +69,6 @@ class _BackupRecoveryViewState extends State { }); } - @override Widget build(BuildContext context) { final isDark = isDarkMode(context); @@ -175,6 +179,23 @@ class _BackupRecoveryViewState extends State { ) : Text(context.lang.twonlySafeRecoverBtn), ), + const SizedBox(height: 16), + MyButton( + variant: MyButtonVariant.secondary, + onPressed: () async { + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (state) => + state.hasStartedPasswordlessRecovery = true, + ); + if (context.mounted) { + await context.push(Routes.recoverPasswordless); + } + }, + child: Text( + context.lang.passwordlessRecoveryRecoverBtn, + ), + ), const Spacer(), const SizedBox(height: 40), ], diff --git a/lib/src/visual/views/onboarding/recover_passwordless.view.dart b/lib/src/visual/views/onboarding/recover_passwordless.view.dart new file mode 100644 index 00000000..0b536839 --- /dev/null +++ b/lib/src/visual/views/onboarding/recover_passwordless.view.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hashlib/random.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/keyvalue.keys.dart'; +import 'package:twonly/src/model/json/onboarding_state.model.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart'; +import 'package:twonly/src/utils/keyvalue.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/elements/my_input.element.dart'; +import 'package:twonly/src/visual/views/onboarding/components/animated_bell_icon.comp.dart'; + +class RecoverPasswordless extends StatefulWidget { + const RecoverPasswordless({super.key}); + + @override + State createState() => _RecoverPasswordlessState(); +} + +class _RecoverPasswordlessState extends State { + bool _isLoading = true; + bool _notificationsEnabled = false; + String _shareUrl = ''; + + OnboardingState? _onboardingState; + Timer? _pollTimer; + + @override + void initState() { + super.initState(); + _initAsync(); + } + + @override + void dispose() { + _pollTimer?.cancel(); + super.dispose(); + } + + Future _initAsync() async { + try { + // 1. Load OnboardingState + final state = await KeyValueStore.getModel( + KeyValueKeys.onboardingState, + ); + + // 2. Generate fields if they are missing + if (state.notificationId == null) { + state + ..notificationId = uuid.v4() + ..downloadAuthToken = getRandomUint8List(32) + ..encryptionKey = getRandomUint8List(32); + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (s) { + s + ..notificationId = state.notificationId + ..downloadAuthToken = state.downloadAuthToken + ..encryptionKey = state.encryptionKey; + }, + ); + } + + // 3. Construct URL + final keyBytes = state.encryptionKey!; + final base64Key = base64Url.encode(keyBytes).replaceAll('=', ''); + _shareUrl = 'https://me.twonly.eu/r/#${state.notificationId}/$base64Key'; + if (!kReleaseMode) Log.info(_shareUrl); + + // 4. Check FCM Notification Settings + final settings = await FirebaseMessaging.instance + .getNotificationSettings(); + _notificationsEnabled = + settings.authorizationStatus == AuthorizationStatus.authorized || + settings.authorizationStatus == AuthorizationStatus.provisional; + + // 5. Register with the server if not registered yet + if (!state.serverRegistered) { + await _registerPasswordlessNotification(state); + } + + _onboardingState = state; + + // 6. If already registered, do an immediate check and start the poll timer + if (state.serverRegistered) _startPollTimer(); + } catch (e) { + Log.warn('Error during passwordless recovery initialization: $e'); + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + + void _startPollTimer() { + if (_pollTimer != null) return; + _pollTimer = Timer.periodic(const Duration(seconds: 10), (_) { + _pollForMessages(); + }); + unawaited(_pollForMessages()); + } + + Future _pollForMessages() async { + if (_onboardingState == null) return; + final didUpdate = + await PasswordlessRecoveryService.checkAndStorePasswordlessMessages( + _onboardingState!, + ); + // _onboardingState was changed in checkAndStorePasswordlessMessages + if (didUpdate && mounted) setState(() => ()); + } + + Future _registerPasswordlessNotification(OnboardingState state) async { + final fcmToken = await FirebaseMessaging.instance.getToken(); + if (!mounted) return; + final res = await apiService.registerPasswordlessNotification( + notificationId: state.notificationId!, + downloadAuthToken: state.downloadAuthToken!, + langCode: Localizations.localeOf(context).languageCode, + googleFcm: fcmToken, + ); + if (res.isSuccess) { + state.serverRegistered = true; + await KeyValueStore.update( + key: KeyValueKeys.onboardingState, + update: (s) => s.serverRegistered = true, + ); + _startPollTimer(); + } + } + + Future _requestNotificationPermission() async { + try { + final settings = await FirebaseMessaging.instance.requestPermission(); + if (settings.authorizationStatus == AuthorizationStatus.authorized || + settings.authorizationStatus == AuthorizationStatus.provisional) { + if (mounted) setState(() => _notificationsEnabled = true); + final state = _onboardingState; + if (state != null) { + await _registerPasswordlessNotification(state); + } + } + } catch (e) { + Log.error('Error requesting notification permission: $e'); + } + } + + Widget _buildProgressSection( + BuildContext context, + bool isDark, + List shares, + ) { + final first = shares.first; + final threshold = first.threshold; + final thresholdReached = shares.length >= threshold; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Column( + children: [ + AvatarIcon( + svg: utf8.decode(first.myAvatarSvg ?? []), + fontSize: 60, + ), + const SizedBox(height: 12), + Text( + first.myDisplayName, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const SizedBox(height: 24), + + Row( + children: [ + Text( + context.lang.recoverPasswordlessSharesReceived( + shares.length, + threshold, + ), + style: Theme.of(context).textTheme.bodyMedium, + ), + const Spacer(), + if (thresholdReached) + const Icon( + Icons.check_circle_rounded, + color: Colors.green, + size: 20, + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: shares.length / threshold, + minHeight: 8, + backgroundColor: isDark + ? Colors.white.withValues(alpha: 0.1) + : Colors.black.withValues(alpha: 0.08), + valueColor: AlwaysStoppedAnimation( + thresholdReached ? Colors.green : context.color.primary, + ), + ), + ), + const SizedBox(height: 16), + + ...shares.map( + (share) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + share.trustedFriendDisplayName, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + + const SizedBox(height: 24), + + if (thresholdReached) ...[ + MyButton( + onPressed: () { + // TODO(recovery): Navigate to the actual recovery flow with the collected shares. + Log.info('Recover now pressed with ${shares.length} shares'); + }, + child: Text(context.lang.recoverPasswordlessRecoverNowBtn), + ), + const SizedBox(height: 16), + ], + ], + ); + } + + @override + Widget build(BuildContext context) { + final isDark = isDarkMode(context); + final shares = _onboardingState?.receivedShares ?? []; + final hasShares = shares.isNotEmpty; + final threshold = hasShares ? shares.first.threshold : 0; + final thresholdReached = hasShares && shares.length >= threshold; + + if (_isLoading) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.passwordlessRecoveryRecoverBtn), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + color: isDark ? Colors.white70 : Colors.black54, + iconSize: 20, + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: const Center( + child: CircularProgressIndicator(), + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text(context.lang.passwordlessRecoveryRecoverBtn), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_rounded), + color: isDark ? Colors.white70 : Colors.black54, + iconSize: 20, + onPressed: () => Navigator.of(context).pop(), + ), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!hasShares) ...[ + Text( + context.lang.recoverPasswordlessExplanation, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 24), + ], + + if (hasShares) _buildProgressSection(context, isDark, shares), + + if (!_notificationsEnabled && !hasShares) ...[ + Container( + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.black.withValues(alpha: 0.06), + ), + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const AnimatedBellIcon(), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .lang + .recoverPasswordlessNotificationCardTitle, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + context + .lang + .recoverPasswordlessNotificationCardSubtitle, + style: const TextStyle(fontSize: 13), + ), + const SizedBox(height: 12), + MyButton( + variant: MyButtonVariant.secondaryDense, + onPressed: _requestNotificationPermission, + child: Text( + context + .lang + .recoverPasswordlessNotificationCardBtn, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 32), + ], + + if (!thresholdReached) ...[ + if (!hasShares) + Container( + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.black.withValues(alpha: 0.06), + ), + ), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.info_outline_rounded, + color: isDark ? Colors.white70 : Colors.black54, + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + context.lang.recoverPasswordlessQrInstructions, + style: const TextStyle(fontSize: 14, height: 1.4), + ), + ), + ], + ), + ), + + if (!hasShares) const SizedBox(height: 20), + + Center( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues( + alpha: isDark ? 0.3 : 0.08, + ), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: QrImageView.withQr( + qr: QrCode.fromData( + data: _shareUrl, + errorCorrectLevel: QrErrorCorrectLevel.M, + ), + eyeStyle: const QrEyeStyle( + color: Colors.black, + borderRadius: 4, + ), + dataModuleStyle: const QrDataModuleStyle( + color: Colors.black, + borderRadius: 4, + ), + gapless: false, + size: 200, + ), + ), + ), + const SizedBox(height: 32), + + MyInput( + controller: TextEditingController(text: _shareUrl), + readOnly: true, + hintText: '', + prefixIcon: const Icon(Icons.link_rounded), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.copy_rounded), + tooltip: context.lang.recoverPasswordlessCopyBtn, + onPressed: () { + Clipboard.setData( + ClipboardData(text: _shareUrl), + ); + showSnackbar( + context, + context.lang.recoverPasswordlessCopiedSnackbar, + level: SnackbarLevel.success, + ); + }, + ), + IconButton( + icon: const Icon(Icons.share_rounded), + tooltip: context.lang.recoverPasswordlessShareBtn, + onPressed: () { + final params = ShareParams( + text: _shareUrl, + ); + SharePlus.instance.share(params); + }, + ), + ], + ), + ), + const SizedBox(height: 24), + ], + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/visual/views/recovery.view.dart b/lib/src/visual/views/recovery_from_secure_storage.view.dart similarity index 100% rename from lib/src/visual/views/recovery.view.dart rename to lib/src/visual/views/recovery_from_secure_storage.view.dart diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart new file mode 100644 index 00000000..9f4caa80 --- /dev/null +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/help_a_friend.passwordless_recovery.view.dart @@ -0,0 +1,324 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart' + show getContactDisplayName; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/services/passwordless_recovery.service.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; +import 'package:twonly/src/visual/elements/my_button.element.dart'; +import 'package:twonly/src/visual/elements/my_input.element.dart'; + +class HelpAFriendPasswordlessRecoveryView extends StatefulWidget { + const HelpAFriendPasswordlessRecoveryView({ + required this.notificationId, + required this.encryptionKey, + super.key, + }); + + final String notificationId; + final List encryptionKey; + + @override + State createState() => + _HelpAFriendPasswordlessRecoveryViewState(); +} + +class _HelpAFriendPasswordlessRecoveryViewState + extends State { + final TextEditingController _searchController = TextEditingController(); + List _contacts = []; + bool _isLoading = false; + late StreamSubscription> _contactSub; + + @override + void initState() { + super.initState(); + _contactSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen(( + update, + ) { + if (mounted) { + setState(() { + _contacts = update; + }); + } + }); + } + + @override + void dispose() { + _searchController.dispose(); + _contactSub.cancel(); + super.dispose(); + } + + Future _onContactSelected(Contact contact) async { + if (contact.recoveryContactsSecretShare == null) { + showSnackbar( + context, + context.lang.passwordlessRecoveryNoShareStored, + level: SnackbarLevel.warning, + ); + return; + } + + await showDialog( + context: context, + builder: (context) { + return ConfirmRecoveryDialog( + contact: contact, + onConfirm: () => _submitShare(contact), + ); + }, + ); + } + + Future _submitShare(Contact contact) async { + setState(() => _isLoading = true); + final res = await PasswordlessRecoveryService.submitRecoveryShare( + widget.notificationId, + widget.encryptionKey, + contact, + ); + if (!mounted) return; + setState(() => _isLoading = false); + + if (res) { + showSnackbar( + context, + context.lang.passwordlessRecoveryShareSent, + level: SnackbarLevel.success, + ); + Navigator.pop(context); + } else { + showSnackbar( + context, + context.lang.passwordlessRecoveryNetworkError, + ); + } + } + + @override + Widget build(BuildContext context) { + final isDark = isDarkMode(context); + final query = _searchController.text.trim().toLowerCase(); + + final filteredContacts = + _contacts.where((c) { + final name = getContactDisplayName(c).toLowerCase(); + final username = c.username.toLowerCase(); + return name.contains(query) || username.contains(query); + }).toList()..sort((a, b) { + final aHasShare = a.recoveryContactsSecretShare != null; + final bHasShare = b.recoveryContactsSecretShare != null; + if (aHasShare && !bHasShare) return -1; + if (!aHasShare && bHasShare) return 1; + return getContactDisplayName(a).compareTo(getContactDisplayName(b)); + }); + + return Scaffold( + appBar: AppBar(title: Text(context.lang.passwordlessRecoveryHelpAFriend)), + body: SafeArea( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.black.withValues(alpha: 0.04), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.black.withValues(alpha: 0.06), + ), + ), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon( + Icons.info_outline_rounded, + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + context.lang.passwordlessRecoverySelectContactDesc, + style: const TextStyle( + fontSize: 14, + height: 1.4, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + MyInput( + controller: _searchController, + hintText: context.lang.passwordlessRecoverySearchContacts, + prefixIcon: const Icon(Icons.search), + onChanged: (val) => setState(() {}), + ), + const SizedBox(height: 16), + Expanded( + child: filteredContacts.isEmpty + ? Center( + child: Text( + context.lang.passwordlessRecoveryNoContactsFound, + ), + ) + : ListView.builder( + itemCount: filteredContacts.length, + itemBuilder: (context, index) { + final contact = filteredContacts[index]; + final hasShare = + contact.recoveryContactsSecretShare != null; + return Opacity( + opacity: hasShare ? 1.0 : 0.5, + child: ListTile( + leading: AvatarIcon( + contactId: contact.userId, + ), + title: Row( + children: [ + Flexible( + child: Text( + getContactDisplayName(contact), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + VerificationBadgeComp( + contact: contact, + size: 14, + clickable: false, + ), + ], + ), + subtitle: hasShare + ? null + : Text( + context.lang.passwordlessRecoveryCantHelpHim, + style: const TextStyle( + color: Colors.grey, + ), + ), + onTap: () => _onContactSelected(contact), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} + +class ConfirmRecoveryDialog extends StatefulWidget { + const ConfirmRecoveryDialog({ + required this.contact, + required this.onConfirm, + super.key, + }); + + final Contact contact; + final VoidCallback onConfirm; + + @override + State createState() => _ConfirmRecoveryDialogState(); +} + +class _ConfirmRecoveryDialogState extends State { + int _secondsRemaining = 10; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (mounted) { + setState(() { + if (_secondsRemaining > 0) { + _secondsRemaining--; + } else { + _timer?.cancel(); + } + }); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final username = widget.contact.username; + final isEnabled = _secondsRemaining == 0; + + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + title: Text( + context.lang.passwordlessRecoveryDoesAskedYou(username), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.lang.passwordlessRecoveryVerifySourceDesc, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.error, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + actions: [ + MyButton( + variant: MyButtonVariant.text, + onPressed: () => Navigator.pop(context), + child: Text(context.lang.passwordlessRecoveryNo), + ), + MyButton( + variant: MyButtonVariant.primaryMiddle, + onPressed: isEnabled + ? () { + Navigator.pop(context); + widget.onConfirm(); + } + : null, + child: Text( + isEnabled + ? context.lang.passwordlessRecoveryYes + : context.lang.passwordlessRecoveryYesWithTimer( + _secondsRemaining.toString(), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart index 40142509..669694ee 100644 --- a/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart +++ b/lib/src/visual/views/settings/backup/passwordless_recovery/setup.passwordless_recovery.view.dart @@ -26,7 +26,7 @@ class PasswordLessRecoverySetup extends StatefulWidget { class _PasswordLessRecoverySetupState extends State { List _selectedContacts = []; - SecondFactorType _secondFactor = SecondFactorType.pin; + SecondFactorType _secondFactor = SecondFactorType.email; final _pinController = TextEditingController(); final _emailController = TextEditingController(); bool _isLoading = false;