mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 05:22:13 +00:00
improve qr code verifications
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
65d188c4f2
commit
304190387d
16 changed files with 336 additions and 125 deletions
|
|
@ -27,7 +27,8 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
KeyVerificationDao(super.db);
|
KeyVerificationDao(super.db);
|
||||||
|
|
||||||
Future<List<VerificationToken>> getRecentVerificationTokens() {
|
Future<List<VerificationToken>> 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(
|
return (select(
|
||||||
verificationTokens,
|
verificationTokens,
|
||||||
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
)..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
|
||||||
|
|
@ -223,4 +224,31 @@ class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -911,8 +911,8 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationTypeSecretQrToken.
|
/// No description provided for @verificationTypeSecretQrToken.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'The other person scanned your QR code.'**
|
/// **'{username} has scanned your QR code.'**
|
||||||
String get verificationTypeSecretQrToken;
|
String verificationTypeSecretQrToken(Object username);
|
||||||
|
|
||||||
/// No description provided for @verificationTypeLink.
|
/// No description provided for @verificationTypeLink.
|
||||||
///
|
///
|
||||||
|
|
@ -2699,7 +2699,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @verificationBadgeGeneralDesc.
|
/// No description provided for @verificationBadgeGeneralDesc.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// 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;
|
String get verificationBadgeGeneralDesc;
|
||||||
|
|
||||||
/// No description provided for @verificationBadgeGreenDesc.
|
/// No description provided for @verificationBadgeGreenDesc.
|
||||||
|
|
@ -2720,6 +2720,24 @@ abstract class AppLocalizations {
|
||||||
/// **'A contact whose identity has *not* yet been verified.'**
|
/// **'A contact whose identity has *not* yet been verified.'**
|
||||||
String get verificationBadgeRedDesc;
|
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.
|
/// No description provided for @chatEntryFlameRestored.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -447,8 +447,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
String get verificationTypeQrScanned => 'Du hast den QR-Code gescannt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeSecretQrToken =>
|
String verificationTypeSecretQrToken(Object username) {
|
||||||
'Die andere Person hat deinen QR-Code gescannt.';
|
return '$username hat deinen QR-Code gescannt.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||||
|
|
@ -1501,7 +1502,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGeneralDesc =>
|
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
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
|
|
@ -1515,6 +1516,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.';
|
'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
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count Flammen wiederhergestellt';
|
return '$count Flammen wiederhergestellt';
|
||||||
|
|
|
||||||
|
|
@ -442,8 +442,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
String get verificationTypeQrScanned => 'You scanned their QR code.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeSecretQrToken =>
|
String verificationTypeSecretQrToken(Object username) {
|
||||||
'The other person scanned your QR code.';
|
return '$username has scanned your QR code.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Verified via link.';
|
String get verificationTypeLink => 'Verified via link.';
|
||||||
|
|
@ -1486,7 +1487,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationBadgeGeneralDesc =>
|
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
|
@override
|
||||||
String get verificationBadgeGreenDesc =>
|
String get verificationBadgeGreenDesc =>
|
||||||
|
|
@ -1500,6 +1501,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get verificationBadgeRedDesc =>
|
String get verificationBadgeRedDesc =>
|
||||||
'A contact whose identity has *not* yet been verified.';
|
'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
|
@override
|
||||||
String chatEntryFlameRestored(Object count) {
|
String chatEntryFlameRestored(Object count) {
|
||||||
return '$count flames restored';
|
return '$count flames restored';
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit a8c5a355abf95578f1bdbf6a71077c5078b9dd93
|
Subproject commit 0675e74501d6610a84273232517652db25965e3f
|
||||||
|
|
@ -97,6 +97,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
$core.List<$core.int>? signedPrekeySignature,
|
$core.List<$core.int>? signedPrekeySignature,
|
||||||
$fixnum.Int64? signedPrekeyId,
|
$fixnum.Int64? signedPrekeyId,
|
||||||
$core.List<$core.int>? secretVerificationToken,
|
$core.List<$core.int>? secretVerificationToken,
|
||||||
|
$fixnum.Int64? timestamp,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (userId != null) result.userId = userId;
|
if (userId != null) result.userId = userId;
|
||||||
|
|
@ -109,6 +110,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
if (signedPrekeyId != null) result.signedPrekeyId = signedPrekeyId;
|
||||||
if (secretVerificationToken != null)
|
if (secretVerificationToken != null)
|
||||||
result.secretVerificationToken = secretVerificationToken;
|
result.secretVerificationToken = secretVerificationToken;
|
||||||
|
if (timestamp != null) result.timestamp = timestamp;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +138,7 @@ class PublicProfile extends $pb.GeneratedMessage {
|
||||||
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
..aInt64(7, _omitFieldNames ? '' : 'signedPrekeyId')
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
8, _omitFieldNames ? '' : 'secretVerificationToken', $pb.PbFieldType.OY)
|
||||||
|
..aInt64(9, _omitFieldNames ? '' : 'timestamp')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$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);
|
$core.bool hasSecretVerificationToken() => $_has(7);
|
||||||
@$pb.TagNumber(8)
|
@$pb.TagNumber(8)
|
||||||
void clearSecretVerificationToken() => $_clearField(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 =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -77,9 +77,19 @@ const PublicProfile$json = {
|
||||||
'10': 'secretVerificationToken',
|
'10': 'secretVerificationToken',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'timestamp',
|
||||||
|
'3': 9,
|
||||||
|
'4': 1,
|
||||||
|
'5': 3,
|
||||||
|
'9': 1,
|
||||||
|
'10': 'timestamp',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_secret_verification_token'},
|
{'1': '_secret_verification_token'},
|
||||||
|
{'1': '_timestamp'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -91,4 +101,5 @@ final $typed_data.Uint8List publicProfileDescriptor = $convert.base64Decode(
|
||||||
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
'lvbl9pZBgFIAEoA1IOcmVnaXN0cmF0aW9uSWQSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cmUY'
|
||||||
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
'BiABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRIoChBzaWduZWRfcHJla2V5X2lkGAcgASgDUg'
|
||||||
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
'5zaWduZWRQcmVrZXlJZBI/ChlzZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2VuGAggASgMSABSF3Nl'
|
||||||
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBQhwKGl9zZWNyZXRfdmVyaWZpY2F0aW9uX3Rva2Vu');
|
'Y3JldFZlcmlmaWNhdGlvblRva2VuiAEBEiEKCXRpbWVzdGFtcBgJIAEoA0gBUgl0aW1lc3RhbX'
|
||||||
|
'CIAQFCHAoaX3NlY3JldF92ZXJpZmljYXRpb25fdG9rZW5CDAoKX3RpbWVzdGFtcA==');
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,5 @@ message PublicProfile {
|
||||||
bytes signed_prekey_signature = 6;
|
bytes signed_prekey_signature = 6;
|
||||||
int64 signed_prekey_id = 7;
|
int64 signed_prekey_id = 7;
|
||||||
optional bytes secret_verification_token = 8;
|
optional bytes secret_verification_token = 8;
|
||||||
|
optional int64 timestamp = 9;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/app.dart';
|
import 'package:twonly/app.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.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_questionnaire.view.dart';
|
||||||
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
import 'package:twonly/src/visual/views/user_study/user_study_welcome.view.dart';
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
final routerProvider = GoRouter(
|
final routerProvider = GoRouter(
|
||||||
|
navigatorKey: rootNavigatorKey,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.home,
|
path: Routes.home,
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,17 @@ import 'dart:typed_data';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:twonly/locator.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/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'
|
||||||
as pb;
|
as pb;
|
||||||
|
import 'package:twonly/src/providers/routing.provider.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.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/identity.signal.dart';
|
||||||
import 'package:twonly/src/services/signal/session.signal.dart';
|
import 'package:twonly/src/services/signal/session.signal.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
|
||||||
class KeyVerificationService {
|
class KeyVerificationService {
|
||||||
static Future<List<int>> getNewSecretVerificationToken() async {
|
static Future<List<int>> getNewSecretVerificationToken() async {
|
||||||
|
|
@ -70,6 +73,18 @@ class KeyVerificationService {
|
||||||
VerificationType.secretQrToken,
|
VerificationType.secretQrToken,
|
||||||
);
|
);
|
||||||
Log.info('Contact was verified via 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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:collection/collection.dart' show ListExtensions;
|
import 'package:collection/collection.dart' show ListExtensions;
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
|
@ -41,6 +42,7 @@ class QrCodeUtils {
|
||||||
signedPrekeySignature: signedPreKey.signature,
|
signedPrekeySignature: signedPreKey.signature,
|
||||||
signedPrekeyId: Int64(signedPreKey.id),
|
signedPrekeyId: Int64(signedPreKey.id),
|
||||||
secretVerificationToken: secretVerificationToken,
|
secretVerificationToken: secretVerificationToken,
|
||||||
|
timestamp: Int64(clock.now().millisecondsSinceEpoch),
|
||||||
);
|
);
|
||||||
|
|
||||||
final data = publicProfile.writeToBuffer();
|
final data = publicProfile.writeToBuffer();
|
||||||
|
|
@ -94,7 +96,18 @@ class QrCodeUtils {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (verificationOk) {
|
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(
|
unawaited(
|
||||||
KeyVerificationService.handleScannedVerificationToken(
|
KeyVerificationService.handleScannedVerificationToken(
|
||||||
contact.userId,
|
contact.userId,
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,12 @@ void _showOverlay({
|
||||||
required Duration displayDuration,
|
required Duration displayDuration,
|
||||||
required void Function(AnimationController) onAnimationControllerInit,
|
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;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
late OverlayEntry overlayEntry;
|
late OverlayEntry overlayEntry;
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ class CameraScannedOverlay extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
getContactDisplayName(c.contact, maxLength: 13),
|
getContactDisplayName(c.contact, maxLength: 9),
|
||||||
),
|
),
|
||||||
Expanded(child: Container()),
|
Expanded(child: Container()),
|
||||||
ColoredBox(
|
ColoredBox(
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,9 @@ import 'package:drift/drift.dart' hide Column;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/contacts.dao.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/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/user_discovery.service.dart';
|
import 'package:twonly/src/services/user_discovery.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.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/components/verification_badge.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/better_list_title.element.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/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/groups/group.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy/user_discovery.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<ContactView> {
|
class _ContactViewState extends State<ContactView> {
|
||||||
Contact? _contact;
|
Contact? _contact;
|
||||||
List<GroupMember> _memberOfGroups = [];
|
List<GroupMember> _memberOfGroups = [];
|
||||||
List<KeyVerification> _keyVerifications = [];
|
|
||||||
List<(Contact, DateTime)> _transferredTrust = [];
|
|
||||||
|
|
||||||
late StreamSubscription<Contact?> _streamContact;
|
late StreamSubscription<Contact?> _streamContact;
|
||||||
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||||
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
|
||||||
late StreamSubscription<List<(Contact, DateTime)>> _streamTransferredTrust;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -63,30 +58,12 @@ class _ContactViewState extends State<ContactView> {
|
||||||
_memberOfGroups = groups;
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_streamContact.cancel();
|
_streamContact.cancel();
|
||||||
_streamMemberOfGroups.cancel();
|
_streamMemberOfGroups.cancel();
|
||||||
_streamKeyVerifications.cancel();
|
|
||||||
_streamTransferredTrust.cancel();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,75 +237,9 @@ class _ContactViewState extends State<ContactView> {
|
||||||
RestoreFlameComp(
|
RestoreFlameComp(
|
||||||
contactId: widget.userId,
|
contactId: widget.userId,
|
||||||
),
|
),
|
||||||
if (_keyVerifications.isEmpty && _transferredTrust.isEmpty)
|
VerificationExpansionTileComp(
|
||||||
BetterListTile(
|
contact: contact,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (userService.currentUser.isUserDiscoveryEnabled)
|
if (userService.currentUser.isUserDiscoveryEnabled)
|
||||||
if (userService.currentUser.userDiscoveryRequiresManualApproval &&
|
if (userService.currentUser.userDiscoveryRequiresManualApproval &&
|
||||||
contact.userDiscoveryManualApproved != true)
|
contact.userDiscoveryManualApproved != true)
|
||||||
|
|
@ -408,19 +319,6 @@ class _ContactViewState extends State<ContactView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<String?> showNicknameChangeDialog(
|
Future<String?> showNicknameChangeDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Contact contact,
|
Contact contact,
|
||||||
|
|
|
||||||
|
|
@ -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<VerificationExpansionTileComp> createState() =>
|
||||||
|
_VerificationExpansionTileCompState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerificationExpansionTileCompState
|
||||||
|
extends State<VerificationExpansionTileComp> {
|
||||||
|
List<KeyVerification> _keyVerifications = [];
|
||||||
|
List<(Contact, DateTime)> _transferredTrust = [];
|
||||||
|
|
||||||
|
late StreamSubscription<List<KeyVerification>> _streamKeyVerifications;
|
||||||
|
late StreamSubscription<List<(Contact, DateTime)>> _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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.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/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/let_your_friends_find_you.setup.dart';
|
||||||
import 'package:twonly/src/visual/views/onboarding/setup/profile.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 {
|
enum SetupPages {
|
||||||
profile,
|
profile,
|
||||||
backup,
|
backup,
|
||||||
addNewContact,
|
|
||||||
verificationBadge,
|
verificationBadge,
|
||||||
shareYourFriends,
|
shareYourFriends,
|
||||||
letYourFriendsFindYou,
|
letYourFriendsFindYou,
|
||||||
|
|
@ -185,8 +183,6 @@ class _SetupViewState extends State<SetupView> {
|
||||||
return const ProfileSetupPage();
|
return const ProfileSetupPage();
|
||||||
case SetupPages.backup:
|
case SetupPages.backup:
|
||||||
return const BackupSetupPage();
|
return const BackupSetupPage();
|
||||||
case SetupPages.addNewContact:
|
|
||||||
return const AddNewContactsPage();
|
|
||||||
case SetupPages.verificationBadge:
|
case SetupPages.verificationBadge:
|
||||||
return const VerificationBadgeSetupPage();
|
return const VerificationBadgeSetupPage();
|
||||||
case SetupPages.shareYourFriends:
|
case SetupPages.shareYourFriends:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue