Jelajahi Sumber

Add password reminder for iOS

Neeraj Gupta 2 tahun lalu
induk
melakukan
94ce181f22

+ 1 - 0
lib/services/update_service.dart

@@ -39,6 +39,7 @@ class UpdateService {
   }
 
   Future<bool> resetChangeLog() {
+    _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);

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

@@ -0,0 +1,350 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/ente_theme_data.dart';
+import 'package:photos/events/subscription_purchased_event.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/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() => _P();
+}
+
+class _P extends State<PasswordReminder> {
+  final _recoveryKey = TextEditingController();
+  final Logger _logger = Logger((_P).toString());
+  bool _password2Visible = false;
+  bool _incorrectPassword = false;
+
+  void _verifyRecoveryKey() async {
+    final dialog = createProgressDialog(context, "Verifying password...");
+    await dialog.show();
+    try {
+      final String inputKey = _recoveryKey.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.",
+      );
+      Bus.instance.fire(SubscriptionPurchasedEvent());
+    } catch (e, s) {
+      _logger.severe("failed to verify password", e, s);
+      await dialog.hide();
+      _incorrectPassword = true;
+      if (mounted) {
+        setState(() => {});
+      }
+      // showShortToast(context, "Failed to verify password");
+    }
+  }
+
+  Future<void> _onChangePasswordClick() async {
+    try {
+      final hasAuthenticated =
+          await LocalAuthenticationService.instance.requestLocalAuthentication(
+        context,
+        "Please authenticate to change your password",
+      );
+      if (hasAuthenticated) {
+        await routeToPage(
+          context,
+          const PasswordEntryPage(
+            mode: PasswordEntryMode.update,
+          ),
+          forceCustomPageRoute: true,
+        );
+      }
+    } 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');
+              // UserRemoteFlagService.instance.stopPasswordReminder().ignore();
+              _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 Colors.yellow;
+                  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
+  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. This will cause you will get logged "
+                        "out when the next version is released. So it "
+                        "is good to make sure you know your password.",
+                        style: enteTheme.textTheme.small
+                            .copyWith(color: enteTheme.colorScheme.textMuted),
+                      ),
+                      const SizedBox(height: 12),
+                      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: _recoveryKey,
+                        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)
+                    ],
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+  }
+}