瀏覽代碼

Merge branch 'master' into redesign-with-components

ashilkn 2 年之前
父節點
當前提交
6b5ef752b5

+ 26 - 0
lib/core/configuration.dart

@@ -323,6 +323,32 @@ class Configuration {
     );
   }
 
+  Future<void> verifyPassword(String password) async {
+    final KeyAttributes attributes = getKeyAttributes()!;
+    _logger.info('state validation done');
+    final kek = await CryptoUtil.deriveKey(
+      utf8.encode(password) as Uint8List,
+      Sodium.base642bin(attributes.kekSalt),
+      attributes.memLimit,
+      attributes.opsLimit,
+    ).onError((e, s) {
+      _logger.severe('deriveKey failed', e, s);
+      throw KeyDerivationError();
+    });
+
+    _logger.info('user-key done');
+    try {
+      final Uint8List key = CryptoUtil.decryptSync(
+        Sodium.base642bin(attributes.encryptedKey),
+        kek,
+        Sodium.base642bin(attributes.keyDecryptionNonce),
+      );
+    } catch (e) {
+      _logger.severe('master-key failed, incorrect password?', e);
+      throw Exception("Incorrect password");
+    }
+  }
+
   Future<KeyAttributes> createNewRecoveryKey() async {
     final masterKey = getKey()!;
     final existingAttributes = getKeyAttributes();

+ 2 - 12
lib/services/billing_service.dart

@@ -7,7 +7,6 @@ import 'package:dio/dio.dart';
 // import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:logging/logging.dart';
-import 'package:photos/core/configuration.dart';
 import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/models/billing_plan.dart';
@@ -30,9 +29,7 @@ class BillingService {
   static final BillingService instance = BillingService._privateConstructor();
 
   final _logger = Logger("BillingService");
-  final _dio = Network.instance.getDio();
   final _enteDio = Network.instance.enteDio;
-  final _config = Configuration.instance;
 
   bool _isOnSubscriptionPage = false;
 
@@ -69,23 +66,16 @@ class BillingService {
   }
 
   Future<BillingPlans> getBillingPlans() {
-    _future ??= (_config.isLoggedIn()
-            ? _fetchPublicBillingPlans()
-            : _fetchPrivateBillingPlans())
-        .then((response) {
+    _future ??= _fetchBillingPlans().then((response) {
       return BillingPlans.fromMap(response.data);
     });
     return _future;
   }
 
-  Future<Response<dynamic>> _fetchPrivateBillingPlans() {
+  Future<Response<dynamic>> _fetchBillingPlans() {
     return _enteDio.get("/billing/user-plans/");
   }
 
-  Future<Response<dynamic>> _fetchPublicBillingPlans() {
-    return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
-  }
-
   Future<Subscription> verifySubscription(
     final productID,
     final verificationData, {

+ 2 - 1
lib/services/update_service.dart

@@ -38,7 +38,8 @@ class UpdateService {
     return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
   }
 
-  Future<bool> resetChangeLog() {
+  Future<bool> resetChangeLog() async {
+    await _prefs.remove("userNotify.passwordReminderFlag");
     return _prefs.remove(changeLogVersionKey);
   }
 

+ 18 - 3
lib/services/user_remote_flag_service.dart

@@ -20,6 +20,8 @@ class UserRemoteFlagService {
       UserRemoteFlagService._privateConstructor();
 
   static const String recoveryVerificationFlag = "recoveryKeyVerified";
+  static const String _passwordReminderFlag = "userNotify"
+      ".passwordReminderFlag";
   static const String needRecoveryKeyVerification =
       "needRecoveryKeyVerification";
 
@@ -27,6 +29,20 @@ class UserRemoteFlagService {
     _prefs = await SharedPreferences.getInstance();
   }
 
+  bool showPasswordReminder() {
+    if (Platform.isAndroid) {
+      return false;
+    }
+    return !_prefs.containsKey(_passwordReminderFlag);
+  }
+
+  Future<bool> stopPasswordReminder() async {
+    if (Platform.isAndroid) {
+      return Future.value(true);
+    }
+    return _prefs.setBool(_passwordReminderFlag, true);
+  }
+
   bool shouldShowRecoveryVerification() {
     if (!_prefs.containsKey(needRecoveryKeyVerification)) {
       // fetch the status from remote
@@ -46,14 +62,13 @@ class UserRemoteFlagService {
   // recovery key in the past or not. This helps in avoid showing the same
   // prompt to the user on re-install or signing into a different device
   Future<void> markRecoveryVerificationAsDone() async {
-    await _updateKeyValue(recoveryVerificationFlag, true.toString());
+    await _updateKeyValue(_passwordReminderFlag, true.toString());
     await _prefs.setBool(needRecoveryKeyVerification, false);
   }
 
   Future<void> _refreshRecoveryVerificationFlag() async {
     _logger.finest('refresh recovery key verification flag');
-    final remoteStatusValue =
-        await _getValue(recoveryVerificationFlag, "false");
+    final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
     final bool isNeedVerificationFlagSet =
         _prefs.containsKey(needRecoveryKeyVerification);
     if (remoteStatusValue.toLowerCase() == "true") {

+ 3 - 0
lib/ui/home/landing_page_widget.dart

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/ui/account/email_entry_page.dart';
 import 'package:photos/ui/account/login_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
@@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   void _navigateToSignUpPage() {
     UpdateService.instance.hideChangeLog().ignore();
+    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const EmailEntryPage();
@@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
 
   void _navigateToSignInPage() {
     UpdateService.instance.hideChangeLog().ignore();
+    UserRemoteFlagService.instance.stopPasswordReminder().ignore();
     Widget page;
     if (Configuration.instance.getEncryptedToken() == null) {
       page = const LoginPage();

+ 6 - 0
lib/ui/home_widget.dart

@@ -25,6 +25,7 @@ import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/services/update_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/states/user_details_state.dart';
 import 'package:photos/theme/colors.dart';
@@ -41,6 +42,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
 import 'package:photos/ui/home/preserve_footer_widget.dart';
 import 'package:photos/ui/home/start_backup_hook_widget.dart';
 import 'package:photos/ui/loading_photos_widget.dart';
+import 'package:photos/ui/notification/prompts/password_reminder.dart';
 import 'package:photos/ui/notification/update/change_log_page.dart';
 import 'package:photos/ui/settings/app_update_dialog.dart';
 import 'package:photos/ui/settings_page.dart';
@@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
     if (!LocalSyncService.instance.hasCompletedFirstImport()) {
       return const LoadingPhotosWidget();
     }
+
+    if (UserRemoteFlagService.instance.showPasswordReminder()) {
+      return const PasswordReminder();
+    }
     if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
       ReceiveSharingIntent.reset();
       return CreateCollectionPage(null, _sharedFiles);

+ 371 - 0
lib/ui/notification/prompts/password_reminder.dart

@@ -0,0 +1,371 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:pedantic/pedantic.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/services/local_authentication_service.dart';
+import 'package:photos/services/user_remote_flag_service.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/ui/account/password_entry_page.dart';
+import 'package:photos/ui/common/gradient_button.dart';
+import 'package:photos/ui/home_widget.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class PasswordReminder extends StatefulWidget {
+  const PasswordReminder({Key? key}) : super(key: key);
+
+  @override
+  State<PasswordReminder> createState() => _PasswordReminderState();
+}
+
+class _PasswordReminderState extends State<PasswordReminder> {
+  final _passwordController = TextEditingController();
+  final Logger _logger = Logger((_PasswordReminderState).toString());
+  bool _password2Visible = false;
+  bool _incorrectPassword = false;
+
+  Future<void> _verifyRecoveryKey() async {
+    final dialog = createProgressDialog(context, "Verifying password...");
+    await dialog.show();
+    try {
+      final String inputKey = _passwordController.text;
+      await Configuration.instance.verifyPassword(inputKey);
+      await dialog.hide();
+      UserRemoteFlagService.instance.stopPasswordReminder().ignore();
+      // todo: change this as per figma once the component is ready
+      await showErrorDialog(
+        context,
+        "Password verified",
+        "Great! Thank you for verifying.\n"
+            "\nPlease"
+            " remember to keep your recovery key safely backed up.",
+      );
+
+      unawaited(
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const HomeWidget();
+            },
+          ),
+          (route) => false,
+        ),
+      );
+    } catch (e, s) {
+      _logger.severe("failed to verify password", e, s);
+      await dialog.hide();
+      _incorrectPassword = true;
+      if (mounted) {
+        setState(() => {});
+      }
+    }
+  }
+
+  Future<void> _onChangePasswordClick() async {
+    try {
+      final hasAuthenticated =
+          await LocalAuthenticationService.instance.requestLocalAuthentication(
+        context,
+        "Please authenticate to change your password",
+      );
+      if (hasAuthenticated) {
+        UserRemoteFlagService.instance.stopPasswordReminder().ignore();
+        await routeToPage(
+          context,
+          const PasswordEntryPage(
+            mode: PasswordEntryMode.update,
+          ),
+          forceCustomPageRoute: true,
+        );
+        unawaited(
+          Navigator.of(context).pushAndRemoveUntil(
+            MaterialPageRoute(
+              builder: (BuildContext context) {
+                return const HomeWidget();
+              },
+            ),
+            (route) => false,
+          ),
+        );
+      }
+    } catch (e) {
+      showGenericErrorDialog(context);
+      return;
+    }
+  }
+
+  Future<void> _onSkipClick() async {
+    final enteTextTheme = getEnteTextTheme(context);
+    final enteColor = getEnteColorScheme(context);
+    final content = Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        Text(
+          "You will not be able to access your photos if you forget "
+          "your password.\n\nIf you do not remember your password, "
+          "now is a good time to change it.",
+          style: enteTextTheme.body.copyWith(
+            color: enteColor.textMuted,
+          ),
+        ),
+        const Padding(padding: EdgeInsets.all(8)),
+        SizedBox(
+          width: double.infinity,
+          height: 52,
+          child: OutlinedButton(
+            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
+              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
+                (Set<MaterialState> states) {
+                  return enteTextTheme.bodyBold;
+                },
+              ),
+            ),
+            onPressed: () async {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+
+              _onChangePasswordClick();
+            },
+            child: const Text(
+              "Change password",
+            ),
+          ),
+        ),
+        const Padding(padding: EdgeInsets.all(8)),
+        SizedBox(
+          width: double.infinity,
+          height: 52,
+          child: OutlinedButton(
+            style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
+              textStyle: MaterialStateProperty.resolveWith<TextStyle>(
+                (Set<MaterialState> states) {
+                  return enteTextTheme.bodyBold;
+                },
+              ),
+              backgroundColor: MaterialStateProperty.resolveWith<Color>(
+                (Set<MaterialState> states) {
+                  return enteColor.fillFaint;
+                },
+              ),
+              foregroundColor: MaterialStateProperty.resolveWith<Color>(
+                (Set<MaterialState> states) {
+                  return Theme.of(context).colorScheme.defaultTextColor;
+                },
+              ),
+            ),
+            onPressed: () async {
+              Navigator.of(context, rootNavigator: true).pop('dialog');
+            },
+            child: Text(
+              "Cancel",
+              style: enteTextTheme.bodyBold,
+            ),
+          ),
+        )
+      ],
+    );
+    return showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        return AlertDialog(
+          backgroundColor: enteColor.backgroundElevated,
+          title: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Icon(
+                Icons.report_outlined,
+                size: 36,
+                color: getEnteColorScheme(context).strokeBase,
+              ),
+            ],
+          ),
+          content: content,
+        );
+      },
+      barrierColor: enteColor.backdropBaseMute,
+    );
+  }
+
+  @override
+  void dispose() {
+    _passwordController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final enteTheme = Theme.of(context).colorScheme.enteTheme;
+    final List<Widget> actions = <Widget>[];
+    actions.add(
+      PopupMenuButton(
+        itemBuilder: (context) {
+          return [
+            PopupMenuItem(
+              value: 1,
+              child: SizedBox(
+                width: 120,
+                height: 32,
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    const Icon(
+                      Icons.report_outlined,
+                      color: warning500,
+                      size: 20,
+                    ),
+                    const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
+                    Text(
+                      "Skip",
+                      style: getEnteTextTheme(context)
+                          .bodyBold
+                          .copyWith(color: warning500),
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ];
+        },
+        onSelected: (value) async {
+          _onSkipClick();
+        },
+      ),
+    );
+
+    return Scaffold(
+      appBar: AppBar(
+        elevation: 0,
+        leading: null,
+        automaticallyImplyLeading: false,
+        actions: actions,
+      ),
+      body: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 20.0),
+        child: LayoutBuilder(
+          builder: (context, constraints) {
+            return SingleChildScrollView(
+              child: ConstrainedBox(
+                constraints: BoxConstraints(
+                  minWidth: constraints.maxWidth,
+                  minHeight: constraints.maxHeight,
+                ),
+                child: IntrinsicHeight(
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.start,
+                    children: [
+                      SizedBox(
+                        width: double.infinity,
+                        child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.start,
+                          mainAxisAlignment: MainAxisAlignment.start,
+                          children: [
+                            Text(
+                              'Password reminder',
+                              style: enteTheme.textTheme.h3Bold,
+                            ),
+                            Text(
+                              Configuration.instance.getEmail()!,
+                              style: enteTheme.textTheme.small.copyWith(
+                                color: enteTheme.colorScheme.textMuted,
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                      const SizedBox(height: 18),
+                      Text(
+                        "Enter your password to ensure you remember it."
+                        "\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
+                        style: enteTheme.textTheme.small
+                            .copyWith(color: enteTheme.colorScheme.textMuted),
+                      ),
+                      const SizedBox(height: 24),
+                      TextFormField(
+                        autofillHints: const [AutofillHints.password],
+                        decoration: InputDecoration(
+                          filled: true,
+                          hintText: "Password",
+                          suffixIcon: IconButton(
+                            icon: Icon(
+                              _password2Visible
+                                  ? Icons.visibility
+                                  : Icons.visibility_off,
+                              color: Theme.of(context).iconTheme.color,
+                              size: 20,
+                            ),
+                            onPressed: () {
+                              setState(() {
+                                _password2Visible = !_password2Visible;
+                              });
+                            },
+                          ),
+                          contentPadding: const EdgeInsets.all(20),
+                          border: UnderlineInputBorder(
+                            borderSide: BorderSide.none,
+                            borderRadius: BorderRadius.circular(6),
+                          ),
+                        ),
+                        style: const TextStyle(
+                          fontSize: 14,
+                          fontFeatures: [FontFeature.tabularFigures()],
+                        ),
+                        controller: _passwordController,
+                        autofocus: false,
+                        autocorrect: false,
+                        obscureText: !_password2Visible,
+                        keyboardType: TextInputType.visiblePassword,
+                        onChanged: (_) {
+                          _incorrectPassword = false;
+                          setState(() {});
+                        },
+                      ),
+                      _incorrectPassword
+                          ? const SizedBox(height: 2)
+                          : const SizedBox.shrink(),
+                      _incorrectPassword
+                          ? Align(
+                              alignment: Alignment.centerLeft,
+                              child: Text(
+                                "Incorrect password",
+                                style: enteTheme.textTheme.small.copyWith(
+                                  color: enteTheme.colorScheme.warning700,
+                                ),
+                              ),
+                            )
+                          : const SizedBox.shrink(),
+                      const SizedBox(height: 12),
+                      Expanded(
+                        child: Container(
+                          alignment: Alignment.bottomCenter,
+                          width: double.infinity,
+                          padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
+                          child: Column(
+                            mainAxisAlignment: MainAxisAlignment.end,
+                            crossAxisAlignment: CrossAxisAlignment.stretch,
+                            children: [
+                              GradientButton(
+                                onTap: _verifyRecoveryKey,
+                                text: "Verify",
+                              ),
+                              const SizedBox(height: 8),
+                            ],
+                          ),
+                        ),
+                      ),
+                      const SizedBox(height: 20)
+                    ],
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+  }
+}

+ 15 - 24
lib/utils/date_time_util.dart

@@ -45,7 +45,7 @@ Map<int, String> _days = {
   7: "Sun",
 };
 
-final currentYear = int.parse(DateTime.now().year.toString());
+final currentYear = DateTime.now().year;
 const searchStartYear = 1970;
 
 //Jun 2022
@@ -267,30 +267,18 @@ bool isValidDate({
   return true;
 }
 
-@Deprecated("Use parseDateTimeV2 ")
-DateTime? parseDateFromFileName(String fileName) {
-  if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
-// Whatsapp media files
-    return DateTime.tryParse(fileName.split('-')[1]);
-  } else if (fileName.startsWith("Screenshot_")) {
-// Screenshots on droid
-    return DateTime.tryParse(
-      (fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'),
-    );
-  } else {
-    return DateTime.tryParse(
-      (fileName)
-          .replaceAll("IMG_", "")
-          .replaceAll("VID_", "")
-          .replaceAll("DCIM_", "")
-          .replaceAll("_", " "),
-    );
-  }
-}
-
 final RegExp exp = RegExp('[\\.A-Za-z]*');
 
-DateTime? parseDateTimeFromFileNameV2(String fileName) {
+DateTime? parseDateTimeFromFileNameV2(
+  String fileName, {
+  /* to avoid parsing incorrect date time from the filename, the max and min
+    year limits the chances of parsing incorrect date times
+    */
+  int minYear = 1990,
+  int? maxYear,
+}) {
+  // add next year to avoid corner cases for 31st Dec
+  maxYear ??= currentYear + 1;
   String val = fileName.replaceAll(exp, '');
   if (val.isNotEmpty && !isNumeric(val[0])) {
     val = val.substring(1, val.length);
@@ -319,7 +307,10 @@ DateTime? parseDateTimeFromFileNameV2(String fileName) {
   if (kDebugMode && result == null) {
     debugPrint("Failed to parse $fileName dateTime from $valForParser");
   }
-  return result;
+  if (result != null && result.year >= minYear && result.year <= maxYear) {
+    return result;
+  }
+  return null;
 }
 
 bool isNumeric(String? s) {

+ 1 - 1
lib/utils/file_uploader.dart

@@ -153,7 +153,7 @@ class FileUploader {
       }
       return CollectionsService.instance
           .addToCollection(collectionID, [uploadedFile]).then((aVoid) {
-        return uploadedFile;
+        return uploadedFile as File;
       });
     });
   }

+ 17 - 2
test/utils/date_time_util_test.dart

@@ -9,14 +9,14 @@ void main() {
       "IMG-20221109-WA0000",
       '''Screenshot_20220807-195908_Firefox''',
       '''Screenshot_20220507-195908''',
-      "2019-02-18 16.00.12-DCMX.png",
+      "2022-02-18 16.00.12-DCMX.png",
       "20221107_231730",
       "2020-11-01 02.31.02",
       "IMG_20210921_144423",
       "2019-10-31 155703",
       "IMG_20210921_144423_783",
       "Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
-      "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg"
+      "Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
     ];
     for (String val in validParsing) {
       final parsedValue = parseDateTimeFromFileNameV2(val);
@@ -31,6 +31,21 @@ void main() {
     }
   });
 
+  test("test invalid datetime parsing", () {
+    final List<String> badParsing = ["Snapchat-431959199.mp4."];
+    for (String val in badParsing) {
+      final parsedValue = parseDateTimeFromFileNameV2(val);
+      expect(
+        parsedValue == null,
+        true,
+        reason: "parsing should have failed $val",
+      );
+      if (kDebugMode) {
+        debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
+      }
+    }
+  });
+
   test("verify constants", () {
     final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
     expect(