refactor for qr scan

This commit is contained in:
Christien Rioux 2023-09-27 13:34:19 -04:00
parent ef2d4ce308
commit e5f1619c65
4 changed files with 499 additions and 582 deletions

View File

@ -83,7 +83,7 @@
"copy_invitation": "Copy Invitation",
"invitation_copied": "Invitation Copied"
},
"contact_invite": {
"invite_dialog": {
"message_from_contact": "Message from contact",
"validating": "Validating...",
"failed_to_accept": "Failed to accept contact invite",
@ -99,6 +99,11 @@
"paste_invite_here": "Paste your contact invite here:",
"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": "Enter PIN",
"reenter_pin": "Re-Enter PIN To Confirm",

View 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));
}
}

View File

@ -1,19 +1,14 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:awesome_extensions/awesome_extensions.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 '../veilid_support/veilid_support.dart';
import 'enter_password.dart';
import 'enter_pin.dart';
import 'profile_widget.dart';
import 'invite_dialog.dart';
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});
@ -32,315 +27,97 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController();
ValidContactInvitation? _validInvitation;
bool _validatingPaste = false;
bool _isAccepting = false;
@override
void initState() {
super.initState();
}
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();
Future<void> _onPasteChanged(
String text,
Future<void> Function({
required Uint8List inviteData,
}) validateInviteData) async {
final lines = text.split('\n');
if (lines.isEmpty) {
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, 'contact_invite.failed_to_accept');
}
}
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;
}
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();
if (lastline <= firstline) {
return;
}
final validInvitation = _validInvitation;
if (validInvitation != null) {
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
// do nothing right now
} else {
if (context.mounted) {
showErrorToast(context, 'contact_invite.failed_to_reject');
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
await validateInviteData(inviteData: inviteData);
}
Future<void> _onPasteChanged(String text) async {
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;
}
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;
}
void onValidationCancelled() {
_pasteTextController.clear();
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
void onValidationSuccess() {
//_pasteTextController.clear();
}
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 scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
//final height = MediaQuery.of(context).size.height;
if (_isAccepting) {
return SizedBox(height: 400, child: waitingPage(context));
}
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>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingLTRB(0, 0, 0, 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
enabled: !_validatingPaste,
onChanged: _onPasteChanged,
style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
keyboardType: TextInputType.multiline,
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')
),
)).paddingLTRB(0, 0, 0, 8),
if (_validatingPaste)
Column(children: [
Text(translate('contact_invite.validating'))
.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,
)
],
),
])
],
),
),
);
return Column(mainAxisSize: MainAxisSize.min, children: [
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingLTRB(0, 0, 0, 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
enabled: !dialogState.isValidating,
onChanged: (text) async =>
_onPasteChanged(text, validateInviteData),
style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
keyboardType: TextInputType.multiline,
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')
),
)).paddingLTRB(0, 0, 0, 8)
]);
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return InviteDialog(
onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed,
inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl);
}
}

View File

@ -1,327 +1,119 @@
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 '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 '../veilid_support/veilid_support.dart';
import 'enter_pin.dart';
import 'profile_widget.dart';
import 'invite_dialog.dart';
class ScanInviteDialog extends ConsumerStatefulWidget {
const ScanInviteDialog({super.key});
@override
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> {
final _pasteTextController = TextEditingController();
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = '';
Timestamp? _expiration;
ValidContactInvitation? _validInvitation;
bool _validatingPaste = false;
bool _isAccepting = false;
// final _pasteTextController = TextEditingController();
@override
void initState() {
super.initState();
}
// Future<void> _onNoneEncryptionSelected(bool selected) async {
// setState(() {
// if (selected) {
// _encryptionKeyType = EncryptionKeyType.none;
// }
// });
// }
// 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) {
// Future<void> _onPasteChanged(
// String text,
// Future<void> Function({
// required Uint8List inviteData,
// }) validateInviteData) async {
// final lines = text.split('\n');
// if (lines.isEmpty) {
// 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;
// }
// final matchpin = await showDialog<String>(
// context: context,
// builder: (context) => EnterPinDialog(
// matchPin: pin,
// 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 = '';
// });
// }
// final inviteDataBase64 = lines.sublist(firstline, lastline).join();
// final inviteData = base64UrlNoPadDecode(inviteDataBase64);
// await validateInviteData(inviteData: inviteData);
// }
// Future<void> _onPasswordEncryptionSelected(bool selected) async {
// setState(() {
// 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();
void onValidationCancelled() {
// _pasteTextController.clear();
}
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, 'paste_invite_dialog.failed_to_reject');
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
void onValidationSuccess() {
//_pasteTextController.clear();
}
Future<void> _onPasteChanged(String text) async {
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;
});
}
void onValidationFailed() {
//_pasteTextController.clear();
}
bool inviteControlIsValid() => false; // _pasteTextController.text.isNotEmpty;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
Widget buildInviteControl(
BuildContext context,
InviteDialogState dialogState,
Future<void> Function({required Uint8List inviteData})
validateInviteData) {
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: 400, child: waitingPage(context));
}
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
).paddingAll(8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
enabled: !_validatingPaste,
onChanged: _onPasteChanged,
style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11),
keyboardType: TextInputType.multiline,
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,
)
],
),
])
],
),
),
);
return Column(mainAxisSize: MainAxisSize.min, children: [
Text(
translate('scan_invite_dialog.scan_invite_here'),
).paddingLTRB(0, 0, 0, 8),
// Container(
// constraints: const BoxConstraints(maxHeight: 200),
// child: TextField(
// enabled: !dialogState.isValidating,
// onChanged: (text) => _onPasteChanged(text, validateInviteData),
// style: textTheme.labelSmall!
// .copyWith(fontFamily: 'Victor Mono', fontSize: 11),
// keyboardType: TextInputType.multiline,
// 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')
// ),
// )).paddingLTRB(0, 0, 0, 8)
]);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return InviteDialog(
onValidationCancelled: onValidationCancelled,
onValidationSuccess: onValidationSuccess,
onValidationFailed: onValidationFailed,
inviteControlIsValid: inviteControlIsValid,
buildInviteControl: buildInviteControl);
}
}