diff --git a/lib/src/database/daos/key_verification.dao.dart b/lib/src/database/daos/key_verification.dao.dart index cdcadea3..60d6ffae 100644 --- a/lib/src/database/daos/key_verification.dao.dart +++ b/lib/src/database/daos/key_verification.dao.dart @@ -27,7 +27,8 @@ class KeyVerificationDao extends DatabaseAccessor KeyVerificationDao(super.db); Future> getRecentVerificationTokens() { - final cutoff = DateTime.now().subtract(const Duration(hours: 24)); + // Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later. + final cutoff = DateTime.now().subtract(const Duration(hours: 1)); return (select( verificationTokens, )..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get(); @@ -223,4 +224,31 @@ class KeyVerificationDao extends DatabaseAccessor Log.error(e); } } + + Future deleteKeyVerification(int contactId) async { + try { + await (delete(keyVerifications)..where((kv) => kv.contactId.equals(contactId))).go(); + if (userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + ); + } + } catch (e) { + Log.error(e); + } + } + + Future deleteKeyVerificationById(int verificationId, int contactId) async { + try { + await (delete(keyVerifications)..where((kv) => kv.verificationId.equals(verificationId))).go(); + final remaining = await getContactVerification(contactId); + if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) { + await FlutterUserDiscovery.updateVerificationStateForUser( + contactId: contactId, + ); + } + } catch (e) { + Log.error(e); + } + } } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index bd6e5335..33019c8d 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -911,8 +911,8 @@ abstract class AppLocalizations { /// No description provided for @verificationTypeSecretQrToken. /// /// In en, this message translates to: - /// **'The other person scanned your QR code.'** - String get verificationTypeSecretQrToken; + /// **'{username} has scanned your QR code.'** + String verificationTypeSecretQrToken(Object username); /// No description provided for @verificationTypeLink. /// @@ -2699,7 +2699,7 @@ abstract class AppLocalizations { /// No description provided for @verificationBadgeGeneralDesc. /// /// In en, this message translates to: - /// **'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'** + /// **'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'** String get verificationBadgeGeneralDesc; /// No description provided for @verificationBadgeGreenDesc. @@ -2720,6 +2720,24 @@ abstract class AppLocalizations { /// **'A contact whose identity has *not* yet been verified.'** String get verificationBadgeRedDesc; + /// No description provided for @deleteVerificationTitle. + /// + /// In en, this message translates to: + /// **'Delete verification?'** + String get deleteVerificationTitle; + + /// No description provided for @deleteVerificationBody. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete this verification?'** + String get deleteVerificationBody; + + /// No description provided for @secretQrTokenVerifiedSnackbar. + /// + /// In en, this message translates to: + /// **'{username} has scanned your QR code and is now verified.'** + String secretQrTokenVerifiedSnackbar(Object username); + /// No description provided for @chatEntryFlameRestored. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 7318ffba..084d44ff 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -447,8 +447,9 @@ class AppLocalizationsDe extends AppLocalizations { String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.'; @override - String get verificationTypeSecretQrToken => - 'Die andere Person hat deinen QR-Code gescannt.'; + String verificationTypeSecretQrToken(Object username) { + return '$username hat deinen QR-Code gescannt.'; + } @override String get verificationTypeLink => 'Per Link verifiziert.'; @@ -1501,7 +1502,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Scanne einen Kontakt, um diesen zu verifizieren.'; + 'Der Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst. Du kannst Kontakte jederzeit verifizieren, indem du deren QR-Code scannst.'; @override String get verificationBadgeGreenDesc => @@ -1515,6 +1516,18 @@ class AppLocalizationsDe extends AppLocalizations { String get verificationBadgeRedDesc => 'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.'; + @override + String get deleteVerificationTitle => 'Verifizierung löschen?'; + + @override + String get deleteVerificationBody => + 'Möchtest du diese Verifizierung wirklich löschen?'; + + @override + String secretQrTokenVerifiedSnackbar(Object username) { + return '$username hat deinen QR-Code gescannt und ist nun verifiziert.'; + } + @override String chatEntryFlameRestored(Object count) { return '$count Flammen wiederhergestellt'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 95a0b491..a5cc0f47 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -442,8 +442,9 @@ class AppLocalizationsEn extends AppLocalizations { String get verificationTypeQrScanned => 'You scanned their QR code.'; @override - String get verificationTypeSecretQrToken => - 'The other person scanned your QR code.'; + String verificationTypeSecretQrToken(Object username) { + return '$username has scanned your QR code.'; + } @override String get verificationTypeLink => 'Verified via link.'; @@ -1486,7 +1487,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get verificationBadgeGeneralDesc => - 'The checkmark gives you the certainty that you are messaging the right person. Scan the contact\'s QR code to verify it.'; + 'The checkmark gives you the certainty that you are messaging the right person. You can verify contacts at any time by scanning their QR code.'; @override String get verificationBadgeGreenDesc => @@ -1500,6 +1501,18 @@ class AppLocalizationsEn extends AppLocalizations { String get verificationBadgeRedDesc => 'A contact whose identity has *not* yet been verified.'; + @override + String get deleteVerificationTitle => 'Delete verification?'; + + @override + String get deleteVerificationBody => + 'Are you sure you want to delete this verification?'; + + @override + String secretQrTokenVerifiedSnackbar(Object username) { + return '$username has scanned your QR code and is now verified.'; + } + @override String chatEntryFlameRestored(Object count) { return '$count flames restored'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index a8c5a355..0675e745 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93 +Subproject commit 0675e74501d6610a84273232517652db25965e3f diff --git a/lib/src/model/protobuf/client/generated/qr.pb.dart b/lib/src/model/protobuf/client/generated/qr.pb.dart index 60000fcc..b1c05e2e 100644 --- a/lib/src/model/protobuf/client/generated/qr.pb.dart +++ b/lib/src/model/protobuf/client/generated/qr.pb.dart @@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage { $core.List<$core.int>? signedPrekeySignature, $fixnum.Int64? signedPrekeyId, $core.List<$core.int>? secretVerificationToken, + $fixnum.Int64? timestamp, }) { final result = create(); if (userId != null) result.userId = userId; @@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage { if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId; if (secretVerificationToken != null) result.secretVerificationToken = secretVerificationToken; + if (timestamp != null) result.timestamp = timestamp; return result; } @@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage { ..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId') ..a<$core.List<$core.int>>( 8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY) + ..aInt64(9, _omitFieldNames ? '' : 'timestamp') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -230,6 +233,15 @@ class PublicProfile extends $pb.GeneratedMessage { $core.bool hasSecretVerificationToken() => $_has(7); @$pb.TagNumber(8) void clearSecretVerificationToken() => $_clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get timestamp => $_getI64(8); + @$pb.TagNumber(9) + set timestamp($fixnum.Int64 value) => $_setInt64(8, value); + @$pb.TagNumber(9) + $core.bool hasTimestamp() => $_has(8); + @$pb.TagNumber(9) + void clearTimestamp() => $_clearField(9); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/qr.pbjson.dart b/lib/src/model/protobuf/client/generated/qr.pbjson.dart index 16c3c2cd..7d971b0e 100644 --- a/lib/src/model/protobuf/client/generated/qr.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/qr.pbjson.dart @@ -77,9 +77,19 @@ const PublicProfile$json = { '10': 'secretVerificationToken', '17': true }, + { + '1': 'timestamp', + '3': 9, + '4': 1, + '5': 3, + '9': 1, + '10': 'timestamp', + '17': true + }, ], '8': [ {'1': '_secret_verification_token'}, + {'1': '_timestamp'}, ], }; @@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode( 'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY' 'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg' '5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl' - 'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu'); + 'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX' + 'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA=='); diff --git a/lib/src/model/protobuf/client/qr.proto b/lib/src/model/protobuf/client/qr.proto index 7193a6ac..fa069485 100644 --- a/lib/src/model/protobuf/client/qr.proto +++ b/lib/src/model/protobuf/client/qr.proto @@ -17,4 +17,5 @@ message PublicProfile { bytes signed_prekey_signature = 6; int64 signed_prekey_id = 7; optional bytes secret_verification_token = 8; + optional int64 timestamp = 9; } diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 129ba449..47408885 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/app.dart'; import 'package:twonly/src/constants/routes.keys.dart'; @@ -47,7 +48,10 @@ import 'package:twonly/src/visual/views/settings/subscription/subscription.view. import 'package:twonly/src/visual/views/user_study/user_study_questionnaire.view.dart'; import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart'; +final GlobalKey rootNavigatorKey = GlobalKey(); + final routerProvider = GoRouter( + navigatorKey: rootNavigatorKey, routes: [ GoRoute( path: Routes.home, diff --git a/lib/src/services/key_verification.service.dart b/lib/src/services/key_verification.service.dart index 410e1b58..f3299ef2 100644 --- a/lib/src/services/key_verification.service.dart +++ b/lib/src/services/key_verification.service.dart @@ -3,14 +3,17 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:twonly/locator.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' as pb; +import 'package:twonly/src/providers/routing.provider.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/snackbar.dart'; class KeyVerificationService { static Future> getNewSecretVerificationToken() async { @@ -70,6 +73,18 @@ class KeyVerificationService { VerificationType.secretQrToken, ); Log.info('Contact was verified via secretQrToken'); + + final contact = await twonlyDB.contactsDao.getContactById(fromUserId); + final context = rootNavigatorKey.currentContext; + if (context != null && context.mounted && contact != null) { + showSnackbar( + context, + context.lang.secretQrTokenVerifiedSnackbar( + getContactDisplayName(contact), + ), + level: SnackbarLevel.success, + ); + } return; } } diff --git a/lib/src/utils/qr.utils.dart b/lib/src/utils/qr.utils.dart index c4f6f389..0f847841 100644 --- a/lib/src/utils/qr.utils.dart +++ b/lib/src/utils/qr.utils.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:clock/clock.dart'; import 'package:collection/collection.dart' show ListExtensions; import 'package:drift/drift.dart' show Value; import 'package:fixnum/fixnum.dart'; @@ -41,6 +42,7 @@ class QrCodeUtils { signedPrekeySignature: signedPreKey.signature, signedPrekeyId: Int64(signedPreKey.id), secretVerificationToken: secretVerificationToken, + timestamp: Int64(clock.now().millisecondsSinceEpoch), ); final data = publicProfile.writeToBuffer(); @@ -94,7 +96,18 @@ class QrCodeUtils { ); if (verificationOk) { - if (profile.hasSecretVerificationToken()) { + var useSecretVerificationToken = profile.hasSecretVerificationToken(); + if (profile.hasTimestamp()) { + // Only notify the scanned user if the QR code was generated within the last 10 minutes. + final timestamp = DateTime.fromMillisecondsSinceEpoch( + profile.timestamp.toInt(), + ); + final tenMinutesAgo = clock.now().subtract(const Duration(minutes: 10)); + if (timestamp.isBefore(tenMinutesAgo)) { + useSecretVerificationToken = false; + } + } + if (useSecretVerificationToken) { unawaited( KeyVerificationService.handleScannedVerificationToken( contact.userId, diff --git a/lib/src/visual/components/snackbar.dart b/lib/src/visual/components/snackbar.dart index e182fa9a..37f015b3 100644 --- a/lib/src/visual/components/snackbar.dart +++ b/lib/src/visual/components/snackbar.dart @@ -62,7 +62,12 @@ void _showOverlay({ required Duration displayDuration, required void Function(AnimationController) onAnimationControllerInit, }) { - final overlayState = Overlay.maybeOf(context); + var overlayState = Overlay.maybeOf(context); + if (overlayState == null) { + if (context is StatefulElement && context.state is NavigatorState) { + overlayState = (context.state as NavigatorState).overlay; + } + } if (overlayState == null) return; late OverlayEntry overlayEntry; diff --git a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart index 30de85cf..9476b222 100644 --- a/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart +++ b/lib/src/visual/views/camera/camera_preview_components/camera_preview_controller_components/camera_scanned_overlay.dart @@ -113,7 +113,7 @@ class CameraScannedOverlay extends StatelessWidget { ), const SizedBox(width: 10), Text( - getContactDisplayName(c.contact, maxLength: 13), + getContactDisplayName(c.contact, maxLength: 9), ), Expanded(child: Container()), ColoredBox( diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 4d6b5ad1..b7d76ba0 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -4,11 +4,9 @@ import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; -import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/user_discovery.service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -20,6 +18,7 @@ import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; +import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; @@ -35,13 +34,9 @@ class ContactView extends StatefulWidget { class _ContactViewState extends State { Contact? _contact; List _memberOfGroups = []; - List _keyVerifications = []; - List<(Contact, DateTime)> _transferredTrust = []; late StreamSubscription _streamContact; late StreamSubscription> _streamMemberOfGroups; - late StreamSubscription> _streamKeyVerifications; - late StreamSubscription> _streamTransferredTrust; @override void initState() { @@ -63,30 +58,12 @@ class _ContactViewState extends State { _memberOfGroups = groups; }); }); - _streamKeyVerifications = twonlyDB.keyVerificationDao - .watchContactVerification(widget.userId) - .listen((update) { - if (!mounted) return; - setState(() { - _keyVerifications = update; - }); - }); - _streamTransferredTrust = twonlyDB.keyVerificationDao - .watchTransferredTrustVerifications(widget.userId) - .listen((update) { - if (!mounted) return; - setState(() { - _transferredTrust = update; - }); - }); } @override void dispose() { _streamContact.cancel(); _streamMemberOfGroups.cancel(); - _streamKeyVerifications.cancel(); - _streamTransferredTrust.cancel(); super.dispose(); } @@ -260,75 +237,9 @@ class _ContactViewState extends State { RestoreFlameComp( contactId: widget.userId, ), - if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) - BetterListTile( - leading: VerificationBadgeComp( - contact: contact, - size: 20, - ), - text: context.lang.contactVerifyNumberTitle, - onTap: () async { - await context.push(Routes.settingsHelpFaqVerifyBadge); - setState(() {}); - }, - ), - if (_keyVerifications.isNotEmpty || _transferredTrust.isNotEmpty) - ExpansionTile( - shape: const RoundedRectangleBorder(), - backgroundColor: context.color.surfaceContainer, - collapsedShape: const RoundedRectangleBorder(), - leading: Padding( - padding: const EdgeInsetsGeometry.only(left: 12, right: 12), - child: VerificationBadgeComp( - contact: contact, - size: 20, - ), - ), - title: Text(context.lang.userVerifiedTitle), - children: [ - ..._keyVerifications.map( - (kv) => ListTile( - dense: true, - title: Text(_verificationTypeLabel(context, kv.type)), - trailing: Text( - DateFormat.yMd( - Localizations.localeOf(context).toString(), - ).format(kv.createdAt), - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 13, - ), - ), - ), - ), - ..._transferredTrust.map( - (tt) => ListTile( - dense: true, - title: Row( - children: [ - Text( - context.lang.contactVerifiedBy( - getContactDisplayName(tt.$1), - ), - ), - VerificationBadgeComp( - contact: tt.$1, - ), - ], - ), - trailing: Text( - DateFormat.yMd( - Localizations.localeOf(context).toString(), - ).format(tt.$2), - style: TextStyle( - color: context.color.onSurfaceVariant, - fontSize: 13, - ), - ), - ), - ), - ], - ), + VerificationExpansionTileComp( + contact: contact, + ), if (userService.currentUser.isUserDiscoveryEnabled) if (userService.currentUser.userDiscoveryRequiresManualApproval && contact.userDiscoveryManualApproved != true) @@ -408,19 +319,6 @@ class _ContactViewState extends State { } } -String _verificationTypeLabel(BuildContext context, VerificationType type) { - return switch (type) { - VerificationType.qrScanned => context.lang.verificationTypeQrScanned, - VerificationType.secretQrToken => - context.lang.verificationTypeSecretQrToken, - VerificationType.link => context.lang.verificationTypeLink, - VerificationType.contactSharedByVerified => - context.lang.verificationTypeContactSharedByVerified, - VerificationType.migratedFromOldVersion => - context.lang.verificationTypeMigratedFromOldVersion, - }; -} - Future showNicknameChangeDialog( BuildContext context, Contact contact, diff --git a/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart new file mode 100644 index 00000000..cf63479b --- /dev/null +++ b/lib/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/database/daos/contacts.dao.dart'; +import 'package:twonly/src/database/tables/contacts.table.dart'; +import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; +import 'package:twonly/src/visual/components/verification_badge.comp.dart'; +import 'package:twonly/src/visual/elements/better_list_title.element.dart'; + +class VerificationExpansionTileComp extends StatefulWidget { + const VerificationExpansionTileComp({ + required this.contact, + super.key, + }); + + final Contact contact; + + @override + State createState() => + _VerificationExpansionTileCompState(); +} + +class _VerificationExpansionTileCompState + extends State { + List _keyVerifications = []; + List<(Contact, DateTime)> _transferredTrust = []; + + late StreamSubscription> _streamKeyVerifications; + late StreamSubscription> _streamTransferredTrust; + + @override + void initState() { + super.initState(); + _streamKeyVerifications = twonlyDB.keyVerificationDao + .watchContactVerification(widget.contact.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _keyVerifications = update; + }); + }); + _streamTransferredTrust = twonlyDB.keyVerificationDao + .watchTransferredTrustVerifications(widget.contact.userId) + .listen((update) { + if (!mounted) return; + setState(() { + _transferredTrust = update; + }); + }); + } + + @override + void dispose() { + _streamKeyVerifications.cancel(); + _streamTransferredTrust.cancel(); + super.dispose(); + } + + String _verificationTypeLabel(BuildContext context, VerificationType type) { + return switch (type) { + VerificationType.qrScanned => context.lang.verificationTypeQrScanned, + VerificationType.secretQrToken => + context.lang.verificationTypeSecretQrToken( + getContactDisplayName(widget.contact), + ), + VerificationType.link => context.lang.verificationTypeLink, + VerificationType.contactSharedByVerified => + context.lang.verificationTypeContactSharedByVerified, + VerificationType.migratedFromOldVersion => + context.lang.verificationTypeMigratedFromOldVersion, + }; + } + + @override + Widget build(BuildContext context) { + if (_keyVerifications.isEmpty && _transferredTrust.isEmpty) { + return BetterListTile( + leading: VerificationBadgeComp( + contact: widget.contact, + size: 20, + ), + text: context.lang.contactVerifyNumberTitle, + onTap: () async { + await context.push(Routes.settingsHelpFaqVerifyBadge); + if (mounted) setState(() {}); + }, + ); + } + + return ExpansionTile( + shape: const RoundedRectangleBorder(), + backgroundColor: context.color.surfaceContainer, + collapsedShape: const RoundedRectangleBorder(), + leading: Padding( + padding: const EdgeInsetsGeometry.only(left: 12, right: 12), + child: VerificationBadgeComp( + contact: widget.contact, + size: 20, + ), + ), + title: Text(context.lang.userVerifiedTitle), + children: [ + ..._keyVerifications.map( + (kv) => ListTile( + dense: true, + contentPadding: const EdgeInsets.only(left: 16), + title: Text(_verificationTypeLabel(context, kv.type)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(kv.createdAt), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + iconSize: 8, + icon: Icon( + FontAwesomeIcons.trash, + size: 8, + color: context.color.onSurfaceVariant, + ), + onPressed: () async { + final confirm = await showAlertDialog( + context, + context.lang.deleteVerificationTitle, + context.lang.deleteVerificationBody, + ); + if (confirm) { + await twonlyDB.keyVerificationDao + .deleteKeyVerificationById( + kv.verificationId, + widget.contact.userId, + ); + } + }, + ), + ], + ), + ), + ), + ..._transferredTrust.map( + (tt) => ListTile( + dense: true, + title: Row( + children: [ + Text( + context.lang.contactVerifiedBy( + getContactDisplayName(tt.$1), + ), + ), + VerificationBadgeComp( + contact: tt.$1, + ), + ], + ), + trailing: Text( + DateFormat.yMd( + Localizations.localeOf(context).toString(), + ).format(tt.$2), + style: TextStyle( + color: context.color.onSurfaceVariant, + fontSize: 13, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/visual/views/onboarding/setup.view.dart b/lib/src/visual/views/onboarding/setup.view.dart index 847cdc50..ff1ffa4e 100644 --- a/lib/src/visual/views/onboarding/setup.view.dart +++ b/lib/src/visual/views/onboarding/setup.view.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart'; import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart'; @@ -15,7 +14,6 @@ import 'package:twonly/src/visual/views/settings/privacy/user_discovery/componen enum SetupPages { profile, backup, - addNewContact, verificationBadge, shareYourFriends, letYourFriendsFindYou, @@ -185,8 +183,6 @@ class _SetupViewState extends State { return const ProfileSetupPage(); case SetupPages.backup: return const BackupSetupPage(); - case SetupPages.addNewContact: - return const AddNewContactsPage(); case SetupPages.verificationBadge: return const VerificationBadgeSetupPage(); case SetupPages.shareYourFriends: