From 82e7002b7fe5021e1d3056027132d0da8dfad9b9 Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Wed, 22 Jan 2020 15:26:21 -0600 Subject: [PATCH 1/7] Add magic email clients feature - add AppSelector type: wraps a custom UIAlertController that contains a list of apps that can be called - add LinkMailPresenter: uses AppSelector to present a list of available email clients. The supported clients are read from EmailClients.plist - update NUXLinkMailViewController, to use LinkMailPresenter --- .../project.pbxproj | 8 + .../NUX/NUXLinkMailViewController.swift | 24 +-- .../Resources/EmailClients.plist | 18 +++ WordPressAuthenticator/UI/AppSelector.swift | 148 ++++++++++++++++++ 4 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 WordPressAuthenticator/Resources/EmailClients.plist create mode 100644 WordPressAuthenticator/UI/AppSelector.swift diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index 974446116..b3fd409fe 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; }; 1A21EE9822832BC300C940C6 /* WordPressComOAuthClientFacade+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */; }; 1A4095182271AEFC009AA86D /* WPAuthenticator-Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */; }; + 3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */; }; 7A7A9B9CD2D81959F9AB9AF6 /* Pods_WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C736FF243DE333FCAB1C2614 /* Pods_WordPressAuthenticator.framework */; }; 982C8E7923021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */; }; 98AA5A5720AA1A7000A5958A /* WPHelpIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA5A5620AA1A7000A5958A /* WPHelpIndicatorView.swift */; }; @@ -149,6 +151,8 @@ 276354F054C34AD36CA32AB6 /* Pods-WordPressAuthenticator.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-alpha.xcconfig"; sourceTree = ""; }; 33FEF45B466FF8EAAE5F3923 /* Pods-WordPressAuthenticator.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release.xcconfig"; sourceTree = ""; }; 37AFD4EF492B00CA7AEC11A3 /* Pods-WordPressAuthenticatorTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticatorTests/Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; sourceTree = ""; }; + 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = EmailClients.plist; sourceTree = ""; }; + 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = ""; }; 5A441EC80D2B8D2209C2E228 /* Pods_WordPressAuthenticatorTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressAuthenticatorTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8F7217C3F7A6285D9C6CF786 /* Pods-WordPressAuthenticator.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-internal.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-internal.xcconfig"; sourceTree = ""; }; 982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginPrologueLoginMethodViewController.swift; sourceTree = ""; }; @@ -338,6 +342,7 @@ isa = PBXGroup; children = ( B56052A32090B2ED001B91FD /* CircularImageView.swift */, + 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */, B5609112208A555500399AE4 /* LoginTextField.swift */, B5609113208A555500399AE4 /* SearchTableViewCell.swift */, B5609114208A555500399AE4 /* SearchTableViewCell.xib */, @@ -477,6 +482,7 @@ children = ( B5A5274020B478160065BE81 /* Animations */, B5E07FF3208FD13800657A9A /* Assets.xcassets */, + 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */, ); path = Resources; sourceTree = ""; @@ -699,6 +705,7 @@ B5609118208A555600399AE4 /* SearchTableViewCell.xib in Resources */, B560913F208A563800399AE4 /* Login.storyboard in Resources */, B5609137208A563800399AE4 /* EmailMagicLink.storyboard in Resources */, + 3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */, FF629D9622393500004C4106 /* WordPressAuthenticator.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -915,6 +922,7 @@ B560911F208A555E00399AE4 /* SignupGoogleViewController.swift in Sources */, B5609142208A563800399AE4 /* LoginNavigationController.swift in Sources */, B56090E4208A4F9D00399AE4 /* WPNUXMainButton.m in Sources */, + 3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */, B560913B208A563800399AE4 /* LoginSelfHostedViewController.swift in Sources */, B5609136208A563800399AE4 /* Login2FAViewController.swift in Sources */, B56090E1208A4F9D00399AE4 /* WPWalkthroughTextField.m in Sources */, diff --git a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift index 395166101..a74a5be09 100644 --- a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift +++ b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift @@ -105,30 +105,10 @@ class NUXLinkMailViewController: LoginViewController { } } } - if MFMailComposeViewController.canSendMail() { - let url = URL(string: "message://")! - UIApplication.shared.open(url) - } else if let googleMailURL = URL(string: "googlegmail://"), - UIApplication.shared.canOpenURL(googleMailURL) { - UIApplication.shared.open(googleMailURL) - } else { - showAlertToCheckEmail() - } + let linkMailPresenter = LinkMailPresenter() + linkMailPresenter.presentEmailClients(on: self) } - func showAlertToCheckEmail() { - let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") - let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") - - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("OK", - comment: "Button title. An acknowledgement of the message displayed in a prompt.")) - self.present(alertController, animated: true, completion: nil) - } - - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { WordPressAuthenticator.track(.loginMagicLinkExited) } diff --git a/WordPressAuthenticator/Resources/EmailClients.plist b/WordPressAuthenticator/Resources/EmailClients.plist new file mode 100644 index 000000000..e085bae01 --- /dev/null +++ b/WordPressAuthenticator/Resources/EmailClients.plist @@ -0,0 +1,18 @@ + + + + + gmail + googlegmail:// + airmail + airmail:// + msOutlook + ms-outlook:// + spark + readdle-spark:// + yahooMail + ymail:// + fastmail + fastmail:// + + diff --git a/WordPressAuthenticator/UI/AppSelector.swift b/WordPressAuthenticator/UI/AppSelector.swift new file mode 100644 index 000000000..473397851 --- /dev/null +++ b/WordPressAuthenticator/UI/AppSelector.swift @@ -0,0 +1,148 @@ +import MessageUI +import UIKit + +/// App selector that selects an app from a list and opens it +/// Note: it's a wrapper of UIAlertController (which cannot be sublcassed) +class AppSelector { + // the action sheet that will contain the list of apps that can be called + let alertController: UIAlertController + + /// initializes the picker with a dictionary. Initialization will fail if an empty/invalid app list is passed + /// - Parameters: + /// - appList: collection of apps to be added to the selector + /// - defaultAction: default action, if not nil, will be the first element of the list + init?(with appList: [String: String], defaultAction: UIAlertAction? = nil) { + /// inline method that builds a list of app calls to be inserted in the action sheet + func buildAppCalls(from appList: [String: String]) -> [UIAlertAction]? { + guard !appList.isEmpty else { + return nil + } + var actions = [UIAlertAction]() + for (name, urlString) in appList { + guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else { + continue + } + actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { action in + UIApplication.shared.open(url) + }) + } + actions.append(UIAlertAction(title: AppSelectorTitles.cancel.localized, style: .cancel, handler: nil)) + + if let action = defaultAction { + actions.insert(action, at: 0) + } + return actions + } + + guard let appCalls = buildAppCalls(from: appList) else { + return nil + } + + alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + appCalls.forEach { + alertController.addAction($0) + } + } +} + + +/// Email Clients Selector +extension AppSelector { + /// initializes the picker with a plist file in a specified bundle + convenience init?(with plistFile: String, in bundle: Bundle, defaultAction: UIAlertAction? = nil) { + + guard let path = bundle.path(forResource: plistFile, ofType: "plist"), + let availableApps = NSDictionary(contentsOfFile: path) as? [String: String], + !availableApps.isEmpty else { + return nil + } + self.init(with: availableApps, defaultAction: defaultAction) + } + + /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist + convenience init?() { + let bundle = Bundle(for: type(of: self)) + let plistFile = "EmailClients" + var defaultAction: UIAlertAction? + + // if available, prepend apple mail + if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { + defaultAction = UIAlertAction(title: AppSelectorTitles.appleMail.localized, style: .default) { action in + UIApplication.shared.open(url) + } + } + self.init(with: plistFile, in: bundle, defaultAction: defaultAction) + } +} + + +/// Email picker presenter +class LinkMailPresenter { + /// Presents the available mail clients in an action sheet. If none is available, + /// Falls back to Apple Mail and opens it. + /// If not even Apple Mail is available, presents an alert to check your email + /// - Parameters: + /// - viewController: the UIViewController that will present the action sheet + /// - appSelector: the app picker that contains the available clients. Nil if no clients are available + /// reads the supported email clients from EmailClients.plist + func presentEmailClients(on viewController: UIViewController, + appSelector: AppSelector? = AppSelector()) { + + guard let picker = appSelector else { + // fall back to Apple Mail if no other clients are installed + if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { + UIApplication.shared.open(url) + } else { + showAlertToCheckEmail(on: viewController) + } + return + } + viewController.present(picker.alertController, animated: true) + } + + private func showAlertToCheckEmail(on viewController: UIViewController) { + let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") + let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") + + let alertController = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + alertController.addCancelActionWithTitle(NSLocalizedString("OK", + comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + viewController.present(alertController, animated: true, completion: nil) + } +} + + +/// Localizable app selector titles +enum AppSelectorTitles: String { + case appleMail + case gmail + case airmail + case msOutlook + case spark + case yahooMail + case fastMail + case cancel + + var localized: String { + switch self { + case .appleMail: + return NSLocalizedString("Mail (Default)", comment: "Label for Apple Mail as default email client") + case .gmail: + return NSLocalizedString("Gmail", comment: "Gmail") + case .airmail: + return NSLocalizedString("Airmail", comment: "Airmail") + case .msOutlook: + return NSLocalizedString("Microsoft Outlook", comment: "Microsoft Outlook") + case .spark: + return NSLocalizedString("Spark", comment: "Spark") + case .yahooMail: + return NSLocalizedString("Yahoo Mail", comment: "Yahoo Mail") + case .fastMail: + return NSLocalizedString("Fastmail", comment: "Fastmail") + case .cancel: + return NSLocalizedString("Cancel", comment: "Cancel") + } + } +} From 483060a88972a745310c9f510159f8799d27822a Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Wed, 22 Jan 2020 15:50:54 -0600 Subject: [PATCH 2/7] update version to 1.10.7-beta.1 --- WordPressAuthenticator.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index c24e204c3..5353201a9 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressAuthenticator" - s.version = "1.10.6" + s.version = "1.10.7-beta.1" s.summary = "WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps." s.description = <<-DESC From a59746afb3742b2da6c598415211dada7fae31a4 Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Thu, 23 Jan 2020 15:07:13 -0600 Subject: [PATCH 3/7] add EmailClients.plist to WordPressAuthenticator bundle, in podspec, update AppSelector to sort the action list alphabetically and use WordPressAuthenticator.bundle to search EmailClients.plist --- WordPressAuthenticator.podspec | 1 + .../project.pbxproj | 20 +++++++++-- .../EmailClients.plist | 0 WordPressAuthenticator/UI/AppSelector.swift | 35 ++++++++++++------- 4 files changed, 41 insertions(+), 15 deletions(-) rename WordPressAuthenticator/Resources/{ => SupportedEmailClients}/EmailClients.plist (100%) diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index 5353201a9..156d5a0a8 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.resource_bundles = { 'WordPressAuthenticatorResources': [ 'WordPressAuthenticator/Resources/Assets.xcassets', + 'WordPressAuthenticator/Resources/SupportedEmailClients/*.plist', 'WordPressAuthenticator/Resources/Animations/*.json', 'WordPressAuthenticator/**/*.{storyboard,xib}' ] diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index b3fd409fe..dc0f61e73 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -298,6 +298,21 @@ path = Private; sourceTree = ""; }; + 3F550D4B23DA3B59007E5897 /* SupportedEmailClients */ = { + isa = PBXGroup; + children = ( + 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */, + ); + path = SupportedEmailClients; + sourceTree = ""; + }; + 3F550D4C23DA4191007E5897 /* UI */ = { + isa = PBXGroup; + children = ( + ); + path = UI; + sourceTree = ""; + }; 6205895375D954F46B1DFE53 /* Pods */ = { isa = PBXGroup; children = ( @@ -341,8 +356,8 @@ B5609094208A4EAF00399AE4 /* UI */ = { isa = PBXGroup; children = ( - B56052A32090B2ED001B91FD /* CircularImageView.swift */, 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */, + B56052A32090B2ED001B91FD /* CircularImageView.swift */, B5609112208A555500399AE4 /* LoginTextField.swift */, B5609113208A555500399AE4 /* SearchTableViewCell.swift */, B5609114208A555500399AE4 /* SearchTableViewCell.xib */, @@ -482,7 +497,7 @@ children = ( B5A5274020B478160065BE81 /* Animations */, B5E07FF3208FD13800657A9A /* Assets.xcassets */, - 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */, + 3F550D4B23DA3B59007E5897 /* SupportedEmailClients */, ); path = Resources; sourceTree = ""; @@ -547,6 +562,7 @@ B501C03D208FC52500D1E58F /* Authenticator */, B501C03B208FC52400D1E58F /* Model */, B501C03F208FC52500D1E58F /* Services */, + 3F550D4C23DA4191007E5897 /* UI */, B5ED7904207E976500A8FD8C /* Info.plist */, ); path = WordPressAuthenticatorTests; diff --git a/WordPressAuthenticator/Resources/EmailClients.plist b/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist similarity index 100% rename from WordPressAuthenticator/Resources/EmailClients.plist rename to WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist diff --git a/WordPressAuthenticator/UI/AppSelector.swift b/WordPressAuthenticator/UI/AppSelector.swift index 473397851..0290f469f 100644 --- a/WordPressAuthenticator/UI/AppSelector.swift +++ b/WordPressAuthenticator/UI/AppSelector.swift @@ -6,7 +6,7 @@ import UIKit class AppSelector { // the action sheet that will contain the list of apps that can be called let alertController: UIAlertController - + /// initializes the picker with a dictionary. Initialization will fail if an empty/invalid app list is passed /// - Parameters: /// - appList: collection of apps to be added to the selector @@ -26,18 +26,23 @@ class AppSelector { UIApplication.shared.open(url) }) } + guard !actions.isEmpty else { + return nil + } + //sort the apps alphabetically + actions = actions.sorted { $0.title ?? "" < $1.title ?? "" } actions.append(UIAlertAction(title: AppSelectorTitles.cancel.localized, style: .cancel, handler: nil)) - + if let action = defaultAction { actions.insert(action, at: 0) } return actions } - + guard let appCalls = buildAppCalls(from: appList) else { return nil } - + alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) appCalls.forEach { alertController.addAction($0) @@ -51,27 +56,31 @@ extension AppSelector { /// initializes the picker with a plist file in a specified bundle convenience init?(with plistFile: String, in bundle: Bundle, defaultAction: UIAlertAction? = nil) { - guard let path = bundle.path(forResource: plistFile, ofType: "plist"), - let availableApps = NSDictionary(contentsOfFile: path) as? [String: String], - !availableApps.isEmpty else { - return nil + guard let plistPath = bundle.path(forResource: plistFile, ofType: "plist"), + let availableApps = NSDictionary(contentsOfFile: plistPath) as? [String: String] else { + return nil } self.init(with: availableApps, defaultAction: defaultAction) } /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist convenience init?() { - let bundle = Bundle(for: type(of: self)) + guard let bundlePath = Bundle(for: type(of: self)) + .path(forResource: "WordPressAuthenticatorResources", ofType: "bundle"), + let wpAuthenticatorBundle = Bundle(path: bundlePath) else { + return nil + } + let plistFile = "EmailClients" var defaultAction: UIAlertAction? - + // if available, prepend apple mail if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { defaultAction = UIAlertAction(title: AppSelectorTitles.appleMail.localized, style: .default) { action in UIApplication.shared.open(url) } } - self.init(with: plistFile, in: bundle, defaultAction: defaultAction) + self.init(with: plistFile, in: wpAuthenticatorBundle, defaultAction: defaultAction) } } @@ -122,7 +131,7 @@ enum AppSelectorTitles: String { case msOutlook case spark case yahooMail - case fastMail + case fastmail case cancel var localized: String { @@ -139,7 +148,7 @@ enum AppSelectorTitles: String { return NSLocalizedString("Spark", comment: "Spark") case .yahooMail: return NSLocalizedString("Yahoo Mail", comment: "Yahoo Mail") - case .fastMail: + case .fastmail: return NSLocalizedString("Fastmail", comment: "Fastmail") case .cancel: return NSLocalizedString("Cancel", comment: "Cancel") From 2b8d9cb0c8b0f58e36b1a5e11c45dbd2028c650f Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Thu, 23 Jan 2020 16:35:46 -0600 Subject: [PATCH 4/7] Add unit tests: - add URLHandler protocol - add conformance to URLHandler for URLSession to inject the dependency in AppSelector - add AppSelectorTests.swift --- .../project.pbxproj | 30 +++++++- .../AppSelector.swift | 49 ++----------- .../LinkMailPresenter.swift | 39 ++++++++++ .../Email Client Picker/URLHandler.swift | 13 ++++ .../NUX/NUXLinkMailViewController.swift | 1 - .../AppSelectorTests.swift | 71 +++++++++++++++++++ 6 files changed, 159 insertions(+), 44 deletions(-) rename WordPressAuthenticator/{UI => Email Client Picker}/AppSelector.swift (64%) create mode 100644 WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift create mode 100644 WordPressAuthenticator/Email Client Picker/URLHandler.swift create mode 100644 WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index dc0f61e73..1dde45a37 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; }; 1A21EE9822832BC300C940C6 /* WordPressComOAuthClientFacade+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */; }; 1A4095182271AEFC009AA86D /* WPAuthenticator-Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */; }; + 3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */; }; + 3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5223DA4AC6007E5897 /* URLHandler.swift */; }; 3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */; }; 3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */; }; 7A7A9B9CD2D81959F9AB9AF6 /* Pods_WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C736FF243DE333FCAB1C2614 /* Pods_WordPressAuthenticator.framework */; }; @@ -151,6 +154,9 @@ 276354F054C34AD36CA32AB6 /* Pods-WordPressAuthenticator.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-alpha.xcconfig"; sourceTree = ""; }; 33FEF45B466FF8EAAE5F3923 /* Pods-WordPressAuthenticator.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release.xcconfig"; sourceTree = ""; }; 37AFD4EF492B00CA7AEC11A3 /* Pods-WordPressAuthenticatorTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticatorTests/Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; sourceTree = ""; }; + 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelectorTests.swift; sourceTree = ""; }; + 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMailPresenter.swift; sourceTree = ""; }; + 3F550D5223DA4AC6007E5897 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = ""; }; 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = EmailClients.plist; sourceTree = ""; }; 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = ""; }; 5A441EC80D2B8D2209C2E228 /* Pods_WordPressAuthenticatorTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressAuthenticatorTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -313,6 +319,24 @@ path = UI; sourceTree = ""; }; + 3F550D4F23DA4A6B007E5897 /* Email Client Picker */ = { + isa = PBXGroup; + children = ( + 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */, + 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */, + 3F550D5223DA4AC6007E5897 /* URLHandler.swift */, + ); + path = "Email Client Picker"; + sourceTree = ""; + }; + 3F550D5423DA5094007E5897 /* Email Client Picker */ = { + isa = PBXGroup; + children = ( + 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */, + ); + path = "Email Client Picker"; + sourceTree = ""; + }; 6205895375D954F46B1DFE53 /* Pods */ = { isa = PBXGroup; children = ( @@ -356,7 +380,6 @@ B5609094208A4EAF00399AE4 /* UI */ = { isa = PBXGroup; children = ( - 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */, B56052A32090B2ED001B91FD /* CircularImageView.swift */, B5609112208A555500399AE4 /* LoginTextField.swift */, B5609113208A555500399AE4 /* SearchTableViewCell.swift */, @@ -540,6 +563,7 @@ children = ( CE1B18CA20EEC31000BECC3F /* Credentials */, B5609099208A4EAF00399AE4 /* Authenticator */, + 3F550D4F23DA4A6B007E5897 /* Email Client Picker */, B560909B208A4EB000399AE4 /* Extensions */, B5ED7917207E993E00A8FD8C /* Logging */, B5609098208A4EAF00399AE4 /* Model */, @@ -559,6 +583,7 @@ B5ED7901207E976500A8FD8C /* WordPressAuthenticatorTests */ = { isa = PBXGroup; children = ( + 3F550D5423DA5094007E5897 /* Email Client Picker */, B501C03D208FC52500D1E58F /* Authenticator */, B501C03B208FC52400D1E58F /* Model */, B501C03F208FC52500D1E58F /* Services */, @@ -903,6 +928,7 @@ CE30A2AD2257CECC00DF3CDA /* AuthenticatorCredentials.swift in Sources */, B5609145208A563800399AE4 /* LoginViewController.swift in Sources */, B5609139208A563800399AE4 /* LoginEmailViewController.swift in Sources */, + 3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */, 98C9195B2308E3DA00A90E12 /* AppleAuthenticator.swift in Sources */, B56090F9208A533200399AE4 /* WordPressAuthenticator+Events.swift in Sources */, 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */, @@ -944,6 +970,7 @@ B56090E1208A4F9D00399AE4 /* WPWalkthroughTextField.m in Sources */, B56090EF208A527000399AE4 /* WPStyleGuide+Login.swift in Sources */, B56090D0208A4F5400399AE4 /* NUXViewControllerBase.swift in Sources */, + 3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */, B56090DE208A4F9D00399AE4 /* WPWalkthroughOverlayView.m in Sources */, B560910A208A54F800399AE4 /* WordPressComAccountService.swift in Sources */, B56090FA208A533200399AE4 /* WordPressAuthenticator.swift in Sources */, @@ -955,6 +982,7 @@ buildActionMask = 2147483647; files = ( B501C045208FC68700D1E58F /* LoginFieldsValidationTests.swift in Sources */, + 3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */, CE16177821B70C1A00B82A47 /* WordPressAuthenticatorDisplayTextTests.swift in Sources */, B501C048208FC79C00D1E58F /* LoginFacadeTests.m in Sources */, B501C046208FC6A700D1E58F /* WordPressAuthenticatorTests.swift in Sources */, diff --git a/WordPressAuthenticator/UI/AppSelector.swift b/WordPressAuthenticator/Email Client Picker/AppSelector.swift similarity index 64% rename from WordPressAuthenticator/UI/AppSelector.swift rename to WordPressAuthenticator/Email Client Picker/AppSelector.swift index 0290f469f..37668d90b 100644 --- a/WordPressAuthenticator/UI/AppSelector.swift +++ b/WordPressAuthenticator/Email Client Picker/AppSelector.swift @@ -11,7 +11,10 @@ class AppSelector { /// - Parameters: /// - appList: collection of apps to be added to the selector /// - defaultAction: default action, if not nil, will be the first element of the list - init?(with appList: [String: String], defaultAction: UIAlertAction? = nil) { + /// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared + init?(with appList: [String: String], + defaultAction: UIAlertAction? = nil, + urlHandler: URLHandler = UIApplication.shared) { /// inline method that builds a list of app calls to be inserted in the action sheet func buildAppCalls(from appList: [String: String]) -> [UIAlertAction]? { guard !appList.isEmpty else { @@ -19,11 +22,11 @@ class AppSelector { } var actions = [UIAlertAction]() for (name, urlString) in appList { - guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else { + guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else { continue } actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { action in - UIApplication.shared.open(url) + urlHandler.open(url, options: [:], completionHandler: nil) }) } guard !actions.isEmpty else { @@ -51,7 +54,7 @@ class AppSelector { } -/// Email Clients Selector +/// Initializers for Email Picker extension AppSelector { /// initializes the picker with a plist file in a specified bundle convenience init?(with plistFile: String, in bundle: Bundle, defaultAction: UIAlertAction? = nil) { @@ -85,44 +88,6 @@ extension AppSelector { } -/// Email picker presenter -class LinkMailPresenter { - /// Presents the available mail clients in an action sheet. If none is available, - /// Falls back to Apple Mail and opens it. - /// If not even Apple Mail is available, presents an alert to check your email - /// - Parameters: - /// - viewController: the UIViewController that will present the action sheet - /// - appSelector: the app picker that contains the available clients. Nil if no clients are available - /// reads the supported email clients from EmailClients.plist - func presentEmailClients(on viewController: UIViewController, - appSelector: AppSelector? = AppSelector()) { - - guard let picker = appSelector else { - // fall back to Apple Mail if no other clients are installed - if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { - UIApplication.shared.open(url) - } else { - showAlertToCheckEmail(on: viewController) - } - return - } - viewController.present(picker.alertController, animated: true) - } - - private func showAlertToCheckEmail(on viewController: UIViewController) { - let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") - let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") - - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("OK", - comment: "Button title. An acknowledgement of the message displayed in a prompt.")) - viewController.present(alertController, animated: true, completion: nil) - } -} - - /// Localizable app selector titles enum AppSelectorTitles: String { case appleMail diff --git a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift new file mode 100644 index 000000000..52c9db0b8 --- /dev/null +++ b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift @@ -0,0 +1,39 @@ +import MessageUI + + +/// Email picker presenter +class LinkMailPresenter { + /// Presents the available mail clients in an action sheet. If none is available, + /// Falls back to Apple Mail and opens it. + /// If not even Apple Mail is available, presents an alert to check your email + /// - Parameters: + /// - viewController: the UIViewController that will present the action sheet + /// - appSelector: the app picker that contains the available clients. Nil if no clients are available + /// reads the supported email clients from EmailClients.plist + func presentEmailClients(on viewController: UIViewController, + appSelector: AppSelector? = AppSelector()) { + + guard let picker = appSelector else { + // fall back to Apple Mail if no other clients are installed + if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { + UIApplication.shared.open(url) + } else { + showAlertToCheckEmail(on: viewController) + } + return + } + viewController.present(picker.alertController, animated: true) + } + + private func showAlertToCheckEmail(on viewController: UIViewController) { + let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") + let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") + + let alertController = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + alertController.addCancelActionWithTitle(NSLocalizedString("OK", + comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + viewController.present(alertController, animated: true, completion: nil) + } +} diff --git a/WordPressAuthenticator/Email Client Picker/URLHandler.swift b/WordPressAuthenticator/Email Client Picker/URLHandler.swift new file mode 100644 index 000000000..24a92f2a3 --- /dev/null +++ b/WordPressAuthenticator/Email Client Picker/URLHandler.swift @@ -0,0 +1,13 @@ +/// Generic type that handles URL Schemes +protocol URLHandler { + /// checks if the specified URL can be opened + func canOpenURL(_ url: URL) -> Bool + /// opens the specified URL + func open(_ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey : Any], + completionHandler completion: ((Bool) -> Void)?) +} + +/// conforms UIApplication to URLHandler to allow dependency injection +extension UIApplication: URLHandler {} + diff --git a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift index a74a5be09..edaf77d21 100644 --- a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift +++ b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift @@ -1,5 +1,4 @@ import UIKit -import MessageUI import WordPressShared diff --git a/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift new file mode 100644 index 000000000..76af4aa6e --- /dev/null +++ b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift @@ -0,0 +1,71 @@ +import XCTest +@testable import WordPressAuthenticator + + +struct URLMocks { + + static let mockAppList = ["gmail": "googlemail://", "airmail": "airmail://"] +} + +class MockUrlHandler: URLHandler { + + var shouldOpenUrls = true + + var canOpenUrlExpectation: XCTestExpectation? + var openUrlExpectation: XCTestExpectation? + + func canOpenURL(_ url: URL) -> Bool { + canOpenUrlExpectation?.fulfill() + canOpenUrlExpectation = nil + return shouldOpenUrls + } + + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any], completionHandler completion: ((Bool) -> Void)?) { + openUrlExpectation?.fulfill() + } +} + +class AppSelectorTests: XCTestCase { + + func testSelectorInitializationSuccess() { + // Given + let urlHandler = MockUrlHandler() + urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") + // When + let appSelector = AppSelector(with: URLMocks.mockAppList, urlHandler: urlHandler) + // Then + XCTAssertNotNil(appSelector) + XCTAssertNotNil(appSelector?.alertController) + XCTAssertEqual(appSelector!.alertController.actions.count, 3) + waitForExpectations(timeout: 4) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testSelectorInitializationFailsWithNoApps() { + // Given + let urlHandler = MockUrlHandler() + // When + let appSelector = AppSelector(with: [:], urlHandler: urlHandler) + // Then + XCTAssertNil(appSelector) + } + + func testSelectorInitializationFailsWithInvalidUrl() { + // Given + let urlHandler = MockUrlHandler() + urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") + urlHandler.shouldOpenUrls = false + // When + let appSelector = AppSelector(with: URLMocks.mockAppList, urlHandler: urlHandler) + // Then + XCTAssertNil(appSelector) + waitForExpectations(timeout: 4) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + } + } +} From fbf6d2b041cada413c7d8b46acae64e1e81bfa4e Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Fri, 24 Jan 2020 10:09:14 -0600 Subject: [PATCH 5/7] * Update AppSelector.swift: refactor initializer * Update AppSelectorTitles: add context to localizable strings * Update LinkMailPresenter: update message to check email, using the user's email address --- .../Email Client Picker/AppSelector.swift | 22 ++++++++++--------- .../LinkMailPresenter.swift | 16 ++++++++++++-- .../NUX/NUXLinkMailViewController.swift | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/WordPressAuthenticator/Email Client Picker/AppSelector.swift b/WordPressAuthenticator/Email Client Picker/AppSelector.swift index 37668d90b..76e6e01ce 100644 --- a/WordPressAuthenticator/Email Client Picker/AppSelector.swift +++ b/WordPressAuthenticator/Email Client Picker/AppSelector.swift @@ -16,10 +16,11 @@ class AppSelector { defaultAction: UIAlertAction? = nil, urlHandler: URLHandler = UIApplication.shared) { /// inline method that builds a list of app calls to be inserted in the action sheet - func buildAppCalls(from appList: [String: String]) -> [UIAlertAction]? { + func makeAlertActions(from appList: [String: String]) -> [UIAlertAction]? { guard !appList.isEmpty else { return nil } + var actions = [UIAlertAction]() for (name, urlString) in appList { guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else { @@ -29,6 +30,7 @@ class AppSelector { urlHandler.open(url, options: [:], completionHandler: nil) }) } + guard !actions.isEmpty else { return nil } @@ -42,7 +44,7 @@ class AppSelector { return actions } - guard let appCalls = buildAppCalls(from: appList) else { + guard let appCalls = makeAlertActions(from: appList) else { return nil } @@ -102,21 +104,21 @@ enum AppSelectorTitles: String { var localized: String { switch self { case .appleMail: - return NSLocalizedString("Mail (Default)", comment: "Label for Apple Mail as default email client") + return NSLocalizedString("Mail (Default)", comment: "Option to select the Apple Mail app when logging in with magic links") case .gmail: - return NSLocalizedString("Gmail", comment: "Gmail") + return NSLocalizedString("Gmail", comment: "Option to select the Gmail app when logging in with magic links") case .airmail: - return NSLocalizedString("Airmail", comment: "Airmail") + return NSLocalizedString("Airmail", comment: "Option to select the Airmail app when logging in with magic links") case .msOutlook: - return NSLocalizedString("Microsoft Outlook", comment: "Microsoft Outlook") + return NSLocalizedString("Microsoft Outlook", comment: "Option to select the Microsft Outlook app when logging in with magic links") case .spark: - return NSLocalizedString("Spark", comment: "Spark") + return NSLocalizedString("Spark", comment: "Option to select the Spark email app when logging in with magic links") case .yahooMail: - return NSLocalizedString("Yahoo Mail", comment: "Yahoo Mail") + return NSLocalizedString("Yahoo Mail", comment: "Option to select the Yahoo Mail app when logging in with magic links") case .fastmail: - return NSLocalizedString("Fastmail", comment: "Fastmail") + return NSLocalizedString("Fastmail", comment: "Option to select the Fastmail app when logging in with magic links") case .cancel: - return NSLocalizedString("Cancel", comment: "Cancel") + return NSLocalizedString("Cancel", comment: "Option to cancel the email app selection when logging in with magic links") } } } diff --git a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift index 52c9db0b8..cdd50a931 100644 --- a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift +++ b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift @@ -3,6 +3,13 @@ import MessageUI /// Email picker presenter class LinkMailPresenter { + + private let emailAddress: String + + init(emailAddress: String) { + self.emailAddress = emailAddress + } + /// Presents the available mail clients in an action sheet. If none is available, /// Falls back to Apple Mail and opens it. /// If not even Apple Mail is available, presents an alert to check your email @@ -26,8 +33,13 @@ class LinkMailPresenter { } private func showAlertToCheckEmail(on viewController: UIViewController) { - let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") - let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") + let title = NSLocalizedString("Check your email!", + comment: "Alert title for check your email during logIn/signUp.") + let message = NSLocalizedString("We just emailed a link to ", + comment: "First part of the message to ask a user to check their email for a WordPress.com email") + + emailAddress + + NSLocalizedString(". Please check your mail app and tap the link to log in.", + comment: "Second part of the message to ask a user to check their email for a WordPress.com email") let alertController = UIAlertController(title: title, message: message, diff --git a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift index edaf77d21..d1fd0d2b5 100644 --- a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift +++ b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift @@ -104,7 +104,7 @@ class NUXLinkMailViewController: LoginViewController { } } } - let linkMailPresenter = LinkMailPresenter() + let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) linkMailPresenter.presentEmailClients(on: self) } From 2f030b33723f8c1092d03aa0e24ec9269806f9a5 Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Wed, 5 Feb 2020 11:39:41 -0600 Subject: [PATCH 6/7] Update LinkMailPresenter, use one string with format for the alert message instead of combined strings --- .../Email Client Picker/LinkMailPresenter.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift index cdd50a931..9095da6f6 100644 --- a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift +++ b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift @@ -35,11 +35,9 @@ class LinkMailPresenter { private func showAlertToCheckEmail(on viewController: UIViewController) { let title = NSLocalizedString("Check your email!", comment: "Alert title for check your email during logIn/signUp.") - let message = NSLocalizedString("We just emailed a link to ", - comment: "First part of the message to ask a user to check their email for a WordPress.com email") - + emailAddress - + NSLocalizedString(". Please check your mail app and tap the link to log in.", - comment: "Second part of the message to ask a user to check their email for a WordPress.com email") + + let message = String.localizedStringWithFormat(NSLocalizedString("We just emailed a link to %@. Please check your mail app and tap the link to log in.", + comment: "message to ask a user to check their email for a WordPress.com email"), emailAddress) let alertController = UIAlertController(title: title, message: message, From c48e1e2137bfe13f26dd80313b4e8571884dcce7 Mon Sep 17 00:00:00 2001 From: Giorgio Ruscigno Date: Wed, 5 Feb 2020 19:20:25 -0600 Subject: [PATCH 7/7] Add sourceView to UIAlertController in AppSelector --- .../Email Client Picker/AppSelector.swift | 20 +++++++++++++++---- .../LinkMailPresenter.swift | 2 +- .../NUX/NUXLinkMailViewController.swift | 4 +++- .../AppSelectorTests.swift | 6 +++--- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/WordPressAuthenticator/Email Client Picker/AppSelector.swift b/WordPressAuthenticator/Email Client Picker/AppSelector.swift index 76e6e01ce..b6ed96773 100644 --- a/WordPressAuthenticator/Email Client Picker/AppSelector.swift +++ b/WordPressAuthenticator/Email Client Picker/AppSelector.swift @@ -11,9 +11,11 @@ class AppSelector { /// - Parameters: /// - appList: collection of apps to be added to the selector /// - defaultAction: default action, if not nil, will be the first element of the list + /// - sourceView: the sourceView to anchor the action sheet to /// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared init?(with appList: [String: String], defaultAction: UIAlertAction? = nil, + sourceView: UIView, urlHandler: URLHandler = UIApplication.shared) { /// inline method that builds a list of app calls to be inserted in the action sheet func makeAlertActions(from appList: [String: String]) -> [UIAlertAction]? { @@ -49,6 +51,8 @@ class AppSelector { } alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.popoverPresentationController?.sourceView = sourceView + alertController.popoverPresentationController?.sourceRect = sourceView.bounds appCalls.forEach { alertController.addAction($0) } @@ -59,17 +63,22 @@ class AppSelector { /// Initializers for Email Picker extension AppSelector { /// initializes the picker with a plist file in a specified bundle - convenience init?(with plistFile: String, in bundle: Bundle, defaultAction: UIAlertAction? = nil) { + convenience init?(with plistFile: String, + in bundle: Bundle, + defaultAction: UIAlertAction? = nil, + sourceView: UIView) { guard let plistPath = bundle.path(forResource: plistFile, ofType: "plist"), let availableApps = NSDictionary(contentsOfFile: plistPath) as? [String: String] else { return nil } - self.init(with: availableApps, defaultAction: defaultAction) + self.init(with: availableApps, + defaultAction: defaultAction, + sourceView: sourceView) } /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist - convenience init?() { + convenience init?(sourceView: UIView) { guard let bundlePath = Bundle(for: type(of: self)) .path(forResource: "WordPressAuthenticatorResources", ofType: "bundle"), let wpAuthenticatorBundle = Bundle(path: bundlePath) else { @@ -85,7 +94,10 @@ extension AppSelector { UIApplication.shared.open(url) } } - self.init(with: plistFile, in: wpAuthenticatorBundle, defaultAction: defaultAction) + self.init(with: plistFile, + in: wpAuthenticatorBundle, + defaultAction: defaultAction, + sourceView: sourceView) } } diff --git a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift index 9095da6f6..f758fb2b3 100644 --- a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift +++ b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift @@ -18,7 +18,7 @@ class LinkMailPresenter { /// - appSelector: the app picker that contains the available clients. Nil if no clients are available /// reads the supported email clients from EmailClients.plist func presentEmailClients(on viewController: UIViewController, - appSelector: AppSelector? = AppSelector()) { + appSelector: AppSelector?) { guard let picker = appSelector else { // fall back to Apple Mail if no other clients are installed diff --git a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift index d1fd0d2b5..4bb212137 100644 --- a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift +++ b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift @@ -104,8 +104,10 @@ class NUXLinkMailViewController: LoginViewController { } } } + let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - linkMailPresenter.presentEmailClients(on: self) + let appSelector = AppSelector(sourceView: sender) + linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) } @IBAction func handleUsePasswordTapped(_ sender: UIButton) { diff --git a/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift index 76af4aa6e..5bc445af5 100644 --- a/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift +++ b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift @@ -32,7 +32,7 @@ class AppSelectorTests: XCTestCase { let urlHandler = MockUrlHandler() urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") // When - let appSelector = AppSelector(with: URLMocks.mockAppList, urlHandler: urlHandler) + let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) // Then XCTAssertNotNil(appSelector) XCTAssertNotNil(appSelector?.alertController) @@ -48,7 +48,7 @@ class AppSelectorTests: XCTestCase { // Given let urlHandler = MockUrlHandler() // When - let appSelector = AppSelector(with: [:], urlHandler: urlHandler) + let appSelector = AppSelector(with: [:], sourceView: UIView(), urlHandler: urlHandler) // Then XCTAssertNil(appSelector) } @@ -59,7 +59,7 @@ class AppSelectorTests: XCTestCase { urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") urlHandler.shouldOpenUrls = false // When - let appSelector = AppSelector(with: URLMocks.mockAppList, urlHandler: urlHandler) + let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) // Then XCTAssertNil(appSelector) waitForExpectations(timeout: 4) { error in