From e5f1619c65f9fdf2ac20d77dc21191f338972a6f Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 27 Sep 2023 13:34:19 -0400 Subject: [PATCH] refactor for qr scan --- assets/i18n/en.json | 7 +- lib/components/invite_dialog.dart | 343 ++++++++++++++++++++++ lib/components/paste_invite_dialog.dart | 373 +++++------------------- lib/components/scan_invite_dialog.dart | 358 +++++------------------ 4 files changed, 499 insertions(+), 582 deletions(-) create mode 100644 lib/components/invite_dialog.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index aa9a056..9c292e5 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -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", diff --git a/lib/components/invite_dialog.dart b/lib/components/invite_dialog.dart new file mode 100644 index 0000000..c28e6d7 --- /dev/null +++ b/lib/components/invite_dialog.dart @@ -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 Function({required Uint8List inviteData}) + validateInviteData) buildInviteControl; + + @override + InviteDialogState createState() => InviteDialogState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has( + 'onValidationCancelled', onValidationCancelled)) + ..add(ObjectFlagProperty.has( + 'onValidationSuccess', onValidationSuccess)) + ..add(ObjectFlagProperty.has( + 'onValidationFailed', onValidationFailed)) + ..add(ObjectFlagProperty.has( + 'inviteControlIsValid', inviteControlIsValid)) + ..add(ObjectFlagProperty< + Widget Function( + BuildContext context, + InviteDialogState dialogState, + Future Function({required Uint8List inviteData}) + validateInviteData)>.has( + 'buildInviteControl', buildInviteControl)); + } +} + +class InviteDialogState extends ConsumerState { + ValidContactInvitation? _validInvitation; + bool _isValidating = false; + bool _isAccepting = false; + + @override + void initState() { + super.initState(); + } + + bool get isValidating => _isValidating; + bool get isAccepting => _isAccepting; + + Future _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 _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 _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( + 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( + 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()!; + // 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.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('isValidating', isValidating)) + ..add(DiagnosticsProperty('isAccepting', isAccepting)); + } +} diff --git a/lib/components/paste_invite_dialog.dart b/lib/components/paste_invite_dialog.dart index 8367f9d..cf423e1 100644 --- a/lib/components/paste_invite_dialog.dart +++ b/lib/components/paste_invite_dialog.dart @@ -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 { final _pasteTextController = TextEditingController(); - ValidContactInvitation? _validInvitation; - bool _validatingPaste = false; - bool _isAccepting = false; - @override void initState() { super.initState(); } - Future _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 _onPasteChanged( + String text, + Future 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 _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 _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( - 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( - 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 Function({required Uint8List inviteData}) + validateInviteData) { final theme = Theme.of(context); //final scale = theme.extension()!; 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: [ - 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); } } diff --git a/lib/components/scan_invite_dialog.dart b/lib/components/scan_invite_dialog.dart index d259de4..8fd9291 100644 --- a/lib/components/scan_invite_dialog.dart +++ b/lib/components/scan_invite_dialog.dart @@ -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 show(BuildContext context) async { + await showStyledDialog( + context: context, + title: translate('scan_invite_dialog.title'), + child: const ScanInviteDialog()); + } } class ScanInviteDialogState extends ConsumerState { - 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 _onNoneEncryptionSelected(bool selected) async { - // setState(() { - // if (selected) { - // _encryptionKeyType = EncryptionKeyType.none; - // } - // }); - // } - - // Future _onPinEncryptionSelected(bool selected) async { - // final description = translate('receive_invite_dialog.pin_description'); - // final pin = await showDialog( - // context: context, - // builder: (context) => EnterPinDialog(description: description)); - // if (pin == null) { + // Future _onPasteChanged( + // String text, + // Future 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( - // 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 _onPasswordEncryptionSelected(bool selected) async { - // setState(() { - // if (selected) { - // _encryptionKeyType = EncryptionKeyType.password; - // } - // }); - // } - - Future _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 _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 _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 Function({required Uint8List inviteData}) + validateInviteData) { final theme = Theme.of(context); //final scale = theme.extension()!; 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: [ - 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); } }