This commit is contained in:
otsmr 2025-05-31 13:42:12 +02:00
parent ebc4570b9b
commit 27fa177a2e
18 changed files with 266 additions and 106 deletions

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "dependencies/flutter_secure_storage"]
path = dependencies/flutter_secure_storage
url = https://github.com/juliansteenbakker/flutter_secure_storage
[submodule "dependencies/flutter_zxing"]
path = dependencies/flutter_zxing
url = https://github.com/khoren93/flutter_zxing.git

View file

@ -1,5 +1,5 @@
{
"files.exclude": {
"files.watcherExclude": {
"dependencies": false
}
}

View file

@ -13,5 +13,9 @@ pub.dev or because they require some special installation.
```bash
git submodule update --init --recursive
cd dependencies/flutter_zxing
git submodule update --init --recursive
./scripts/update_ios_macos_src.s
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dependencies/flutter_zxing vendored Submodule

@ -0,0 +1 @@
Subproject commit ba65f2fb4a09f4e68f6de64aa1de41ba3dc4977e

View file

@ -71,6 +71,8 @@ PODS:
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- flutter_zxing (0.0.1):
- Flutter
- gal (1.0.0):
- Flutter
- FlutterMacOS
@ -234,6 +236,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- flutter_zxing (from `.symlinks/plugins/flutter_zxing/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- GoogleUtilities
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
@ -290,6 +293,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
flutter_zxing:
:path: ".symlinks/plugins/flutter_zxing/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
image_picker_ios:
@ -337,6 +342,7 @@ SPEC CHECKSUMS:
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_secure_storage_darwin: ce237a8775b39723566dc72571190a3769d70468
flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7

View file

@ -131,6 +131,7 @@
"settingsAccountDeleteModalTitle": "Bist du sicher?",
"settingsAccountDeleteModalBody": "Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.",
"contactVerifyNumberTitle": "Sicherheitsnummer verifizieren",
"contactVerifyNumberTapToScan": "Zum Scannen tippen",
"contactVerifyNumberMarkAsVerified": "Als verifiziert markieren",
"contactVerifyNumberClearVerification": "Verifizierung aufheben",
"contactVerifyNumberLongDesc": "Um die Ende-zu-Ende-Verschlüsselung mit {username} zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.",

View file

@ -231,6 +231,8 @@
"@settingsAccountDeleteModalBody": {},
"contactVerifyNumberTitle": "Verify safety number",
"@contactVerifyNumberTitle": {},
"contactVerifyNumberTapToScan": "Tap to scan",
"@contactVerifyNumberTapToScan": {},
"contactVerifyNumberMarkAsVerified": "Mark as verified",
"@contactVerifyNumberMarkAsVerified": {},
"contactVerifyNumberClearVerification": "Clear verification",

View file

@ -788,6 +788,12 @@ abstract class AppLocalizations {
/// **'Verify safety number'**
String get contactVerifyNumberTitle;
/// No description provided for @contactVerifyNumberTapToScan.
///
/// In en, this message translates to:
/// **'Tap to scan'**
String get contactVerifyNumberTapToScan;
/// No description provided for @contactVerifyNumberMarkAsVerified.
///
/// In en, this message translates to:

View file

@ -385,6 +385,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get contactVerifyNumberTitle => 'Sicherheitsnummer verifizieren';
@override
String get contactVerifyNumberTapToScan => 'Zum Scannen tippen';
@override
String get contactVerifyNumberMarkAsVerified => 'Als verifiziert markieren';

View file

@ -380,6 +380,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get contactVerifyNumberTitle => 'Verify safety number';
@override
String get contactVerifyNumberTapToScan => 'Tap to scan';
@override
String get contactVerifyNumberMarkAsVerified => 'Mark as verified';

View file

@ -30,7 +30,7 @@ class FormattedStringWidget extends StatelessWidget {
Widget build(BuildContext context) {
return SelectableText(
formatString(longString),
style: TextStyle(fontSize: 18, color: Colors.black),
style: TextStyle(fontSize: 16, color: Colors.black),
textAlign: TextAlign.center,
);
}

View file

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lottie/lottie.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/signal/session.signal.dart';
import 'package:twonly/src/views/components/format_long_string.dart';
@ -10,7 +12,9 @@ import 'package:flutter/material.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/contact/contact_verify_qr_scan.view.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:image/image.dart' as imglib;
class ContactVerifyView extends StatefulWidget {
const ContactVerifyView(this.contact, {super.key});
@ -20,31 +24,127 @@ class ContactVerifyView extends StatefulWidget {
State<ContactVerifyView> createState() => _ContactVerifyViewState();
}
enum ScanResult { None, Success, Failed }
class _ContactVerifyViewState extends State<ContactVerifyView> {
Fingerprint? fingerprint;
Fingerprint? _fingerprint;
late Contact _contact;
late StreamSubscription<Contact?> _contactSub;
ScanResult _scanResult = ScanResult.None;
Uint8List? _qrCodeImageBytes;
@override
void initState() {
super.initState();
_contact = widget.contact;
loadAsync();
}
Future loadAsync() async {
fingerprint = await generateSessionFingerPrint(widget.contact.userId);
setState(() {});
@override
void dispose() {
_contactSub.cancel();
super.dispose();
}
Future loadAsync() async {
_fingerprint = await generateSessionFingerPrint(widget.contact.userId);
if (_fingerprint != null) {
final Encode result = zx.encodeBarcode(
contents: base64Encode(
_fingerprint!.scannableFingerprint.fingerprints,
),
params: EncodeParams(
format: Format.qrCode,
width: 150,
height: 150,
margin: 0,
eccLevel: EccLevel.low,
),
);
if (result.isValid && result.data != null) {
final img = imglib.Image.fromBytes(
width: 150,
height: 150,
bytes: result.data!.buffer,
numChannels: 1,
);
_qrCodeImageBytes = imglib.encodePng(img);
}
}
@override
Widget build(BuildContext context) {
Stream<Contact?> contact = twonlyDB.contactsDao
.getContactByUserId(widget.contact.userId)
.watchSingleOrNull();
_contactSub = contact.listen((contact) {
if (contact == null) return;
setState(() {
_contact = contact;
});
});
setState(() {});
}
Future openQrScanner() async {
if (_fingerprint == null) return;
bool? isValid = await Navigator.push(context, MaterialPageRoute(
builder: (context) {
return ContactVerifyQrScanView(
widget.contact,
fingerprint: _fingerprint!,
);
},
));
if (isValid == null) {
return; // user just returned...
}
if (isValid) {
_scanResult = ScanResult.Success;
updateUserVerifyState(true);
} else {
_scanResult = ScanResult.Failed;
updateUserVerifyState(false);
}
setState(() {});
}
Future updateUserVerifyState(bool verified) async {
final update = ContactsCompanion(verified: Value(verified));
await twonlyDB.contactsDao.updateContact(_contact.userId, update);
}
Widget get qrWidget => (_qrCodeImageBytes == null)
? SizedBox(
width: 150,
height: 150,
)
: Image.memory(_qrCodeImageBytes!);
Widget get resultAnimation => SizedBox(
width: 150,
child: Lottie.asset(
(_scanResult == ScanResult.Success)
? 'assets/animations/success.json'
: 'assets/animations/failed.json',
repeat: false,
onLoaded: (p0) {
Future.delayed(Duration(seconds: 3), () {
if (mounted) {
setState(() {
_scanResult = ScanResult.None;
});
}
});
},
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.contactVerifyNumberTitle),
),
body: (fingerprint == null)
body: (_fingerprint == null)
? Center(child: CircularProgressIndicator())
: ListView(
children: [
@ -57,6 +157,8 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).colorScheme.primary,
),
child: GestureDetector(
onTap: openQrScanner,
child: Column(
children: [
Container(
@ -64,26 +166,33 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
borderRadius: BorderRadius.circular(10),
color: Colors.white,
),
child: QrImageView(
data: base64Encode(fingerprint!
.scannableFingerprint.fingerprints),
version: QrVersions.auto,
size: 150.0,
),
),
padding: EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
(_scanResult == ScanResult.None)
? qrWidget
: resultAnimation,
SizedBox(height: 10),
SizedBox(
width: 200,
child: Text(
"QR Code scanning is coming soon. Please compare the numbers manual.",
style:
TextStyle(color: Colors.black, fontSize: 10),
(_scanResult == ScanResult.None)
? context
.lang.contactVerifyNumberTapToScan
: "",
style: TextStyle(
color: Colors.black,
fontSize: 15,
),
textAlign: TextAlign.center,
),
),
],
),
),
SizedBox(height: 20),
FormattedStringWidget(
fingerprint!.displayableFingerprint
_fingerprint!.displayableFingerprint
.getDisplayText(),
),
],
@ -91,21 +200,14 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
),
),
),
StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
return Padding(
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text(
context.lang.contactVerifyNumberLongDesc(
getContactDisplayName(snapshot.data!)),
getContactDisplayName(_contact)),
textAlign: TextAlign.center,
),
);
},
),
Padding(
padding:
@ -133,36 +235,19 @@ class _ContactVerifyViewState extends State<ContactVerifyView> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamBuilder(
stream: contact,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return Container();
}
final contact = snapshot.data!;
if (contact.verified) {
return OutlinedButton.icon(
onPressed: () {
final update =
ContactsCompanion(verified: Value(false));
twonlyDB.contactsDao
.updateContact(contact.userId, update);
},
(_contact.verified)
? OutlinedButton.icon(
onPressed: () => updateUserVerifyState(false),
label: Text(
context.lang.contactVerifyNumberClearVerification),
);
}
return FilledButton.icon(
)
: FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.shieldHeart),
onPressed: () {
final update = ContactsCompanion(verified: Value(true));
twonlyDB.contactsDao
.updateContact(contact.userId, update);
},
label: Text(context.lang.contactVerifyNumberMarkAsVerified),
);
},
onPressed: () => updateUserVerifyState(true),
label: Text(
context.lang.contactVerifyNumberMarkAsVerified,
),
)
],
),
),

View file

@ -0,0 +1,43 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
class ContactVerifyQrScanView extends StatefulWidget {
const ContactVerifyQrScanView(this.contact,
{super.key, required this.fingerprint});
final Fingerprint fingerprint;
final Contact contact;
@override
State<ContactVerifyQrScanView> createState() =>
_ContactVerifyQrScanViewState();
}
class _ContactVerifyQrScanViewState extends State<ContactVerifyQrScanView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ReaderWidget(
onScan: (result) async {
bool isValid = false;
try {
if (result.text != null) {
Uint8List otherFingerPrint = base64Decode(result.text!);
isValid = widget.fingerprint.scannableFingerprint.compareTo(
otherFingerPrint,
);
}
} catch (e) {
Log.error("$e");
}
return Navigator.pop(context, isValid);
},
),
);
}
}

View file

@ -63,40 +63,51 @@ class CreditsView extends StatelessWidget {
)),
),
UrlListTitle(
title: "Free selfie fast Animation",
title: "selfie fast Animation",
subtitle: "Brandon Ambuila",
url:
"https://lottiefiles.com/free-animation/selfie-fast-JZx4Ftrg1E",
),
UrlListTitle(
title: "Free Security status - Safe Animation",
title: "Security status - Safe Animation",
subtitle: "Yogesh Pal",
url:
"https://lottiefiles.com/free-animation/security-status-safe-CePJPAwLVx",
),
UrlListTitle(
title: "Free send mail Animation",
title: "send mail Animation",
subtitle: "jignesh gajjar",
url: "https://lottiefiles.com/free-animation/send-mail-3pvzm2kmNq",
),
UrlListTitle(
title: "Free Present for you Animation",
title: "Present for you Animation",
subtitle: "Tatsiana Melnikova",
url:
"https://lottiefiles.com/free-animation/present-for-you-QalWyuNptY",
),
UrlListTitle(
title: "Free Take a photo Animation",
title: "Take a photo Animation",
subtitle: "Nguyễn Như Lân",
url:
"https://lottiefiles.com/free-animation/take-a-photo-CzOUerxwPP?color-palette=true",
),
UrlListTitle(
title: "Kostenlose Valentine's Day-Animation",
title: "Valentine's Day-Animation",
subtitle: "Strezha",
url:
"https://lottiefiles.com/de/free-animation/valentines-day-1UiMkPHnPK?color-palette=true",
),
UrlListTitle(
title: "success-Animation",
subtitle: "Aman Awasthy",
url:
"https://lottiefiles.com/de/free-animation/success-tick-cuwjLHAR7g",
),
UrlListTitle(
title: "Failed-Animation",
subtitle: "Ahmed Shami أحمد شامي",
url: "https://lottiefiles.com/de/free-animation/failed-e5cQFDEtLv",
),
const Divider(),
ListTile(
title: Center(
@ -105,18 +116,6 @@ class CreditsView extends StatelessWidget {
style: TextStyle(fontWeight: FontWeight.bold),
)),
),
UrlListTitle(
title: "Germany",
subtitle: "by GDJ",
url:
"https://pixabay.com/vectors/republic-germany-deutschland-map-1220652/",
),
UrlListTitle(
title: "Frankfurt am Main",
subtitle: "by GDJ",
url:
"https://pixabay.com/vectors/frankfurt-germany-skyline-cityscape-3166262/",
),
UrlListTitle(
title: "Avo Cardio",
subtitle: "by RalfDesign",
@ -129,12 +128,6 @@ class CreditsView extends StatelessWidget {
url:
"https://pixabay.com/illustrations/sloth-swimming-summer-pool-cartoon-4575121/",
),
UrlListTitle(
title: "Sloth",
subtitle: "by RalfDesign",
url:
"https://pixabay.com/illustrations/sloth-swimming-summer-pool-cartoon-4575121/",
),
UrlListTitle(
title: "Duck",
subtitle: "by lachkegeetanjali",

View file

@ -716,6 +716,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_zxing:
dependency: "direct main"
description:
path: "dependencies/flutter_zxing"
relative: true
source: path
version: "2.1.0"
font_awesome_flutter:
dependency: "direct main"
description:

View file

@ -25,6 +25,8 @@ dependencies:
# flutter_secure_storage: ^10.0.0-beta.4
flutter_secure_storage:
path: ./dependencies/flutter_secure_storage/flutter_secure_storage
flutter_zxing:
path: ./dependencies/flutter_zxing
font_awesome_flutter: ^10.8.0
gal: ^2.3.1
hand_signature: ^3.0.3
@ -46,7 +48,6 @@ dependencies:
protobuf: ^4.0.0
cryptography_plus: ^2.7.0
provider: ^6.1.2
qr_flutter: ^4.1.0
restart_app: ^1.3.2
screenshot: ^3.0.0
url_launcher: ^6.3.1