Browse Source

Merge branch 'master' into redesign-file-info

ashilkn 3 years ago
parent
commit
0a71c1aaa8

+ 11 - 0
lib/models/delete_account.dart

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

+ 71 - 0
lib/services/user_service.dart

@@ -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();

+ 257 - 0
lib/ui/account/delete_account_page.dart

@@ -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;
+      },
+    );
+  }
+}

+ 1 - 1
lib/ui/account/ott_verification_page.dart

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

+ 52 - 58
lib/ui/grant_permissions_widget.dart

@@ -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, 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'),
-                      ],
-                    ),
+              ),
+              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'),
+                    ],
                   ),
                 ),
-              ],
-            ),
-          ],
+              ),
+            ],
+          ),
         ),
       ),
       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"),

+ 4 - 79
lib/ui/settings/danger_section_widget.dart

@@ -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(

+ 1 - 1
pubspec.yaml

@@ -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"