Merge branch 'master' into redesign-file-info

This commit is contained in:
ashilkn 2022-07-11 09:37:24 +05:30
commit 0a71c1aaa8
7 changed files with 398 additions and 140 deletions

View file

@ -0,0 +1,11 @@
import 'package:flutter/foundation.dart';
class DeleteChallengeResponse {
final bool allowDelete;
final String encryptedChallenge;
DeleteChallengeResponse({
@required this.allowDelete,
this.encryptedChallenge,
});
}

View file

@ -10,6 +10,7 @@ import 'package:photos/core/network.dart';
import 'package:photos/db/public_keys_db.dart';
import 'package:photos/events/two_factor_status_change_event.dart';
import 'package:photos/events/user_details_changed_event.dart';
import 'package:photos/models/delete_account.dart';
import 'package:photos/models/key_attributes.dart';
import 'package:photos/models/key_gen_result.dart';
import 'package:photos/models/public_key.dart';
@ -209,6 +210,76 @@ class UserService {
}
}
Future<DeleteChallengeResponse> getDeleteChallenge(
BuildContext context,
) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/users/delete-challenge",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
},
),
);
if (response != null && response.statusCode == 200) {
// clear data
await dialog.hide();
return DeleteChallengeResponse(
allowDelete: response.data["allowDelete"] as bool,
encryptedChallenge: response.data["encryptedChallenge"],
);
} else {
throw Exception("delete action failed");
}
} catch (e) {
_logger.severe(e);
await dialog.hide();
await showGenericErrorDialog(context);
return null;
}
}
Future<void> deleteAccount(
BuildContext context,
String challengeResponse,
) async {
final dialog = createProgressDialog(context, "Deleting account...");
await dialog.show();
try {
final response = await _dio.delete(
_config.getHttpEndpoint() + "/users/delete",
data: {
"challenge": challengeResponse,
},
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
},
),
);
if (response != null && response.statusCode == 200) {
// clear data
await Configuration.instance.logout();
await dialog.hide();
showToast(
context,
"We have deleted your account and scheduled your uploaded data "
"for deletion.",
);
Navigator.of(context).popUntil((route) => route.isFirst);
} else {
throw Exception("delete action failed");
}
} catch (e) {
_logger.severe(e);
await dialog.hide();
showGenericErrorDialog(context);
}
}
Future<void> verifyEmail(BuildContext context, String ott) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();

View file

@ -0,0 +1,257 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/delete_account.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common/dialogs.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/utils/auth_util.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher.dart';
class DeleteAccountPage extends StatelessWidget {
const DeleteAccountPage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext 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: 32, horizontal: 24),
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
"💔",
style: TextStyle(
fontSize: 100,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
Center(
child: Text(
"We'll be sorry to see you go. Are you facing some issue?",
style: Theme.of(context).textTheme.subtitle2,
),
),
const SizedBox(
height: 8,
),
RichText(
// textAlign: TextAlign.center,
text: TextSpan(
children: const [
TextSpan(text: "Please write to us at "),
TextSpan(
text: "feedback@ente.io",
style: TextStyle(color: Color.fromRGBO(29, 185, 84, 1)),
),
TextSpan(
text: ", maybe there is a way we can help.",
),
],
style: Theme.of(context).textTheme.subtitle2,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
GradientButton(
text: "Yes, send feedback",
paddingValue: 4,
iconData: Icons.check,
onTap: () async {
await launchUrl(
Uri(
scheme: "mailto",
path: 'feedback@ente.io',
),
);
},
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
),
InkWell(
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
side: const BorderSide(
color: Colors.redAccent,
),
padding: const EdgeInsets.symmetric(
vertical: 18,
horizontal: 10,
),
backgroundColor: Colors.white,
),
label: const Text(
"No, delete account",
style: TextStyle(
color: Colors.redAccent, // same for both themes
),
textAlign: TextAlign.center,
),
onPressed: () async => {await _initiateDelete(context)},
icon: const Icon(
Icons.no_accounts,
color: Colors.redAccent,
),
),
),
),
],
),
),
),
);
}
Future<void> _initiateDelete(BuildContext context) async {
AppLock.of(context).setEnabled(false);
String reason = "Please authenticate to initiate account deletion";
final result = await requestAuthentication(reason);
AppLock.of(context).setEnabled(
Configuration.instance.shouldShowLockScreen(),
);
if (!result) {
showToast(context, reason);
return;
}
final deleteChallengeResponse =
await UserService.instance.getDeleteChallenge(context);
if (deleteChallengeResponse == null) {
return;
}
if (deleteChallengeResponse.allowDelete) {
await _confirmAndDelete(context, deleteChallengeResponse);
} else {
await _requestEmailForDeletion(context);
}
}
Future<void> _confirmAndDelete(
BuildContext context,
DeleteChallengeResponse response,
) async {
final choice = await showChoiceDialog(
context,
'Are you sure you want to delete your account?',
'Your uploaded data will be scheduled for deletion, and your account '
'will be permanently deleted. \n\nThis action is not reversible.',
firstAction: 'Cancel',
secondAction: 'Delete',
firstActionColor: Theme.of(context).colorScheme.onSurface,
secondActionColor: Colors.red,
);
if (choice != DialogUserChoice.secondChoice) {
return;
}
final decryptChallenge = CryptoUtil.openSealSync(
Sodium.base642bin(response.encryptedChallenge),
Sodium.base642bin(Configuration.instance.getKeyAttributes().publicKey),
Configuration.instance.getSecretKey(),
);
final challengeResponseStr = utf8.decode(decryptChallenge);
await UserService.instance.deleteAccount(context, challengeResponseStr);
}
Future<void> _requestEmailForDeletion(BuildContext context) async {
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');
try {
final Email email = Email(
recipients: ['account-deletion@ente.io'],
isHTML: false,
);
await FlutterEmailSender.send(email);
} catch (e) {
launch("mailto:account-deletion@ente.io");
}
},
),
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;
},
);
}
}

View file

@ -159,7 +159,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
controller: _verificationCodeController,
autofocus: false,
autocorrect: false,
keyboardType: TextInputType.visiblePassword,
keyboardType: TextInputType.number,
onChanged: (_) {
setState(() {});
},

View file

@ -12,67 +12,61 @@ class GrantPermissionsWidget extends StatelessWidget {
MediaQuery.of(context).platformBrightness == Brightness.light;
return Scaffold(
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Center(
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 100, 0, 50),
child: Stack(
alignment: Alignment.center,
children: [
isLightMode
? Image.asset(
'assets/loading_photos_background.png',
color: Colors.white.withOpacity(0.4),
colorBlendMode: BlendMode.modulate,
)
: Image.asset(
'assets/loading_photos_background_dark.png',
),
Center(
child: Column(
children: [
const SizedBox(height: 42),
Image.asset(
"assets/gallery_locked.png",
height: 160,
),
],
child: Padding(
padding: const EdgeInsets.only(top: 20, bottom: 120),
child: Column(
children: [
Center(
child: Stack(
alignment: Alignment.center,
children: [
isLightMode
? Image.asset(
'assets/loading_photos_background.png',
color: Colors.white.withOpacity(0.4),
colorBlendMode: BlendMode.modulate,
)
: Image.asset(
'assets/loading_photos_background_dark.png',
),
),
],
Center(
child: Column(
children: [
const SizedBox(height: 42),
Image.asset(
"assets/gallery_locked.png",
height: 160,
),
],
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: RichText(
text: TextSpan(
style: Theme.of(context)
.textTheme
.headline5
.copyWith(fontWeight: FontWeight.w700),
children: [
const TextSpan(text: 'ente '),
TextSpan(
text: "needs permission to ",
style: Theme.of(context)
.textTheme
.headline5
.copyWith(fontWeight: FontWeight.w400),
),
const TextSpan(text: 'preserve your photos'),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 105),
child: RichText(
text: TextSpan(
style: Theme.of(context)
.textTheme
.headline5
.copyWith(fontWeight: FontWeight.w700),
children: [
const TextSpan(text: 'ente '),
TextSpan(
text: "needs permission to ",
style: Theme.of(context)
.textTheme
.headline5
.copyWith(fontWeight: FontWeight.w400),
),
const TextSpan(text: 'preserve your photos'),
],
),
),
),
],
),
],
),
],
),
),
),
floatingActionButton: Container(
@ -87,10 +81,10 @@ class GrantPermissionsWidget extends StatelessWidget {
],
),
width: double.infinity,
padding: EdgeInsets.only(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: Platform.isIOS ? 40 : 16,
bottom: 16,
),
child: OutlinedButton(
child: const Text("Grant permission"),

View file

@ -1,11 +1,11 @@
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/account/delete_account_page.dart';
import 'package:photos/ui/settings/common_settings.dart';
import 'package:photos/ui/settings/settings_section_title.dart';
import 'package:photos/ui/settings/settings_text_item.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:photos/utils/navigation_util.dart';
class DangerSectionWidget extends StatefulWidget {
const DangerSectionWidget({Key key}) : super(key: key);
@ -39,8 +39,8 @@ class _DangerSectionWidgetState extends State<DangerSectionWidget> {
sectionOptionDivider,
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
_onDeleteAccountTapped();
onTap: () async {
routeToPage(context, const DeleteAccountPage());
},
child: const SettingsTextItem(
text: "Delete account",
@ -51,81 +51,6 @@ class _DangerSectionWidgetState extends State<DangerSectionWidget> {
);
}
Future<void> _onDeleteAccountTapped() async {
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');
try {
final Email email = Email(
recipients: ['account-deletion@ente.io'],
isHTML: false,
);
await FlutterEmailSender.send(email);
} catch (e) {
launch("mailto:account-deletion@ente.io");
}
},
),
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;
},
);
}
Future<void> _onLogoutTapped() async {
AlertDialog alert = AlertDialog(
title: const Text(

View file

@ -11,7 +11,7 @@ description: ente photos application
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.11+341
version: 0.6.14+344
environment:
sdk: ">=2.10.0 <3.0.0"