ente/lib/ui/account/delete_account_page.dart
2023-03-15 23:06:01 +05:30

349 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import "package:dropdown_button2/dropdown_button2.dart";
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
import 'package:photos/core/configuration.dart';
import 'package:photos/models/delete_account.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/email_util.dart';
import "package:photos/utils/toast_util.dart";
class DeleteAccountPage extends StatefulWidget {
const DeleteAccountPage({
Key? key,
}) : super(key: key);
@override
State<DeleteAccountPage> createState() => _DeleteAccountPageState();
}
class _DeleteAccountPageState extends State<DeleteAccountPage> {
bool _hasConfirmedDeletion = false;
final _feedbackTextCtrl = TextEditingController();
final String _defaultSelection = 'Select reason';
late String _dropdownValue = _defaultSelection;
late final List<String> _deletionReason = [
_defaultSelection,
'Its missing a key feature that I need',
'The app or a certain feature does not \nbehave as I think it should',
'I found another service that I like better',
'I use a different account',
'My reason isnt listed',
];
final List<int> _reasonIndexesWhereFeedbackIsNecessary = [1, 2, 5];
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text("Delete account"),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Text(
"What is the main reason you are deleting your account?",
style: getEnteTextTheme(context).body,
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: 48,
decoration: BoxDecoration(
color: colorScheme.fillFaint,
borderRadius: BorderRadius.circular(8),
),
child: DropdownButton2<String>(
alignment: AlignmentDirectional.topStart,
value: _dropdownValue,
onChanged: (String? newValue) {
setState(() {
_dropdownValue = newValue!;
});
},
underline: const SizedBox(),
items: _deletionReason
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
enabled: value != _defaultSelection,
alignment: Alignment.centerLeft,
child: Text(
value,
style: value != _defaultSelection
? getEnteTextTheme(context).small
: getEnteTextTheme(context).smallMuted,
),
);
}).toList(),
),
),
const SizedBox(height: 24),
Text(
"We are sorry to see you go. Please share your feedback to "
"help us improve.",
style: getEnteTextTheme(context).body,
),
const SizedBox(height: 4),
TextFormField(
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: colorScheme.strokeFaint, width: 1),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide:
BorderSide(color: colorScheme.strokeFaint, width: 1),
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.transparent,
hintText: "Feedback",
contentPadding: const EdgeInsets.all(12),
),
controller: _feedbackTextCtrl,
autofocus: false,
autocorrect: false,
keyboardType: TextInputType.multiline,
minLines: 3,
maxLines: null,
onChanged: (_) {
setState(() {});
},
),
_shouldAskForFeedback()
? SizedBox(
height: 42,
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
"Kindly help us with this information",
style: getEnteTextTheme(context)
.smallBold
.copyWith(color: colorScheme.warning700),
),
),
)
: const SizedBox(height: 42),
GestureDetector(
onTap: () {
setState(() {
_hasConfirmedDeletion = !_hasConfirmedDeletion;
});
},
child: Row(
children: [
Checkbox(
value: _hasConfirmedDeletion,
side: CheckboxTheme.of(context).side,
onChanged: (value) {
setState(() {
_hasConfirmedDeletion = value!;
});
},
),
Expanded(
child: Text(
"Yes, I want to permanently delete this account and "
"all its data.",
style: getEnteTextTheme(context).bodyMuted,
textAlign: TextAlign.left,
),
)
],
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ButtonWidget(
buttonType: ButtonType.critical,
labelText: "Confirm Account Deletion",
isDisabled: _shouldBlockDeletion(),
onTap: () async {
await _initiateDelete(context);
},
shouldSurfaceExecutionStates: true,
),
const SizedBox(height: 8),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: "Cancel",
onTap: () async {
Navigator.of(context).pop();
},
),
const SafeArea(
child: SizedBox(
height: 12,
),
),
],
),
),
],
),
),
),
);
}
bool _shouldBlockDeletion() {
return !_hasConfirmedDeletion ||
_dropdownValue == _defaultSelection ||
_shouldAskForFeedback();
}
bool _shouldAskForFeedback() {
return (_reasonIndexesWhereFeedbackIsNecessary
.contains(_deletionReason.indexOf(_dropdownValue)) &&
_feedbackTextCtrl.text.trim().isEmpty);
}
Future<void> _initiateDelete(BuildContext context) async {
final choice = await showChoiceDialog(
context,
title: "Confirm Account Deletion",
body: "You are about to permanently delete your account and all its data."
"\nThis action is irreversible.",
firstButtonLabel: "Delete Account Permanently",
firstButtonType: ButtonType.critical,
firstButtonOnTap: () async {
final deleteChallengeResponse =
await UserService.instance.getDeleteChallenge(context);
if (deleteChallengeResponse == null) {
return;
}
if (deleteChallengeResponse.allowDelete) {
await _delete(context, deleteChallengeResponse);
} else {
await _requestEmailForDeletion(context);
}
},
isDismissible: false,
);
if (choice!.action == ButtonAction.error) {
await showGenericErrorDialog(context: context);
}
}
Future<void> _delete(
BuildContext context,
DeleteChallengeResponse response,
) async {
try {
final decryptChallenge = CryptoUtil.openSealSync(
CryptoUtil.base642bin(response.encryptedChallenge),
CryptoUtil.base642bin(
Configuration.instance.getKeyAttributes()!.publicKey,
),
Configuration.instance.getSecretKey()!,
);
final challengeResponseStr = utf8.decode(decryptChallenge);
await UserService.instance.deleteAccount(
context,
challengeResponseStr,
reasonCategory: _dropdownValue,
feedback: _feedbackTextCtrl.text.trim(),
);
Navigator.of(context).popUntil((route) => route.isFirst);
showShortToast(context, "Your account has been deleted");
} catch (e, s) {
Logger("DeleteAccount").severe("failed to delete", e, s);
showGenericErrorDialog(context: context);
}
}
Future<void> _requestEmailForDeletion(BuildContext context) async {
final AlertDialog alert = AlertDialog(
title: const Text(
"Delete account",
style: TextStyle(
color: Colors.red,
),
),
content: RichText(
text: TextSpan(
children: [
const TextSpan(
text: "Please send an email to ",
),
TextSpan(
text: "account-deletion@ente.io",
style: TextStyle(
color: Colors.orange[300],
),
),
const TextSpan(
text:
" from your registered email address.\n\nYour request will be processed within 72 hours.",
),
],
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
fontSize: 16,
),
),
),
actions: [
TextButton(
child: const Text(
"Send email",
style: TextStyle(
color: Colors.red,
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
await sendEmail(
context,
to: 'account-deletion@ente.io',
subject: '[Delete account]',
);
},
),
TextButton(
child: Text(
"Ok",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
}