veilidchat/lib/contact_invitation/views/invitation_dialog.dart

325 lines
11 KiB
Dart
Raw Normal View History

2023-09-27 13:34:19 -04:00
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
2024-06-15 23:29:15 -04:00
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
2023-09-27 13:34:19 -04:00
2024-01-09 20:58:27 -05:00
import '../../account_manager/account_manager.dart';
2024-01-30 19:47:22 -05:00
import '../../contacts/contacts.dart';
import '../../theme/theme.dart';
2024-01-09 20:58:27 -05:00
import '../../tools/tools.dart';
2024-01-30 14:14:11 -05:00
import '../contact_invitation.dart';
2023-09-27 13:34:19 -04:00
2024-04-05 22:03:04 -04:00
class InvitationDialog extends StatefulWidget {
const InvitationDialog(
2024-06-15 23:29:15 -04:00
{required Locator locator,
2024-02-14 21:33:15 -05:00
required this.onValidationCancelled,
2023-09-27 13:34:19 -04:00
required this.onValidationSuccess,
required this.onValidationFailed,
required this.inviteControlIsValid,
required this.buildInviteControl,
2024-06-15 23:29:15 -04:00
super.key})
: _locator = locator;
2023-09-27 13:34:19 -04:00
final void Function() onValidationCancelled;
final void Function() onValidationSuccess;
final void Function() onValidationFailed;
final bool Function() inviteControlIsValid;
final Widget Function(
BuildContext context,
2024-04-05 22:03:04 -04:00
InvitationDialogState dialogState,
2023-09-27 13:34:19 -04:00
Future<void> Function({required Uint8List inviteData})
validateInviteData) buildInviteControl;
2024-06-15 23:29:15 -04:00
final Locator _locator;
2023-09-27 13:34:19 -04:00
@override
2024-04-05 22:03:04 -04:00
InvitationDialogState createState() => InvitationDialogState();
2023-09-27 13:34:19 -04:00
@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,
2024-04-05 22:03:04 -04:00
InvitationDialogState dialogState,
2023-09-27 13:34:19 -04:00
Future<void> Function({required Uint8List inviteData})
validateInviteData)>.has(
2024-06-15 23:29:15 -04:00
'buildInviteControl', buildInviteControl));
2023-09-27 13:34:19 -04:00
}
}
2024-04-05 22:03:04 -04:00
class InvitationDialogState extends State<InvitationDialog> {
2023-09-27 13:34:19 -04:00
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);
2024-06-16 22:12:24 -04:00
final accountInfo = widget._locator<AccountInfoCubit>().state;
2024-06-15 23:29:15 -04:00
final contactList = widget._locator<ContactListCubit>();
2023-09-27 13:34:19 -04:00
setState(() {
_isAccepting = true;
});
final validInvitation = _validInvitation;
if (validInvitation != null) {
2024-01-30 14:14:11 -05:00
final acceptedContact = await validInvitation.accept();
2023-09-27 13:34:19 -04:00
if (acceptedContact != null) {
// initiator when accept is received will create
// contact in the case of a 'note to self'
2024-06-16 22:12:24 -04:00
final isSelf = accountInfo.unlockedAccountInfo!.identityPublicKey ==
2024-06-07 14:42:04 -04:00
acceptedContact.remoteIdentity.currentInstance.publicKey;
2023-09-27 13:34:19 -04:00
if (!isSelf) {
2024-01-30 19:47:22 -05:00
await contactList.createContact(
2024-06-15 23:29:15 -04:00
profile: acceptedContact.remoteProfile,
2024-06-07 14:42:04 -04:00
remoteSuperIdentity: acceptedContact.remoteIdentity,
2023-09-27 13:34:19 -04:00
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
}
} else {
2024-04-05 22:03:04 -04:00
if (mounted) {
showErrorToast(context, 'invitation_dialog.failed_to_accept');
2023-09-27 13:34:19 -04:00
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _onReject() async {
final navigator = Navigator.of(context);
setState(() {
_isAccepting = true;
});
final validInvitation = _validInvitation;
if (validInvitation != null) {
2024-01-30 14:14:11 -05:00
if (await validInvitation.reject()) {
2023-09-27 13:34:19 -04:00
// do nothing right now
} else {
2024-04-05 22:03:04 -04:00
if (mounted) {
showErrorToast(context, 'invitation_dialog.failed_to_reject');
2023-09-27 13:34:19 -04:00
}
}
}
setState(() {
_isAccepting = false;
});
navigator.pop();
}
Future<void> _validateInviteData({
required Uint8List inviteData,
}) async {
try {
2024-01-30 14:14:11 -05:00
final contactInvitationListCubit =
2024-06-15 23:29:15 -04:00
widget._locator<ContactInvitationListCubit>();
2023-09-27 13:34:19 -04:00
setState(() {
_isValidating = true;
_validInvitation = null;
});
2024-01-30 14:14:11 -05:00
final validatedContactInvitation =
await contactInvitationListCubit.validateInvitation(
inviteData: inviteData,
getEncryptionKeyCallback:
(cs, encryptionKeyType, encryptedSecret) async {
String encryptionKey;
switch (encryptionKeyType) {
case EncryptionKeyType.none:
encryptionKey = '';
case EncryptionKeyType.pin:
final description =
2024-04-05 22:03:04 -04:00
translate('invitation_dialog.protected_with_pin');
if (!mounted) {
2024-01-30 14:14:11 -05:00
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 =
2024-04-05 22:03:04 -04:00
translate('invitation_dialog.protected_with_password');
if (!mounted) {
2024-01-30 14:14:11 -05:00
return null;
}
final password = await showDialog<String>(
context: context,
builder: (context) =>
EnterPasswordDialog(description: description));
if (password == null) {
return null;
}
encryptionKey = password;
2023-09-27 13:34:19 -04:00
}
2024-01-30 14:14:11 -05:00
return encryptionKeyType.decryptSecretFromBytes(
secretBytes: encryptedSecret,
cryptoKind: cs.kind(),
encryptionKey: encryptionKey);
});
2023-09-27 13:34:19 -04:00
// 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:
2024-04-05 22:03:04 -04:00
errorText = translate('invitation_dialog.invalid_invitation');
2023-09-27 13:34:19 -04:00
case EncryptionKeyType.pin:
2024-04-05 22:03:04 -04:00
errorText = translate('invitation_dialog.invalid_pin');
2023-09-27 13:34:19 -04:00
case EncryptionKeyType.password:
2024-04-05 22:03:04 -04:00
errorText = translate('invitation_dialog.invalid_password');
2023-09-27 13:34:19 -04:00
}
2024-04-05 22:03:04 -04:00
if (mounted) {
2023-09-27 13:34:19 -04:00
showErrorToast(context, errorText);
}
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
2024-04-21 22:16:22 -04:00
} on VeilidAPIException catch (e) {
late final String errorText;
if (e is VeilidAPIExceptionTryAgain) {
errorText = translate('invitation_dialog.try_again_online');
} else {
errorText = translate('invitation_dialog.invalid_invitation');
}
if (mounted) {
showErrorToast(context, errorText);
}
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
2023-09-28 12:51:44 -04:00
} on Exception catch (e) {
log.debug('exception: $e', e);
2023-09-27 13:34:19 -04:00
setState(() {
_isValidating = false;
_validInvitation = null;
widget.onValidationFailed();
});
rethrow;
}
}
List<Widget> _buildPreAccept() => <Widget>[
if (!_isValidating && _validInvitation == null)
widget.buildInviteControl(context, this, _validateInviteData),
if (_isValidating)
Column(children: [
Text(translate('invitation_dialog.validating'))
.paddingLTRB(0, 0, 0, 16),
buildProgressIndicator().paddingAll(16),
]).toCenter(),
if (_validInvitation == null &&
!_isValidating &&
widget.inviteControlIsValid())
Column(children: [
Text(translate('invitation_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(profile: _validInvitation!.remoteProfile))
.paddingLTRB(0, 0, 0, 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.check_circle),
label: Text(translate('button.accept')),
onPressed: _onAccept,
).paddingLTRB(0, 0, 8, 0),
ElevatedButton.icon(
icon: const Icon(Icons.cancel),
label: Text(translate('button.reject')),
onPressed: _onReject,
).paddingLTRB(8, 0, 0, 0)
],
),
])
];
2023-09-27 13:34:19 -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;
// final height = MediaQuery.of(context).size.height;
final dismissible = !_isAccepting && !_isValidating;
2023-09-27 13:34:19 -04:00
final dialog = ConstrainedBox(
2023-09-27 13:34:19 -04:00
constraints: const BoxConstraints(maxHeight: 400, maxWidth: 400),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: _isAccepting
? [buildProgressIndicator().paddingAll(16)]
: _buildPreAccept()),
2023-09-27 13:34:19 -04:00
),
);
return PopControl(dismissible: dismissible, child: dialog);
2023-09-27 13:34:19 -04:00
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<bool>('isValidating', isValidating))
..add(DiagnosticsProperty<bool>('isAccepting', isAccepting));
}
}