mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-06-07 14:12:41 -04:00
refactor for qr scan
This commit is contained in:
parent
ef2d4ce308
commit
e5f1619c65
4 changed files with 499 additions and 582 deletions
|
@ -83,7 +83,7 @@
|
||||||
"copy_invitation": "Copy Invitation",
|
"copy_invitation": "Copy Invitation",
|
||||||
"invitation_copied": "Invitation Copied"
|
"invitation_copied": "Invitation Copied"
|
||||||
},
|
},
|
||||||
"contact_invite": {
|
"invite_dialog": {
|
||||||
"message_from_contact": "Message from contact",
|
"message_from_contact": "Message from contact",
|
||||||
"validating": "Validating...",
|
"validating": "Validating...",
|
||||||
"failed_to_accept": "Failed to accept contact invite",
|
"failed_to_accept": "Failed to accept contact invite",
|
||||||
|
@ -99,6 +99,11 @@
|
||||||
"paste_invite_here": "Paste your contact invite here:",
|
"paste_invite_here": "Paste your contact invite here:",
|
||||||
"paste": "Paste"
|
"paste": "Paste"
|
||||||
},
|
},
|
||||||
|
"scan_invite_dialog": {
|
||||||
|
"title": "Scan Contact Invite",
|
||||||
|
"scan_invite_here": "Scan your contact invite QR code here:",
|
||||||
|
"scan": "Scan"
|
||||||
|
},
|
||||||
"enter_pin_dialog": {
|
"enter_pin_dialog": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "Enter PIN",
|
||||||
"reenter_pin": "Re-Enter PIN To Confirm",
|
"reenter_pin": "Re-Enter PIN To Confirm",
|
||||||
|
|
343
lib/components/invite_dialog.dart
Normal file
343
lib/components/invite_dialog.dart
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
|
||||||
|
import '../entities/local_account.dart';
|
||||||
|
import '../providers/account.dart';
|
||||||
|
import '../providers/contact.dart';
|
||||||
|
import '../providers/contact_invite.dart';
|
||||||
|
import '../tools/tools.dart';
|
||||||
|
import 'enter_password.dart';
|
||||||
|
import 'enter_pin.dart';
|
||||||
|
import 'profile_widget.dart';
|
||||||
|
|
||||||
|
class InviteDialog extends ConsumerStatefulWidget {
|
||||||
|
const InviteDialog(
|
||||||
|
{required this.onValidationCancelled,
|
||||||
|
required this.onValidationSuccess,
|
||||||
|
required this.onValidationFailed,
|
||||||
|
required this.inviteControlIsValid,
|
||||||
|
required this.buildInviteControl,
|
||||||
|
super.key});
|
||||||
|
|
||||||
|
final void Function() onValidationCancelled;
|
||||||
|
final void Function() onValidationSuccess;
|
||||||
|
final void Function() onValidationFailed;
|
||||||
|
final bool Function() inviteControlIsValid;
|
||||||
|
final Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
InviteDialogState dialogState,
|
||||||
|
Future<void> Function({required Uint8List inviteData})
|
||||||
|
validateInviteData) buildInviteControl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
InviteDialogState createState() => InviteDialogState();
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(ObjectFlagProperty<void Function()>.has(
|
||||||
|
'onValidationCancelled', onValidationCancelled))
|
||||||
|
..add(ObjectFlagProperty<void Function()>.has(
|
||||||
|
'onValidationSuccess', onValidationSuccess))
|
||||||
|
..add(ObjectFlagProperty<void Function()>.has(
|
||||||
|
'onValidationFailed', onValidationFailed))
|
||||||
|
..add(ObjectFlagProperty<void Function()>.has(
|
||||||
|
'inviteControlIsValid', inviteControlIsValid))
|
||||||
|
..add(ObjectFlagProperty<
|
||||||
|
Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
InviteDialogState dialogState,
|
||||||
|
Future<void> Function({required Uint8List inviteData})
|
||||||
|
validateInviteData)>.has(
|
||||||
|
'buildInviteControl', buildInviteControl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InviteDialogState extends ConsumerState<InviteDialog> {
|
||||||
|
ValidContactInvitation? _validInvitation;
|
||||||
|
bool _isValidating = false;
|
||||||
|
bool _isAccepting = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isValidating => _isValidating;
|
||||||
|
bool get isAccepting => _isAccepting;
|
||||||
|
|
||||||
|
Future<void> _onAccept() async {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = true;
|
||||||
|
});
|
||||||
|
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||||
|
if (activeAccountInfo == null) {
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = false;
|
||||||
|
});
|
||||||
|
navigator.pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final validInvitation = _validInvitation;
|
||||||
|
if (validInvitation != null) {
|
||||||
|
final acceptedContact =
|
||||||
|
await acceptContactInvitation(activeAccountInfo, validInvitation);
|
||||||
|
if (acceptedContact != null) {
|
||||||
|
// initiator when accept is received will create
|
||||||
|
// contact in the case of a 'note to self'
|
||||||
|
final isSelf =
|
||||||
|
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
|
||||||
|
acceptedContact.remoteIdentity.identityPublicKey;
|
||||||
|
if (!isSelf) {
|
||||||
|
await createContact(
|
||||||
|
activeAccountInfo: activeAccountInfo,
|
||||||
|
profile: acceptedContact.profile,
|
||||||
|
remoteIdentity: acceptedContact.remoteIdentity,
|
||||||
|
remoteConversationRecordKey:
|
||||||
|
acceptedContact.remoteConversationRecordKey,
|
||||||
|
localConversationRecordKey:
|
||||||
|
acceptedContact.localConversationRecordKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ref
|
||||||
|
..invalidate(fetchContactInvitationRecordsProvider)
|
||||||
|
..invalidate(fetchContactListProvider);
|
||||||
|
} else {
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast(context, 'invite_dialog.failed_to_accept');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = false;
|
||||||
|
});
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onReject() async {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = true;
|
||||||
|
});
|
||||||
|
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
||||||
|
if (activeAccountInfo == null) {
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = false;
|
||||||
|
});
|
||||||
|
navigator.pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final validInvitation = _validInvitation;
|
||||||
|
if (validInvitation != null) {
|
||||||
|
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
|
||||||
|
// do nothing right now
|
||||||
|
} else {
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast(context, 'invite_dialog.failed_to_reject');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isAccepting = false;
|
||||||
|
});
|
||||||
|
navigator.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _validateInviteData({
|
||||||
|
required Uint8List inviteData,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final activeAccountInfo =
|
||||||
|
await ref.read(fetchActiveAccountProvider.future);
|
||||||
|
if (activeAccountInfo == null) {
|
||||||
|
setState(() {
|
||||||
|
_isValidating = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final contactInvitationRecords =
|
||||||
|
await ref.read(fetchContactInvitationRecordsProvider.future);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isValidating = true;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
|
final validatedContactInvitation = await validateContactInvitation(
|
||||||
|
activeAccountInfo: activeAccountInfo,
|
||||||
|
contactInvitationRecords: contactInvitationRecords,
|
||||||
|
inviteData: inviteData,
|
||||||
|
getEncryptionKeyCallback:
|
||||||
|
(cs, encryptionKeyType, encryptedSecret) async {
|
||||||
|
String encryptionKey;
|
||||||
|
switch (encryptionKeyType) {
|
||||||
|
case EncryptionKeyType.none:
|
||||||
|
encryptionKey = '';
|
||||||
|
case EncryptionKeyType.pin:
|
||||||
|
final description =
|
||||||
|
translate('invite_dialog.protected_with_pin');
|
||||||
|
if (!context.mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final pin = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => EnterPinDialog(
|
||||||
|
reenter: false, description: description));
|
||||||
|
if (pin == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
encryptionKey = pin;
|
||||||
|
case EncryptionKeyType.password:
|
||||||
|
final description =
|
||||||
|
translate('invite_dialog.protected_with_password');
|
||||||
|
if (!context.mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final password = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
EnterPasswordDialog(description: description));
|
||||||
|
if (password == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
encryptionKey = password;
|
||||||
|
}
|
||||||
|
return decryptSecretFromBytes(
|
||||||
|
secretBytes: encryptedSecret,
|
||||||
|
cryptoKind: cs.kind(),
|
||||||
|
encryptionKeyType: encryptionKeyType,
|
||||||
|
encryptionKey: encryptionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if validation was cancelled
|
||||||
|
if (validatedContactInvitation == null) {
|
||||||
|
setState(() {
|
||||||
|
_isValidating = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
widget.onValidationCancelled();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify expiration
|
||||||
|
// xxx
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
widget.onValidationSuccess();
|
||||||
|
_isValidating = false;
|
||||||
|
_validInvitation = validatedContactInvitation;
|
||||||
|
});
|
||||||
|
} on ContactInviteInvalidKeyException catch (e) {
|
||||||
|
String errorText;
|
||||||
|
switch (e.type) {
|
||||||
|
case EncryptionKeyType.none:
|
||||||
|
errorText = translate('invite_dialog.invalid_invitation');
|
||||||
|
case EncryptionKeyType.pin:
|
||||||
|
errorText = translate('invite_dialog.invalid_pin');
|
||||||
|
case EncryptionKeyType.password:
|
||||||
|
errorText = translate('invite_dialog.invalid_password');
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast(context, errorText);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isValidating = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
widget.onValidationFailed();
|
||||||
|
});
|
||||||
|
} on Exception catch (_) {
|
||||||
|
setState(() {
|
||||||
|
_isValidating = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
widget.onValidationFailed();
|
||||||
|
});
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: prefer_expression_function_bodies
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// final theme = Theme.of(context);
|
||||||
|
// final scale = theme.extension<ScaleScheme>()!;
|
||||||
|
// final textTheme = theme.textTheme;
|
||||||
|
// final height = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
|
if (_isAccepting) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 300,
|
||||||
|
width: 300,
|
||||||
|
child: buildProgressIndicator(context).toCenter())
|
||||||
|
.paddingAll(16);
|
||||||
|
}
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
widget.buildInviteControl(context, this, _validateInviteData),
|
||||||
|
if (_isValidating)
|
||||||
|
Column(children: [
|
||||||
|
Text(translate('invite_dialog.validating'))
|
||||||
|
.paddingLTRB(0, 0, 0, 16),
|
||||||
|
buildProgressIndicator(context).paddingAll(16),
|
||||||
|
]).toCenter(),
|
||||||
|
if (_validInvitation == null &&
|
||||||
|
!_isValidating &&
|
||||||
|
widget.inviteControlIsValid())
|
||||||
|
Column(children: [
|
||||||
|
Text(translate('invite_dialog.invalid_invitation')),
|
||||||
|
const Icon(Icons.error)
|
||||||
|
]).paddingAll(16).toCenter(),
|
||||||
|
if (_validInvitation != null && !_isValidating)
|
||||||
|
Column(children: [
|
||||||
|
Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 64),
|
||||||
|
width: double.infinity,
|
||||||
|
child: ProfileWidget(
|
||||||
|
name: _validInvitation!
|
||||||
|
.contactRequestPrivate.profile.name,
|
||||||
|
title: _validInvitation!
|
||||||
|
.contactRequestPrivate.profile.title))
|
||||||
|
.paddingLTRB(0, 0, 0, 8),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check_circle),
|
||||||
|
label: Text(translate('button.accept')),
|
||||||
|
onPressed: _onAccept,
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.cancel),
|
||||||
|
label: Text(translate('button.reject')),
|
||||||
|
onPressed: _onReject,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(DiagnosticsProperty<bool>('isValidating', isValidating))
|
||||||
|
..add(DiagnosticsProperty<bool>('isAccepting', isAccepting));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
|
||||||
import '../entities/local_account.dart';
|
|
||||||
import '../providers/account.dart';
|
|
||||||
import '../providers/contact.dart';
|
|
||||||
import '../providers/contact_invite.dart';
|
|
||||||
import '../tools/tools.dart';
|
import '../tools/tools.dart';
|
||||||
import '../veilid_support/veilid_support.dart';
|
import '../veilid_support/veilid_support.dart';
|
||||||
import 'enter_password.dart';
|
import 'invite_dialog.dart';
|
||||||
import 'enter_pin.dart';
|
|
||||||
import 'profile_widget.dart';
|
|
||||||
|
|
||||||
class PasteInviteDialog extends ConsumerStatefulWidget {
|
class PasteInviteDialog extends ConsumerStatefulWidget {
|
||||||
const PasteInviteDialog({super.key});
|
const PasteInviteDialog({super.key});
|
||||||
|
@ -32,315 +27,97 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
|
||||||
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
||||||
final _pasteTextController = TextEditingController();
|
final _pasteTextController = TextEditingController();
|
||||||
|
|
||||||
ValidContactInvitation? _validInvitation;
|
|
||||||
bool _validatingPaste = false;
|
|
||||||
bool _isAccepting = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAccept() async {
|
Future<void> _onPasteChanged(
|
||||||
final navigator = Navigator.of(context);
|
String text,
|
||||||
|
Future<void> Function({
|
||||||
setState(() {
|
required Uint8List inviteData,
|
||||||
_isAccepting = true;
|
}) validateInviteData) async {
|
||||||
});
|
final lines = text.split('\n');
|
||||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
if (lines.isEmpty) {
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final validInvitation = _validInvitation;
|
|
||||||
if (validInvitation != null) {
|
var firstline =
|
||||||
final acceptedContact =
|
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
||||||
await acceptContactInvitation(activeAccountInfo, validInvitation);
|
firstline += 1;
|
||||||
if (acceptedContact != null) {
|
|
||||||
// initiator when accept is received will create
|
var lastline =
|
||||||
// contact in the case of a 'note to self'
|
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
||||||
final isSelf =
|
if (lastline == -1) {
|
||||||
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
|
lastline = lines.length;
|
||||||
acceptedContact.remoteIdentity.identityPublicKey;
|
|
||||||
if (!isSelf) {
|
|
||||||
await createContact(
|
|
||||||
activeAccountInfo: activeAccountInfo,
|
|
||||||
profile: acceptedContact.profile,
|
|
||||||
remoteIdentity: acceptedContact.remoteIdentity,
|
|
||||||
remoteConversationRecordKey:
|
|
||||||
acceptedContact.remoteConversationRecordKey,
|
|
||||||
localConversationRecordKey:
|
|
||||||
acceptedContact.localConversationRecordKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ref
|
|
||||||
..invalidate(fetchContactInvitationRecordsProvider)
|
|
||||||
..invalidate(fetchContactListProvider);
|
|
||||||
} else {
|
|
||||||
if (context.mounted) {
|
|
||||||
showErrorToast(context, 'contact_invite.failed_to_accept');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setState(() {
|
if (lastline <= firstline) {
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onReject() async {
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = true;
|
|
||||||
});
|
|
||||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final validInvitation = _validInvitation;
|
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
||||||
if (validInvitation != null) {
|
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||||
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
|
|
||||||
// do nothing right now
|
await validateInviteData(inviteData: inviteData);
|
||||||
} else {
|
|
||||||
if (context.mounted) {
|
|
||||||
showErrorToast(context, 'contact_invite.failed_to_reject');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onPasteChanged(String text) async {
|
void onValidationCancelled() {
|
||||||
try {
|
_pasteTextController.clear();
|
||||||
final lines = text.split('\n');
|
|
||||||
if (lines.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstline =
|
|
||||||
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
|
||||||
firstline += 1;
|
|
||||||
|
|
||||||
var lastline =
|
|
||||||
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
|
||||||
if (lastline == -1) {
|
|
||||||
lastline = lines.length;
|
|
||||||
}
|
|
||||||
if (lastline <= firstline) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
|
||||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
|
||||||
|
|
||||||
final activeAccountInfo =
|
|
||||||
await ref.read(fetchActiveAccountProvider.future);
|
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final contactInvitationRecords =
|
|
||||||
await ref.read(fetchContactInvitationRecordsProvider.future);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = true;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
final validatedContactInvitation = await validateContactInvitation(
|
|
||||||
activeAccountInfo: activeAccountInfo,
|
|
||||||
contactInvitationRecords: contactInvitationRecords,
|
|
||||||
inviteData: inviteData,
|
|
||||||
getEncryptionKeyCallback:
|
|
||||||
(cs, encryptionKeyType, encryptedSecret) async {
|
|
||||||
String encryptionKey;
|
|
||||||
switch (encryptionKeyType) {
|
|
||||||
case EncryptionKeyType.none:
|
|
||||||
encryptionKey = '';
|
|
||||||
case EncryptionKeyType.pin:
|
|
||||||
final description =
|
|
||||||
translate('contact_invite.protected_with_pin');
|
|
||||||
if (!context.mounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final pin = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => EnterPinDialog(
|
|
||||||
reenter: false, description: description));
|
|
||||||
if (pin == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
encryptionKey = pin;
|
|
||||||
case EncryptionKeyType.password:
|
|
||||||
final description =
|
|
||||||
translate('contact_invite.protected_with_password');
|
|
||||||
if (!context.mounted) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final password = await showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
EnterPasswordDialog(description: description));
|
|
||||||
if (password == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
encryptionKey = password;
|
|
||||||
}
|
|
||||||
return decryptSecretFromBytes(
|
|
||||||
secretBytes: encryptedSecret,
|
|
||||||
cryptoKind: cs.kind(),
|
|
||||||
encryptionKeyType: encryptionKeyType,
|
|
||||||
encryptionKey: encryptionKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if validation was cancelled
|
|
||||||
if (validatedContactInvitation == null) {
|
|
||||||
setState(() {
|
|
||||||
_pasteTextController.text = '';
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify expiration
|
|
||||||
// xxx
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = validatedContactInvitation;
|
|
||||||
});
|
|
||||||
} on ContactInviteInvalidKeyException catch (e) {
|
|
||||||
String errorText;
|
|
||||||
switch (e.type) {
|
|
||||||
case EncryptionKeyType.none:
|
|
||||||
errorText = translate('contact_invite.invalid_invitation');
|
|
||||||
case EncryptionKeyType.pin:
|
|
||||||
errorText = translate('contact_invite.invalid_pin');
|
|
||||||
case EncryptionKeyType.password:
|
|
||||||
errorText = translate('contact_invite.invalid_password');
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
showErrorToast(context, errorText);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_pasteTextController.text = '';
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
} on Exception catch (_) {
|
|
||||||
setState(() {
|
|
||||||
_pasteTextController.text = '';
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void onValidationSuccess() {
|
||||||
// ignore: prefer_expression_function_bodies
|
//_pasteTextController.clear();
|
||||||
Widget build(BuildContext context) {
|
}
|
||||||
|
|
||||||
|
void onValidationFailed() {
|
||||||
|
_pasteTextController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inviteControlIsValid() => _pasteTextController.text.isNotEmpty;
|
||||||
|
|
||||||
|
Widget buildInviteControl(
|
||||||
|
BuildContext context,
|
||||||
|
InviteDialogState dialogState,
|
||||||
|
Future<void> Function({required Uint8List inviteData})
|
||||||
|
validateInviteData) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
//final scale = theme.extension<ScaleScheme>()!;
|
//final scale = theme.extension<ScaleScheme>()!;
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
//final height = MediaQuery.of(context).size.height;
|
//final height = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
if (_isAccepting) {
|
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
return SizedBox(height: 400, child: waitingPage(context));
|
Text(
|
||||||
}
|
translate('paste_invite_dialog.paste_invite_here'),
|
||||||
return ConstrainedBox(
|
).paddingLTRB(0, 0, 0, 8),
|
||||||
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
|
Container(
|
||||||
child: SingleChildScrollView(
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
padding: const EdgeInsets.all(16),
|
child: TextField(
|
||||||
child: Column(
|
enabled: !dialogState.isValidating,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
onChanged: (text) async =>
|
||||||
mainAxisSize: MainAxisSize.min,
|
_onPasteChanged(text, validateInviteData),
|
||||||
children: <Widget>[
|
style: textTheme.labelSmall!
|
||||||
Text(
|
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
|
||||||
translate('paste_invite_dialog.paste_invite_here'),
|
keyboardType: TextInputType.multiline,
|
||||||
).paddingLTRB(0, 0, 0, 8),
|
maxLines: null,
|
||||||
Container(
|
controller: _pasteTextController,
|
||||||
constraints: const BoxConstraints(maxHeight: 200),
|
decoration: const InputDecoration(
|
||||||
child: TextField(
|
border: OutlineInputBorder(),
|
||||||
enabled: !_validatingPaste,
|
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||||
onChanged: _onPasteChanged,
|
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
||||||
style: textTheme.labelSmall!
|
'---- END VEILIDCHAT CONTACT INVITE -----\n',
|
||||||
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
|
//labelText: translate('paste_invite_dialog.paste')
|
||||||
keyboardType: TextInputType.multiline,
|
),
|
||||||
maxLines: null,
|
)).paddingLTRB(0, 0, 0, 8)
|
||||||
controller: _pasteTextController,
|
]);
|
||||||
decoration: const InputDecoration(
|
}
|
||||||
border: OutlineInputBorder(),
|
|
||||||
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
@override
|
||||||
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
// ignore: prefer_expression_function_bodies
|
||||||
'---- END VEILIDCHAT CONTACT INVITE -----\n',
|
Widget build(BuildContext context) {
|
||||||
//labelText: translate('paste_invite_dialog.paste')
|
return InviteDialog(
|
||||||
),
|
onValidationCancelled: onValidationCancelled,
|
||||||
)).paddingLTRB(0, 0, 0, 8),
|
onValidationSuccess: onValidationSuccess,
|
||||||
if (_validatingPaste)
|
onValidationFailed: onValidationFailed,
|
||||||
Column(children: [
|
inviteControlIsValid: inviteControlIsValid,
|
||||||
Text(translate('contact_invite.validating'))
|
buildInviteControl: buildInviteControl);
|
||||||
.paddingLTRB(0, 0, 0, 8),
|
|
||||||
buildProgressIndicator(context),
|
|
||||||
]).paddingAll(16).toCenter(),
|
|
||||||
if (_validInvitation == null &&
|
|
||||||
!_validatingPaste &&
|
|
||||||
_pasteTextController.text.isNotEmpty)
|
|
||||||
Column(children: [
|
|
||||||
Text(translate('contact_invite.invalid_invitation')),
|
|
||||||
const Icon(Icons.error)
|
|
||||||
]).paddingAll(16).toCenter(),
|
|
||||||
if (_validInvitation != null && !_validatingPaste)
|
|
||||||
Column(children: [
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 64),
|
|
||||||
width: double.infinity,
|
|
||||||
child: ProfileWidget(
|
|
||||||
name: _validInvitation!
|
|
||||||
.contactRequestPrivate.profile.name,
|
|
||||||
title: _validInvitation!
|
|
||||||
.contactRequestPrivate.profile.title))
|
|
||||||
.paddingLTRB(0, 0, 0, 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.check_circle),
|
|
||||||
label: Text(translate('button.accept')),
|
|
||||||
onPressed: _onAccept,
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.cancel),
|
|
||||||
label: Text(translate('button.reject')),
|
|
||||||
onPressed: _onReject,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
])
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,327 +1,119 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:quickalert/quickalert.dart';
|
|
||||||
|
|
||||||
import '../entities/local_account.dart';
|
|
||||||
import '../providers/account.dart';
|
|
||||||
import '../providers/contact.dart';
|
|
||||||
import '../providers/contact_invite.dart';
|
|
||||||
import '../tools/tools.dart';
|
import '../tools/tools.dart';
|
||||||
import '../veilid_support/veilid_support.dart';
|
import 'invite_dialog.dart';
|
||||||
import 'enter_pin.dart';
|
|
||||||
import 'profile_widget.dart';
|
|
||||||
|
|
||||||
class ScanInviteDialog extends ConsumerStatefulWidget {
|
class ScanInviteDialog extends ConsumerStatefulWidget {
|
||||||
const ScanInviteDialog({super.key});
|
const ScanInviteDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ScanInviteDialogState createState() => ScanInviteDialogState();
|
ScanInviteDialogState createState() => ScanInviteDialogState();
|
||||||
|
|
||||||
|
static Future<void> show(BuildContext context) async {
|
||||||
|
await showStyledDialog<void>(
|
||||||
|
context: context,
|
||||||
|
title: translate('scan_invite_dialog.title'),
|
||||||
|
child: const ScanInviteDialog());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
|
class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
|
||||||
final _pasteTextController = TextEditingController();
|
// final _pasteTextController = TextEditingController();
|
||||||
|
|
||||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
|
||||||
String _encryptionKey = '';
|
|
||||||
Timestamp? _expiration;
|
|
||||||
ValidContactInvitation? _validInvitation;
|
|
||||||
bool _validatingPaste = false;
|
|
||||||
bool _isAccepting = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> _onNoneEncryptionSelected(bool selected) async {
|
// Future<void> _onPasteChanged(
|
||||||
// setState(() {
|
// String text,
|
||||||
// if (selected) {
|
// Future<void> Function({
|
||||||
// _encryptionKeyType = EncryptionKeyType.none;
|
// required Uint8List inviteData,
|
||||||
// }
|
// }) validateInviteData) async {
|
||||||
// });
|
// final lines = text.split('\n');
|
||||||
// }
|
// if (lines.isEmpty) {
|
||||||
|
|
||||||
// Future<void> _onPinEncryptionSelected(bool selected) async {
|
|
||||||
// final description = translate('receive_invite_dialog.pin_description');
|
|
||||||
// final pin = await showDialog<String>(
|
|
||||||
// context: context,
|
|
||||||
// builder: (context) => EnterPinDialog(description: description));
|
|
||||||
// if (pin == null) {
|
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
// // ignore: use_build_context_synchronously
|
|
||||||
// if (!context.mounted) {
|
// var firstline =
|
||||||
|
// lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
||||||
|
// firstline += 1;
|
||||||
|
|
||||||
|
// var lastline =
|
||||||
|
// lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
||||||
|
// if (lastline == -1) {
|
||||||
|
// lastline = lines.length;
|
||||||
|
// }
|
||||||
|
// if (lastline <= firstline) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
// final matchpin = await showDialog<String>(
|
// final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
||||||
// context: context,
|
// final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||||
// builder: (context) => EnterPinDialog(
|
|
||||||
// matchPin: pin,
|
// await validateInviteData(inviteData: inviteData);
|
||||||
// description: description,
|
|
||||||
// ));
|
|
||||||
// if (matchpin == null) {
|
|
||||||
// return;
|
|
||||||
// } else if (pin == matchpin) {
|
|
||||||
// setState(() {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.pin;
|
|
||||||
// _encryptionKey = pin;
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// // ignore: use_build_context_synchronously
|
|
||||||
// if (!context.mounted) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// showErrorToast(
|
|
||||||
// context, translate('receive_invite_dialog.pin_does_not_match'));
|
|
||||||
// setState(() {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.none;
|
|
||||||
// _encryptionKey = '';
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Future<void> _onPasswordEncryptionSelected(bool selected) async {
|
void onValidationCancelled() {
|
||||||
// setState(() {
|
// _pasteTextController.clear();
|
||||||
// if (selected) {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.password;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> _onAccept() async {
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = true;
|
|
||||||
});
|
|
||||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final validInvitation = _validInvitation;
|
|
||||||
if (validInvitation != null) {
|
|
||||||
final acceptedContact =
|
|
||||||
await acceptContactInvitation(activeAccountInfo, validInvitation);
|
|
||||||
if (acceptedContact != null) {
|
|
||||||
await createContact(
|
|
||||||
activeAccountInfo: activeAccountInfo,
|
|
||||||
profile: acceptedContact.profile,
|
|
||||||
remoteIdentity: acceptedContact.remoteIdentity,
|
|
||||||
remoteConversationRecordKey:
|
|
||||||
acceptedContact.remoteConversationRecordKey,
|
|
||||||
localConversationRecordKey:
|
|
||||||
acceptedContact.localConversationRecordKey,
|
|
||||||
);
|
|
||||||
ref
|
|
||||||
..invalidate(fetchContactInvitationRecordsProvider)
|
|
||||||
..invalidate(fetchContactListProvider);
|
|
||||||
} else {
|
|
||||||
if (context.mounted) {
|
|
||||||
showErrorToast(context, 'paste_invite_dialog.failed_to_accept');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onReject() async {
|
void onValidationSuccess() {
|
||||||
final navigator = Navigator.of(context);
|
//_pasteTextController.clear();
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = true;
|
|
||||||
});
|
|
||||||
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
|
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final validInvitation = _validInvitation;
|
|
||||||
if (validInvitation != null) {
|
|
||||||
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
|
|
||||||
// do nothing right now
|
|
||||||
} else {
|
|
||||||
if (context.mounted) {
|
|
||||||
showErrorToast(context, 'paste_invite_dialog.failed_to_reject');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_isAccepting = false;
|
|
||||||
});
|
|
||||||
navigator.pop();
|
|
||||||
}
|
}
|
||||||
|
void onValidationFailed() {
|
||||||
Future<void> _onPasteChanged(String text) async {
|
//_pasteTextController.clear();
|
||||||
try {
|
|
||||||
final lines = text.split('\n');
|
|
||||||
if (lines.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstline =
|
|
||||||
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
|
|
||||||
firstline += 1;
|
|
||||||
|
|
||||||
var lastline =
|
|
||||||
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
|
|
||||||
if (lastline == -1) {
|
|
||||||
lastline = lines.length;
|
|
||||||
}
|
|
||||||
if (lastline <= firstline) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
|
||||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
|
||||||
|
|
||||||
final activeAccountInfo =
|
|
||||||
await ref.read(fetchActiveAccountProvider.future);
|
|
||||||
if (activeAccountInfo == null) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = true;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
// final validatedContactInvitation = await validateContactInvitation(
|
|
||||||
// activeAccountInfo: activeAccountInfo,
|
|
||||||
// inviteData: inviteData,
|
|
||||||
// getEncryptionKeyCallback: (encryptionKeyType, encryptedSecret) async {
|
|
||||||
// switch (encryptionKeyType) {
|
|
||||||
// case EncryptionKeyType.none:
|
|
||||||
// return SecretKey.fromBytes(encryptedSecret);
|
|
||||||
// case EncryptionKeyType.pin:
|
|
||||||
// //xxx
|
|
||||||
// return SecretKey.fromBytes(encryptedSecret);
|
|
||||||
// case EncryptionKeyType.password:
|
|
||||||
// //xxx
|
|
||||||
// return SecretKey.fromBytes(encryptedSecret);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// Verify expiration
|
|
||||||
// xxx
|
|
||||||
|
|
||||||
// setState(() {
|
|
||||||
// _validatingPaste = false;
|
|
||||||
// _validInvitation = validatedContactInvitation;
|
|
||||||
// });
|
|
||||||
} on Exception catch (_) {
|
|
||||||
setState(() {
|
|
||||||
_validatingPaste = false;
|
|
||||||
_validInvitation = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty;
|
||||||
|
|
||||||
@override
|
Widget buildInviteControl(
|
||||||
// ignore: prefer_expression_function_bodies
|
BuildContext context,
|
||||||
Widget build(BuildContext context) {
|
InviteDialogState dialogState,
|
||||||
|
Future<void> Function({required Uint8List inviteData})
|
||||||
|
validateInviteData) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
//final scale = theme.extension<ScaleScheme>()!;
|
//final scale = theme.extension<ScaleScheme>()!;
|
||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
//final height = MediaQuery.of(context).size.height;
|
//final height = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
if (_isAccepting) {
|
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||||
return SizedBox(height: 400, child: waitingPage(context));
|
Text(
|
||||||
}
|
translate('scan_invite_dialog.scan_invite_here'),
|
||||||
return ConstrainedBox(
|
).paddingLTRB(0, 0, 0, 8),
|
||||||
constraints: const BoxConstraints(maxHeight: 400),
|
// Container(
|
||||||
child: SingleChildScrollView(
|
// constraints: const BoxConstraints(maxHeight: 200),
|
||||||
padding: const EdgeInsets.all(8),
|
// child: TextField(
|
||||||
child: Column(
|
// enabled: !dialogState.isValidating,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// onChanged: (text) => _onPasteChanged(text, validateInviteData),
|
||||||
mainAxisSize: MainAxisSize.min,
|
// style: textTheme.labelSmall!
|
||||||
children: <Widget>[
|
// .copyWith(fontFamily: 'Victor Mono', fontSize: 11),
|
||||||
Text(
|
// keyboardType: TextInputType.multiline,
|
||||||
translate('paste_invite_dialog.paste_invite_here'),
|
// maxLines: null,
|
||||||
).paddingAll(8),
|
// controller: _pasteTextController,
|
||||||
Container(
|
// decoration: const InputDecoration(
|
||||||
constraints: const BoxConstraints(maxHeight: 200),
|
// border: OutlineInputBorder(),
|
||||||
child: TextField(
|
// hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
||||||
enabled: !_validatingPaste,
|
// 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
||||||
onChanged: _onPasteChanged,
|
// '---- END VEILIDCHAT CONTACT INVITE -----\n',
|
||||||
style: textTheme.labelSmall!
|
// //labelText: translate('paste_invite_dialog.paste')
|
||||||
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
|
// ),
|
||||||
keyboardType: TextInputType.multiline,
|
// )).paddingLTRB(0, 0, 0, 8)
|
||||||
maxLines: null,
|
]);
|
||||||
controller: _pasteTextController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
hintText: '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n'
|
|
||||||
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n'
|
|
||||||
'---- END VEILIDCHAT CONTACT INVITE -----\n',
|
|
||||||
//labelText: translate('paste_invite_dialog.paste')
|
|
||||||
),
|
|
||||||
).paddingAll(8)),
|
|
||||||
if (_validatingPaste)
|
|
||||||
Column(children: [
|
|
||||||
Text(translate('paste_invite_dialog.validating')),
|
|
||||||
buildProgressIndicator(context),
|
|
||||||
]),
|
|
||||||
if (_validInvitation == null &&
|
|
||||||
!_validatingPaste &&
|
|
||||||
_pasteTextController.text.isNotEmpty)
|
|
||||||
Column(children: [
|
|
||||||
Text(translate('paste_invite_dialog.invalid_invitation')),
|
|
||||||
const Icon(Icons.error)
|
|
||||||
]).paddingAll(16).toCenter(),
|
|
||||||
if (_validInvitation != null && !_validatingPaste)
|
|
||||||
Column(children: [
|
|
||||||
ProfileWidget(
|
|
||||||
name: _validInvitation!.contactRequestPrivate.profile.name,
|
|
||||||
title:
|
|
||||||
_validInvitation!.contactRequestPrivate.profile.title),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.check_circle),
|
|
||||||
label: Text(translate('button.accept')),
|
|
||||||
onPressed: _onAccept,
|
|
||||||
),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.cancel),
|
|
||||||
label: Text(translate('button.reject')),
|
|
||||||
onPressed: _onReject,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
])
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
// ignore: prefer_expression_function_bodies
|
||||||
super.debugFillProperties(properties);
|
Widget build(BuildContext context) {
|
||||||
|
return InviteDialog(
|
||||||
|
onValidationCancelled: onValidationCancelled,
|
||||||
|
onValidationSuccess: onValidationSuccess,
|
||||||
|
onValidationFailed: onValidationFailed,
|
||||||
|
inviteControlIsValid: inviteControlIsValid,
|
||||||
|
buildInviteControl: buildInviteControl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue