mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
add version to manual verifiy numbers
This commit is contained in:
parent
3bc446553a
commit
5786effc5b
27 changed files with 1644 additions and 257 deletions
2
.github/workflows/dev_github.yml
vendored
2
.github/workflows/dev_github.yml
vendored
|
|
@ -4,7 +4,7 @@ on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- disabled_dev
|
||||||
paths:
|
paths:
|
||||||
- lib/**/*.dart
|
- lib/**/*.dart
|
||||||
|
|
||||||
|
|
|
||||||
12
lib/app.dart
12
lib/app.dart
|
|
@ -192,16 +192,18 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
HomeViewState.streamSharedLink.add(uri);
|
HomeViewState.streamSharedLink.add(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to all events (initial link and further)
|
Future<void> handleIncomingUrl(Uri uri) async {
|
||||||
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Log.info('Got link via app links: ${uri.scheme}');
|
Log.info('Handling incoming link: ${uri.scheme}://${uri.host}');
|
||||||
if (!await handleIntentUrl(context, uri)) {
|
if (!await handleIntentUrl(context, uri)) {
|
||||||
if (uri.scheme.startsWith('http')) {
|
if (uri.scheme.startsWith('http')) {
|
||||||
handleShareLink(uri);
|
handleShareLink(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Subscribe to all events (initial link and further)
|
||||||
|
_deepLinkSub = AppLinks().uriLinkStream.listen(handleIncomingUrl);
|
||||||
|
|
||||||
void handleShareMedia(String path, MediaType type) {
|
void handleShareMedia(String path, MediaType type) {
|
||||||
HomeViewState.pendingSharedMedia = (path, type);
|
HomeViewState.pendingSharedMedia = (path, type);
|
||||||
|
|
@ -212,7 +214,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
|
||||||
|
|
||||||
_intentStreamSub = initIntentStreams(
|
_intentStreamSub = initIntentStreams(
|
||||||
context,
|
context,
|
||||||
handleShareLink,
|
handleIncomingUrl,
|
||||||
handleShareMedia,
|
handleShareMedia,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ class RustSignal {
|
||||||
contactId: contactId,
|
contactId: contactId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static Future<String?> getSafetyNumber({required PlatformInt64 contactId}) =>
|
||||||
|
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetSafetyNumber(
|
||||||
|
contactId: contactId,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<Uint8List> getUserPublicKey() =>
|
static Future<Uint8List> getUserPublicKey() =>
|
||||||
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
RustLib.instance.api.crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
||||||
|
|
||||||
|
|
@ -57,6 +62,14 @@ class RustSignal {
|
||||||
bundle: bundle,
|
bundle: bundle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Removes the current session with a contact and immediately establishes
|
||||||
|
/// a fresh one while requiring the contact's stored identity key to match.
|
||||||
|
static Future<void> resetContactSession({required PlatformInt64 contactId}) =>
|
||||||
|
RustLib.instance.api
|
||||||
|
.crateBridgeWrapperSignalRustSignalResetContactSession(
|
||||||
|
contactId: contactId,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => 0;
|
int get hashCode => 0;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
String get codegenVersion => '2.12.0';
|
String get codegenVersion => '2.12.0';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get rustContentHash => -2068471902;
|
int get rustContentHash => -1675944024;
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
|
|
@ -699,6 +699,10 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required PlatformInt64 contactId,
|
required PlatformInt64 contactId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<String?> crateBridgeWrapperSignalRustSignalGetSafetyNumber({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey();
|
||||||
|
|
||||||
Future<void> crateBridgeWrapperSignalRustSignalProcessPrekeyBundle({
|
Future<void> crateBridgeWrapperSignalRustSignalProcessPrekeyBundle({
|
||||||
|
|
@ -707,6 +711,10 @@ abstract class RustLibApi extends BaseApi {
|
||||||
required FrbPreKeyBundle bundle,
|
required FrbPreKeyBundle bundle,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
});
|
||||||
|
|
||||||
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
||||||
required List<int> secret,
|
required List<int> secret,
|
||||||
required int total,
|
required int total,
|
||||||
|
|
@ -6206,6 +6214,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["contactId"],
|
argNames: ["contactId"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> crateBridgeWrapperSignalRustSignalGetSafetyNumber({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_i_64(contactId, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 159,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_opt_String,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
constMeta: kCrateBridgeWrapperSignalRustSignalGetSafetyNumberConstMeta,
|
||||||
|
argValues: [contactId],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta
|
||||||
|
get kCrateBridgeWrapperSignalRustSignalGetSafetyNumberConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "rust_signal_get_safety_number",
|
||||||
|
argNames: ["contactId"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey() {
|
Future<Uint8List> crateBridgeWrapperSignalRustSignalGetUserPublicKey() {
|
||||||
return handler.executeNormal(
|
return handler.executeNormal(
|
||||||
|
|
@ -6215,7 +6257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 159,
|
funcId: 160,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6253,7 +6295,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 160,
|
funcId: 161,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6276,6 +6318,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
argNames: ["name", "deviceId", "bundle"],
|
argNames: ["name", "deviceId", "bundle"],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> crateBridgeWrapperSignalRustSignalResetContactSession({
|
||||||
|
required PlatformInt64 contactId,
|
||||||
|
}) {
|
||||||
|
return handler.executeNormal(
|
||||||
|
NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
|
sse_encode_i_64(contactId, serializer);
|
||||||
|
pdeCallFfi(
|
||||||
|
generalizedFrbRustBinding,
|
||||||
|
serializer,
|
||||||
|
funcId: 162,
|
||||||
|
port: port_,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
codec: SseCodec(
|
||||||
|
decodeSuccessData: sse_decode_unit,
|
||||||
|
decodeErrorData: sse_decode_AnyhowException,
|
||||||
|
),
|
||||||
|
constMeta:
|
||||||
|
kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta,
|
||||||
|
argValues: [contactId],
|
||||||
|
apiImpl: this,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskConstMeta
|
||||||
|
get kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta =>
|
||||||
|
const TaskConstMeta(
|
||||||
|
debugName: "rust_signal_reset_contact_session",
|
||||||
|
argNames: ["contactId"],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
Future<List<Uint8List>> crateBridgeWrapperRustUtilsGenerateShares({
|
||||||
required List<int> secret,
|
required List<int> secret,
|
||||||
|
|
@ -6292,7 +6369,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 161,
|
funcId: 163,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6327,7 +6404,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 162,
|
funcId: 164,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6372,7 +6449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 163,
|
funcId: 165,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6415,7 +6492,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 164,
|
funcId: 166,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6449,7 +6526,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 165,
|
funcId: 167,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6484,7 +6561,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 166,
|
funcId: 168,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6519,7 +6596,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 167,
|
funcId: 169,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6552,7 +6629,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return pdeCallFfi(
|
return pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 168,
|
funcId: 170,
|
||||||
)!;
|
)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
|
|
@ -6592,7 +6669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 169,
|
funcId: 171,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6637,7 +6714,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 170,
|
funcId: 172,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6667,7 +6744,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 171,
|
funcId: 173,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6700,7 +6777,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 172,
|
funcId: 174,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6735,7 +6812,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
pdeCallFfi(
|
pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 173,
|
funcId: 175,
|
||||||
port: port_,
|
port: port_,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -6774,7 +6851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||||
return pdeCallFfi(
|
return pdeCallFfi(
|
||||||
generalizedFrbRustBinding,
|
generalizedFrbRustBinding,
|
||||||
serializer,
|
serializer,
|
||||||
funcId: 174,
|
funcId: 176,
|
||||||
)!;
|
)!;
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,10 @@ class UserConfig {
|
||||||
bool autoStoreAllSendUnlimitedMediaFiles;
|
bool autoStoreAllSendUnlimitedMediaFiles;
|
||||||
bool typingIndicators;
|
bool typingIndicators;
|
||||||
bool showRestoreFlame;
|
bool showRestoreFlame;
|
||||||
|
|
||||||
|
/// Unused: the best friend is derived from `total_media_counter` whenever
|
||||||
|
/// it is needed (`Group::best_friend_group_id`). Kept so configurations
|
||||||
|
/// written by older versions still deserialize.
|
||||||
String? myBestFriendGroupId;
|
String? myBestFriendGroupId;
|
||||||
DateTime? signalLastSignedPreKeyUpdated;
|
DateTime? signalLastSignedPreKeyUpdated;
|
||||||
DateTime? signalLastPqcPreKeysUploaded;
|
DateTime? signalLastPqcPreKeysUploaded;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class Routes {
|
||||||
static const String settingsHelpFaq = '/settings/help/faq';
|
static const String settingsHelpFaq = '/settings/help/faq';
|
||||||
static const String settingsHelpFaqVerifyBadge =
|
static const String settingsHelpFaqVerifyBadge =
|
||||||
'/settings/help/faq/verifybadge';
|
'/settings/help/faq/verifybadge';
|
||||||
|
static const String settingsHelpFaqManualVerification =
|
||||||
|
'/settings/help/faq/verifybadge/manual';
|
||||||
static const String settingsHelpContactUs = '/settings/help/contact_us';
|
static const String settingsHelpContactUs = '/settings/help/contact_us';
|
||||||
static const String settingsHelpNews = '/settings/help/news';
|
static const String settingsHelpNews = '/settings/help/news';
|
||||||
static const String settingsHelpDiagnostics = '/settings/help/diagnostics';
|
static const String settingsHelpDiagnostics = '/settings/help/diagnostics';
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ enum VerificationType {
|
||||||
migratedFromOldVersion,
|
migratedFromOldVersion,
|
||||||
qrScanned,
|
qrScanned,
|
||||||
link,
|
link,
|
||||||
|
manual,
|
||||||
secretQrToken,
|
secretQrToken,
|
||||||
contactSharedByVerified,
|
contactSharedByVerified,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -968,6 +968,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Verified via link.'**
|
/// **'Verified via link.'**
|
||||||
String get verificationTypeLink;
|
String get verificationTypeLink;
|
||||||
|
|
||||||
|
/// No description provided for @verificationTypeManual.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Safety number compared manually.'**
|
||||||
|
String get verificationTypeManual;
|
||||||
|
|
||||||
/// No description provided for @verificationTypeMigratedFromOldVersion.
|
/// No description provided for @verificationTypeMigratedFromOldVersion.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -998,6 +1004,42 @@ abstract class AppLocalizations {
|
||||||
/// **'Block'**
|
/// **'Block'**
|
||||||
String get contactBlock;
|
String get contactBlock;
|
||||||
|
|
||||||
|
/// No description provided for @contactTroubleshoot.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Troubleshoot connection'**
|
||||||
|
String get contactTroubleshoot;
|
||||||
|
|
||||||
|
/// No description provided for @contactResetSessionTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset connection with {username}?'**
|
||||||
|
String contactResetSessionTitle(String username);
|
||||||
|
|
||||||
|
/// No description provided for @contactResetSessionBody.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.'**
|
||||||
|
String get contactResetSessionBody;
|
||||||
|
|
||||||
|
/// No description provided for @contactResetSessionConfirm.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Reset connection'**
|
||||||
|
String get contactResetSessionConfirm;
|
||||||
|
|
||||||
|
/// No description provided for @contactResetSessionSuccess.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The encrypted connection with {username} was re-established.'**
|
||||||
|
String contactResetSessionSuccess(String username);
|
||||||
|
|
||||||
|
/// No description provided for @contactResetSessionFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The connection could not be reset. Check your internet connection and try again.'**
|
||||||
|
String get contactResetSessionFailed;
|
||||||
|
|
||||||
/// No description provided for @contactBlockTitle.
|
/// No description provided for @contactBlockTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2684,6 +2726,48 @@ abstract class AppLocalizations {
|
||||||
/// **'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'**
|
/// **'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!'**
|
||||||
String get linkPubkeyDoesNotMatch;
|
String get linkPubkeyDoesNotMatch;
|
||||||
|
|
||||||
|
/// No description provided for @verificationMissingSession.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.'**
|
||||||
|
String get verificationMissingSession;
|
||||||
|
|
||||||
|
/// No description provided for @verificationLinkInvalidTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Invalid verification link'**
|
||||||
|
String get verificationLinkInvalidTitle;
|
||||||
|
|
||||||
|
/// No description provided for @verificationLinkInvalid.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.'**
|
||||||
|
String get verificationLinkInvalid;
|
||||||
|
|
||||||
|
/// No description provided for @verificationLinkErrorTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Verification unavailable'**
|
||||||
|
String get verificationLinkErrorTitle;
|
||||||
|
|
||||||
|
/// No description provided for @verificationLinkError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'twonly could not check this verification link. Please try again.'**
|
||||||
|
String get verificationLinkError;
|
||||||
|
|
||||||
|
/// No description provided for @verificationOwnProfileTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This is your profile'**
|
||||||
|
String get verificationOwnProfileTitle;
|
||||||
|
|
||||||
|
/// No description provided for @verificationOwnProfile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'You scanned your own verification code.'**
|
||||||
|
String get verificationOwnProfile;
|
||||||
|
|
||||||
/// No description provided for @startWithCameraOpen.
|
/// No description provided for @startWithCameraOpen.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2870,6 +2954,60 @@ 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 @manualVerificationOpen.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Safety numbers'**
|
||||||
|
String get manualVerificationOpen;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Compare safety number'**
|
||||||
|
String get manualVerificationTitle;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationMarkVerified.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Mark as verified'**
|
||||||
|
String get manualVerificationMarkVerified;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Compare this safety number with {username}. If both devices show the same number, your end-to-end encryption is verified.'**
|
||||||
|
String manualVerificationDescription(String username);
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationMissingKeyTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No safety number available'**
|
||||||
|
String get manualVerificationMissingKeyTitle;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationMissingKeyDescription.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'There is no Signal session with {username} yet. Exchange a message first, then try again.'**
|
||||||
|
String manualVerificationMissingKeyDescription(String username);
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationLoadFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The safety number could not be loaded.'**
|
||||||
|
String get manualVerificationLoadFailed;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationRetry.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Try again'**
|
||||||
|
String get manualVerificationRetry;
|
||||||
|
|
||||||
|
/// No description provided for @manualVerificationSuccess.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'You compared the safety number with {username} and confirmed their identity.'**
|
||||||
|
String manualVerificationSuccess(String username);
|
||||||
|
|
||||||
/// No description provided for @scanNow.
|
/// No description provided for @scanNow.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -481,6 +481,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Per Link verifiziert.';
|
String get verificationTypeLink => 'Per Link verifiziert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationTypeManual => 'Sicherheitsnummer manuell verglichen.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeMigratedFromOldVersion =>
|
String get verificationTypeMigratedFromOldVersion =>
|
||||||
'Von alter Version migriert';
|
'Von alter Version migriert';
|
||||||
|
|
@ -497,6 +500,30 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get contactBlock => 'Blockieren';
|
String get contactBlock => 'Blockieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTroubleshoot => 'Verbindungsprobleme beheben';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactResetSessionTitle(String username) {
|
||||||
|
return 'Verbindung zu $username zurücksetzen?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionBody =>
|
||||||
|
'twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionConfirm => 'Verbindung zurücksetzen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactResetSessionSuccess(String username) {
|
||||||
|
return 'Die verschlüsselte Verbindung zu $username wurde neu aufgebaut.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionFailed =>
|
||||||
|
'Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactBlockTitle(Object username) {
|
String contactBlockTitle(Object username) {
|
||||||
return 'Blockiere $username';
|
return 'Blockiere $username';
|
||||||
|
|
@ -1504,6 +1531,31 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get linkPubkeyDoesNotMatch =>
|
String get linkPubkeyDoesNotMatch =>
|
||||||
'Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!';
|
'Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationMissingSession =>
|
||||||
|
'Für diesen Kontakt ist noch keine sichere Sitzung gespeichert. Tauscht zuerst eine Nachricht aus und lass dir danach einen neuen Verifizierungslink senden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkInvalidTitle => 'Ungültiger Verifizierungslink';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkInvalid =>
|
||||||
|
'Der Verifizierungslink ist unvollständig oder ungültig. Bitte die Person, ihn erneut aus ihrem twonly-Profil zu teilen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkErrorTitle => 'Verifizierung nicht möglich';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkError =>
|
||||||
|
'twonly konnte diesen Verifizierungslink nicht prüfen. Versuche es bitte erneut.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationOwnProfileTitle => 'Das ist dein Profil';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationOwnProfile =>
|
||||||
|
'Du hast deinen eigenen Verifizierungscode gescannt.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startWithCameraOpen => 'Mit geöffneter Kamera starten';
|
String get startWithCameraOpen => 'Mit geöffneter Kamera starten';
|
||||||
|
|
||||||
|
|
@ -1626,6 +1678,41 @@ 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 manualVerificationOpen => 'Sicherheitsnummern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationTitle => 'Sicherheitsnummer vergleichen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationMarkVerified => 'Als verifiziert markieren';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationDescription(String username) {
|
||||||
|
return 'Vergleiche diese Sicherheitsnummer mit $username. Wenn auf beiden Geräten dieselbe Nummer angezeigt wird, ist eure Ende-zu-Ende-Verschlüsselung verifiziert.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationMissingKeyTitle =>
|
||||||
|
'Keine Sicherheitsnummer verfügbar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationMissingKeyDescription(String username) {
|
||||||
|
return 'Mit $username besteht noch keine Signal-Sitzung. Tauscht zuerst eine Nachricht aus und versucht es danach erneut.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationLoadFailed =>
|
||||||
|
'Die Sicherheitsnummer konnte nicht geladen werden.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationRetry => 'Erneut versuchen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationSuccess(String username) {
|
||||||
|
return 'Du hast die Sicherheitsnummer mit $username verglichen und die Identität bestätigt.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanNow => 'Jetzt scannen';
|
String get scanNow => 'Jetzt scannen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -478,6 +478,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get verificationTypeLink => 'Verified via link.';
|
String get verificationTypeLink => 'Verified via link.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationTypeManual => 'Safety number compared manually.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get verificationTypeMigratedFromOldVersion =>
|
String get verificationTypeMigratedFromOldVersion =>
|
||||||
'Migrated from old version.';
|
'Migrated from old version.';
|
||||||
|
|
@ -494,6 +497,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get contactBlock => 'Block';
|
String get contactBlock => 'Block';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactTroubleshoot => 'Troubleshoot connection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactResetSessionTitle(String username) {
|
||||||
|
return 'Reset connection with $username?';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionBody =>
|
||||||
|
'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionConfirm => 'Reset connection';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String contactResetSessionSuccess(String username) {
|
||||||
|
return 'The encrypted connection with $username was re-established.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get contactResetSessionFailed =>
|
||||||
|
'The connection could not be reset. Check your internet connection and try again.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String contactBlockTitle(Object username) {
|
String contactBlockTitle(Object username) {
|
||||||
return 'Block $username';
|
return 'Block $username';
|
||||||
|
|
@ -1492,6 +1519,31 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get linkPubkeyDoesNotMatch =>
|
String get linkPubkeyDoesNotMatch =>
|
||||||
'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!';
|
'The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationMissingSession =>
|
||||||
|
'There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkInvalidTitle => 'Invalid verification link';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkInvalid =>
|
||||||
|
'The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkErrorTitle => 'Verification unavailable';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationLinkError =>
|
||||||
|
'twonly could not check this verification link. Please try again.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationOwnProfileTitle => 'This is your profile';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get verificationOwnProfile =>
|
||||||
|
'You scanned your own verification code.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startWithCameraOpen => 'Start with camera open';
|
String get startWithCameraOpen => 'Start with camera open';
|
||||||
|
|
||||||
|
|
@ -1612,6 +1664,40 @@ 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 manualVerificationOpen => 'Safety numbers';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationTitle => 'Compare safety number';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationMarkVerified => 'Mark as verified';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationDescription(String username) {
|
||||||
|
return 'Compare this safety number with $username. If both devices show the same number, your end-to-end encryption is verified.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationMissingKeyTitle => 'No safety number available';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationMissingKeyDescription(String username) {
|
||||||
|
return 'There is no Signal session with $username yet. Exchange a message first, then try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationLoadFailed =>
|
||||||
|
'The safety number could not be loaded.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manualVerificationRetry => 'Try again';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String manualVerificationSuccess(String username) {
|
||||||
|
return 'You compared the safety number with $username and confirmed their identity.';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get scanNow => 'Scan now';
|
String get scanNow => 'Scan now';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,32 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"verificationTypeLink": "Per Link verifiziert.",
|
"verificationTypeLink": "Per Link verifiziert.",
|
||||||
|
"verificationTypeManual": "Sicherheitsnummer manuell verglichen.",
|
||||||
"verificationTypeMigratedFromOldVersion": "Von alter Version migriert",
|
"verificationTypeMigratedFromOldVersion": "Von alter Version migriert",
|
||||||
"contactViewMessage": "Nachricht",
|
"contactViewMessage": "Nachricht",
|
||||||
"contactNickname": "Spitzname",
|
"contactNickname": "Spitzname",
|
||||||
"contactNicknameNew": "Neuer Spitzname",
|
"contactNicknameNew": "Neuer Spitzname",
|
||||||
"contactBlock": "Blockieren",
|
"contactBlock": "Blockieren",
|
||||||
|
"contactTroubleshoot": "Verbindungsprobleme beheben",
|
||||||
|
"contactResetSessionTitle": "Verbindung zu {username} zurücksetzen?",
|
||||||
|
"@contactResetSessionTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contactResetSessionBody": "twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.",
|
||||||
|
"contactResetSessionConfirm": "Verbindung zurücksetzen",
|
||||||
|
"contactResetSessionSuccess": "Die verschlüsselte Verbindung zu {username} wurde neu aufgebaut.",
|
||||||
|
"@contactResetSessionSuccess": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contactResetSessionFailed": "Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.",
|
||||||
"contactRemove": "Kontakt löschen",
|
"contactRemove": "Kontakt löschen",
|
||||||
"contactRemoveTitle": "{username} löschen?",
|
"contactRemoveTitle": "{username} löschen?",
|
||||||
"contactRemoveBody": "Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.",
|
"contactRemoveBody": "Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.",
|
||||||
|
|
@ -487,6 +508,13 @@
|
||||||
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
|
"gotLinkFromFriend": "Ja, der Link kommt direkt von der Person.",
|
||||||
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
|
"couldNotVerifyUsername": "{username} konnte nicht verifiziert werden",
|
||||||
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
|
"linkPubkeyDoesNotMatch": "Der öffentliche Schlüssel im Link stimmt nicht mit dem für diesen Kontakt gespeicherten öffentlichen Schlüssel überein. Triff die Person persönlich und scanne den QR-Code direkt!",
|
||||||
|
"verificationMissingSession": "Für diesen Kontakt ist noch keine sichere Sitzung gespeichert. Tauscht zuerst eine Nachricht aus und lass dir danach einen neuen Verifizierungslink senden.",
|
||||||
|
"verificationLinkInvalidTitle": "Ungültiger Verifizierungslink",
|
||||||
|
"verificationLinkInvalid": "Der Verifizierungslink ist unvollständig oder ungültig. Bitte die Person, ihn erneut aus ihrem twonly-Profil zu teilen.",
|
||||||
|
"verificationLinkErrorTitle": "Verifizierung nicht möglich",
|
||||||
|
"verificationLinkError": "twonly konnte diesen Verifizierungslink nicht prüfen. Versuche es bitte erneut.",
|
||||||
|
"verificationOwnProfileTitle": "Das ist dein Profil",
|
||||||
|
"verificationOwnProfile": "Du hast deinen eigenen Verifizierungscode gescannt.",
|
||||||
"startWithCameraOpen": "Mit geöffneter Kamera starten",
|
"startWithCameraOpen": "Mit geöffneter Kamera starten",
|
||||||
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
|
"showImagePreviewWhenSending": "Bildvorschau bei der Auswahl von Empfängern anzeigen",
|
||||||
"verifiedPublicKey": "Die Identität von {username} wurde erfolgreich überprüft.",
|
"verifiedPublicKey": "Die Identität von {username} wurde erfolgreich überprüft.",
|
||||||
|
|
@ -518,6 +546,36 @@
|
||||||
"verificationBadgeGreenDesc": "Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.",
|
"verificationBadgeGreenDesc": "Ein Kontakt, den du über den QR-code *persönlich verifiziert* hast.",
|
||||||
"verificationBadgeYellowDesc": "Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.",
|
"verificationBadgeYellowDesc": "Ein Kontakt, der von mind. einem *deiner Kontakte verifiziert* wurde.",
|
||||||
"verificationBadgeRedDesc": "Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.",
|
"verificationBadgeRedDesc": "Ein Kontakt, dessen Identität noch *nicht überprüft* wurde.",
|
||||||
|
"manualVerificationOpen": "Sicherheitsnummern",
|
||||||
|
"manualVerificationTitle": "Sicherheitsnummer vergleichen",
|
||||||
|
"manualVerificationMarkVerified": "Als verifiziert markieren",
|
||||||
|
"manualVerificationDescription": "Vergleiche diese Sicherheitsnummer mit {username}. Wenn auf beiden Geräten dieselbe Nummer angezeigt wird, ist eure Ende-zu-Ende-Verschlüsselung verifiziert.",
|
||||||
|
"@manualVerificationDescription": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manualVerificationMissingKeyTitle": "Keine Sicherheitsnummer verfügbar",
|
||||||
|
"manualVerificationMissingKeyDescription": "Mit {username} besteht noch keine Signal-Sitzung. Tauscht zuerst eine Nachricht aus und versucht es danach erneut.",
|
||||||
|
"@manualVerificationMissingKeyDescription": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manualVerificationLoadFailed": "Die Sicherheitsnummer konnte nicht geladen werden.",
|
||||||
|
"manualVerificationRetry": "Erneut versuchen",
|
||||||
|
"manualVerificationSuccess": "Du hast die Sicherheitsnummer mit {username} verglichen und die Identität bestätigt.",
|
||||||
|
"@manualVerificationSuccess": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"scanNow": "Jetzt scannen",
|
"scanNow": "Jetzt scannen",
|
||||||
"qrScannerVerifyHint": "Zum Verifizieren muss die andere Person ihren QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
"qrScannerVerifyHint": "Zum Verifizieren muss die andere Person ihren QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
||||||
"qrScannerVerifyUserHint": "Zum Verifizieren muss {username} seinen QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
"qrScannerVerifyUserHint": "Zum Verifizieren muss {username} seinen QR-Code öffnen (Chat-Liste > QR-Code-Button unten rechts)",
|
||||||
|
|
|
||||||
|
|
@ -170,11 +170,32 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"verificationTypeLink": "Verified via link.",
|
"verificationTypeLink": "Verified via link.",
|
||||||
|
"verificationTypeManual": "Safety number compared manually.",
|
||||||
"verificationTypeMigratedFromOldVersion": "Migrated from old version.",
|
"verificationTypeMigratedFromOldVersion": "Migrated from old version.",
|
||||||
"contactViewMessage": "Message",
|
"contactViewMessage": "Message",
|
||||||
"contactNickname": "Nickname",
|
"contactNickname": "Nickname",
|
||||||
"contactNicknameNew": "New nickname",
|
"contactNicknameNew": "New nickname",
|
||||||
"contactBlock": "Block",
|
"contactBlock": "Block",
|
||||||
|
"contactTroubleshoot": "Troubleshoot connection",
|
||||||
|
"contactResetSessionTitle": "Reset connection with {username}?",
|
||||||
|
"@contactResetSessionTitle": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contactResetSessionBody": "twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.",
|
||||||
|
"contactResetSessionConfirm": "Reset connection",
|
||||||
|
"contactResetSessionSuccess": "The encrypted connection with {username} was re-established.",
|
||||||
|
"@contactResetSessionSuccess": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contactResetSessionFailed": "The connection could not be reset. Check your internet connection and try again.",
|
||||||
"contactBlockTitle": "Block {username}",
|
"contactBlockTitle": "Block {username}",
|
||||||
"@contactBlockTitle": {
|
"@contactBlockTitle": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
@ -497,6 +518,13 @@
|
||||||
"gotLinkFromFriend": "Yes, I got the link from my friend!",
|
"gotLinkFromFriend": "Yes, I got the link from my friend!",
|
||||||
"couldNotVerifyUsername": "Could not verify {username}",
|
"couldNotVerifyUsername": "Could not verify {username}",
|
||||||
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
|
"linkPubkeyDoesNotMatch": "The public key in the link does not match the public key stored for this contact. Try to meet your friend in person and scan the QR code directly!",
|
||||||
|
"verificationMissingSession": "There is no secure session stored for this contact yet. Exchange a message first, then ask the person to share a new verification link.",
|
||||||
|
"verificationLinkInvalidTitle": "Invalid verification link",
|
||||||
|
"verificationLinkInvalid": "The verification link is incomplete or invalid. Ask the person to share it again from their twonly profile.",
|
||||||
|
"verificationLinkErrorTitle": "Verification unavailable",
|
||||||
|
"verificationLinkError": "twonly could not check this verification link. Please try again.",
|
||||||
|
"verificationOwnProfileTitle": "This is your profile",
|
||||||
|
"verificationOwnProfile": "You scanned your own verification code.",
|
||||||
"startWithCameraOpen": "Start with camera open",
|
"startWithCameraOpen": "Start with camera open",
|
||||||
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
|
"showImagePreviewWhenSending": "Display image preview when selecting recipients",
|
||||||
"verifiedPublicKey": "The identity of {username} has been successfully verified.",
|
"verifiedPublicKey": "The identity of {username} has been successfully verified.",
|
||||||
|
|
@ -528,6 +556,36 @@
|
||||||
"verificationBadgeGreenDesc": "A contact you have *personally verified* using the QR code.",
|
"verificationBadgeGreenDesc": "A contact you have *personally verified* using the QR code.",
|
||||||
"verificationBadgeYellowDesc": "A contact who has been verified by at least one of *your contacts*.",
|
"verificationBadgeYellowDesc": "A contact who has been verified by at least one of *your contacts*.",
|
||||||
"verificationBadgeRedDesc": "A contact whose identity has *not* yet been verified.",
|
"verificationBadgeRedDesc": "A contact whose identity has *not* yet been verified.",
|
||||||
|
"manualVerificationOpen": "Safety numbers",
|
||||||
|
"manualVerificationTitle": "Compare safety number",
|
||||||
|
"manualVerificationMarkVerified": "Mark as verified",
|
||||||
|
"manualVerificationDescription": "Compare this safety number with {username}. If both devices show the same number, your end-to-end encryption is verified.",
|
||||||
|
"@manualVerificationDescription": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manualVerificationMissingKeyTitle": "No safety number available",
|
||||||
|
"manualVerificationMissingKeyDescription": "There is no Signal session with {username} yet. Exchange a message first, then try again.",
|
||||||
|
"@manualVerificationMissingKeyDescription": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manualVerificationLoadFailed": "The safety number could not be loaded.",
|
||||||
|
"manualVerificationRetry": "Try again",
|
||||||
|
"manualVerificationSuccess": "You compared the safety number with {username} and confirmed their identity.",
|
||||||
|
"@manualVerificationSuccess": {
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"scanNow": "Scan now",
|
"scanNow": "Scan now",
|
||||||
"qrScannerVerifyHint": "To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)",
|
"qrScannerVerifyHint": "To verify, the other person must open their QR code (Chat Lists > QR code button at the bottom right)",
|
||||||
"qrScannerVerifyUserHint": "To verify, {username} must open their QR code (Chat Lists > QR code button at the bottom right)",
|
"qrScannerVerifyUserHint": "To verify, {username} must open their QR code (Chat Lists > QR code button at the bottom right)",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import 'package:twonly/src/visual/views/settings/help/diagnostics.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/faq/verification_badge_faq.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/help.view.dart';
|
||||||
|
import 'package:twonly/src/visual/views/settings/help/manual_verification.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/help/news.view.dart';
|
import 'package:twonly/src/visual/views/settings/help/news.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
import 'package:twonly/src/visual/views/settings/notification.view.dart';
|
||||||
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
import 'package:twonly/src/visual/views/settings/privacy.view.dart';
|
||||||
|
|
@ -255,6 +256,14 @@ final routerProvider = GoRouter(
|
||||||
builder: (context, state) => VerificationBadeFaqView(
|
builder: (context, state) => VerificationBadeFaqView(
|
||||||
contact: state.extra as Contact?,
|
contact: state.extra as Contact?,
|
||||||
),
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'manual',
|
||||||
|
builder: (context, state) => ManualVerificationView(
|
||||||
|
contact: state.extra! as Contact,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,27 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/core/bridge/wrapper/signal.dart';
|
import 'package:twonly/core/bridge/wrapper/signal.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/tables/contacts.table.dart';
|
import 'package:twonly/src/database/tables/contacts.table.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/services/passwordless_recovery.service.dart';
|
import 'package:twonly/src/services/passwordless_recovery.service.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/utils/qr.utils.dart';
|
import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_failure_dialog.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
import 'package:twonly/src/visual/views/contact/add_contact_via_qr_link.view.dart';
|
||||||
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
|
||||||
|
|
||||||
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
if (!uri.scheme.startsWith('http')) return false;
|
if (uri.scheme != 'http' && uri.scheme != 'https') return false;
|
||||||
if (uri.host != 'me.twonly.eu') return false;
|
if (uri.host != 'me.twonly.eu') return false;
|
||||||
if (uri.hasEmptyPath) return false;
|
if (uri.hasEmptyPath) {
|
||||||
|
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is the QR code link which was
|
// Check if this is the QR code link which was
|
||||||
// therefore scanned with the system camera
|
// therefore scanned with the system camera
|
||||||
|
|
@ -37,37 +44,47 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
|
|
||||||
if (!context.mounted) return false;
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
if (result != null) {
|
switch (result.status) {
|
||||||
final (profile, contact, verificationOk) = result;
|
case QrCodeLinkStatus.newContact:
|
||||||
|
|
||||||
if (profile.username == userService.currentUser.username) {
|
|
||||||
await context.push(Routes.settingsPublicProfile);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contact != null) {
|
|
||||||
if (verificationOk) {
|
|
||||||
await context.push(Routes.profileContact(contact.userId));
|
|
||||||
} else {
|
|
||||||
await _pubKeysDoNotMatch(context, contact.username);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await context.navPush(
|
await context.navPush(
|
||||||
AddContactViaQrLinkView(
|
AddContactViaQrLinkView(
|
||||||
profile: profile,
|
profile: result.profile!,
|
||||||
qrCodeLink: uri.toString(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
case QrCodeLinkStatus.verified:
|
||||||
|
final contact = result.contact!;
|
||||||
|
await VerificationSuccessDialog.show(context, contact);
|
||||||
|
if (context.mounted) {
|
||||||
|
await context.push(Routes.profileContact(contact.userId));
|
||||||
|
}
|
||||||
|
case QrCodeLinkStatus.ownProfile:
|
||||||
|
case QrCodeLinkStatus.invalid:
|
||||||
|
case QrCodeLinkStatus.keyMismatch:
|
||||||
|
case QrCodeLinkStatus.missingSession:
|
||||||
|
case QrCodeLinkStatus.error:
|
||||||
|
await showQrCodeVerificationFailure(context, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final publicKey = uri.hasFragment ? uri.fragment : null;
|
final rawPathSegments = uri.pathSegments
|
||||||
final userPaths = uri.path.split('/');
|
.where((part) => part.isNotEmpty)
|
||||||
if (userPaths.length != 2) return false;
|
.toList();
|
||||||
final username = userPaths[1];
|
if (rawPathSegments.firstOrNull == 'qr' ||
|
||||||
|
rawPathSegments.firstOrNull == 'r') {
|
||||||
|
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final publicKey = uri.hasFragment && uri.fragment.isNotEmpty
|
||||||
|
? uri.fragment
|
||||||
|
: null;
|
||||||
|
if (rawPathSegments.length != 1) {
|
||||||
|
if (context.mounted) await _showInvalidVerificationLink(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final username = rawPathSegments.single;
|
||||||
|
|
||||||
if (!context.mounted) return false;
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
|
|
@ -80,86 +97,147 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
|
||||||
'Opened via deep link!: username = $username public_key = ${uri.fragment}',
|
'Opened via deep link!: username = $username public_key = ${uri.fragment}',
|
||||||
);
|
);
|
||||||
|
|
||||||
final contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
Uint8List? receivedPublicKey;
|
||||||
|
if (publicKey != null) {
|
||||||
|
try {
|
||||||
|
receivedPublicKey = base64Url.decode(publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn('Invalid public key in verification link: $e');
|
||||||
|
await _showInvalidVerificationLink(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
late final List<Contact> contacts;
|
||||||
|
try {
|
||||||
|
contacts = await twonlyDB.contactsDao.getContactsByUsername(username);
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn('Could not load contact for verification link: $e');
|
||||||
|
if (context.mounted) await _showVerificationLinkError(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!context.mounted) return true;
|
if (!context.mounted) return true;
|
||||||
|
|
||||||
if (contacts.isEmpty) {
|
if (contacts.isEmpty) {
|
||||||
// User does not yet exists, making a request...
|
// User does not yet exists, making a request...
|
||||||
Uint8List? publicKeyBytes;
|
|
||||||
if (publicKey != null) {
|
|
||||||
publicKeyBytes = base64Url.decode(publicKey);
|
|
||||||
}
|
|
||||||
await context.navPush(
|
await context.navPush(
|
||||||
AddNewUserView(
|
AddNewUserView(
|
||||||
username: username,
|
username: username,
|
||||||
publicKey: publicKeyBytes,
|
publicKey: receivedPublicKey,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicKey != null) {
|
final contact = contacts.first;
|
||||||
try {
|
if (publicKey == null) {
|
||||||
final contact = contacts.first;
|
await VerificationFailureDialog.show(
|
||||||
final storedPublicKey = await RustSignal.getContactPublicKey(
|
context,
|
||||||
contactId: contact.userId,
|
title: context.lang.couldNotVerifyUsername(
|
||||||
|
getContactDisplayName(contact),
|
||||||
|
),
|
||||||
|
message: context.lang.verificationLinkInvalid,
|
||||||
|
contact: contact,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final storedPublicKey = await RustSignal.getContactPublicKey(
|
||||||
|
contactId: contact.userId,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return true;
|
||||||
|
if (storedPublicKey == null) {
|
||||||
|
await showQrCodeVerificationFailure(
|
||||||
|
context,
|
||||||
|
QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.missingSession,
|
||||||
|
contact: contact,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final receivedPublicKey = base64Url.decode(publicKey);
|
return true;
|
||||||
if (storedPublicKey == null ||
|
}
|
||||||
receivedPublicKey.isEmpty ||
|
if (receivedPublicKey == null || receivedPublicKey.isEmpty) {
|
||||||
!context.mounted) {
|
await showQrCodeVerificationFailure(
|
||||||
return true;
|
context,
|
||||||
}
|
QrCodeLinkResult(
|
||||||
if (storedPublicKey.equals(receivedPublicKey)) {
|
status: QrCodeLinkStatus.invalid,
|
||||||
final markAsVerified = await showAlertDialog(
|
contact: contact,
|
||||||
context,
|
),
|
||||||
context.lang.linkFromUsername(contact.username),
|
);
|
||||||
context.lang.linkFromUsernameLong,
|
return true;
|
||||||
customOk: context.lang.gotLinkFromFriend,
|
}
|
||||||
|
if (storedPublicKey.equals(receivedPublicKey)) {
|
||||||
|
final markAsVerified = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.linkFromUsername(contact.username),
|
||||||
|
context.lang.linkFromUsernameLong,
|
||||||
|
customOk: context.lang.gotLinkFromFriend,
|
||||||
|
);
|
||||||
|
if (markAsVerified) {
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||||
|
contact.userId,
|
||||||
|
VerificationType.link,
|
||||||
);
|
);
|
||||||
if (markAsVerified) {
|
|
||||||
await twonlyDB.keyVerificationDao.addKeyVerification(
|
|
||||||
contact.userId,
|
|
||||||
VerificationType.link,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
await context.push(Routes.profileContact(contact.userId));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await _pubKeysDoNotMatch(context, contact.username);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
if (context.mounted) {
|
||||||
Log.warn(e);
|
await context.push(Routes.profileContact(contact.userId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await showQrCodeVerificationFailure(
|
||||||
|
context,
|
||||||
|
QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.keyMismatch,
|
||||||
|
contact: contact,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn(e);
|
||||||
|
if (context.mounted) {
|
||||||
|
await showQrCodeVerificationFailure(
|
||||||
|
context,
|
||||||
|
QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.error,
|
||||||
|
contact: contact,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pubKeysDoNotMatch(BuildContext context, String username) async {
|
Future<void> _showInvalidVerificationLink(BuildContext context) {
|
||||||
await showAlertDialog(
|
return VerificationFailureDialog.show(
|
||||||
context,
|
context,
|
||||||
context.lang.couldNotVerifyUsername(username),
|
title: context.lang.verificationLinkInvalidTitle,
|
||||||
context.lang.linkPubkeyDoesNotMatch,
|
message: context.lang.verificationLinkInvalid,
|
||||||
customCancel: '',
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showVerificationLinkError(BuildContext context) {
|
||||||
|
return VerificationFailureDialog.show(
|
||||||
|
context,
|
||||||
|
title: context.lang.verificationLinkErrorTitle,
|
||||||
|
message: context.lang.verificationLinkError,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<List<SharedFile>> initIntentStreams(
|
StreamSubscription<List<SharedFile>> initIntentStreams(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
void Function(Uri) onUrlCallBack,
|
Future<void> Function(Uri) onUrlCallBack,
|
||||||
void Function(String, MediaType) onMediaCallBack,
|
void Function(String, MediaType) onMediaCallBack,
|
||||||
) {
|
) {
|
||||||
FlutterSharingIntent.instance.getInitialSharing().then((f) {
|
FlutterSharingIntent.instance.getInitialSharing().then((f) async {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
await handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||||
});
|
});
|
||||||
|
|
||||||
return FlutterSharingIntent.instance.getMediaStream().listen(
|
return FlutterSharingIntent.instance.getMediaStream().listen(
|
||||||
(f) {
|
(f) async {
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
await handleIntentSharedFile(context, f, onUrlCallBack, onMediaCallBack);
|
||||||
},
|
},
|
||||||
// ignore: inference_failure_on_untyped_parameter
|
// ignore: inference_failure_on_untyped_parameter
|
||||||
onError: (err) {
|
onError: (err) {
|
||||||
|
|
@ -171,7 +249,7 @@ StreamSubscription<List<SharedFile>> initIntentStreams(
|
||||||
Future<void> handleIntentSharedFile(
|
Future<void> handleIntentSharedFile(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<SharedFile> files,
|
List<SharedFile> files,
|
||||||
void Function(Uri) onUrlCallBack,
|
Future<void> Function(Uri) onUrlCallBack,
|
||||||
void Function(String, MediaType) onMediaCallBack,
|
void Function(String, MediaType) onMediaCallBack,
|
||||||
) async {
|
) async {
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
|
|
@ -189,7 +267,7 @@ Future<void> handleIntentSharedFile(
|
||||||
if (file.value?.startsWith('http') ?? false) {
|
if (file.value?.startsWith('http') ?? false) {
|
||||||
final uri = Uri.parse(file.value!);
|
final uri = Uri.parse(file.value!);
|
||||||
Log.info('Got link via handle intent share file: ${uri.scheme}');
|
Log.info('Got link via handle intent share file: ${uri.scheme}');
|
||||||
onUrlCallBack(uri);
|
await onUrlCallBack(uri);
|
||||||
}
|
}
|
||||||
case SharedMediaType.IMAGE:
|
case SharedMediaType.IMAGE:
|
||||||
var type = MediaType.image;
|
var type = MediaType.image;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,28 @@ import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
|
||||||
import 'package:twonly/src/services/key_verification.service.dart';
|
import 'package:twonly/src/services/key_verification.service.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
||||||
|
enum QrCodeLinkStatus {
|
||||||
|
invalid,
|
||||||
|
ownProfile,
|
||||||
|
newContact,
|
||||||
|
verified,
|
||||||
|
keyMismatch,
|
||||||
|
missingSession,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
class QrCodeLinkResult {
|
||||||
|
const QrCodeLinkResult({
|
||||||
|
required this.status,
|
||||||
|
this.profile,
|
||||||
|
this.contact,
|
||||||
|
});
|
||||||
|
|
||||||
|
final QrCodeLinkStatus status;
|
||||||
|
final PublicProfile? profile;
|
||||||
|
final Contact? contact;
|
||||||
|
}
|
||||||
|
|
||||||
class QrCodeUtils {
|
class QrCodeUtils {
|
||||||
static String linkPrefix = 'https://me.twonly.eu/qr/#';
|
static String linkPrefix = 'https://me.twonly.eu/qr/#';
|
||||||
|
|
||||||
|
|
@ -45,8 +67,7 @@ class QrCodeUtils {
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns: profile, NEW_USER=true/VERIFIED_USER=false, VERIFICATION_OK
|
static Future<QrCodeLinkResult> handleQrCodeLink(
|
||||||
static Future<(PublicProfile, Contact?, bool)?> handleQrCodeLink(
|
|
||||||
String link,
|
String link,
|
||||||
) async {
|
) async {
|
||||||
late PublicProfile profile;
|
late PublicProfile profile;
|
||||||
|
|
@ -54,35 +75,53 @@ class QrCodeUtils {
|
||||||
try {
|
try {
|
||||||
final bytes = base64Url.decode(link.replaceFirst(linkPrefix, ''));
|
final bytes = base64Url.decode(link.replaceFirst(linkPrefix, ''));
|
||||||
final envelope = QREnvelope.fromBuffer(bytes);
|
final envelope = QREnvelope.fromBuffer(bytes);
|
||||||
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) return null;
|
if (envelope.type != QREnvelope_Type.PUBLIC_PROFILE) {
|
||||||
|
return const QrCodeLinkResult(status: QrCodeLinkStatus.invalid);
|
||||||
|
}
|
||||||
profile = PublicProfile.fromBuffer(envelope.data);
|
profile = PublicProfile.fromBuffer(envelope.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error(e);
|
||||||
return null;
|
return const QrCodeLinkResult(status: QrCodeLinkStatus.invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
final contact = await twonlyDB.contactsDao.getContactById(
|
try {
|
||||||
profile.userId.toInt(),
|
final contact = await twonlyDB.contactsDao.getContactById(
|
||||||
);
|
profile.userId.toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
if (contact == null) {
|
if (profile.userId.toInt() == userService.currentUser.userId) {
|
||||||
if (profile.username == userService.currentUser.username) {
|
return QrCodeLinkResult(
|
||||||
return null;
|
status: QrCodeLinkStatus.ownProfile,
|
||||||
|
profile: profile,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// NEW_USER
|
|
||||||
return (profile, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
final storedPublicKey = await RustSignal.getContactPublicKey(
|
if (contact == null) {
|
||||||
contactId: contact.userId,
|
return QrCodeLinkResult(
|
||||||
);
|
status: QrCodeLinkStatus.newContact,
|
||||||
if (storedPublicKey == null) return null;
|
profile: profile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final verificationOk = profile.publicIdentityKey.equals(
|
final storedPublicKey = await RustSignal.getContactPublicKey(
|
||||||
storedPublicKey.toList(),
|
contactId: contact.userId,
|
||||||
);
|
);
|
||||||
|
if (storedPublicKey == null) {
|
||||||
|
return QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.missingSession,
|
||||||
|
profile: profile,
|
||||||
|
contact: contact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile.publicIdentityKey.equals(storedPublicKey.toList())) {
|
||||||
|
return QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.keyMismatch,
|
||||||
|
profile: profile,
|
||||||
|
contact: contact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (verificationOk) {
|
|
||||||
var useSecretVerificationToken = profile.hasSecretVerificationToken();
|
var useSecretVerificationToken = profile.hasSecretVerificationToken();
|
||||||
if (profile.hasTimestamp()) {
|
if (profile.hasTimestamp()) {
|
||||||
// Only notify the scanned user if the QR code was generated within the last 10 minutes.
|
// Only notify the scanned user if the QR code was generated within the last 10 minutes.
|
||||||
|
|
@ -107,9 +146,18 @@ class QrCodeUtils {
|
||||||
contact.userId,
|
contact.userId,
|
||||||
VerificationType.qrScanned,
|
VerificationType.qrScanned,
|
||||||
);
|
);
|
||||||
|
return QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.verified,
|
||||||
|
profile: profile,
|
||||||
|
contact: contact,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Failed to verify QR-code profile: $e');
|
||||||
|
return QrCodeLinkResult(
|
||||||
|
status: QrCodeLinkStatus.error,
|
||||||
|
profile: profile,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (profile, contact, verificationOk);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
140
lib/src/visual/components/verification_failure_dialog.comp.dart
Normal file
140
lib/src/visual/components/verification_failure_dialog.comp.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||||
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
|
import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
|
class VerificationFailureDialog extends StatelessWidget {
|
||||||
|
const VerificationFailureDialog({
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
this.contact,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String message;
|
||||||
|
final Contact? contact;
|
||||||
|
|
||||||
|
static Future<void> show(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String message,
|
||||||
|
Contact? contact,
|
||||||
|
}) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => VerificationFailureDialog(
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
contact: contact,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
backgroundColor: context.color.surface,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (contact != null) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AvatarIcon(contactId: contact!.userId, fontSize: 16),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
getContactDisplayName(contact!),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
Container(
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: context.color.errorContainer,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.gpp_bad_outlined,
|
||||||
|
size: 52,
|
||||||
|
color: context.color.onErrorContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: context.color.onSurface.withValues(alpha: 0.72),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: MyButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(context.lang.close),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showQrCodeVerificationFailure(
|
||||||
|
BuildContext context,
|
||||||
|
QrCodeLinkResult result,
|
||||||
|
) {
|
||||||
|
final contact = result.contact;
|
||||||
|
final title = contact == null
|
||||||
|
? switch (result.status) {
|
||||||
|
QrCodeLinkStatus.invalid => context.lang.verificationLinkInvalidTitle,
|
||||||
|
QrCodeLinkStatus.ownProfile =>
|
||||||
|
context.lang.verificationOwnProfileTitle,
|
||||||
|
_ => context.lang.verificationLinkErrorTitle,
|
||||||
|
}
|
||||||
|
: context.lang.couldNotVerifyUsername(getContactDisplayName(contact));
|
||||||
|
|
||||||
|
final message = switch (result.status) {
|
||||||
|
QrCodeLinkStatus.keyMismatch => context.lang.linkPubkeyDoesNotMatch,
|
||||||
|
QrCodeLinkStatus.missingSession => context.lang.verificationMissingSession,
|
||||||
|
QrCodeLinkStatus.invalid => context.lang.verificationLinkInvalid,
|
||||||
|
QrCodeLinkStatus.ownProfile => context.lang.verificationOwnProfile,
|
||||||
|
_ => context.lang.verificationLinkError,
|
||||||
|
};
|
||||||
|
|
||||||
|
return VerificationFailureDialog.show(
|
||||||
|
context,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
contact: contact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
|
import 'package:twonly/src/visual/components/add_contact_dialog.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
import 'package:twonly/src/visual/components/alert.dialog.dart';
|
||||||
import 'package:twonly/src/visual/components/snackbar.dart';
|
import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_failure_dialog.comp.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
import 'package:twonly/src/visual/views/camera/camera_preview_components/camera_preview_controller_view.dart';
|
||||||
|
|
@ -521,112 +522,126 @@ class MainCameraController {
|
||||||
Future<void> _processBarcode(InputImage inputImage) async {
|
Future<void> _processBarcode(InputImage inputImage) async {
|
||||||
if (_isBusy) return;
|
if (_isBusy) return;
|
||||||
_isBusy = true;
|
_isBusy = true;
|
||||||
final barcodes = await _barcodeScanner.processImage(inputImage);
|
try {
|
||||||
final controller = cameraController;
|
final barcodes = await _barcodeScanner.processImage(inputImage);
|
||||||
if (inputImage.metadata?.size != null &&
|
final controller = cameraController;
|
||||||
inputImage.metadata?.rotation != null &&
|
if (inputImage.metadata?.size != null &&
|
||||||
controller != null) {
|
inputImage.metadata?.rotation != null &&
|
||||||
final painter = BarcodeDetectorPainter(
|
controller != null) {
|
||||||
barcodes,
|
final painter = BarcodeDetectorPainter(
|
||||||
inputImage.metadata!.size,
|
barcodes,
|
||||||
inputImage.metadata!.rotation,
|
inputImage.metadata!.size,
|
||||||
controller.description.lensDirection,
|
inputImage.metadata!.rotation,
|
||||||
);
|
controller.description.lensDirection,
|
||||||
qrCodePain = CustomPaint(painter: painter);
|
);
|
||||||
|
qrCodePain = CustomPaint(painter: painter);
|
||||||
|
|
||||||
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
|
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
|
||||||
if (timeSharedLinkWasSetWithQr!.isAfter(
|
if (timeSharedLinkWasSetWithQr!.isAfter(
|
||||||
DateTime.now().subtract(const Duration(seconds: 2)),
|
DateTime.now().subtract(const Duration(seconds: 2)),
|
||||||
)) {
|
)) {
|
||||||
setSharedLinkForPreview(null);
|
setSharedLinkForPreview(null);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (final barcode in barcodes) {
|
|
||||||
if (barcode.displayValue == null) continue;
|
|
||||||
final link = barcode.displayValue!;
|
|
||||||
|
|
||||||
if (link.startsWith(PasswordlessRecoveryService.linkPrefix)) {
|
|
||||||
await PasswordlessRecoveryService.handleRecoveryLink(link);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link.startsWith(QrCodeUtils.linkPrefix)) {
|
for (final barcode in barcodes) {
|
||||||
if (_handledProfileLinks.contains(link)) continue;
|
if (barcode.displayValue == null) continue;
|
||||||
_handledProfileLinks.add(link);
|
final link = barcode.displayValue!;
|
||||||
|
|
||||||
final res = await QrCodeUtils.handleQrCodeLink(link);
|
if (link.startsWith(PasswordlessRecoveryService.linkPrefix)) {
|
||||||
if (res == null) continue;
|
await PasswordlessRecoveryService.handleRecoveryLink(link);
|
||||||
final (profile, contact, verificationOk) = res;
|
continue;
|
||||||
|
|
||||||
if (contact?.blocked ?? false) {
|
|
||||||
await twonlyDB.contactsDao.updateContact(
|
|
||||||
contact!.userId,
|
|
||||||
const ContactsCompanion(blocked: Value(false)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact == null || contact.deletedByUser) {
|
if (link.startsWith(QrCodeUtils.linkPrefix)) {
|
||||||
final context = cameraPreviewKey.currentContext;
|
if (_handledProfileLinks.contains(link)) continue;
|
||||||
if (context != null && context.mounted) {
|
_handledProfileLinks.add(link);
|
||||||
unawaited(HapticFeedback.heavyImpact());
|
|
||||||
final shouldRequest = await AddContactDialog.show(
|
final result = await QrCodeUtils.handleQrCodeLink(link);
|
||||||
context,
|
final profile = result.profile;
|
||||||
profile.username,
|
final contact = result.contact;
|
||||||
|
|
||||||
|
if (contact?.blocked ?? false) {
|
||||||
|
await twonlyDB.contactsDao.updateContact(
|
||||||
|
contact!.userId,
|
||||||
|
const ContactsCompanion(blocked: Value(false)),
|
||||||
);
|
);
|
||||||
if (shouldRequest == true && context.mounted) {
|
}
|
||||||
final success = await addNewContactFromPublicProfile(profile);
|
|
||||||
if (context.mounted) {
|
if (result.status == QrCodeLinkStatus.newContact) {
|
||||||
if (success) {
|
final context = cameraPreviewKey.currentContext;
|
||||||
showSnackbar(
|
if (profile != null && context != null && context.mounted) {
|
||||||
context,
|
unawaited(HapticFeedback.heavyImpact());
|
||||||
context.lang.requestedUserToastText(profile.username),
|
final shouldRequest = await AddContactDialog.show(
|
||||||
level: SnackbarLevel.success,
|
context,
|
||||||
);
|
profile.username,
|
||||||
} else {
|
);
|
||||||
await showAlertDialog(
|
if (shouldRequest == true && context.mounted) {
|
||||||
context,
|
final success = await addNewContactFromPublicProfile(profile);
|
||||||
context.lang.addFriendTitle,
|
if (context.mounted) {
|
||||||
context.lang.additionalUserAddError(profile.username),
|
if (success) {
|
||||||
customCancel: '',
|
showSnackbar(
|
||||||
);
|
context,
|
||||||
|
context.lang.requestedUserToastText(profile.username),
|
||||||
|
level: SnackbarLevel.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.addFriendTitle,
|
||||||
|
context.lang.additionalUserAddError(profile.username),
|
||||||
|
customCancel: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status != QrCodeLinkStatus.verified) {
|
||||||
|
final context = cameraPreviewKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
unawaited(HapticFeedback.heavyImpact());
|
||||||
|
await showQrCodeVerificationFailure(context, result);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact != null && contactsVerified[contact.userId] == null) {
|
||||||
|
contactsVerified[contact.userId] = ScannedVerifiedContact(
|
||||||
|
contact: contact,
|
||||||
|
verificationOk: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
unawaited(HapticFeedback.heavyImpact());
|
||||||
|
final context = cameraPreviewKey.currentContext;
|
||||||
|
if (context != null && context.mounted) {
|
||||||
|
await VerificationSuccessDialog.show(context, contact);
|
||||||
|
onVerificationSuccessDismissed?.call(contact);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contactsVerified[contact.userId] == null) {
|
if (link.startsWith('http://') || link.startsWith('https://')) {
|
||||||
contactsVerified[contact.userId] = ScannedVerifiedContact(
|
scannedUrl = link;
|
||||||
contact: contact,
|
if (sharedLinkForPreview == null) {
|
||||||
verificationOk: verificationOk,
|
timeSharedLinkWasSetWithQr = clock.now();
|
||||||
);
|
setSharedLinkForPreview(
|
||||||
|
Uri.parse(scannedUrl!),
|
||||||
unawaited(HapticFeedback.heavyImpact());
|
generatePreview: false,
|
||||||
final context = cameraPreviewKey.currentContext;
|
);
|
||||||
if (verificationOk && context != null && context.mounted) {
|
|
||||||
await VerificationSuccessDialog.show(context, contact);
|
|
||||||
onVerificationSuccessDismissed?.call(contact);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link.startsWith('http://') || link.startsWith('https://')) {
|
|
||||||
scannedUrl = link;
|
|
||||||
if (sharedLinkForPreview == null) {
|
|
||||||
timeSharedLinkWasSetWithQr = clock.now();
|
|
||||||
setSharedLinkForPreview(
|
|
||||||
Uri.parse(scannedUrl!),
|
|
||||||
generatePreview: false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Log.error('QR-code processing failed: $e\n$stackTrace');
|
||||||
|
} finally {
|
||||||
|
_isBusy = false;
|
||||||
|
setState?.call();
|
||||||
}
|
}
|
||||||
_isBusy = false;
|
|
||||||
setState?.call();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processFaces(InputImage inputImage) async {
|
Future<void> _processFaces(InputImage inputImage) async {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
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: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/model/protobuf/client/generated/qr.pb.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';
|
||||||
|
|
@ -15,12 +10,10 @@ import 'package:twonly/src/visual/components/snackbar.dart';
|
||||||
class AddContactViaQrLinkView extends StatefulWidget {
|
class AddContactViaQrLinkView extends StatefulWidget {
|
||||||
const AddContactViaQrLinkView({
|
const AddContactViaQrLinkView({
|
||||||
required this.profile,
|
required this.profile,
|
||||||
this.qrCodeLink,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final PublicProfile profile;
|
final PublicProfile profile;
|
||||||
final String? qrCodeLink;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AddContactViaQrLinkView> createState() =>
|
State<AddContactViaQrLinkView> createState() =>
|
||||||
|
|
@ -36,31 +29,15 @@ class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userData = FrbUserData(
|
final success = await addNewContactFromPublicProfile(widget.profile);
|
||||||
userId: widget.profile.userId.toInt(),
|
if (!success) {
|
||||||
publicIdentityKey: Uint8List.fromList(widget.profile.publicIdentityKey),
|
if (mounted) {
|
||||||
username: Uint8List.fromList(utf8.encode(widget.profile.username)),
|
showSnackbar(
|
||||||
);
|
context,
|
||||||
|
context.lang.additionalUserAddError(widget.profile.username),
|
||||||
final added = await twonlyDB.contactsDao.insertOnConflictUpdate(
|
);
|
||||||
ContactsCompanion(
|
|
||||||
username: Value(widget.profile.username),
|
|
||||||
userId: Value(widget.profile.userId.toInt()),
|
|
||||||
requested: const Value(false),
|
|
||||||
blocked: const Value(false),
|
|
||||||
deletedByUser: const Value(false),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (added > 0) {
|
|
||||||
await RustApi.tryRequestContactById(
|
|
||||||
contactId: userData.userId,
|
|
||||||
expectedPublicKey: userData.publicIdentityKey,
|
|
||||||
);
|
|
||||||
if (widget.qrCodeLink != null) {
|
|
||||||
// As the user does now exist he can now be marked as verified
|
|
||||||
await QrCodeUtils.handleQrCodeLink(widget.qrCodeLink!);
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ 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:twonly/core/bridge/wrapper/signal.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';
|
||||||
|
|
@ -38,6 +39,7 @@ class ContactView extends StatefulWidget {
|
||||||
class _ContactViewState extends State<ContactView> {
|
class _ContactViewState extends State<ContactView> {
|
||||||
Contact? _contact;
|
Contact? _contact;
|
||||||
List<GroupMember> _memberOfGroups = [];
|
List<GroupMember> _memberOfGroups = [];
|
||||||
|
bool _isResettingSignalSession = false;
|
||||||
|
|
||||||
late StreamSubscription<Contact?> _streamContact;
|
late StreamSubscription<Contact?> _streamContact;
|
||||||
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
late StreamSubscription<List<GroupMember>> _streamMemberOfGroups;
|
||||||
|
|
@ -138,6 +140,34 @@ class _ContactViewState extends State<ContactView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleResetSignalSession(Contact contact) async {
|
||||||
|
if (_isResettingSignalSession) return;
|
||||||
|
|
||||||
|
final displayName = getContactDisplayName(contact);
|
||||||
|
final confirmed = await showAlertDialog(
|
||||||
|
context,
|
||||||
|
context.lang.contactResetSessionTitle(displayName),
|
||||||
|
context.lang.contactResetSessionBody,
|
||||||
|
customOk: context.lang.contactResetSessionConfirm,
|
||||||
|
);
|
||||||
|
if (!confirmed || !mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isResettingSignalSession = true);
|
||||||
|
final result = await rustApiResult(
|
||||||
|
RustSignal.resetContactSession(contactId: contact.userId),
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() => _isResettingSignalSession = false);
|
||||||
|
showSnackbar(
|
||||||
|
context,
|
||||||
|
result.isSuccess
|
||||||
|
? context.lang.contactResetSessionSuccess(displayName)
|
||||||
|
: context.lang.contactResetSessionFailed,
|
||||||
|
level: result.isSuccess ? SnackbarLevel.success : SnackbarLevel.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_contact == null) return Container();
|
if (_contact == null) return Container();
|
||||||
|
|
@ -285,6 +315,19 @@ class _ContactViewState extends State<ContactView> {
|
||||||
text: context.lang.contactBlock,
|
text: context.lang.contactBlock,
|
||||||
onTap: () => handleUserBlockRequest(contact),
|
onTap: () => handleUserBlockRequest(contact),
|
||||||
),
|
),
|
||||||
|
BetterListTile(
|
||||||
|
icon: FontAwesomeIcons.screwdriverWrench,
|
||||||
|
text: context.lang.contactTroubleshoot,
|
||||||
|
trailing: _isResettingSignalSession
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: _isResettingSignalSession
|
||||||
|
? null
|
||||||
|
: () => handleResetSignalSession(contact),
|
||||||
|
),
|
||||||
BetterListTile(
|
BetterListTile(
|
||||||
icon: FontAwesomeIcons.userMinus,
|
icon: FontAwesomeIcons.userMinus,
|
||||||
iconSize: 16,
|
iconSize: 16,
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class _VerificationExpansionTileCompState
|
||||||
getContactDisplayName(widget.contact),
|
getContactDisplayName(widget.contact),
|
||||||
),
|
),
|
||||||
VerificationType.link => context.lang.verificationTypeLink,
|
VerificationType.link => context.lang.verificationTypeLink,
|
||||||
|
VerificationType.manual => context.lang.verificationTypeManual,
|
||||||
VerificationType.contactSharedByVerified =>
|
VerificationType.contactSharedByVerified =>
|
||||||
verifier != null
|
verifier != null
|
||||||
? context.lang.contactVerifiedBy(getContactDisplayName(verifier))
|
? context.lang.contactVerifiedBy(getContactDisplayName(verifier))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
class VerificationBadeFaqView extends StatefulWidget {
|
class VerificationBadeFaqView extends StatefulWidget {
|
||||||
const VerificationBadeFaqView({super.key, this.contact});
|
const VerificationBadeFaqView({super.key, this.contact});
|
||||||
|
|
@ -27,6 +31,31 @@ class _VerificationBadeFaqViewState extends State<VerificationBadeFaqView> {
|
||||||
displayButtons: true,
|
displayButtons: true,
|
||||||
contact: widget.contact,
|
contact: widget.contact,
|
||||||
),
|
),
|
||||||
|
if (widget.contact != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Center(
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: () => context.push(
|
||||||
|
Routes.settingsHelpFaqManualVerification,
|
||||||
|
extra: widget.contact,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const FaIcon(FontAwesomeIcons.fingerprint, size: 20),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Text(context.lang.manualVerificationOpen),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
274
lib/src/visual/views/settings/help/manual_verification.view.dart
Normal file
274
lib/src/visual/views/settings/help/manual_verification.view.dart
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:twonly/core/bridge/wrapper/signal.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/twonly.db.dart';
|
||||||
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
import 'package:twonly/src/visual/components/verification_success_dialog.comp.dart';
|
||||||
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
|
||||||
|
class ManualVerificationView extends StatefulWidget {
|
||||||
|
const ManualVerificationView({required this.contact, super.key});
|
||||||
|
|
||||||
|
final Contact contact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ManualVerificationView> createState() => _ManualVerificationViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ManualVerificationViewState extends State<ManualVerificationView> {
|
||||||
|
late Future<String?> _safetyNumber;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSafetyNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadSafetyNumber() {
|
||||||
|
_safetyNumber = RustSignal.getSafetyNumber(
|
||||||
|
contactId: widget.contact.userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _markAsVerified() async {
|
||||||
|
if (_isSaving) return;
|
||||||
|
setState(() => _isSaving = true);
|
||||||
|
|
||||||
|
await twonlyDB.keyVerificationDao.addKeyVerification(
|
||||||
|
widget.contact.userId,
|
||||||
|
VerificationType.manual,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
final displayName = getContactDisplayName(widget.contact);
|
||||||
|
await VerificationSuccessDialog.show(
|
||||||
|
context,
|
||||||
|
widget.contact,
|
||||||
|
message: context.lang.manualVerificationSuccess(displayName),
|
||||||
|
);
|
||||||
|
if (mounted) context.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final displayName = getContactDisplayName(widget.contact);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.manualVerificationTitle),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: FutureBuilder<String?>(
|
||||||
|
future: _safetyNumber,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return _SafetyNumberError(
|
||||||
|
message: context.lang.manualVerificationLoadFailed,
|
||||||
|
onRetry: () => setState(_loadSafetyNumber),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final safetyNumber = snapshot.data;
|
||||||
|
if (safetyNumber == null) {
|
||||||
|
return _SafetyNumberError(
|
||||||
|
title: context.lang.manualVerificationMissingKeyTitle,
|
||||||
|
message: context.lang.manualVerificationMissingKeyDescription(
|
||||||
|
displayName,
|
||||||
|
),
|
||||||
|
onRetry: () => setState(_loadSafetyNumber),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _SafetyNumberContent(
|
||||||
|
safetyNumber: safetyNumber,
|
||||||
|
description: context.lang.manualVerificationDescription(
|
||||||
|
displayName,
|
||||||
|
),
|
||||||
|
isSaving: _isSaving,
|
||||||
|
onVerify: _markAsVerified,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SafetyNumberContent extends StatelessWidget {
|
||||||
|
const _SafetyNumberContent({
|
||||||
|
required this.safetyNumber,
|
||||||
|
required this.description,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.onVerify,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String safetyNumber;
|
||||||
|
final String description;
|
||||||
|
final bool isSaving;
|
||||||
|
final VoidCallback onVerify;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final groups = <String>[
|
||||||
|
for (var offset = 0; offset < safetyNumber.length; offset += 5)
|
||||||
|
safetyNumber.substring(offset, offset + 5),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 28, 24, 40),
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 32, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.fingerprint,
|
||||||
|
size: 42,
|
||||||
|
color: context.color.onPrimaryContainer,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
for (var row = 0; row < 3; row++) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
for (final group in groups.skip(row * 4).take(4))
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(
|
||||||
|
group,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onPrimaryContainer,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (row < 2) const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
IntrinsicWidth(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.primaryMiddle,
|
||||||
|
onPressed: isSaving ? null : onVerify,
|
||||||
|
child: isSaving
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
context.lang.manualVerificationMarkVerified,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
|
child: Text(
|
||||||
|
description,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
fontSize: 14,
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SafetyNumberError extends StatelessWidget {
|
||||||
|
const _SafetyNumberError({
|
||||||
|
required this.message,
|
||||||
|
required this.onRetry,
|
||||||
|
this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? title;
|
||||||
|
final String message;
|
||||||
|
final VoidCallback onRetry;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.key_off_rounded,
|
||||||
|
size: 54,
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
if (title != null) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
title!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.color.onSurfaceVariant,
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.45,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: onRetry,
|
||||||
|
child: Text(context.lang.manualVerificationRetry),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::bridge::get_twonly_flutter;
|
use crate::bridge::get_twonly_flutter;
|
||||||
|
use crate::context::Context;
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
|
use crate::services::contacts::ContactService;
|
||||||
use crate::signal::engine::{FrbPqcPreKey, FrbPreKeyBundle};
|
use crate::signal::engine::{FrbPqcPreKey, FrbPreKeyBundle};
|
||||||
|
use crate::signal::reset::SessionResetLimiter;
|
||||||
|
|
||||||
pub struct RustSignal {}
|
pub struct RustSignal {}
|
||||||
|
|
||||||
|
|
@ -59,4 +62,34 @@ impl RustSignal {
|
||||||
.get_contact_identity_key(&contact_id.to_string())
|
.get_contact_identity_key(&contact_id.to_string())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_safety_number(contact_id: i64) -> Result<Option<String>> {
|
||||||
|
let guard = get_twonly_flutter()?.signal_engine.lock().await;
|
||||||
|
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
||||||
|
engine.get_safety_number(contact_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the current session with a contact and immediately establishes
|
||||||
|
/// a fresh one while requiring the contact's stored identity key to match.
|
||||||
|
pub async fn reset_contact_session(contact_id: i64) -> Result<()> {
|
||||||
|
let ctx = Context::get_static()?;
|
||||||
|
let expected_public_key = {
|
||||||
|
let guard = ctx.signal_engine.lock().await;
|
||||||
|
let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?;
|
||||||
|
let public_key = engine
|
||||||
|
.get_contact_identity_key(&contact_id.to_string())
|
||||||
|
.await?;
|
||||||
|
engine
|
||||||
|
.reset_session(&contact_id.to_string(), 1, true)
|
||||||
|
.await?;
|
||||||
|
public_key
|
||||||
|
};
|
||||||
|
|
||||||
|
ContactService::new(ctx)
|
||||||
|
.establish_signal_session(contact_id, expected_public_key)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let database = ctx.rust_db.read().await.clone();
|
||||||
|
SessionResetLimiter::clear(&database.pool, &contact_id.to_string(), 1).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2068471902;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1675944024;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -5735,6 +5735,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_signal_get_safety_number",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::bridge::wrapper::signal::RustSignal::get_safety_number(
|
||||||
|
api_contact_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(
|
fn wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -5817,6 +5857,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_signal_reset_contact_session",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_contact_id = <i64>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok =
|
||||||
|
crate::bridge::wrapper::signal::RustSignal::reset_contact_session(
|
||||||
|
api_contact_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(
|
fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -7982,20 +8062,22 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
156 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
156 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
157 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
157 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
158 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
158 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
159 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
159 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
||||||
160 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
160 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
161 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
161 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
162 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
162 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
||||||
163 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
163 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||||
164 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
164 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||||
165 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
165 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
166 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
166 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||||
167 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
167 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||||
169 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
168 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||||
170 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
169 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||||
171 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
171 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||||
172 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
172 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||||
173 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
173 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
174 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
175 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8013,12 +8095,12 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||||
51 => {
|
51 => {
|
||||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
168 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
170 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||||
ptr,
|
ptr,
|
||||||
rust_vec_len,
|
rust_vec_len,
|
||||||
data_len,
|
data_len,
|
||||||
),
|
),
|
||||||
174 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
176 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ use crate::error::{Result, TwonlyError};
|
||||||
use crate::user_config::UserConfig;
|
use crate::user_config::UserConfig;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use libsignal_protocol::{
|
use libsignal_protocol::{
|
||||||
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey,
|
message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, Fingerprint,
|
||||||
IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle,
|
GenericSignedPreKey, IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId,
|
||||||
PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SessionStore,
|
KyberPreKeyStore, PreKeyBundle, PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress,
|
||||||
SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp,
|
PublicKey, SessionStore, SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
@ -50,6 +50,29 @@ pub struct FrbPqcPreKey {
|
||||||
pub kyber_pre_key_signature: Vec<u8>,
|
pub kyber_pre_key_signature: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_safety_number(
|
||||||
|
local_id: &str,
|
||||||
|
local_key_bytes: &[u8],
|
||||||
|
remote_id: &str,
|
||||||
|
remote_key_bytes: &[u8],
|
||||||
|
) -> Result<String> {
|
||||||
|
let local_key = IdentityKey::decode(local_key_bytes)
|
||||||
|
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
||||||
|
let remote_key = IdentityKey::decode(remote_key_bytes)
|
||||||
|
.map_err(|error| TwonlyError::Signal(error.to_string()))?;
|
||||||
|
|
||||||
|
Fingerprint::new(
|
||||||
|
2,
|
||||||
|
5_200,
|
||||||
|
local_id.as_bytes(),
|
||||||
|
&local_key,
|
||||||
|
remote_id.as_bytes(),
|
||||||
|
&remote_key,
|
||||||
|
)
|
||||||
|
.and_then(|fingerprint| fingerprint.display_string())
|
||||||
|
.map_err(|error| TwonlyError::Signal(error.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
impl RustSignalEngine {
|
impl RustSignalEngine {
|
||||||
pub async fn on_connected(&self, ctx: &Arc<crate::context::Context>) -> Result<()> {
|
pub async fn on_connected(&self, ctx: &Arc<crate::context::Context>) -> Result<()> {
|
||||||
let base = UserConfig::load_required_from(ctx)?;
|
let base = UserConfig::load_required_from(ctx)?;
|
||||||
|
|
@ -165,6 +188,26 @@ impl RustSignalEngine {
|
||||||
Ok(identity)
|
Ok(identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_safety_number(&self, contact_id: i64) -> Result<Option<String>> {
|
||||||
|
let Some(contact_key_bytes) = self
|
||||||
|
.get_contact_identity_key(&contact_id.to_string())
|
||||||
|
.await?
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Signal's standard 60-digit numeric fingerprint. Both participants
|
||||||
|
// derive the same display string because libsignal sorts the two
|
||||||
|
// independently generated fingerprint halves before displaying them.
|
||||||
|
create_safety_number(
|
||||||
|
&self.local_name,
|
||||||
|
&self.get_identity_key().await?,
|
||||||
|
&contact_id.to_string(),
|
||||||
|
&contact_key_bytes,
|
||||||
|
)
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn generate_identity_key_pair() -> Result<Vec<u8>> {
|
fn generate_identity_key_pair() -> Result<Vec<u8>> {
|
||||||
let mut csprng = rand::rngs::StdRng::from_os_rng();
|
let mut csprng = rand::rngs::StdRng::from_os_rng();
|
||||||
|
|
@ -678,6 +721,29 @@ mod tests {
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn safety_number_matches_signal_format_and_is_symmetric() {
|
||||||
|
let alice_key =
|
||||||
|
hex::decode("0506863bc66d02b40d27b8d49ca7c09e9239236f9d7d25d6fcca5ce13c7064d868")
|
||||||
|
.unwrap();
|
||||||
|
let bob_key =
|
||||||
|
hex::decode("05f781b6fb32fed9ba1cf2de978d4d5da28dc34046ae814402b5c0dbd96fda907b")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let alice =
|
||||||
|
create_safety_number("+14152222222", &alice_key, "+14153333333", &bob_key).unwrap();
|
||||||
|
let bob =
|
||||||
|
create_safety_number("+14153333333", &bob_key, "+14152222222", &alice_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(alice, bob);
|
||||||
|
assert_eq!(
|
||||||
|
alice,
|
||||||
|
"300354477692869396892869876765458257569162576843440918079131"
|
||||||
|
);
|
||||||
|
assert_eq!(alice.len(), 60);
|
||||||
|
assert!(alice.bytes().all(|byte| byte.is_ascii_digit()));
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_test_engine(name: &str) -> (RustSignalEngine, tempfile::TempDir) {
|
async fn create_test_engine(name: &str) -> (RustSignalEngine, tempfile::TempDir) {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let db_path = dir.path().join(format!("{}.db", name));
|
let db_path = dir.path().join(format!("{}.db", name));
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,16 @@ void main() {
|
||||||
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('isContactVerified returns true for manual comparison', () async {
|
||||||
|
await insertContact(10);
|
||||||
|
await addDirectVerification(10, VerificationType.manual);
|
||||||
|
|
||||||
|
final verifications = await twonlyDB.keyVerificationDao
|
||||||
|
.getContactVerification(10);
|
||||||
|
expect(verifications.single.type, VerificationType.manual);
|
||||||
|
expect(await twonlyDB.keyVerificationDao.isContactVerified(10), true);
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'isContactVerified returns false when no verification exists',
|
'isContactVerified returns false when no verification exists',
|
||||||
() async {
|
() async {
|
||||||
|
|
|
||||||
56
test/services/verification_link_handling_test.dart
Normal file
56
test/services/verification_link_handling_test.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:twonly/src/services/intent/links.intent.dart';
|
||||||
|
import 'package:twonly/src/utils/qr.utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
test('invalid QR profile links return a visible failure status', () async {
|
||||||
|
final result = await QrCodeUtils.handleQrCodeLink(
|
||||||
|
'${QrCodeUtils.linkPrefix}not-a-valid-profile',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.status, QrCodeLinkStatus.invalid);
|
||||||
|
expect(result.profile, isNull);
|
||||||
|
expect(result.contact, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shared URLs are awaited by the common URL handler', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
late BuildContext context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (builderContext) {
|
||||||
|
context = builderContext;
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Uri? receivedUri;
|
||||||
|
var callbackFinished = false;
|
||||||
|
await handleIntentSharedFile(
|
||||||
|
context,
|
||||||
|
[
|
||||||
|
SharedFile(
|
||||||
|
value: 'https://me.twonly.eu/alice#public-key',
|
||||||
|
type: SharedMediaType.URL,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
(uri) async {
|
||||||
|
receivedUri = uri;
|
||||||
|
await Future<void>.value();
|
||||||
|
callbackFinished = true;
|
||||||
|
},
|
||||||
|
(_, _) {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(receivedUri, Uri.parse('https://me.twonly.eu/alice#public-key'));
|
||||||
|
expect(callbackFinished, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue