Skip to content
This repository was archived by the owner on Feb 5, 2025. It is now read-only.

Commit 1470051

Browse files
authored
Merge pull request #178 from wordpress-mobile/feature/magic-email-clients
V. 1.10.7-beta.1 - Magic Email Clients
2 parents 37c83db + c48e1e2 commit 1470051

File tree

8 files changed

+344
-23
lines changed

8 files changed

+344
-23
lines changed

WordPressAuthenticator.podspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "WordPressAuthenticator"
3-
s.version = "1.10.6"
3+
s.version = "1.10.7-beta.1"
44
s.summary = "WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps."
55

66
s.description = <<-DESC
@@ -21,6 +21,7 @@ Pod::Spec.new do |s|
2121
s.resource_bundles = {
2222
'WordPressAuthenticatorResources': [
2323
'WordPressAuthenticator/Resources/Assets.xcassets',
24+
'WordPressAuthenticator/Resources/SupportedEmailClients/*.plist',
2425
'WordPressAuthenticator/Resources/Animations/*.json',
2526
'WordPressAuthenticator/**/*.{storyboard,xib}'
2627
]

WordPressAuthenticator.xcodeproj/project.pbxproj

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; };
1111
1A21EE9822832BC300C940C6 /* WordPressComOAuthClientFacade+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */; };
1212
1A4095182271AEFC009AA86D /* WPAuthenticator-Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */; settings = {ATTRIBUTES = (Private, ); }; };
13+
3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */; };
14+
3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */; };
15+
3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5223DA4AC6007E5897 /* URLHandler.swift */; };
16+
3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */; };
17+
3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */; };
1318
7A7A9B9CD2D81959F9AB9AF6 /* Pods_WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C736FF243DE333FCAB1C2614 /* Pods_WordPressAuthenticator.framework */; };
1419
982C8E7923021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */; };
1520
98AA5A5720AA1A7000A5958A /* WPHelpIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA5A5620AA1A7000A5958A /* WPHelpIndicatorView.swift */; };
@@ -149,6 +154,11 @@
149154
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 = "<group>"; };
150155
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 = "<group>"; };
151156
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 = "<group>"; };
157+
3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelectorTests.swift; sourceTree = "<group>"; };
158+
3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMailPresenter.swift; sourceTree = "<group>"; };
159+
3F550D5223DA4AC6007E5897 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = "<group>"; };
160+
3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = EmailClients.plist; sourceTree = "<group>"; };
161+
3FFF2FC223D7F53200D38C77 /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = "<group>"; };
152162
5A441EC80D2B8D2209C2E228 /* Pods_WordPressAuthenticatorTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressAuthenticatorTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
153163
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 = "<group>"; };
154164
982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginPrologueLoginMethodViewController.swift; sourceTree = "<group>"; };
@@ -294,6 +304,39 @@
294304
path = Private;
295305
sourceTree = "<group>";
296306
};
307+
3F550D4B23DA3B59007E5897 /* SupportedEmailClients */ = {
308+
isa = PBXGroup;
309+
children = (
310+
3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */,
311+
);
312+
path = SupportedEmailClients;
313+
sourceTree = "<group>";
314+
};
315+
3F550D4C23DA4191007E5897 /* UI */ = {
316+
isa = PBXGroup;
317+
children = (
318+
);
319+
path = UI;
320+
sourceTree = "<group>";
321+
};
322+
3F550D4F23DA4A6B007E5897 /* Email Client Picker */ = {
323+
isa = PBXGroup;
324+
children = (
325+
3FFF2FC223D7F53200D38C77 /* AppSelector.swift */,
326+
3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */,
327+
3F550D5223DA4AC6007E5897 /* URLHandler.swift */,
328+
);
329+
path = "Email Client Picker";
330+
sourceTree = "<group>";
331+
};
332+
3F550D5423DA5094007E5897 /* Email Client Picker */ = {
333+
isa = PBXGroup;
334+
children = (
335+
3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */,
336+
);
337+
path = "Email Client Picker";
338+
sourceTree = "<group>";
339+
};
297340
6205895375D954F46B1DFE53 /* Pods */ = {
298341
isa = PBXGroup;
299342
children = (
@@ -477,6 +520,7 @@
477520
children = (
478521
B5A5274020B478160065BE81 /* Animations */,
479522
B5E07FF3208FD13800657A9A /* Assets.xcassets */,
523+
3F550D4B23DA3B59007E5897 /* SupportedEmailClients */,
480524
);
481525
path = Resources;
482526
sourceTree = "<group>";
@@ -519,6 +563,7 @@
519563
children = (
520564
CE1B18CA20EEC31000BECC3F /* Credentials */,
521565
B5609099208A4EAF00399AE4 /* Authenticator */,
566+
3F550D4F23DA4A6B007E5897 /* Email Client Picker */,
522567
B560909B208A4EB000399AE4 /* Extensions */,
523568
B5ED7917207E993E00A8FD8C /* Logging */,
524569
B5609098208A4EAF00399AE4 /* Model */,
@@ -538,9 +583,11 @@
538583
B5ED7901207E976500A8FD8C /* WordPressAuthenticatorTests */ = {
539584
isa = PBXGroup;
540585
children = (
586+
3F550D5423DA5094007E5897 /* Email Client Picker */,
541587
B501C03D208FC52500D1E58F /* Authenticator */,
542588
B501C03B208FC52400D1E58F /* Model */,
543589
B501C03F208FC52500D1E58F /* Services */,
590+
3F550D4C23DA4191007E5897 /* UI */,
544591
B5ED7904207E976500A8FD8C /* Info.plist */,
545592
);
546593
path = WordPressAuthenticatorTests;
@@ -699,6 +746,7 @@
699746
B5609118208A555600399AE4 /* SearchTableViewCell.xib in Resources */,
700747
B560913F208A563800399AE4 /* Login.storyboard in Resources */,
701748
B5609137208A563800399AE4 /* EmailMagicLink.storyboard in Resources */,
749+
3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */,
702750
FF629D9622393500004C4106 /* WordPressAuthenticator.podspec in Resources */,
703751
);
704752
runOnlyForDeploymentPostprocessing = 0;
@@ -880,6 +928,7 @@
880928
CE30A2AD2257CECC00DF3CDA /* AuthenticatorCredentials.swift in Sources */,
881929
B5609145208A563800399AE4 /* LoginViewController.swift in Sources */,
882930
B5609139208A563800399AE4 /* LoginEmailViewController.swift in Sources */,
931+
3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */,
883932
98C9195B2308E3DA00A90E12 /* AppleAuthenticator.swift in Sources */,
884933
B56090F9208A533200399AE4 /* WordPressAuthenticator+Events.swift in Sources */,
885934
020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */,
@@ -915,11 +964,13 @@
915964
B560911F208A555E00399AE4 /* SignupGoogleViewController.swift in Sources */,
916965
B5609142208A563800399AE4 /* LoginNavigationController.swift in Sources */,
917966
B56090E4208A4F9D00399AE4 /* WPNUXMainButton.m in Sources */,
967+
3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */,
918968
B560913B208A563800399AE4 /* LoginSelfHostedViewController.swift in Sources */,
919969
B5609136208A563800399AE4 /* Login2FAViewController.swift in Sources */,
920970
B56090E1208A4F9D00399AE4 /* WPWalkthroughTextField.m in Sources */,
921971
B56090EF208A527000399AE4 /* WPStyleGuide+Login.swift in Sources */,
922972
B56090D0208A4F5400399AE4 /* NUXViewControllerBase.swift in Sources */,
973+
3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */,
923974
B56090DE208A4F9D00399AE4 /* WPWalkthroughOverlayView.m in Sources */,
924975
B560910A208A54F800399AE4 /* WordPressComAccountService.swift in Sources */,
925976
B56090FA208A533200399AE4 /* WordPressAuthenticator.swift in Sources */,
@@ -931,6 +982,7 @@
931982
buildActionMask = 2147483647;
932983
files = (
933984
B501C045208FC68700D1E58F /* LoginFieldsValidationTests.swift in Sources */,
985+
3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */,
934986
CE16177821B70C1A00B82A47 /* WordPressAuthenticatorDisplayTextTests.swift in Sources */,
935987
B501C048208FC79C00D1E58F /* LoginFacadeTests.m in Sources */,
936988
B501C046208FC6A700D1E58F /* WordPressAuthenticatorTests.swift in Sources */,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import MessageUI
2+
import UIKit
3+
4+
/// App selector that selects an app from a list and opens it
5+
/// Note: it's a wrapper of UIAlertController (which cannot be sublcassed)
6+
class AppSelector {
7+
// the action sheet that will contain the list of apps that can be called
8+
let alertController: UIAlertController
9+
10+
/// initializes the picker with a dictionary. Initialization will fail if an empty/invalid app list is passed
11+
/// - Parameters:
12+
/// - appList: collection of apps to be added to the selector
13+
/// - defaultAction: default action, if not nil, will be the first element of the list
14+
/// - sourceView: the sourceView to anchor the action sheet to
15+
/// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared
16+
init?(with appList: [String: String],
17+
defaultAction: UIAlertAction? = nil,
18+
sourceView: UIView,
19+
urlHandler: URLHandler = UIApplication.shared) {
20+
/// inline method that builds a list of app calls to be inserted in the action sheet
21+
func makeAlertActions(from appList: [String: String]) -> [UIAlertAction]? {
22+
guard !appList.isEmpty else {
23+
return nil
24+
}
25+
26+
var actions = [UIAlertAction]()
27+
for (name, urlString) in appList {
28+
guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else {
29+
continue
30+
}
31+
actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { action in
32+
urlHandler.open(url, options: [:], completionHandler: nil)
33+
})
34+
}
35+
36+
guard !actions.isEmpty else {
37+
return nil
38+
}
39+
//sort the apps alphabetically
40+
actions = actions.sorted { $0.title ?? "" < $1.title ?? "" }
41+
actions.append(UIAlertAction(title: AppSelectorTitles.cancel.localized, style: .cancel, handler: nil))
42+
43+
if let action = defaultAction {
44+
actions.insert(action, at: 0)
45+
}
46+
return actions
47+
}
48+
49+
guard let appCalls = makeAlertActions(from: appList) else {
50+
return nil
51+
}
52+
53+
alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
54+
alertController.popoverPresentationController?.sourceView = sourceView
55+
alertController.popoverPresentationController?.sourceRect = sourceView.bounds
56+
appCalls.forEach {
57+
alertController.addAction($0)
58+
}
59+
}
60+
}
61+
62+
63+
/// Initializers for Email Picker
64+
extension AppSelector {
65+
/// initializes the picker with a plist file in a specified bundle
66+
convenience init?(with plistFile: String,
67+
in bundle: Bundle,
68+
defaultAction: UIAlertAction? = nil,
69+
sourceView: UIView) {
70+
71+
guard let plistPath = bundle.path(forResource: plistFile, ofType: "plist"),
72+
let availableApps = NSDictionary(contentsOfFile: plistPath) as? [String: String] else {
73+
return nil
74+
}
75+
self.init(with: availableApps,
76+
defaultAction: defaultAction,
77+
sourceView: sourceView)
78+
}
79+
80+
/// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist
81+
convenience init?(sourceView: UIView) {
82+
guard let bundlePath = Bundle(for: type(of: self))
83+
.path(forResource: "WordPressAuthenticatorResources", ofType: "bundle"),
84+
let wpAuthenticatorBundle = Bundle(path: bundlePath) else {
85+
return nil
86+
}
87+
88+
let plistFile = "EmailClients"
89+
var defaultAction: UIAlertAction?
90+
91+
// if available, prepend apple mail
92+
if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") {
93+
defaultAction = UIAlertAction(title: AppSelectorTitles.appleMail.localized, style: .default) { action in
94+
UIApplication.shared.open(url)
95+
}
96+
}
97+
self.init(with: plistFile,
98+
in: wpAuthenticatorBundle,
99+
defaultAction: defaultAction,
100+
sourceView: sourceView)
101+
}
102+
}
103+
104+
105+
/// Localizable app selector titles
106+
enum AppSelectorTitles: String {
107+
case appleMail
108+
case gmail
109+
case airmail
110+
case msOutlook
111+
case spark
112+
case yahooMail
113+
case fastmail
114+
case cancel
115+
116+
var localized: String {
117+
switch self {
118+
case .appleMail:
119+
return NSLocalizedString("Mail (Default)", comment: "Option to select the Apple Mail app when logging in with magic links")
120+
case .gmail:
121+
return NSLocalizedString("Gmail", comment: "Option to select the Gmail app when logging in with magic links")
122+
case .airmail:
123+
return NSLocalizedString("Airmail", comment: "Option to select the Airmail app when logging in with magic links")
124+
case .msOutlook:
125+
return NSLocalizedString("Microsoft Outlook", comment: "Option to select the Microsft Outlook app when logging in with magic links")
126+
case .spark:
127+
return NSLocalizedString("Spark", comment: "Option to select the Spark email app when logging in with magic links")
128+
case .yahooMail:
129+
return NSLocalizedString("Yahoo Mail", comment: "Option to select the Yahoo Mail app when logging in with magic links")
130+
case .fastmail:
131+
return NSLocalizedString("Fastmail", comment: "Option to select the Fastmail app when logging in with magic links")
132+
case .cancel:
133+
return NSLocalizedString("Cancel", comment: "Option to cancel the email app selection when logging in with magic links")
134+
}
135+
}
136+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import MessageUI
2+
3+
4+
/// Email picker presenter
5+
class LinkMailPresenter {
6+
7+
private let emailAddress: String
8+
9+
init(emailAddress: String) {
10+
self.emailAddress = emailAddress
11+
}
12+
13+
/// Presents the available mail clients in an action sheet. If none is available,
14+
/// Falls back to Apple Mail and opens it.
15+
/// If not even Apple Mail is available, presents an alert to check your email
16+
/// - Parameters:
17+
/// - viewController: the UIViewController that will present the action sheet
18+
/// - appSelector: the app picker that contains the available clients. Nil if no clients are available
19+
/// reads the supported email clients from EmailClients.plist
20+
func presentEmailClients(on viewController: UIViewController,
21+
appSelector: AppSelector?) {
22+
23+
guard let picker = appSelector else {
24+
// fall back to Apple Mail if no other clients are installed
25+
if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") {
26+
UIApplication.shared.open(url)
27+
} else {
28+
showAlertToCheckEmail(on: viewController)
29+
}
30+
return
31+
}
32+
viewController.present(picker.alertController, animated: true)
33+
}
34+
35+
private func showAlertToCheckEmail(on viewController: UIViewController) {
36+
let title = NSLocalizedString("Check your email!",
37+
comment: "Alert title for check your email during logIn/signUp.")
38+
39+
let message = String.localizedStringWithFormat(NSLocalizedString("We just emailed a link to %@. Please check your mail app and tap the link to log in.",
40+
comment: "message to ask a user to check their email for a WordPress.com email"), emailAddress)
41+
42+
let alertController = UIAlertController(title: title,
43+
message: message,
44+
preferredStyle: .alert)
45+
alertController.addCancelActionWithTitle(NSLocalizedString("OK",
46+
comment: "Button title. An acknowledgement of the message displayed in a prompt."))
47+
viewController.present(alertController, animated: true, completion: nil)
48+
}
49+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/// Generic type that handles URL Schemes
2+
protocol URLHandler {
3+
/// checks if the specified URL can be opened
4+
func canOpenURL(_ url: URL) -> Bool
5+
/// opens the specified URL
6+
func open(_ url: URL,
7+
options: [UIApplication.OpenExternalURLOptionsKey : Any],
8+
completionHandler completion: ((Bool) -> Void)?)
9+
}
10+
11+
/// conforms UIApplication to URLHandler to allow dependency injection
12+
extension UIApplication: URLHandler {}
13+

0 commit comments

Comments
 (0)