diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index ce25ff4b8..721bd6214 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.name = 'WordPressAuthenticator' - s.version = '2.2.1-beta.2' + s.version = '2.2.1-beta.3' s.summary = 'WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps.' s.description = <<-DESC diff --git a/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift b/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift index 58d689c1f..b766c5b83 100644 --- a/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift +++ b/WordPressAuthenticator/Analytics/AuthenticatorAnalyticsTracker.swift @@ -83,6 +83,10 @@ public class AuthenticatorAnalyticsTracker { /// case loginWithSiteAddress = "login_site_address" + /// This flow starts when the user wants to troubleshoot their site by inputting its address + /// + case siteDiscovery = "site_discovery" + /// This flow represents the signup (when the user inputs an email that’s not registered with a .com account) /// case signup diff --git a/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift b/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift index 7a0c2ceea..9d7938c42 100644 --- a/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift +++ b/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift @@ -266,6 +266,14 @@ import WordPressKit return SiteAddressViewController.instantiate(from: .siteAddress) } + /// Returns a Site Address view controller and triggers the protocol method `troubleshootSite` after fetching the site info. + /// + @objc public class func siteDiscoveryUI() -> UIViewController? { + return SiteAddressViewController.instantiate(from: .siteAddress) { coder in + SiteAddressViewController(isSiteDiscovery: true, coder: coder) + } + } + // Helper used by WPAuthTokenIssueSolver @objc public class func signinForWPCom(dotcomEmailAddress: String?, dotcomUsername: String?, onDismissed: ((_ cancelled: Bool) -> Void)? = nil) -> UIViewController { diff --git a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorDelegateProtocol.swift b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorDelegateProtocol.swift index 90bb5dd07..e9b4dbf0d 100644 --- a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorDelegateProtocol.swift +++ b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorDelegateProtocol.swift @@ -90,6 +90,15 @@ public protocol WordPressAuthenticatorDelegate: AnyObject { /// func sync(credentials: AuthenticatorCredentials, onCompletion: @escaping () -> Void) + /// Signals to the Host App that a WordPress site is available and needs validated. + /// This method is only triggered in the site discovery flow. + /// + /// - Parameters: + /// - siteInfo: The fetched site information - can be nil the site doesn't exist or have WordPress + /// - navigationController: the current navigation stack of the site discovery flow. + /// + func troubleshootSite(_ siteInfo: WordPressComSiteInfo?, in navigationController: UINavigationController?) + /// Signals the Host App that a given Analytics Event has occurred. /// func track(event: WPAnalyticsStat) @@ -102,3 +111,11 @@ public protocol WordPressAuthenticatorDelegate: AnyObject { /// func track(event: WPAnalyticsStat, error: Error) } + +/// Extension with default implementation for optional delegate methods. +/// +public extension WordPressAuthenticatorDelegate { + func troubleshootSite(_ siteInfo: WordPressComSiteInfo?, in navigationController: UINavigationController?) { + // No-op + } +} diff --git a/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift b/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift index 0dc219b5e..dbead11b0 100644 --- a/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift +++ b/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift @@ -22,8 +22,8 @@ enum Storyboard: String { /// Returns a view controller from a Storyboard /// assuming the identifier is the same as the class name. /// - func instantiateViewController(ofClass classType: T.Type) -> T? { + func instantiateViewController(ofClass classType: T.Type, creator: ((NSCoder) -> UIViewController?)? = nil) -> T? { let identifier = classType.classNameWithoutNamespaces - return instance.instantiateViewController(withIdentifier: identifier) as? T + return instance.instantiateViewController(identifier: identifier, creator: creator) as? T } } diff --git a/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift b/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift index 730866d38..0a9539a3d 100644 --- a/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift +++ b/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift @@ -5,7 +5,7 @@ extension UIViewController { /// Convenience method to instantiate a view controller from a storyboard. /// - static func instantiate(from storyboard: Storyboard) -> Self? { - return storyboard.instantiateViewController(ofClass: self) + static func instantiate(from storyboard: Storyboard, creator: ((NSCoder) -> UIViewController?)? = nil) -> Self? { + return storyboard.instantiateViewController(ofClass: self, creator: creator) } } diff --git a/WordPressAuthenticator/Unified Auth/View Related/Site Address/SiteAddressViewController.swift b/WordPressAuthenticator/Unified Auth/View Related/Site Address/SiteAddressViewController.swift index 7984b4d85..620cffaf2 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/Site Address/SiteAddressViewController.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/Site Address/SiteAddressViewController.swift @@ -28,6 +28,20 @@ final class SiteAddressViewController: LoginViewController { /// should show an activity indicator. private var viewIsLoading: Bool = false + /// Whether the protocol method `troubleshootSite` should be triggered after site info is fetched. + /// + private let isSiteDiscovery: Bool + + init?(isSiteDiscovery: Bool, coder: NSCoder) { + self.isSiteDiscovery = isSiteDiscovery + super.init(coder: coder) + } + + required init?(coder: NSCoder) { + self.isSiteDiscovery = false + super.init(coder: coder) + } + // MARK: - Actions @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { tracker.track(click: .submit) @@ -59,7 +73,11 @@ final class SiteAddressViewController: LoginViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - tracker.set(flow: .loginWithSiteAddress) + if isSiteDiscovery { + tracker.set(flow: .siteDiscovery) + } else { + tracker.set(flow: .loginWithSiteAddress) + } if isMovingToParent { tracker.track(step: .start) @@ -416,8 +434,42 @@ private extension SiteAddressViewController { configureViewLoading(true) + guard let url = URL(string: loginFields.siteAddress) else { + configureViewLoading(false) + return displayError(message: Localization.invalidURL, moveVoiceOverFocus: true) + } + + // Checks that the site exists + var request = URLRequest(url: url) + request.httpMethod = "HEAD" + request.timeoutInterval = 10.0 // waits for 10 seconds + let task = URLSession.shared.dataTask(with: request) { [weak self] _, _, error in + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + if let error = error { + self.configureViewLoading(false) + + if self.authenticationDelegate.shouldHandleError(error) { + self.authenticationDelegate.handleError(error) { customUI in + self.pushCustomUI(customUI) + } + return + } + + return self.displayError(message: Localization.nonExistentSiteError, moveVoiceOverFocus: true) + } + + // Proceeds to check for the site's WordPress + self.guessXMLRPCURL(for: self.loginFields.siteAddress) + } + } + task.resume() + } + + func guessXMLRPCURL(for siteAddress: String) { let facade = WordPressXMLRPCAPIFacade() - facade.guessXMLRPCURL(forSite: loginFields.siteAddress, success: { [weak self] (url) in + facade.guessXMLRPCURL(forSite: siteAddress, success: { [weak self] (url) in // Success! We now know that we have a valid XML-RPC endpoint. // At this point, we do NOT know if this is a WP.com site or a self-hosted site. if let url = url { @@ -439,6 +491,11 @@ private extension SiteAddressViewController { // WordPressAuthenticator.track(.loginFailed, error: error) self.configureViewLoading(false) + guard self.isSiteDiscovery == false else { + WordPressAuthenticator.shared.delegate?.troubleshootSite(nil, in: self.navigationController) + return + } + let err = self.originalErrorOrError(error: error as NSError) /// Check if the host app wants to provide custom UI to handle the error. @@ -515,6 +572,11 @@ private extension SiteAddressViewController { loginFields.siteAddress = verifiedSiteAddress } + guard isSiteDiscovery == false else { + WordPressAuthenticator.shared.delegate?.troubleshootSite(siteInfo, in: navigationController) + return + } + guard siteInfo?.isWPCom == false else { showGetStarted() return @@ -611,3 +673,14 @@ private extension SiteAddressViewController { present(alertController, animated: true) } } + +private extension SiteAddressViewController { + enum Localization { + static let invalidURL = NSLocalizedString( + "Invalid URL. Please double-check and try again.", + comment: "Error message shown when the input URL is invalid.") + static let nonExistentSiteError = NSLocalizedString( + "Cannot access the site at this address. Please double-check and try again.", + comment: "Error message shown when the input URL does not point to an existing site.") + } +}