diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0de687d4e..381a73b8d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -139,6 +139,8 @@ PODS: - nanopb/encode (2.30908.0) - open_file (0.0.1): - Flutter + - open_mail_app (0.0.1): + - Flutter - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -223,6 +225,7 @@ DEPENDENCIES: - motionphoto (from `.symlinks/plugins/motionphoto/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - open_file (from `.symlinks/plugins/open_file/ios`) + - open_mail_app (from `.symlinks/plugins/open_mail_app/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - permission_handler (from `.symlinks/plugins/permission_handler/ios`) @@ -315,6 +318,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/move_to_background/ios" open_file: :path: ".symlinks/plugins/open_file/ios" + open_mail_app: + :path: ".symlinks/plugins/open_mail_app/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_ios: @@ -387,6 +392,7 @@ SPEC CHECKSUMS: move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d + open_mail_app: 794172f6a22cd16319d3ddaf45e945b2f74952b0 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fcffe7d0a..2364db665 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -299,6 +299,7 @@ "${BUILT_PRODUCTS_DIR}/move_to_background/move_to_background.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/open_file/open_file.framework", + "${BUILT_PRODUCTS_DIR}/open_mail_app/open_mail_app.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", @@ -360,6 +361,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/move_to_background.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_mail_app.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7172f2013..92cb0a5a0 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -22,6 +22,18 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + LSApplicationQueriesSchemes + + googlegmail + x-dispatch + readdle-spark + airmail + ms-outlook + ymail + fastmail + superhuman + protonmail + CFBundleURLTypes diff --git a/lib/ui/account/delete_account_page.dart b/lib/ui/account/delete_account_page.dart index 30c99ae69..17177ece7 100644 --- a/lib/ui/account/delete_account_page.dart +++ b/lib/ui/account/delete_account_page.dart @@ -1,7 +1,6 @@ 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'; @@ -11,8 +10,8 @@ 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/email_util.dart'; import 'package:photos/utils/toast_util.dart'; -import 'package:url_launcher/url_launcher.dart'; class DeleteAccountPage extends StatelessWidget { const DeleteAccountPage({ @@ -81,11 +80,10 @@ class DeleteAccountPage extends StatelessWidget { paddingValue: 4, iconData: Icons.check, onTap: () async { - await launchUrl( - Uri( - scheme: "mailto", - path: 'feedback@ente.io', - ), + await sendEmail( + context, + to: 'feedback@ente.io', + subject: '[Feedback]', ); }, ), @@ -222,15 +220,11 @@ class DeleteAccountPage extends StatelessWidget { ), 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"); - } + await sendEmail( + context, + to: 'account-deletion@ente.io', + subject: '[Delete account]', + ); }, ), TextButton( diff --git a/lib/ui/payment/stripe_subscription_page.dart b/lib/ui/payment/stripe_subscription_page.dart index 5b14edfc3..2d17449cd 100644 --- a/lib/ui/payment/stripe_subscription_page.dart +++ b/lib/ui/payment/stripe_subscription_page.dart @@ -21,7 +21,7 @@ import 'package:photos/ui/payment/subscription_plan_widget.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; import 'package:step_progress_indicator/step_progress_indicator.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class StripeSubscriptionPage extends StatefulWidget { final bool isOnboarding; @@ -224,14 +224,14 @@ class _StripeSubscriptionPageState extends State { await _launchStripePortal(); break; case kPlayStore: - launch( + launchUrlString( "https://play.google.com/store/account/subscriptions?sku=" + _currentSubscription.productID + "&package=io.ente.photos", ); break; case kAppStore: - launch("https://apps.apple.com/account/billing"); + launchUrlString("https://apps.apple.com/account/billing"); break; default: _logger.severe( diff --git a/lib/ui/payment/subscription_page.dart b/lib/ui/payment/subscription_page.dart index 4472d62da..f1e42bcb0 100644 --- a/lib/ui/payment/subscription_page.dart +++ b/lib/ui/payment/subscription_page.dart @@ -20,7 +20,7 @@ import 'package:photos/ui/payment/subscription_common_widgets.dart'; import 'package:photos/ui/payment/subscription_plan_widget.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class SubscriptionPage extends StatefulWidget { final bool isOnboarding; @@ -213,13 +213,13 @@ class _SubscriptionPageState extends State { return; } if (Platform.isAndroid) { - launch( + launchUrlString( "https://play.google.com/store/account/subscriptions?sku=" + _currentSubscription.productID + "&package=io.ente.photos", ); } else { - launch("https://apps.apple.com/account/billing"); + launchUrlString("https://apps.apple.com/account/billing"); } }, child: Container( diff --git a/lib/ui/settings/backup_section_widget.dart b/lib/ui/settings/backup_section_widget.dart index d0746df3a..2f90e01cc 100644 --- a/lib/ui/settings/backup_section_widget.dart +++ b/lib/ui/settings/backup_section_widget.dart @@ -18,7 +18,7 @@ import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; import 'package:photos/utils/toast_util.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class BackupSectionWidget extends StatefulWidget { const BackupSectionWidget({Key key}) : super(key: key); @@ -229,11 +229,13 @@ class BackupSectionWidgetState extends State { Navigator.of(context, rootNavigator: true).pop('dialog'); // TODO: Replace with https://pub.dev/packages/in_app_review if (Platform.isAndroid) { - launch( + launchUrlString( "https://play.google.com/store/apps/details?id=io.ente.photos", ); } else { - launch("https://apps.apple.com/in/app/ente-photos/id1542026904"); + launchUrlString( + "https://apps.apple.com/in/app/ente-photos/id1542026904", + ); } }, ), @@ -289,11 +291,13 @@ class BackupSectionWidgetState extends State { Navigator.of(context, rootNavigator: true).pop('dialog'); // TODO: Replace with https://pub.dev/packages/in_app_review if (Platform.isAndroid) { - launch( + launchUrlString( "https://play.google.com/store/apps/details?id=io.ente.photos", ); } else { - launch("https://apps.apple.com/in/app/ente-photos/id1542026904"); + launchUrlString( + "https://apps.apple.com/in/app/ente-photos/id1542026904", + ); } }, ), diff --git a/lib/ui/settings/social_section_widget.dart b/lib/ui/settings/social_section_widget.dart index 352da3ee3..7756622d7 100644 --- a/lib/ui/settings/social_section_widget.dart +++ b/lib/ui/settings/social_section_widget.dart @@ -6,7 +6,7 @@ import 'package:photos/services/update_service.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:url_launcher/url_launcher_string.dart'; class SocialSectionWidget extends StatelessWidget { const SocialSectionWidget({Key key}) : super(key: key); @@ -26,7 +26,7 @@ class SocialSectionWidget extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - launch("https://twitter.com/enteio"); + launchUrlString("https://twitter.com/enteio"); }, child: const SettingsTextItem(text: "Twitter", icon: Icons.navigate_next), @@ -35,7 +35,7 @@ class SocialSectionWidget extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - launch("https://ente.io/discord"); + launchUrlString("https://ente.io/discord"); }, child: const SettingsTextItem(text: "Discord", icon: Icons.navigate_next), @@ -44,7 +44,7 @@ class SocialSectionWidget extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { - launch("https://reddit.com/r/enteio"); + launchUrlString("https://reddit.com/r/enteio"); }, child: const SettingsTextItem(text: "Reddit", icon: Icons.navigate_next), @@ -58,11 +58,11 @@ class SocialSectionWidget extends StatelessWidget { behavior: HitTestBehavior.translucent, onTap: () { if (Platform.isAndroid) { - launch( + launchUrlString( "https://play.google.com/store/apps/details?id=io.ente.photos", ); } else { - launch( + launchUrlString( "https://apps.apple.com/in/app/ente-photos/id1542026904", ); } diff --git a/lib/ui/settings/support_section_widget.dart b/lib/ui/settings/support_section_widget.dart index 6fa59c82c..5ba272303 100644 --- a/lib/ui/settings/support_section_widget.dart +++ b/lib/ui/settings/support_section_widget.dart @@ -2,16 +2,13 @@ import 'dart:io'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/ui/common/web_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:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; -import 'package:url_launcher/url_launcher.dart'; class SupportSectionWidget extends StatelessWidget { const SupportSectionWidget({Key key}) : super(key: key); @@ -34,16 +31,7 @@ class SupportSectionWidget extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { - try { - final Uri emailLaunchUri = Uri( - scheme: 'mailto', - path: kSupportEmail, - ); - launchUrl(emailLaunchUri); - } catch (e) { - Logger("SupportSection").severe(e); - showErrorDialog(context, "", "Please email us at $kSupportEmail"); - } + await sendEmail(context, to: kSupportEmail); }, child: const SettingsTextItem(text: "Email", icon: Icons.navigate_next), diff --git a/lib/utils/email_util.dart b/lib/utils/email_util.dart index 48e2129e0..8b0b6d748 100644 --- a/lib/utils/email_util.dart +++ b/lib/utils/email_util.dart @@ -2,10 +2,13 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:email_validator/email_validator.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:logging/logging.dart'; +import 'package:open_mail_app/open_mail_app.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/error-reporting/super_logging.dart'; @@ -13,7 +16,9 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/ui/common/dialogs.dart'; import 'package:photos/ui/tools/debug/log_file_viewer.dart'; import 'package:photos/utils/dialog_util.dart'; +import 'package:photos/utils/toast_util.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; final Logger _logger = Logger('email_util'); @@ -163,7 +168,7 @@ Future shareLogs( "Email logs", "Please send the logs to $toEmail", firstAction: "Copy email", - secondAction: "Send", + secondAction: "Export logs", ); if (result != null && result == DialogUserChoice.firstChoice) { await Clipboard.setData(ClipboardData(text: toEmail)); @@ -174,3 +179,115 @@ Future shareLogs( sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2), ); } + +Future sendEmail( + BuildContext context, { + @required String to, + String subject, + String body, +}) async { + try { + String clientDebugInfo = await _clientInfo(); + EmailContent email = EmailContent( + to: [ + to, + ], + subject: subject ?? '[Support]', + body: (body ?? '') + clientDebugInfo, + ); + if (Platform.isAndroid) { + // Special handling due to issue in proton mail android client + // https://github.com/ente-io/frame/pull/253 + final Uri params = Uri( + scheme: 'mailto', + path: to, + query: 'subject=${email.subject}&body=${email.body}', + ); + if (await canLaunchUrl(params)) { + await launchUrl(params); + } else { + // this will trigger _showNoMailAppsDialog + throw Exception('Could not launch ${params.toString()}'); + } + } else { + OpenMailAppResult result = await OpenMailApp.composeNewEmailInMailApp( + nativePickerTitle: 'Select email app', + emailContent: email, + ); + if (!result.didOpen && !result.canOpen) { + _showNoMailAppsDialog(context, to); + } else if (!result.didOpen && result.canOpen) { + showCupertinoModalPopup( + context: context, + builder: (_) => CupertinoActionSheet( + title: Text("Select mail app \n $to"), + actions: [ + for (var app in result.options) + CupertinoActionSheetAction( + child: Text(app.name), + onPressed: () { + final content = email; + if (content != null) { + OpenMailApp.composeNewEmailInSpecificMailApp( + mailApp: app, + emailContent: content, + ); + } else { + OpenMailApp.openSpecificMailApp(app); + } + + Navigator.pop(context); + }, + ), + ], + cancelButton: CupertinoActionSheetAction( + child: const Text("Cancel"), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ); + } + } + } catch (e) { + _logger.severe("Failed to send email to $to", e); + _showNoMailAppsDialog(context, to); + } +} + +Future _clientInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + String debugInfo = '\n\n\n\n ------------------- \nFollowing information can ' + 'help us in debugging if you are facing any issue ' + '\nRegistered email: ${Configuration.instance.getEmail()}' + '\nClient: ${packageInfo.packageName}' + '\nVersion : ${packageInfo.version}'; + return debugInfo; +} + +void _showNoMailAppsDialog(BuildContext context, String toEmail) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Please email us at $toEmail'), + actions: [ + TextButton( + child: const Text("Copy email"), + onPressed: () async { + await Clipboard.setData(ClipboardData(text: toEmail)); + showShortToast(context, 'Copied'); + }, + ), + TextButton( + child: const Text("OK"), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ); + }, + ); +} diff --git a/pubspec.lock b/pubspec.lock index 1fd47f4a7..7807d53ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -809,6 +809,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.1" + open_mail_app: + dependency: "direct main" + description: + name: open_mail_app + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" package_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f66304c44..eb46a33e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,7 +82,9 @@ dependencies: motionphoto: git: "https://github.com/ente-io/motionphoto.git" move_to_background: ^1.0.2 + open_file: ^3.2.1 + open_mail_app: ^0.4.5 package_info_plus: ^1.0.1 page_transition: ^2.0.2 path_provider: ^2.0.1