浏览代码

Merge branch 'master' into show-all-collections-in-file-info

ashilkn 2 年之前
父节点
当前提交
7966b2fe4c

+ 2 - 0
lib/core/errors.dart

@@ -6,6 +6,8 @@ class InvalidFileUploadState extends AssertionError {
   InvalidFileUploadState(String message) : super(message);
   InvalidFileUploadState(String message) : super(message);
 }
 }
 
 
+class SubscriptionAlreadyClaimedError extends Error {}
+
 class WiFiUnavailableError extends Error {}
 class WiFiUnavailableError extends Error {}
 
 
 class SyncStopRequestedError extends Error {}
 class SyncStopRequestedError extends Error {}

+ 7 - 0
lib/services/billing_service.dart

@@ -6,6 +6,7 @@ import 'package:dio/dio.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
+import 'package:photos/core/errors.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/core/network.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/subscription.dart';
 import 'package:photos/models/subscription.dart';
@@ -111,6 +112,12 @@ class BillingService {
         ),
         ),
       );
       );
       return Subscription.fromMap(response.data["subscription"]);
       return Subscription.fromMap(response.data["subscription"]);
+    } on DioError catch (e) {
+      if (e.response != null && e.response.statusCode == 409) {
+        throw SubscriptionAlreadyClaimedError();
+      } else {
+        rethrow;
+      }
     } catch (e, s) {
     } catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
       rethrow;
       rethrow;

+ 68 - 0
lib/services/local_authentication_service.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:local_auth/local_auth.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ui/tools/app_lock.dart';
+import 'package:photos/utils/auth_util.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/toast_util.dart';
+
+class LocalAuthenticationService {
+  LocalAuthenticationService._privateConstructor();
+  static final LocalAuthenticationService instance =
+      LocalAuthenticationService._privateConstructor();
+
+  Future<bool> requestLocalAuthentication(
+    BuildContext context,
+    String infoMessage,
+  ) async {
+    if (await _isLocalAuthSupportedOnDevice()) {
+      AppLock.of(context).setEnabled(false);
+      final result = await requestAuthentication(infoMessage);
+      AppLock.of(context).setEnabled(
+        Configuration.instance.shouldShowLockScreen(),
+      );
+      if (!result) {
+        showToast(context, infoMessage);
+        return false;
+      } else {
+        return true;
+      }
+    }
+    return true;
+  }
+
+  Future<bool> requestLocalAuthForLockScreen(
+    BuildContext context,
+    bool shouldEnableLockScreen,
+    String infoMessage,
+    String errorDialogContent, [
+    String errorDialogTitle = "",
+  ]) async {
+    if (await LocalAuthentication().isDeviceSupported()) {
+      AppLock.of(context).disable();
+      final result = await requestAuthentication(
+        infoMessage,
+      );
+      if (result) {
+        AppLock.of(context).setEnabled(shouldEnableLockScreen);
+        await Configuration.instance
+            .setShouldShowLockScreen(shouldEnableLockScreen);
+        return true;
+      } else {
+        AppLock.of(context)
+            .setEnabled(Configuration.instance.shouldShowLockScreen());
+      }
+    } else {
+      showErrorDialog(
+        context,
+        errorDialogTitle,
+        errorDialogContent,
+      );
+    }
+    return false;
+  }
+
+  Future<bool> _isLocalAuthSupportedOnDevice() async {
+    return await LocalAuthentication().isDeviceSupported();
+  }
+}

+ 26 - 30
lib/ui/account/delete_account_page.dart

@@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/models/delete_account.dart';
 import 'package:photos/models/delete_account.dart';
+import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/dialogs.dart';
 import 'package:photos/ui/common/gradient_button.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/crypto_util.dart';
 import 'package:photos/utils/email_util.dart';
 import 'package:photos/utils/email_util.dart';
-import 'package:photos/utils/toast_util.dart';
 
 
 class DeleteAccountPage extends StatelessWidget {
 class DeleteAccountPage extends StatelessWidget {
   const DeleteAccountPage({
   const DeleteAccountPage({
@@ -144,36 +142,34 @@ class DeleteAccountPage extends StatelessWidget {
     BuildContext context,
     BuildContext context,
     DeleteChallengeResponse response,
     DeleteChallengeResponse response,
   ) async {
   ) async {
-    AppLock.of(context).setEnabled(false);
-    const 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 choice = await showChoiceDialog(
+    final hasAuthenticated =
+        await LocalAuthenticationService.instance.requestLocalAuthentication(
       context,
       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,
+      "Please authenticate to initiate account deletion",
     );
     );
-    if (choice != DialogUserChoice.secondChoice) {
-      return;
+
+    if (hasAuthenticated) {
+      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);
     }
     }
-    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 {
   Future<void> _requestEmailForDeletion(BuildContext context) async {

+ 13 - 0
lib/ui/payment/subscription_page.dart

@@ -4,6 +4,7 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:in_app_purchase/in_app_purchase.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/core/errors.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/events/subscription_purchased_event.dart';
 import 'package:photos/models/billing_plan.dart';
 import 'package:photos/models/billing_plan.dart';
@@ -93,6 +94,18 @@ class _SubscriptionPageState extends State<SubscriptionPage> {
             if (widget.isOnboarding) {
             if (widget.isOnboarding) {
               Navigator.of(context).popUntil((route) => route.isFirst);
               Navigator.of(context).popUntil((route) => route.isFirst);
             }
             }
+          } on SubscriptionAlreadyClaimedError catch (e) {
+            _logger.warning("subscription is already claimed ", e);
+            await _dialog.hide();
+            final String title = "${Platform.isAndroid ? "Play" : "App"}"
+                "Store subscription";
+            final String id =
+                Platform.isAndroid ? "Google Play ID" : "Apple ID";
+            final String message = '''Your $id is already linked to another
+             ente account.\nIf you would like to use your $id with this 
+             account, please contact our support''';
+            showErrorDialog(context, title, message);
+            return;
           } catch (e) {
           } catch (e) {
             _logger.warning("Could not complete payment ", e);
             _logger.warning("Could not complete payment ", e);
             await _dialog.hide();
             await _dialog.hide();

+ 52 - 62
lib/ui/settings/account_section_widget.dart

@@ -1,7 +1,7 @@
 import 'package:expandable/expandable.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
-import 'package:photos/core/configuration.dart';
+import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/account/change_email_dialog.dart';
 import 'package:photos/ui/account/change_email_dialog.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
 import 'package:photos/ui/account/password_entry_page.dart';
@@ -9,11 +9,8 @@ import 'package:photos/ui/account/recovery_key_page.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
-import 'package:photos/ui/tools/app_lock.dart';
-import 'package:photos/utils/auth_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/navigation_util.dart';
-import 'package:photos/utils/toast_util.dart';
 
 
 class AccountSectionWidget extends StatefulWidget {
 class AccountSectionWidget extends StatefulWidget {
   const AccountSectionWidget({Key key}) : super(key: key);
   const AccountSectionWidget({Key key}) : super(key: key);
@@ -27,7 +24,7 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return ExpandablePanel(
     return ExpandablePanel(
       header: const SettingsSectionTitle("Account"),
       header: const SettingsSectionTitle("Account"),
-      collapsed: Container(),
+      collapsed: const SizedBox.shrink(),
       expanded: _getSectionOptions(context),
       expanded: _getSectionOptions(context),
       theme: getExpandableTheme(context),
       theme: getExpandableTheme(context),
     );
     );
@@ -39,32 +36,29 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
         GestureDetector(
         GestureDetector(
           behavior: HitTestBehavior.translucent,
           behavior: HitTestBehavior.translucent,
           onTap: () async {
           onTap: () async {
-            AppLock.of(context).setEnabled(false);
-            const String reason = "Please authenticate to view your recovery key";
-            final result = await requestAuthentication(reason);
-            AppLock.of(context)
-                .setEnabled(Configuration.instance.shouldShowLockScreen());
-            if (!result) {
-              showToast(context, reason);
-              return;
-            }
-
-            String recoveryKey;
-            try {
-              recoveryKey = await _getOrCreateRecoveryKey();
-            } catch (e) {
-              showGenericErrorDialog(context);
-              return;
-            }
-            routeToPage(
+            final hasAuthenticated = await LocalAuthenticationService.instance
+                .requestLocalAuthentication(
               context,
               context,
-              RecoveryKeyPage(
-                recoveryKey,
-                "OK",
-                showAppBar: true,
-                onDone: () {},
-              ),
+              "Please authenticate to view your recovery key",
             );
             );
+            if (hasAuthenticated) {
+              String recoveryKey;
+              try {
+                recoveryKey = await _getOrCreateRecoveryKey();
+              } catch (e) {
+                showGenericErrorDialog(context);
+                return;
+              }
+              routeToPage(
+                context,
+                RecoveryKeyPage(
+                  recoveryKey,
+                  "OK",
+                  showAppBar: true,
+                  onDone: () {},
+                ),
+              );
+            }
           },
           },
           child: const SettingsTextItem(
           child: const SettingsTextItem(
             text: "Recovery key",
             text: "Recovery key",
@@ -75,23 +69,21 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
         GestureDetector(
         GestureDetector(
           behavior: HitTestBehavior.translucent,
           behavior: HitTestBehavior.translucent,
           onTap: () async {
           onTap: () async {
-            AppLock.of(context).setEnabled(false);
-            const String reason = "Please authenticate to change your email";
-            final result = await requestAuthentication(reason);
-            AppLock.of(context)
-                .setEnabled(Configuration.instance.shouldShowLockScreen());
-            if (!result) {
-              showToast(context, reason);
-              return;
-            }
-            showDialog(
-              context: context,
-              builder: (BuildContext context) {
-                return const ChangeEmailDialog();
-              },
-              barrierColor: Colors.black.withOpacity(0.85),
-              barrierDismissible: false,
+            final hasAuthenticated = await LocalAuthenticationService.instance
+                .requestLocalAuthentication(
+              context,
+              "Please authenticate to change your email",
             );
             );
+            if (hasAuthenticated) {
+              showDialog(
+                context: context,
+                builder: (BuildContext context) {
+                  return const ChangeEmailDialog();
+                },
+                barrierColor: Colors.black.withOpacity(0.85),
+                barrierDismissible: false,
+              );
+            }
           },
           },
           child: const SettingsTextItem(
           child: const SettingsTextItem(
             text: "Change email",
             text: "Change email",
@@ -102,24 +94,22 @@ class AccountSectionWidgetState extends State<AccountSectionWidget> {
         GestureDetector(
         GestureDetector(
           behavior: HitTestBehavior.translucent,
           behavior: HitTestBehavior.translucent,
           onTap: () async {
           onTap: () async {
-            AppLock.of(context).setEnabled(false);
-            const String reason = "Please authenticate to change your password";
-            final result = await requestAuthentication(reason);
-            AppLock.of(context)
-                .setEnabled(Configuration.instance.shouldShowLockScreen());
-            if (!result) {
-              showToast(context, reason);
-              return;
-            }
-            Navigator.of(context).push(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return const PasswordEntryPage(
-                    mode: PasswordEntryMode.update,
-                  );
-                },
-              ),
+            final hasAuthenticated = await LocalAuthenticationService.instance
+                .requestLocalAuthentication(
+              context,
+              "Please authenticate to change your password",
             );
             );
+            if (hasAuthenticated) {
+              Navigator.of(context).push(
+                MaterialPageRoute(
+                  builder: (BuildContext context) {
+                    return const PasswordEntryPage(
+                      mode: PasswordEntryMode.update,
+                    );
+                  },
+                ),
+              );
+            }
           },
           },
           child: const SettingsTextItem(
           child: const SettingsTextItem(
             text: "Change password",
             text: "Change password",

+ 32 - 42
lib/ui/settings/security_section_widget.dart

@@ -8,15 +8,13 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/ente_theme_data.dart';
 import 'package:photos/events/two_factor_status_change_event.dart';
 import 'package:photos/events/two_factor_status_change_event.dart';
+import 'package:photos/services/local_authentication_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/services/user_service.dart';
 import 'package:photos/ui/account/sessions_page.dart';
 import 'package:photos/ui/account/sessions_page.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/common/loading_widget.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/common_settings.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_section_title.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
 import 'package:photos/ui/settings/settings_text_item.dart';
-import 'package:photos/ui/tools/app_lock.dart';
-import 'package:photos/utils/auth_util.dart';
-import 'package:photos/utils/toast_util.dart';
 
 
 class SecuritySectionWidget extends StatefulWidget {
 class SecuritySectionWidget extends StatefulWidget {
   const SecuritySectionWidget({Key key}) : super(key: key);
   const SecuritySectionWidget({Key key}) : super(key: key);
@@ -26,9 +24,6 @@ class SecuritySectionWidget extends StatefulWidget {
 }
 }
 
 
 class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
 class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
-  static const kAuthToViewSessions =
-      "Please authenticate to view your active sessions";
-
   final _config = Configuration.instance;
   final _config = Configuration.instance;
 
 
   StreamSubscription<TwoFactorStatusChangeEvent> _twoFactorStatusChangeEvent;
   StreamSubscription<TwoFactorStatusChangeEvent> _twoFactorStatusChangeEvent;
@@ -82,21 +77,18 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                       return Switch.adaptive(
                       return Switch.adaptive(
                         value: snapshot.data,
                         value: snapshot.data,
                         onChanged: (value) async {
                         onChanged: (value) async {
-                          AppLock.of(context).setEnabled(false);
-                          const String reason =
-                              "Please authenticate to configure two-factor authentication";
-                          final result = await requestAuthentication(reason);
-                          AppLock.of(context).setEnabled(
-                            Configuration.instance.shouldShowLockScreen(),
+                          final hasAuthenticated =
+                              await LocalAuthenticationService.instance
+                                  .requestLocalAuthentication(
+                            context,
+                            "Please authenticate to configure two-factor authentication",
                           );
                           );
-                          if (!result) {
-                            showToast(context, reason);
-                            return;
-                          }
-                          if (value) {
-                            UserService.instance.setupTwoFactor(context);
-                          } else {
-                            _disableTwoFactor();
+                          if (hasAuthenticated) {
+                            if (value) {
+                              UserService.instance.setupTwoFactor(context);
+                            } else {
+                              _disableTwoFactor();
+                            }
                           }
                           }
                         },
                         },
                       );
                       );
@@ -129,17 +121,16 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             Switch.adaptive(
             Switch.adaptive(
               value: _config.shouldShowLockScreen(),
               value: _config.shouldShowLockScreen(),
               onChanged: (value) async {
               onChanged: (value) async {
-                AppLock.of(context).disable();
-                final result = await requestAuthentication(
+                final hasAuthenticated = await LocalAuthenticationService
+                    .instance
+                    .requestLocalAuthForLockScreen(
+                  context,
+                  value,
                   "Please authenticate to change lockscreen setting",
                   "Please authenticate to change lockscreen setting",
+                  "To enable lockscreen, please setup device passcode or screen lock in your system settings.",
                 );
                 );
-                if (result) {
-                  AppLock.of(context).setEnabled(value);
-                  _config.setShouldShowLockScreen(value);
+                if (hasAuthenticated) {
                   setState(() {});
                   setState(() {});
-                } else {
-                  AppLock.of(context)
-                      .setEnabled(_config.shouldShowLockScreen());
                 }
                 }
               },
               },
             ),
             ),
@@ -250,21 +241,20 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
       GestureDetector(
       GestureDetector(
         behavior: HitTestBehavior.translucent,
         behavior: HitTestBehavior.translucent,
         onTap: () async {
         onTap: () async {
-          AppLock.of(context).setEnabled(false);
-          final result = await requestAuthentication(kAuthToViewSessions);
-          AppLock.of(context)
-              .setEnabled(Configuration.instance.shouldShowLockScreen());
-          if (!result) {
-            showToast(context, kAuthToViewSessions);
-            return;
-          }
-          Navigator.of(context).push(
-            MaterialPageRoute(
-              builder: (BuildContext context) {
-                return const SessionsPage();
-              },
-            ),
+          final hasAuthenticated = await LocalAuthenticationService.instance
+              .requestLocalAuthentication(
+            context,
+            "Please authenticate to view your active sessions",
           );
           );
+          if (hasAuthenticated) {
+            Navigator.of(context).push(
+              MaterialPageRoute(
+                builder: (BuildContext context) {
+                  return const SessionsPage();
+                },
+              ),
+            );
+          }
         },
         },
         child: const SettingsTextItem(
         child: const SettingsTextItem(
           text: "Active sessions",
           text: "Active sessions",

+ 10 - 3
lib/ui/tools/deduplicate_page.dart

@@ -175,7 +175,14 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
             shrinkWrap: true,
             shrinkWrap: true,
           ),
           ),
         ),
         ),
-        _selectedFiles.isEmpty ? const SizedBox.shrink() : _getDeleteButton(),
+        _selectedFiles.isEmpty
+            ? const SizedBox.shrink()
+            : Column(
+                children: [
+                  _getDeleteButton(),
+                  const SizedBox(height: crossAxisSpacing / 2),
+                ],
+              ),
       ],
       ],
     );
     );
   }
   }
@@ -317,7 +324,7 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
       width: double.infinity,
       width: double.infinity,
       child: SafeArea(
       child: SafeArea(
         child: Padding(
         child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 2),
+          padding: const EdgeInsets.symmetric(horizontal: crossAxisSpacing / 2),
           child: TextButton(
           child: TextButton(
             style: OutlinedButton.styleFrom(
             style: OutlinedButton.styleFrom(
               backgroundColor:
               backgroundColor:
@@ -326,7 +333,7 @@ class _DeduplicatePageState extends State<DeduplicatePage> {
             child: Column(
             child: Column(
               mainAxisAlignment: MainAxisAlignment.end,
               mainAxisAlignment: MainAxisAlignment.end,
               children: [
               children: [
-                const Padding(padding: EdgeInsets.all(crossAxisSpacing / 2)),
+                const Padding(padding: EdgeInsets.all(4)),
                 Text(
                 Text(
                   text,
                   text,
                   style: TextStyle(
                   style: TextStyle(

+ 6 - 4
lib/utils/dialog_util.dart

@@ -31,10 +31,12 @@ Future<dynamic> showErrorDialog(
 ) {
 ) {
   final AlertDialog alert = AlertDialog(
   final AlertDialog alert = AlertDialog(
     shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
     shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
-    title: Text(
-      title,
-      style: Theme.of(context).textTheme.headline6,
-    ),
+    title: title.isEmpty
+        ? const SizedBox.shrink()
+        : Text(
+            title,
+            style: Theme.of(context).textTheme.headline6,
+          ),
     content: Text(content),
     content: Text(content),
     actions: [
     actions: [
       TextButton(
       TextButton(