new add contact view for scanned qr via link

This commit is contained in:
otsmr 2026-04-25 00:24:03 +02:00
parent 3c91f99008
commit 646b9c22d3
11 changed files with 174 additions and 11 deletions

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -115,7 +116,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _isUserCreated = false; bool _isUserCreated = false;
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = false; bool _skipBackup = kDebugMode;
bool _isTwonlyLocked = true; bool _isTwonlyLocked = true;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);

View file

@ -4,11 +4,11 @@ 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/visual/views/camera/camera_qr_scanner.view.dart'; import 'package:twonly/src/visual/views/camera/camera_qr_scanner.view.dart';
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart'; import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
import 'package:twonly/src/visual/views/chats/add_new_user.view.dart';
import 'package:twonly/src/visual/views/chats/archived_chats.view.dart'; import 'package:twonly/src/visual/views/chats/archived_chats.view.dart';
import 'package:twonly/src/visual/views/chats/chat_messages.view.dart'; import 'package:twonly/src/visual/views/chats/chat_messages.view.dart';
import 'package:twonly/src/visual/views/chats/media_viewer.view.dart'; import 'package:twonly/src/visual/views/chats/media_viewer.view.dart';
import 'package:twonly/src/visual/views/chats/start_new_chat.view.dart'; import 'package:twonly/src/visual/views/chats/start_new_chat.view.dart';
import 'package:twonly/src/visual/views/contact/add_new_contact.view.dart';
import 'package:twonly/src/visual/views/contact/contact.view.dart'; import 'package:twonly/src/visual/views/contact/contact.view.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart'; import 'package:twonly/src/visual/views/groups/group_create_select_members.view.dart';

View file

@ -19,7 +19,8 @@ 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/views/camera/share_image_editor.view.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor.view.dart';
import 'package:twonly/src/visual/views/chats/add_new_user.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';
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.startsWith('http')) return false;
@ -50,9 +51,9 @@ Future<bool> handleIntentUrl(BuildContext context, Uri uri) async {
} }
} else { } else {
await context.navPush( await context.navPush(
AddNewUserView( AddContactViaQrLinkView(
username: profile.username, profile: profile,
publicKey: Uint8List.fromList(profile.publicIdentityKey), qrCodeLink: uri.toString(),
), ),
); );
} }

View file

@ -78,7 +78,7 @@ class QrCodeUtils {
profile.userId.toInt(), profile.userId.toInt(),
); );
if (contact == null || !contact.accepted) { if (contact == null) {
if (profile.username == userService.currentUser.username) { if (profile.username == userService.currentUser.username) {
return null; return null;
} }

View file

@ -0,0 +1,154 @@
import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/client/generated/qr.pb.dart';
import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/qr.utils.dart';
class AddContactViaQrLinkView extends StatefulWidget {
const AddContactViaQrLinkView({
required this.profile,
this.qrCodeLink,
super.key,
});
final PublicProfile profile;
final String? qrCodeLink;
@override
State<AddContactViaQrLinkView> createState() =>
_AddContactViaQrLinkViewState();
}
class _AddContactViaQrLinkViewState extends State<AddContactViaQrLinkView> {
bool _isLoading = false;
Future<void> _sendFollowRequest() async {
setState(() {
_isLoading = true;
});
try {
final userData = server.Response_UserData(
userId: widget.profile.userId,
publicIdentityKey: widget.profile.publicIdentityKey,
signedPrekey: widget.profile.signedPrekey,
signedPrekeySignature: widget.profile.signedPrekeySignature,
signedPrekeyId: widget.profile.signedPrekeyId,
username: utf8.encode(widget.profile.username),
registrationId: widget.profile.registrationId,
);
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 importSignalContactAndCreateRequest(userData);
if (widget.qrCodeLink != null) {
// As the user does now exist he can now be marked as verified
await QrCodeUtils.handleQrCodeLink(widget.qrCodeLink!);
}
}
if (mounted) {
context.pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.lang.addFriendTitle),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(),
CircleAvatar(
radius: 50,
backgroundColor: context.color.primaryContainer,
child: FaIcon(
FontAwesomeIcons.user,
size: 40,
color: context.color.onPrimaryContainer,
),
),
const SizedBox(height: 20),
Text(
widget.profile.username,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: context.color.onSurface,
),
),
const SizedBox(height: 10),
Text(
context.lang.userFoundBody,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: context.color.onSurfaceVariant,
),
),
const Spacer(),
const SizedBox(width: 16),
Center(
child: FilledButton(
onPressed: _isLoading ? null : _sendFollowRequest,
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(context.lang.createContactRequest),
),
),
const SizedBox(height: 8),
Center(
child: OutlinedButton(
onPressed: _isLoading ? null : () => context.pop(),
child: Text(context.lang.cancel),
),
),
const SizedBox(height: 20),
],
),
),
),
);
}
}

View file

@ -14,8 +14,8 @@ import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/chats/add_new_user_components/friend_suggestions.comp.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
import 'package:twonly/src/visual/views/chats/add_new_user_components/open_requests_list.comp.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart';
class AddNewUserView extends StatefulWidget { class AddNewUserView extends StatefulWidget {
const AddNewUserView({ const AddNewUserView({

View file

@ -11,7 +11,7 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/elements/headline.element.dart';
import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/chats/add_new_user_components/friend_suggestions.comp.dart'; import 'package:twonly/src/visual/views/contact/add_new_contact_components/friend_suggestions.comp.dart';
class OpenRequestsListComp extends StatelessWidget { class OpenRequestsListComp extends StatelessWidget {
const OpenRequestsListComp({ const OpenRequestsListComp({

View file

@ -18,7 +18,7 @@ import 'package:twonly/src/visual/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart'; import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/views/contact/components/restore_flame.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart';
import 'package:twonly/src/visual/views/groups/group.view.dart'; import 'package:twonly/src/visual/views/groups/group.view.dart';
class ContactView extends StatefulWidget { class ContactView extends StatefulWidget {

View file

@ -70,6 +70,13 @@ class HomeViewState extends State<HomeView> {
unawaited(_mainCameraController.selectCamera(0, true)); unawaited(_mainCameraController.selectCamera(0, true));
unawaited(_initAsync()); unawaited(_initAsync());
handleIntentUrl(
context,
Uri.parse(
'https://me.twonly.eu/qr/#EAAauAEIgLDN0Nm7oKh0EghoYWhoaGhoaBohBRZQ8w_zpm1v7SRTdc8GEOMAxuf1caGDlBa-v0ZiTw9qIiEF05juEs1c3yw0STiSwQR7lowDX5hBaxN4YFR0HhkopGIoudTO5wIyQFQRtU1aO7P7O5s2ekB1ppAost3iQQizwhFObjOLgHQnpwcnwEONXZzSADYqCeEoNcvyE45w0v21z1Imhozk3Q44oI0GQhA9U_chIJwwZ7J9fpeXODZF',
),
);
// Subscribe to all events (initial link and further) // Subscribe to all events (initial link and further)
_deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async {
if (!mounted) return; if (!mounted) return;