veilidchat/lib/components/paste_invite_dialog.dart

347 lines
12 KiB
Dart
Raw Normal View History

2023-08-05 01:00:46 -04:00
import 'dart:async';
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';
2023-08-05 23:58:13 -04:00
import '../providers/contact_invite.dart';
2023-08-05 01:00:46 -04:00
import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
2023-09-26 22:32:13 -04:00
import 'enter_password.dart';
2023-08-05 01:00:46 -04:00
import 'enter_pin.dart';
2023-08-05 12:38:03 -04:00
import 'profile_widget.dart';
2023-08-05 01:00:46 -04:00
class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key});
@override
PasteInviteDialogState createState() => PasteInviteDialogState();
2023-09-23 12:56:54 -04:00
static Future<void> show(BuildContext context) async {
2023-09-23 22:19:53 -04:00
await showStyledDialog<void>(
2023-09-23 12:56:54 -04:00
context: context,
2023-09-23 22:19:53 -04:00
title: translate('paste_invite_dialog.title'),
child: const PasteInviteDialog());
2023-09-23 12:56:54 -04:00
}
2023-08-05 01:00:46 -04:00
}
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController();
2023-08-05 12:38:03 -04:00
ValidContactInvitation? _validInvitation;
bool _validatingPaste = false;
2023-08-05 13:50:31 -04:00
bool _isAccepting = false;
2023-08-05 01:00:46 -04:00
@override
void initState() {
super.initState();
}
2023-08-05 12:38:03 -04:00
Future<void> _onAccept() async {
2023-08-05 13:50:31 -04:00
final navigator = Navigator.of(context);
setState(() {
_isAccepting = true;
});
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
setState(() {
_isAccepting = false;
});
navigator.pop();
return;
2023-08-05 12:38:03 -04:00
}
2023-08-05 13:50:31 -04:00
final validInvitation = _validInvitation;
if (validInvitation != null) {
2023-08-05 21:01:27 -04:00
final acceptedContact =
await acceptContactInvitation(activeAccountInfo, validInvitation);
if (acceptedContact != null) {
2023-09-25 22:59:28 -04:00
// 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,
);
}
2023-08-05 21:01:27 -04:00
ref
..invalidate(fetchContactInvitationRecordsProvider)
..invalidate(fetchContactListProvider);
} else {
if (context.mounted) {
2023-09-24 22:35:54 -04:00
showErrorToast(context, 'contact_invite.failed_to_accept');
2023-08-05 21:01:27 -04:00
}
}
2023-08-05 13:50:31 -04:00
}
setState(() {
_isAccepting = false;
});
navigator.pop();
2023-08-05 12:38:03 -04:00
}
Future<void> _onReject() async {
2023-08-05 13:50:31 -04:00
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) {
2023-08-05 21:01:27 -04:00
if (await rejectContactInvitation(activeAccountInfo, validInvitation)) {
// do nothing right now
} else {
if (context.mounted) {
2023-09-24 22:35:54 -04:00
showErrorToast(context, 'contact_invite.failed_to_reject');
2023-08-05 21:01:27 -04:00
}
}
2023-08-05 12:38:03 -04:00
}
2023-08-05 13:50:31 -04:00
setState(() {
_isAccepting = false;
});
navigator.pop();
2023-08-05 12:38:03 -04:00
}
2023-08-05 01:00:46 -04:00
Future<void> _onPasteChanged(String text) async {
try {
final lines = text.split('\n');
if (lines.isEmpty) {
2023-08-05 12:38:03 -04:00
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
2023-08-05 01:00:46 -04:00
return;
}
var firstline =
lines.indexWhere((element) => element.contains('BEGIN VEILIDCHAT'));
firstline += 1;
var lastline =
lines.indexWhere((element) => element.contains('END VEILIDCHAT'));
2023-08-05 01:15:08 -04:00
if (lastline == -1) {
2023-08-05 01:00:46 -04:00
lastline = lines.length;
}
if (lastline <= firstline) {
2023-08-05 12:38:03 -04:00
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
2023-08-05 01:00:46 -04:00
return;
}
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
2023-08-05 01:15:08 -04:00
2023-09-24 22:35:54 -04:00
final activeAccountInfo =
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo == null) {
setState(() {
_validatingPaste = false;
_validInvitation = null;
});
return;
}
2023-09-25 22:59:28 -04:00
final contactInvitationRecords =
await ref.read(fetchContactInvitationRecordsProvider.future);
2023-09-24 22:35:54 -04:00
2023-08-05 12:38:03 -04:00
setState(() {
_validatingPaste = true;
_validInvitation = null;
});
final validatedContactInvitation = await validateContactInvitation(
2023-09-24 22:35:54 -04:00
activeAccountInfo: activeAccountInfo,
2023-09-25 22:59:28 -04:00
contactInvitationRecords: contactInvitationRecords,
2023-09-24 22:35:54 -04:00
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,
2023-09-26 22:32:13 -04:00
builder: (context) => EnterPinDialog(
reenter: false, description: description));
2023-09-24 22:35:54 -04:00
if (pin == null) {
return null;
}
encryptionKey = pin;
case EncryptionKeyType.password:
final description =
2023-09-26 22:32:13 -04:00
translate('contact_invite.protected_with_password');
2023-09-24 22:35:54 -04:00
if (!context.mounted) {
return null;
}
final password = await showDialog<String>(
context: context,
builder: (context) =>
2023-09-26 22:32:13 -04:00
EnterPasswordDialog(description: description));
2023-09-24 22:35:54 -04:00
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(() {
2023-09-26 22:32:13 -04:00
_pasteTextController.text = '';
2023-09-24 22:35:54 -04:00
_validatingPaste = false;
_validInvitation = null;
});
return;
}
2023-08-05 12:38:03 -04:00
// Verify expiration
// xxx
2023-08-05 01:00:46 -04:00
2023-08-05 12:38:03 -04:00
setState(() {
_validatingPaste = false;
_validInvitation = validatedContactInvitation;
2023-08-05 01:00:46 -04:00
});
2023-09-24 22:35:54 -04:00
} on ContactInviteInvalidKeyException catch (e) {
String errorText;
switch (e.type) {
case EncryptionKeyType.none:
errorText = translate('contact_invite.invalid_invitation');
case EncryptionKeyType.pin:
2023-09-26 22:32:13 -04:00
errorText = translate('contact_invite.invalid_pin');
case EncryptionKeyType.password:
2023-09-24 22:35:54 -04:00
errorText = translate('contact_invite.invalid_password');
}
if (context.mounted) {
showErrorToast(context, errorText);
}
setState(() {
2023-09-26 22:32:13 -04:00
_pasteTextController.text = '';
2023-09-24 22:35:54 -04:00
_validatingPaste = false;
_validInvitation = null;
});
2023-08-05 01:00:46 -04:00
} on Exception catch (_) {
2023-08-05 12:38:03 -04:00
setState(() {
2023-09-26 22:32:13 -04:00
_pasteTextController.text = '';
2023-08-05 12:38:03 -04:00
_validatingPaste = false;
_validInvitation = null;
});
2023-09-24 22:35:54 -04:00
rethrow;
2023-08-05 01:00:46 -04:00
}
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme;
2023-08-05 12:38:03 -04:00
//final height = MediaQuery.of(context).size.height;
2023-08-05 01:00:46 -04:00
2023-08-05 13:50:31 -04:00
if (_isAccepting) {
return SizedBox(height: 400, child: waitingPage(context));
}
2023-08-05 14:31:32 -04:00
return ConstrainedBox(
2023-09-23 12:56:54 -04:00
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
2023-08-05 01:00:46 -04:00
child: SingleChildScrollView(
2023-09-23 12:56:54 -04:00
padding: const EdgeInsets.all(16),
2023-08-05 01:00:46 -04:00
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
translate('paste_invite_dialog.paste_invite_here'),
2023-09-23 12:56:54 -04:00
).paddingLTRB(0, 0, 0, 8),
2023-08-05 01:00:46 -04:00
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: TextField(
2023-08-05 12:38:03 -04:00
enabled: !_validatingPaste,
2023-08-05 01:00:46 -04:00
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')
),
2023-09-23 12:56:54 -04:00
)).paddingLTRB(0, 0, 0, 8),
2023-08-05 13:50:31 -04:00
if (_validatingPaste)
Column(children: [
2023-09-24 22:35:54 -04:00
Text(translate('contact_invite.validating'))
2023-09-23 12:56:54 -04:00
.paddingLTRB(0, 0, 0, 8),
2023-08-05 13:50:31 -04:00
buildProgressIndicator(context),
2023-09-23 12:56:54 -04:00
]).paddingAll(16).toCenter(),
2023-08-05 14:31:32 -04:00
if (_validInvitation == null &&
!_validatingPaste &&
_pasteTextController.text.isNotEmpty)
Column(children: [
2023-09-24 22:35:54 -04:00
Text(translate('contact_invite.invalid_invitation')),
2023-08-05 14:31:32 -04:00
const Icon(Icons.error)
]).paddingAll(16).toCenter(),
2023-08-05 12:38:03 -04:00
if (_validInvitation != null && !_validatingPaste)
Column(children: [
2023-09-23 12:56:54 -04:00
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),
2023-08-05 12:38:03 -04:00
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,
)
],
),
])
2023-08-05 01:00:46 -04:00
],
),
),
2023-08-05 14:31:32 -04:00
);
2023-08-05 01:00:46 -04:00
}
}