diff --git a/.rubocop.yml b/.rubocop.yml index ac394adddb51..fd109537d10c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,6 @@ AllCops: Exclude: - DerivedData/**/* - vendor/**/* - - WordPressAuthenticator/**/* - WordPressKit/**/* NewCops: enable diff --git a/Modules/Package.swift b/Modules/Package.swift index eb49cb186880..80168575fa30 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -373,11 +373,6 @@ enum XcodeSupport { .library(name: "XcodeTarget_WordPressKitTests", targets: ["XcodeTarget_WordPressKitTests"]), .library(name: "XcodeTarget_WordPressData", targets: ["XcodeTarget_WordPressData"]), .library(name: "XcodeTarget_WordPressDataTests", targets: ["XcodeTarget_WordPressDataTests"]), - .library(name: "XcodeTarget_WordPressAuthentificator", targets: ["XcodeTarget_WordPressAuthentificator"]), - .library( - name: "XcodeTarget_WordPressAuthentificatorTests", - targets: ["XcodeTarget_WordPressAuthentificatorTests"] - ), .library(name: "XcodeTarget_ShareExtension", targets: ["XcodeTarget_ShareExtension"]), .library(name: "XcodeTarget_DraftActionExtension", targets: ["XcodeTarget_DraftActionExtension"]), .library( @@ -391,18 +386,6 @@ enum XcodeSupport { } static var targets: [Target] { - let wordPresAuthentificatorDependencies: [Target.Dependency] = [ - "BuildSettingsKit", - "WordPressShared", - "WordPressUI", - "WordPressKit", - .product(name: "Gridicons", package: "Gridicons-iOS"), - .product(name: "NSURL-IDN", package: "NSURL-IDN"), - .product(name: "SVProgressHUD", package: "SVProgressHUD"), - .product(name: "Gravatar", package: "Gravatar-SDK-iOS"), - .product(name: "GravatarUI", package: "Gravatar-SDK-iOS") - ] - let shareAndDraftExtensionsDependencies: [Target.Dependency] = [ "AztecExtensions", "BuildSettingsKit", @@ -529,11 +512,6 @@ enum XcodeSupport { "WordPressKit" ] ), - .xcodeTarget("XcodeTarget_WordPressAuthentificator", dependencies: wordPresAuthentificatorDependencies), - .xcodeTarget( - "XcodeTarget_WordPressAuthentificatorTests", - dependencies: wordPresAuthentificatorDependencies + testDependencies - ), .xcodeTarget("XcodeTarget_ShareExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget("XcodeTarget_DraftActionExtension", dependencies: shareAndDraftExtensionsDependencies), .xcodeTarget( diff --git a/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift b/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift deleted file mode 100644 index 2189cf311564..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/FancyAlertViewController+LoginError.swift +++ /dev/null @@ -1,171 +0,0 @@ -import UIKit -import SafariServices -import WordPressUI -import WordPressKit - -extension FancyAlertViewController { - private struct Strings { - static let OK = NSLocalizedString("OK", comment: "Ok button for dismissing alert helping users understand their site address") - static let moreHelp = NSLocalizedString("Need more help?", comment: "Title of the more help button on alert helping users understand their site address") - } - - typealias ButtonConfig = FancyAlertViewController.Config.ButtonConfig - - private static func defaultButton(onTap: (() -> ())? = nil) -> ButtonConfig { - return ButtonConfig(Strings.OK) { controller, _ in - controller.dismiss(animated: true, completion: { - onTap?() - }) - } - } - - // MARK: - Error Handling - - /// Get an alert for the specified error. - /// The view is configured differently depending on the kind of error. - /// - /// - Parameters: - /// - error: An NSError instance - /// - loginFields: A LoginFields instance. - /// - sourceTag: The sourceTag that is the context of the error. - /// - /// - Returns: A FancyAlertViewController instance. - /// - static func alertForError(_ originalError: Error, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag) -> FancyAlertViewController { - let error = originalError as NSError - var message = error.localizedDescription - - WPLogError(message) - - if sourceTag == .jetpackLogin && error.domain == WordPressAuthenticator.errorDomain && error.code == NSURLErrorBadURL { - if WordPressAuthenticator.shared.delegate?.supportEnabled == true { - // TODO: Placeholder Jetpack login error message. Needs updating with final wording. 2017-06-15 Aerych. - message = NSLocalizedString("We're not able to connect to the Jetpack site at that URL. Contact us for assistance.", comment: "Error message shown when having trouble connecting to a Jetpack site.") - return alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag) - } - } - - if error.domain != WordPressOrgXMLRPCApi.errorDomain && error.code != NSURLErrorBadURL { - if WordPressAuthenticator.shared.delegate?.supportEnabled == true { - return alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag) - } - - return alertForGenericErrorMessage(message, loginFields: loginFields, sourceTag: sourceTag) - } - - if error.code == 403 { - message = NSLocalizedString("Incorrect username or password. Please try entering your login details again.", comment: "An error message shown when a user signed in with incorrect credentials.") - } - - if message.trim().isEmpty { - message = NSLocalizedString("Log in failed. Please try again.", comment: "A generic error message for a failed log in.") - } - - if error.code == NSURLErrorBadURL { - return alertForBadURL(with: message) - } - - return alertForGenericErrorMessage(message, loginFields: loginFields, sourceTag: sourceTag) - } - - /// Shows a generic error message. - /// - /// - Parameter message: The error message to show. - /// - private static func alertForGenericErrorMessage(_ message: String, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag) -> FancyAlertViewController { - let moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - guard let sourceViewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController, - let authDelegate = WordPressAuthenticator.shared.delegate - else { - return - } - - let state = AuthenticatorAnalyticsTracker.shared.state - authDelegate.presentSupport(from: sourceViewController, sourceTag: sourceTag, lastStep: state.lastStep, lastFlow: state.lastFlow) - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } - - /// Shows a generic error message. - /// If Support is enabled, the view is configured so the user can open Support for assistance. - /// - /// - Parameter message: The error message to show. - /// - Parameter sourceTag: tag of the source of the error - /// - static func alertForGenericErrorMessageWithHelpButton(_ message: String, loginFields: LoginFields, sourceTag: WordPressSupportSourceTag, onDismiss: (() -> ())? = nil) -> FancyAlertViewController { - - // If support is not enabled, don't add a Help Button since it won't do anything. - var moreHelpButton: ButtonConfig? - - if WordPressAuthenticator.shared.delegate?.supportEnabled == false { - WPLogInfo("Error Alert: Support not enabled. Hiding Help button.") - } else { - moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - // Find the topmost view controller that we can present from - guard let appDelegate = UIApplication.shared.delegate, - let window = appDelegate.window, - let viewController = window?.topmostPresentedViewController, - WordPressAuthenticator.shared.delegate?.supportEnabled == true - else { - return - } - - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: viewController, sourceTag: sourceTag) - } - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(onTap: onDismiss), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } - - private static func alertForBadURL(with message: String) -> FancyAlertViewController { - let moreHelpButton = ButtonConfig(Strings.moreHelp) { controller, _ in - controller.dismiss(animated: true) { - // Find the topmost view controller that we can present from - guard let viewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController, - let url = URL(string: "https://apps.wordpress.org/support/#faq-ios-3") - else { - return - } - - let safariViewController = SFSafariViewController(url: url) - safariViewController.modalPresentationStyle = .pageSheet - viewController.present(safariViewController, animated: true, completion: nil) - } - } - - let config = FancyAlertViewController.Config(titleText: "", - bodyText: message, - headerImage: nil, - dividerPosition: .top, - defaultButton: defaultButton(), - cancelButton: nil, - moreInfoButton: moreHelpButton, - titleAccessoryButton: nil, - dismissAction: nil) - return FancyAlertViewController.controllerWithConfiguration(configuration: config) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift deleted file mode 100644 index dddb827a6b50..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/NSObject+Helpers.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -// MARK: - NSObject Helper Methods -// -extension NSObject { - - /// Returns the receiver's classname as a string, not including the namespace. - /// - class var classNameWithoutNamespaces: String { - return String(describing: self) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/String+Underline.swift b/Sources/WordPressAuthenticator/Extensions/String+Underline.swift deleted file mode 100644 index 0d836a9c8707..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/String+Underline.swift +++ /dev/null @@ -1,26 +0,0 @@ -import UIKit - -extension String { - /// Creates an attributed string from one underlined section that's surrounded by underscores - /// - /// - Parameters: - /// - color: foreground color to use for the string (optional) - /// - underlineColor: foreground color to use for the underlined section (optional) - /// - Returns: Attributed string - /// - Note: "this _is_ underlined" would under the "is" - func underlined(color: UIColor? = nil, underlineColor: UIColor? = nil) -> NSAttributedString { - let labelParts = self.components(separatedBy: "_") - let firstPart = labelParts[0] - let underlinePart = labelParts.indices.contains(1) ? labelParts[1] : "" - let lastPart = labelParts.indices.contains(2) ? labelParts[2] : "" - - let foregroundColor = color ?? UIColor.black - let underlineForegroundColor = underlineColor ?? foregroundColor - - let underlinedString = NSMutableAttributedString(string: firstPart, attributes: [.foregroundColor: foregroundColor]) - underlinedString.append(NSAttributedString(string: underlinePart, attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue, .foregroundColor: underlineForegroundColor])) - underlinedString.append(NSAttributedString(string: lastPart, attributes: [.foregroundColor: foregroundColor])) - - return underlinedString - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift b/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift deleted file mode 100644 index cd05f518e53f..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIButton+Styles.swift +++ /dev/null @@ -1,16 +0,0 @@ -import UIKit -import WordPressShared - -extension UIButton { - /// Applies the style that looks like a plain text link. - func applyLinkButtonStyle() { - backgroundColor = .clear - titleLabel?.font = WPStyleGuide.fontForTextStyle(.body) - titleLabel?.textAlignment = .natural - - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - setTitleColor(buttonTitleColor, for: .normal) - setTitleColor(buttonHighlightColor, for: .highlighted) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift b/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift deleted file mode 100644 index be6e9b763a86..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIImage+Assets.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit - -// MARK: - Named Assets -// -extension UIImage { - /// Returns the Link Image. - /// - static var linkFieldImage: UIImage { - return UIImage(named: "icon-url-field", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Default Magic Link Image. - /// - static var magicLinkImage: UIImage { - return UIImage(named: "login-magic-link", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Link Image. - /// - @objc - public static var googleIcon: UIImage { - return UIImage(named: "google", in: bundle, compatibleWith: nil) ?? UIImage() - } - - /// Returns the Phone Icon. - /// - @objc - public static var phoneIcon: UIImage { - return UIImage(named: "phone-icon", in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) ?? UIImage() - } - - /// Returns the Key Icon. - /// - @objc - public static var keyIcon: UIImage { - return UIImage(named: "key-icon", in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) ?? UIImage() - } - - /// Returns WordPressAuthenticator's Bundle - /// - private static var bundle: Bundle { - return WordPressAuthenticator.bundle - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift b/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift deleted file mode 100644 index d0b3ff46a686..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift +++ /dev/null @@ -1,79 +0,0 @@ -import UIKit - -extension UIPasteboard { - - /// Detects patterns and values from the UIPasteboard. This will not trigger the pasteboard alert in iOS 14. - /// - Parameters: - /// - patterns: The patterns to detect. - /// - completion: Called with the patterns and values if any were detected, otherwise contains the errors from UIPasteboard. - @available(iOS 14.0, *) - func detect(patterns: Set, completion: @escaping (Result<[UIPasteboard.DetectionPattern: Any], Error>) -> Void) { - UIPasteboard.general.detectPatterns(for: patterns) { result in - switch result { - case .success(let detections): - guard detections.isEmpty == false else { - DispatchQueue.main.async { - completion(.success([UIPasteboard.DetectionPattern: Any]())) - } - return - } - UIPasteboard.general.detectValues(for: patterns) { valuesResult in - DispatchQueue.main.async { - completion(valuesResult) - } - } - case .failure(let error): - completion(.failure(error)) - } - } - } - - /// Attempts to detect and return a authenticator code from the pasteboard. - /// Expects to run on main thread. - /// - Parameters: - /// - completion: Called with a length digit authentication code on success - @available(iOS 14.0, *) - public func detectAuthenticatorCode(length: Int = 6, completion: @escaping (Result) -> Void) { - UIPasteboard.general.detect(patterns: [.number]) { result in - switch result { - case .success(let detections): - guard let firstMatch = detections.first else { - completion(.success("")) - return - } - guard let matchedNumber = firstMatch.value as? NSNumber else { - completion(.success("")) - return - } - - let authenticationCode = matchedNumber.stringValue - - /// Reject numbers with decimal points or signs in them - let codeCharacterSet = CharacterSet(charactersIn: authenticationCode) - if !codeCharacterSet.isSubset(of: CharacterSet.decimalDigits) { - completion(.success("")) - return - } - - /// We need length digits. No more, no less. - if authenticationCode.count > length { - completion(.success("")) - return - } else if authenticationCode.count == length { - completion(.success(authenticationCode)) - return - } - - let missingDigits = 6 - authenticationCode.count - let paddingZeros = String(repeating: "0", count: missingDigits) - let paddedAuthenticationCode = paddingZeros + authenticationCode - - completion(.success(paddedAuthenticationCode)) - return - case .failure(let error): - completion(.failure(error)) - return - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift deleted file mode 100644 index 796ab269c7c6..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIStoryboard+Helpers.swift +++ /dev/null @@ -1,30 +0,0 @@ -import UIKit - -// MARK: - Storyboard enum -enum Storyboard: String { - case login = "Login" - case signup = "Signup" - case getStarted = "GetStarted" - case unifiedSignup = "UnifiedSignup" - case unifiedLoginMagicLink = "LoginMagicLink" - case emailMagicLink = "EmailMagicLink" - case siteAddress = "SiteAddress" - case googleAuth = "GoogleAuth" - case googleSignupConfirmation = "GoogleSignupConfirmation" - case twoFA = "TwoFA" - case password = "Password" - case verifyEmail = "VerifyEmail" - case nuxButtonView = "NUXButtonView" - - var instance: UIStoryboard { - return UIStoryboard(name: self.rawValue, bundle: WordPressAuthenticator.bundle) - } - - /// Returns a view controller from a Storyboard - /// assuming the identifier is the same as the class name. - /// - func instantiateViewController(ofClass classType: T.Type, creator: ((NSCoder) -> UIViewController?)? = nil) -> T? { - let identifier = classType.classNameWithoutNamespaces - return instance.instantiateViewController(identifier: identifier, creator: creator) as? T - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift deleted file mode 100644 index 972468aa3140..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UITableView+Helpers.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit - -extension UITableView { - /// Called in view controller's `viewDidLayoutSubviews`. If table view has a footer view, calculates the new height. - /// If new height is different from current height, updates the footer view with the new height and reassigns the table footer view. - /// Note: make sure the top-level footer view (`tableView.tableFooterView`) is frame based as a container of the Auto Layout based subview. - func updateFooterHeight() { - if let footerView = tableFooterView { - let targetSize = CGSize(width: footerView.frame.width, height: 0) - let newSize = footerView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow) - let newHeight = newSize.height - var currentFrame = footerView.frame - if newHeight != currentFrame.size.height { - currentFrame.size.height = newHeight - footerView.frame = currentFrame - tableFooterView = footerView - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift b/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift deleted file mode 100644 index 36e29efa3cbe..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIView+AuthHelpers.swift +++ /dev/null @@ -1,18 +0,0 @@ -import UIKit - -/// UIView class methods -/// -extension UIView { - /// Returns the Nib associated with the received: It's filename is expected to match the Class Name - /// - class func loadNib() -> UINib { - return UINib(nibName: classNameWithoutNamespaces, bundle: WordPressAuthenticator.bundle) - } - - /// Returns the first Object contained within the nib with a name whose name matches with the receiver's type. - /// Note: On error this method is expected to break, by design! - /// - class func instantiateFromNib() -> T { - return loadNib().instantiate(withOwner: nil, options: nil).first as! T - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift b/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift deleted file mode 100644 index b1d721a9d119..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIViewController+Dismissal.swift +++ /dev/null @@ -1,22 +0,0 @@ -import UIKit - -extension UIViewController { - - /// Depending on how a VC is presented we need to check different things to know whether it's being dismissed or not. - /// A VC presented as the first VC in a navigation controller needs to check if the navigation controller is being dismissed. - /// A VC added to an existing navigation controller is dismissed when `isMovingFromParent` is `true`. - /// For any other scenario `isBeingDismissed` will do. - /// - var isBeingDismissedInAnyWay: Bool { - isMovingFromParent || isBeingDismissed || (navigationController?.isBeingDismissed ?? false) - } - - /// Depending on how a VC is presented we need to check different things to know whether it's being presented or not. - /// A VC presented as the first VC in a navigation controller needs to check if the navigation controller is being presented. - /// A VC added to an existing navigation controller is presented when `isMovingToParent` is `true`. - /// For any other scenario `isBeingPresented` will do. - /// - var isBeingPresentedInAnyWay: Bool { - isMovingToParent || isBeingPresented || (navigationController?.isBeingPresented ?? false) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift b/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift deleted file mode 100644 index c35a378e910d..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/UIViewController+Helpers.swift +++ /dev/null @@ -1,11 +0,0 @@ -import UIKit - -// MARK: - UIViewController Helpers -extension UIViewController { - - /// Convenience method to instantiate a view controller from a storyboard. - /// - static func instantiate(from storyboard: Storyboard, creator: ((NSCoder) -> UIViewController?)? = nil) -> Self? { - return storyboard.instantiateViewController(ofClass: self, creator: creator) - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift b/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift deleted file mode 100644 index 230d3616d603..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/URL+JetpackConnect.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -extension URL { - public var isJetpackConnect: Bool { - query?.contains("&source=jetpack") ?? false - } -} diff --git a/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift b/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift deleted file mode 100644 index 4326e870ff30..000000000000 --- a/Sources/WordPressAuthenticator/Extensions/WPStyleGuide+Login.swift +++ /dev/null @@ -1,343 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import Gridicons -import AuthenticationServices - -final class SubheadlineButton: UIButton { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - titleLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - setTitleColor(WordPressAuthenticator.shared.style.textButtonColor, for: .normal) - setTitleColor(WordPressAuthenticator.shared.style.textButtonHighlightColor, for: .highlighted) - } - } -} - -extension WPStyleGuide { - - private struct Constants { - static let textButtonMinHeight: CGFloat = 40.0 - static let googleIconOffset: CGFloat = -1.0 - static let googleIconButtonSize: CGFloat = 15.0 - static let domainsIconPaddingToRemove: CGFloat = 2.0 - static let domainsIconSize = CGSize(width: 18, height: 18) - static let verticalLabelSpacing: CGFloat = 10.0 - } - - /// Calculate the border based on the display - /// - class var hairlineBorderWidth: CGFloat { - return 1.0 / UIScreen.main.scale - } - - /// Common view style for signin view controllers. - /// - /// - Parameters: - /// - view: The view to style. - /// - class func configureColorsForSigninView(_ view: UIView) { - view.backgroundColor = wordPressBlue() - } - - /// Configures a plain text button with default styles. - /// - class func configureTextButton(_ button: UIButton) { - button.setTitleColor(WordPressAuthenticator.shared.style.textButtonColor, for: .normal) - button.setTitleColor(WordPressAuthenticator.shared.style.textButtonHighlightColor, for: .highlighted) - } - - /// - class func edgeInsetForLoginTextFields() -> UIEdgeInsets { - return UIEdgeInsets(top: 7, left: 20, bottom: 7, right: 20) - } - - class func textInsetsForLoginTextFieldWithLeftView() -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) - } - - /// Return the system font in medium weight for the given style - /// - /// - note: iOS won't return UIFontWeightMedium for dynamic system font :( - /// So instead get the dynamic font size, then ask for the non-dynamic font at that size - /// - class func mediumWeightFont(forStyle style: UIFont.TextStyle, maximumPointSize: CGFloat = WPStyleGuide.maxFontSize) -> UIFont { - let fontToGetSize = WPStyleGuide.fontForTextStyle(style) - let maxAllowedFontSize = CGFloat.minimum(fontToGetSize.pointSize, maximumPointSize) - return UIFont.systemFont(ofSize: maxAllowedFontSize, weight: .medium) - } - - // MARK: - Login Button Methods - - /// Creates a button for Google Sign-in with hyperlink style. - /// - /// - Returns: A properly styled UIButton - /// - class func googleLoginButton() -> UIButton { - let baseString = NSLocalizedString("{G} Log in with Google.", comment: "Label for button to log in using Google. The {G} will be replaced with the Google logo.") - - let attrStrNormal = googleButtonString(baseString, linkColor: WordPressAuthenticator.shared.style.textButtonColor) - let attrStrHighlight = googleButtonString(baseString, linkColor: WordPressAuthenticator.shared.style.textButtonHighlightColor) - - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - /// Creates an attributed string that includes the Google logo. - /// - /// - Parameters: - /// - forHyperlink: Indicates if the string will be displayed in a hyperlink. - /// Otherwise, it will be styled to be displayed on a NUXButton. - /// - Returns: A properly styled NSAttributedString - /// - class func formattedGoogleString(forHyperlink: Bool = false) -> NSAttributedString { - - let googleAttachment = NSTextAttachment() - let googleIcon = UIImage.googleIcon - googleAttachment.image = googleIcon - - if forHyperlink { - // Create an attributed string that contains the Google icon. - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - googleAttachment.bounds = CGRect(x: 0, - y: font.descender + Constants.googleIconOffset, - width: googleIcon.size.width, - height: googleIcon.size.height) - - return NSAttributedString(attachment: googleAttachment) - } else { - let nuxButtonTitleFont = WPStyleGuide.mediumWeightFont(forStyle: .title3) - let googleTitle = NSLocalizedString("Continue with Google", - comment: "Button title. Tapping begins log in using Google.") - return attributedStringwithLogo(googleIcon, - imageSize: .init(width: Constants.googleIconButtonSize, height: Constants.googleIconButtonSize), - title: googleTitle, - titleFont: nuxButtonTitleFont) - } - } - - /// Creates an attributed string that includes the Apple logo. - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func formattedAppleString() -> NSAttributedString { - let attributedString = NSMutableAttributedString() - - let appleSymbol = "" - let appleSymbolAttributes: [NSAttributedString.Key: Any] = [ - .font: UIFont.systemFont(ofSize: 23) - ] - attributedString.append(NSAttributedString(string: appleSymbol, attributes: appleSymbolAttributes)) - - // Add leading non-breaking space to separate the button text from the Apple symbol. - let space = "\u{00a0}\u{00a0}" - attributedString.append(NSAttributedString(string: space)) - - let title = NSLocalizedString("Continue with Apple", comment: "Button title. Tapping begins log in using Apple.") - attributedString.append(NSAttributedString(string: title)) - - return NSAttributedString(attributedString: attributedString) - } - - /// Creates an attributed string that includes the `linkFieldImage` - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func formattedSignInWithSiteCredentialsString() -> NSAttributedString { - let title = WordPressAuthenticator.shared.displayStrings.signInWithSiteCredentialsButtonTitle - let globe = UIImage.gridicon(.globe) - let image = globe.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) ?? globe - return attributedStringwithLogo(image, - imageSize: image.size, - title: title, - titleFont: WPStyleGuide.mediumWeightFont(forStyle: .title3)) - } - - /// Creates a button for Self-hosted Login - /// - /// - Returns: A properly styled UIButton - /// - class func selfHostedLoginButton(alignment: UIControl.NaturalContentHorizontalAlignment = .leading) -> UIButton { - - let style = WordPressAuthenticator.shared.style - - let button: UIButton - - if WordPressAuthenticator.shared.configuration.showLoginOptions { - let baseString = NSLocalizedString("Or log in by _entering your site address_.", comment: "Label for button to log in using site address. Underscores _..._ denote underline.") - - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: alignment) - } else { - let baseString = NSLocalizedString("Enter the address of the WordPress site you'd like to connect.", comment: "Label for button to log in using your site address.") - - let attrStrNormal = selfHostedButtonString(baseString, linkColor: style.textButtonColor) - let attrStrHighlight = selfHostedButtonString(baseString, linkColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - button.accessibilityIdentifier = "Self Hosted Login Button" - - return button - } - - /// Creates a button for wpcom signup on the email screen - /// - /// - Returns: A UIButton styled for wpcom signup - /// - Note: This button is only used during Jetpack setup, not the usual flows - /// - class func wpcomSignupButton() -> UIButton { - let style = WordPressAuthenticator.shared.style - let baseString = NSLocalizedString("Don't have an account? _Sign up_", comment: "Label for button to log in using your site address. The underscores _..._ denote underline") - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font) - } - - /// Creates a button to open our T&C - /// - /// - Returns: A properly styled UIButton - /// - class func termsButton() -> UIButton { - let style = WordPressAuthenticator.shared.style - - let baseString = NSLocalizedString("By signing up, you agree to our _Terms of Service_.", comment: "Legal disclaimer for signup buttons, the underscores _..._ denote underline") - - let attrStrNormal = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonColor) - let attrStrHighlight = baseString.underlined(color: style.subheadlineColor, underlineColor: style.textButtonHighlightColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .footnote) - - return textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: .center) - } - - /// Creates a button to open our T&C. - /// Specifically, the Sign Up verbiage on the Get Started view. - /// - Returns: A properly styled UIButton - /// - class func signupTermsButton() -> UIButton { - let unifiedStyle = WordPressAuthenticator.shared.unifiedStyle - let originalStyle = WordPressAuthenticator.shared.style - let baseString = WordPressAuthenticator.shared.displayStrings.signupTermsOfService - let textColor = unifiedStyle?.textSubtleColor ?? originalStyle.subheadlineColor - let linkColor = unifiedStyle?.textButtonColor ?? originalStyle.textButtonColor - - let attrStrNormal = baseString.underlined(color: textColor, underlineColor: linkColor) - let attrStrHighlight = baseString.underlined(color: textColor, underlineColor: linkColor) - let font = WPStyleGuide.mediumWeightFont(forStyle: .footnote) - - let button = textButton(normal: attrStrNormal, highlighted: attrStrHighlight, font: font, alignment: .center, forUnified: true) - button.titleLabel?.textAlignment = .center - return button - } - - private class func textButton(normal normalString: NSAttributedString, highlighted highlightString: NSAttributedString, font: UIFont, alignment: UIControl.NaturalContentHorizontalAlignment = .leading, forUnified: Bool = false) -> UIButton { - let button = SubheadlineButton() - button.clipsToBounds = true - - button.naturalContentHorizontalAlignment = alignment - button.translatesAutoresizingMaskIntoConstraints = false - button.titleLabel?.font = font - button.titleLabel?.numberOfLines = 0 - button.titleLabel?.lineBreakMode = .byWordWrapping - button.setTitleColor(WordPressAuthenticator.shared.style.subheadlineColor, for: .normal) - - // These constraints work around some issues with multiline buttons and - // vertical layout. Without them the button's height may not account - // for the titleLabel's height. - - let verticalLabelSpacing = forUnified ? 0 : Constants.verticalLabelSpacing - button.titleLabel?.topAnchor.constraint(equalTo: button.topAnchor, constant: verticalLabelSpacing).isActive = true - button.titleLabel?.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -verticalLabelSpacing).isActive = true - button.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.textButtonMinHeight).isActive = true - - button.setAttributedTitle(normalString, for: .normal) - button.setAttributedTitle(highlightString, for: .highlighted) - return button - } - - private class func googleButtonString(_ baseString: String, linkColor: UIColor) -> NSAttributedString { - let labelParts = baseString.components(separatedBy: "{G}") - - let firstPart = labelParts[0] - // 👇 don't want to crash when a translation lacks "{G}" - let lastPart = labelParts.indices.contains(1) ? labelParts[1] : "" - - let labelString = NSMutableAttributedString(string: firstPart, attributes: [.foregroundColor: WPStyleGuide.greyDarken30()]) - - if !lastPart.isEmpty { - labelString.append(formattedGoogleString(forHyperlink: true)) - } - - labelString.append(NSAttributedString(string: lastPart, attributes: [.foregroundColor: linkColor])) - - return labelString - } - - private class func selfHostedButtonString(_ buttonText: String, linkColor: UIColor) -> NSAttributedString { - let font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - - let titleParagraphStyle = NSMutableParagraphStyle() - titleParagraphStyle.alignment = .left - - let labelString = NSMutableAttributedString(string: "") - - if let originalDomainsIcon = UIImage.gridicon(.domains).imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) { - var domainsIcon = originalDomainsIcon.cropping(to: CGRect(x: Constants.domainsIconPaddingToRemove, - y: Constants.domainsIconPaddingToRemove, - width: originalDomainsIcon.size.width - Constants.domainsIconPaddingToRemove * 2, - height: originalDomainsIcon.size.height - Constants.domainsIconPaddingToRemove * 2)) - domainsIcon = domainsIcon.resized(to: Constants.domainsIconSize) - let domainsAttachment = NSTextAttachment() - domainsAttachment.image = domainsIcon - domainsAttachment.bounds = CGRect(x: 0, y: font.descender, width: domainsIcon.size.width, height: domainsIcon.size.height) - let iconString = NSAttributedString(attachment: domainsAttachment) - labelString.append(iconString) - } - labelString.append(NSAttributedString(string: " " + buttonText, attributes: [.foregroundColor: linkColor])) - - return labelString - } -} - -// MARK: Attributed String Helpers -// -private extension WPStyleGuide { - - /// Creates an attributed string with a logo and title. - /// The logo is prepended to the title. - /// - /// - Parameters: - /// - logoImage: UIImage representing the logo - /// - imageSize: Size of the UIImage - /// - title: title String to be appended to the logoImage - /// - titleFont: UIFont for the title String - /// - /// - Returns: A properly styled NSAttributedString to be displayed on a NUXButton. - /// - class func attributedStringwithLogo(_ logoImage: UIImage, - imageSize: CGSize, - title: String, - titleFont: UIFont) -> NSAttributedString { - let attachment = NSTextAttachment() - attachment.image = logoImage - - attachment.bounds = CGRect(x: 0, y: (titleFont.capHeight - imageSize.height) / 2, - width: imageSize.width, height: imageSize.height) - - let buttonString = NSMutableAttributedString(attachment: attachment) - // Add leading non-breaking spaces to separate the button text from the logo. - let title = "\u{00a0}\u{00a0}" + title - buttonString.append(NSAttributedString(string: title)) - - return buttonString - } -} diff --git a/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift deleted file mode 100644 index 24c755e2453c..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/AppSelector.swift +++ /dev/null @@ -1,130 +0,0 @@ -import UIKit -import MessageUI - -/// App selector that selects an app from a list and opens it -/// Note: it's a wrapper of UIAlertController (which cannot be sublcassed) -public 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 - /// - sourceView: the sourceView to anchor the action sheet to - /// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared - public 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]? { - guard !appList.isEmpty else { - return nil - } - - var actions = [UIAlertAction]() - for (name, urlString) in appList { - guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else { - continue - } - actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { _ in - urlHandler.open(url, options: [:], completionHandler: nil) - }) - } - - 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 = makeAlertActions(from: appList) else { - return nil - } - - alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alertController.popoverPresentationController?.sourceView = sourceView - alertController.popoverPresentationController?.sourceRect = sourceView.bounds - appCalls.forEach { - alertController.addAction($0) - } - } -} - -/// Initializers for Email Picker -public extension AppSelector { - /// initializes the picker with a plist file in a specified bundle - 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, - sourceView: sourceView) - } - - /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist - convenience init?(sourceView: UIView) { - let wpAuthenticatorBundle = WordPressAuthenticator.bundle - - 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) { _ in - UIApplication.shared.open(url) - } - } - self.init(with: plistFile, - in: wpAuthenticatorBundle, - defaultAction: defaultAction, - sourceView: sourceView) - } -} - -/// 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: "Option to select the Apple Mail app when logging in with magic links") - case .gmail: - return NSLocalizedString("Gmail", comment: "Option to select the Gmail app when logging in with magic links") - case .airmail: - return NSLocalizedString("Airmail", comment: "Option to select the Airmail app when logging in with magic links") - case .msOutlook: - return NSLocalizedString("Microsoft Outlook", comment: "Option to select the Microsft Outlook app when logging in with magic links") - case .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: "Option to select the Yahoo Mail app when logging in with magic links") - case .fastmail: - return NSLocalizedString("Fastmail", comment: "Option to select the Fastmail app when logging in with magic links") - case .cancel: - return NSLocalizedString("Cancel", comment: "Option to cancel the email app selection when logging in with magic links") - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift deleted file mode 100644 index 7e12c0b3317b..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/LinkMailPresenter.swift +++ /dev/null @@ -1,48 +0,0 @@ -import MessageUI - -/// Email picker presenter -public class LinkMailPresenter { - - private let emailAddress: String - - public 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 - /// - 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 - public func presentEmailClients(on viewController: UIViewController, - 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("Check your email!", - comment: "Alert title for check your email during logIn/signUp.") - - 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, - 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/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift b/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift deleted file mode 100644 index dfe18e973192..000000000000 --- a/Sources/WordPressAuthenticator/Features/EmailClientPicker/URLHandler.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -/// Generic type that handles URL Schemes -public 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: (@MainActor @Sendable (Bool) -> Void)?) -} - -/// conforms UIApplication to URLHandler to allow dependency injection -extension UIApplication: URLHandler {} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift deleted file mode 100644 index bc1a292725a4..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButton.swift +++ /dev/null @@ -1,266 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -public struct NUXButtonStyle { - public let normal: ButtonStyle - public let highlighted: ButtonStyle - public let disabled: ButtonStyle - - public struct ButtonStyle { - public let backgroundColor: UIColor - public let borderColor: UIColor - public let titleColor: UIColor - - public init(backgroundColor: UIColor, borderColor: UIColor, titleColor: UIColor) { - self.backgroundColor = backgroundColor - self.borderColor = borderColor - self.titleColor = titleColor - } - } - - public init(normal: ButtonStyle, highlighted: ButtonStyle, disabled: ButtonStyle) { - self.normal = normal - self.highlighted = highlighted - self.disabled = disabled - } - - public static var linkButtonStyle: NUXButtonStyle { - let backgroundColor = UIColor.clear - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - - let normalButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonTitleColor) - let highlightedButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonHighlightColor) - let disabledButtonStyle = ButtonStyle(backgroundColor: backgroundColor, - borderColor: backgroundColor, - titleColor: buttonTitleColor.withAlphaComponent(0.5)) - return NUXButtonStyle(normal: normalButtonStyle, - highlighted: highlightedButtonStyle, - disabled: disabledButtonStyle) - } -} -/// A stylized button used by Login controllers. It also can display a `UIActivityIndicatorView`. -@objc open class NUXButton: UIButton { - @objc var isAnimating: Bool { - return activityIndicator.isAnimating - } - - var buttonStyle: NUXButtonStyle? - - open override var isEnabled: Bool { - didSet { - activityIndicator.color = activityIndicatorColor(isEnabled: isEnabled) - } - } - - @objc let activityIndicator: UIActivityIndicatorView = { - let indicator = UIActivityIndicatorView(style: .medium) - indicator.hidesWhenStopped = true - return indicator - }() - - var titleFont = WPStyleGuide.mediumWeightFont(forStyle: .title3) - - override open func layoutSubviews() { - super.layoutSubviews() - - if activityIndicator.isAnimating { - titleLabel?.frame = CGRect.zero - - var frm = activityIndicator.frame - frm.origin.x = (frame.width - frm.width) / 2.0 - frm.origin.y = (frame.height - frm.height) / 2.0 - activityIndicator.frame = frm.integral - } - } - - open override func tintColorDidChange() { - // Update colors when toggling light/dark mode. - super.tintColorDidChange() - configureBackgrounds() - configureTitleColors() - - if socialService == .apple { - setAttributedTitle(WPStyleGuide.formattedAppleString(), for: .normal) - } - } - - // MARK: - Instance Methods - - /// Toggles the visibility of the activity indicator. When visible the button - /// title is hidden. - /// - /// - Parameter show: True to show the spinner. False hides it. - /// - open func showActivityIndicator(_ show: Bool) { - if show { - activityIndicator.startAnimating() - } else { - activityIndicator.stopAnimating() - } - setNeedsLayout() - } - - func didChangePreferredContentSize() { - titleLabel?.adjustsFontForContentSizeCategory = true - } - - func customizeFont(_ font: UIFont) { - titleFont = font - } - - /// Indicates if the current instance should be rendered with the "Primary" Style. - /// - @IBInspectable public var isPrimary: Bool = false { - didSet { - configureBackgrounds() - configureTitleColors() - } - } - - var socialService: SocialServiceName? - - // MARK: - LifeCycle Methods - - open override func didMoveToWindow() { - super.didMoveToWindow() - configureAppearance() - } - - open override func awakeFromNib() { - super.awakeFromNib() - configureAppearance() - } - - /// Setup: Everything = [Insets, Backgrounds, titleColor(s), titleLabel] - /// - private func configureAppearance() { - configureInsets() - configureBackgrounds() - configureActivityIndicator() - configureTitleColors() - configureTitleLabel() - } - - /// Setup: NUXButton's Default Settings - /// - private func configureInsets() { - contentEdgeInsets = UIImage.DefaultRenderMetrics.contentInsets - } - - /// Setup: ActivityIndicator - /// - private func configureActivityIndicator() { - activityIndicator.color = activityIndicatorColor() - addSubview(activityIndicator) - } - - /// Setup: BackgroundImage - /// - private func configureBackgrounds() { - guard let buttonStyle else { - legacyConfigureBackgrounds() - return - } - - let normalImage = UIImage.renderBackgroundImage(fill: buttonStyle.normal.backgroundColor, - border: buttonStyle.normal.borderColor) - - let highlightedImage = UIImage.renderBackgroundImage(fill: buttonStyle.highlighted.backgroundColor, - border: buttonStyle.highlighted.borderColor) - - let disabledImage = UIImage.renderBackgroundImage(fill: buttonStyle.disabled.backgroundColor, - border: buttonStyle.disabled.borderColor) - - setBackgroundImage(normalImage, for: .normal) - setBackgroundImage(highlightedImage, for: .highlighted) - setBackgroundImage(disabledImage, for: .disabled) - } - - /// Fallback method to configure the background colors based on the shared `WordPressAuthenticatorStyle` - /// - private func legacyConfigureBackgrounds() { - let style = WordPressAuthenticator.shared.style - - let normalImage: UIImage - let highlightedImage: UIImage - let disabledImage = UIImage.renderBackgroundImage(fill: style.disabledBackgroundColor, - border: style.disabledBorderColor) - - if isPrimary { - normalImage = UIImage.renderBackgroundImage(fill: style.primaryNormalBackgroundColor, - border: style.primaryNormalBorderColor) - highlightedImage = UIImage.renderBackgroundImage(fill: style.primaryHighlightBackgroundColor, - border: style.primaryHighlightBorderColor) - } else { - normalImage = UIImage.renderBackgroundImage(fill: style.secondaryNormalBackgroundColor, - border: style.secondaryNormalBorderColor) - highlightedImage = UIImage.renderBackgroundImage(fill: style.secondaryHighlightBackgroundColor, - border: style.secondaryHighlightBorderColor) - } - - setBackgroundImage(normalImage, for: .normal) - setBackgroundImage(highlightedImage, for: .highlighted) - setBackgroundImage(disabledImage, for: .disabled) - } - - /// Setup: TitleColor - /// - private func configureTitleColors() { - guard let buttonStyle else { - legacyConfigureTitleColors() - return - } - - setTitleColor(buttonStyle.normal.titleColor, for: .normal) - setTitleColor(buttonStyle.highlighted.titleColor, for: .highlighted) - setTitleColor(buttonStyle.disabled.titleColor, for: .disabled) - } - - /// Fallback method to configure the title colors based on the shared `WordPressAuthenticatorStyle` - /// - private func legacyConfigureTitleColors() { - let style = WordPressAuthenticator.shared.style - let titleColorNormal = isPrimary ? style.primaryTitleColor : style.secondaryTitleColor - - setTitleColor(titleColorNormal, for: .normal) - setTitleColor(titleColorNormal, for: .highlighted) - setTitleColor(style.disabledTitleColor, for: .disabled) - } - - /// Setup: TitleLabel - /// - private func configureTitleLabel() { - titleLabel?.font = self.titleFont - titleLabel?.adjustsFontForContentSizeCategory = true - titleLabel?.textAlignment = .center - } - - /// Returns the current color that should be used for the activity indicator - /// - private func activityIndicatorColor(isEnabled: Bool = true) -> UIColor { - guard let style = buttonStyle else { - let style = WordPressAuthenticator.shared.style - - return isEnabled ? style.primaryTitleColor : style.disabledButtonActivityIndicatorColor - } - - return isEnabled ? style.normal.titleColor : style.disabled.titleColor - } -} - -// MARK: - -// -extension NUXButton { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard deleted file mode 100644 index 31db7e8cf3c6..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonView.storyboard +++ /dev/null @@ -1,206 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift deleted file mode 100644 index bf5e2457049d..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXButtonViewController.swift +++ /dev/null @@ -1,315 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared - -public protocol NUXButtonViewControllerDelegate: AnyObject { - func primaryButtonPressed() - func secondaryButtonPressed() - func tertiaryButtonPressed() -} - -extension NUXButtonViewControllerDelegate { - public func secondaryButtonPressed() {} - public func tertiaryButtonPressed() {} -} - -struct NUXButtonConfig { - typealias CallBackType = () -> Void - - let title: String? - let attributedTitle: NSAttributedString? - let socialService: SocialServiceName? - let isPrimary: Bool - let configureBodyFontForTitle: Bool? - let accessibilityIdentifier: String? - let callback: CallBackType? - - init(title: String? = nil, attributedTitle: NSAttributedString? = nil, socialService: SocialServiceName? = nil, isPrimary: Bool, configureBodyFontForTitle: Bool? = nil, accessibilityIdentifier: String? = nil, callback: CallBackType?) { - self.title = title - self.attributedTitle = attributedTitle - self.socialService = socialService - self.isPrimary = isPrimary - self.configureBodyFontForTitle = configureBodyFontForTitle - self.accessibilityIdentifier = accessibilityIdentifier - self.callback = callback - } -} - -open class NUXButtonViewController: UIViewController { - typealias CallBackType = () -> Void - - // MARK: - Properties - - @IBOutlet var stackView: UIStackView? - @IBOutlet var bottomButton: NUXButton? - @IBOutlet var topButton: NUXButton? - @IBOutlet var tertiaryButton: NUXButton? - @IBOutlet var buttonHolder: UIView? - - @IBOutlet private var shadowView: UIImageView? - @IBOutlet private var shadowViewEdgeConstraints: [NSLayoutConstraint]! - - /// Used to constrain the shadow view outside of the - /// bounds of this view controller. - var shadowLayoutGuide: UILayoutGuide? { - didSet { - updateShadowViewEdgeConstraints() - } - } - - open weak var delegate: NUXButtonViewControllerDelegate? - open var backgroundColor: UIColor? - - private var topButtonConfig: NUXButtonConfig? - private var bottomButtonConfig: NUXButtonConfig? - private var tertiaryButtonConfig: NUXButtonConfig? - - public var topButtonStyle: NUXButtonStyle? - public var bottomButtonStyle: NUXButtonStyle? - public var tertiaryButtonStyle: NUXButtonStyle? - - private let style = WordPressAuthenticator.shared.style - - // MARK: - View - - override open func viewDidLoad() { - super.viewDidLoad() - view.translatesAutoresizingMaskIntoConstraints = false - - shadowView?.image = style.buttonViewTopShadowImage - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configure(button: bottomButton, withConfig: bottomButtonConfig, and: bottomButtonStyle) - configure(button: topButton, withConfig: topButtonConfig, and: topButtonStyle) - configure(button: tertiaryButton, withConfig: tertiaryButtonConfig, and: tertiaryButtonStyle) - - buttonHolder?.backgroundColor = backgroundColor - } - - private func configure(button: NUXButton?, withConfig buttonConfig: NUXButtonConfig?, and style: NUXButtonStyle?) { - if let buttonConfig, let button { - - if let attributedTitle = buttonConfig.attributedTitle { - button.setAttributedTitle(attributedTitle, for: .normal) - button.socialService = buttonConfig.socialService - } else { - button.setTitle(buttonConfig.title, for: .normal) - } - - button.accessibilityIdentifier = buttonConfig.accessibilityIdentifier ?? accessibilityIdentifier(for: buttonConfig.title) - button.isPrimary = buttonConfig.isPrimary - - if buttonConfig.configureBodyFontForTitle == true { - button.customizeFont(WPStyleGuide.mediumWeightFont(forStyle: .body)) - } - - button.buttonStyle = style - - button.isHidden = false - } else { - button?.isHidden = true - } - } - - private func updateShadowViewEdgeConstraints() { - guard let layoutGuide = shadowLayoutGuide, - let shadowView else { - return - } - - NSLayoutConstraint.deactivate(shadowViewEdgeConstraints) - shadowView.translatesAutoresizingMaskIntoConstraints = false - - shadowViewEdgeConstraints = [ - layoutGuide.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor), - layoutGuide.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor), - ] - - NSLayoutConstraint.activate(shadowViewEdgeConstraints) - } - - // MARK: public API - - /// Public method to set the button titles. - /// - /// - Parameters: - /// - primary: Title string for primary button. Required. - /// - primaryAccessibilityId: Accessibility identifier string for primary button. Optional. - /// - secondary: Title string for secondary button. Optional. - /// - secondaryAccessibilityId: Accessibility identifier string for secondary button. Optional. - /// - tertiary: Title string for the tertiary button. Optional. - /// - tertiaryAccessibilityId: Accessibility identifier string for tertiary button. Optional. - /// - public func setButtonTitles(primary: String, primaryAccessibilityId: String? = nil, secondary: String? = nil, secondaryAccessibilityId: String? = nil, tertiary: String? = nil, tertiaryAccessibilityId: String? = nil) { - bottomButtonConfig = NUXButtonConfig(title: primary, isPrimary: true, accessibilityIdentifier: primaryAccessibilityId, callback: nil) - if let secondaryTitle = secondary { - topButtonConfig = NUXButtonConfig(title: secondaryTitle, isPrimary: false, accessibilityIdentifier: secondaryAccessibilityId, callback: nil) - } - if let tertiaryTitle = tertiary { - tertiaryButtonConfig = NUXButtonConfig(title: tertiaryTitle, isPrimary: false, accessibilityIdentifier: tertiaryAccessibilityId, callback: nil) - } - } - - func setupTopButton(title: String, isPrimary: Bool = false, configureBodyFontForTitle: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - topButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTopButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - topButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func setupBottomButton(title: String, isPrimary: Bool = false, configureBodyFontForTitle: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - bottomButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - // Sets up bottom button using `NSAttributedString` as title - // - func setupBottomButton(attributedTitle: NSAttributedString, - isPrimary: Bool = false, - configureBodyFontForTitle: Bool = false, - accessibilityIdentifier: String? = nil, - onTap callback: @escaping CallBackType) { - bottomButtonConfig = NUXButtonConfig(attributedTitle: attributedTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: configureBodyFontForTitle, - accessibilityIdentifier: accessibilityIdentifier, - callback: callback) - } - - func setupButtomButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - bottomButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func setupTertiaryButton(attributedTitle: NSAttributedString, isPrimary: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - tertiaryButton?.isHidden = false - tertiaryButtonConfig = NUXButtonConfig(attributedTitle: attributedTitle, isPrimary: isPrimary, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTertiaryButton(title: String, isPrimary: Bool = false, accessibilityIdentifier: String? = nil, onTap callback: @escaping CallBackType) { - tertiaryButton?.isHidden = false - tertiaryButtonConfig = NUXButtonConfig(title: title, isPrimary: isPrimary, accessibilityIdentifier: accessibilityIdentifier, callback: callback) - } - - func setupTertiaryButtonFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) { - tertiaryButtonConfig = buttonConfigFor(socialService: socialService, onTap: callback) - } - - func hideShadowView() { - shadowView?.isHidden = true - } - - public func setTopButtonState(isLoading: Bool, isEnabled: Bool) { - topButton?.showActivityIndicator(isLoading) - topButton?.isEnabled = isEnabled - } - - public func setBottomButtonState(isLoading: Bool, isEnabled: Bool) { - bottomButton?.showActivityIndicator(isLoading) - bottomButton?.isEnabled = isEnabled - } - - public func setTertiaryButtonState(isLoading: Bool, isEnabled: Bool) { - tertiaryButton?.showActivityIndicator(isLoading) - tertiaryButton?.isEnabled = isEnabled - } - - // MARK: - Helpers - - private func buttonConfigFor(socialService: SocialServiceName, onTap callback: @escaping CallBackType) -> NUXButtonConfig { - - var attributedTitle = NSAttributedString() - var accessibilityIdentifier = String() - - switch socialService { - case .google: - attributedTitle = WPStyleGuide.formattedGoogleString() - accessibilityIdentifier = "Continue with Google Button" - case .apple: - attributedTitle = WPStyleGuide.formattedAppleString() - accessibilityIdentifier = "Continue with Apple Button" - } - - return NUXButtonConfig(attributedTitle: attributedTitle, - socialService: socialService, - isPrimary: false, - accessibilityIdentifier: accessibilityIdentifier, - callback: callback) - } - - private func accessibilityIdentifier(for string: String?) -> String { - return "\(string ?? "") Button" - } - - // MARK: - Button Handling - - @IBAction func primaryButtonPressed(_ sender: Any) { - guard let callback = bottomButtonConfig?.callback else { - delegate?.primaryButtonPressed() - return - } - callback() - } - - @IBAction func secondaryButtonPressed(_ sender: Any) { - guard let callback = topButtonConfig?.callback else { - delegate?.secondaryButtonPressed() - return - } - callback() - } - - @IBAction func tertiaryButtonPressed(_ sender: Any) { - guard let callback = tertiaryButtonConfig?.callback else { - delegate?.tertiaryButtonPressed() - return - } - callback() - } - - // MARK: - Dynamic type - - func didChangePreferredContentSize() { - configure(button: bottomButton, withConfig: bottomButtonConfig, and: bottomButtonStyle) - configure(button: topButton, withConfig: topButtonConfig, and: topButtonStyle) - configure(button: tertiaryButton, withConfig: tertiaryButtonConfig, and: tertiaryButtonStyle) - } -} - -extension NUXButtonViewController { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} - -extension NUXButtonViewController { - - /// Sets the parentViewControlleras the receiver instance's container. Plus: the containerView will also get the receiver's - /// view, attached to it's edges. This is effectively analog to using an Embed Segue with the NUXButtonViewController. - /// - public func move(to parentViewController: UIViewController, into containerView: UIView) { - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - containerView.pinSubviewToAllEdges(view) - - willMove(toParent: parentViewController) - parentViewController.addChild(self) - didMove(toParent: parentViewController) - } - - /// Returns a new NUXButtonViewController Instance - /// - public class func instance() -> NUXButtonViewController { - let storyboard = UIStoryboard(name: "NUXButtonView", bundle: WordPressAuthenticator.bundle) - guard let buttonViewController = storyboard.instantiateViewController(withIdentifier: "ButtonView") as? NUXButtonViewController else { - fatalError() - } - - return buttonViewController - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift deleted file mode 100644 index 4cc29dd963a7..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/Button/NUXStackedButtonsViewController.swift +++ /dev/null @@ -1,263 +0,0 @@ -import UIKit -import WordPressShared - -struct StackedButton { - enum StackView { - case top - case bottom - } - - let stackView: StackView - let style: NUXButtonStyle? - - var config: NUXButtonConfig { - NUXButtonConfig(title: title, isPrimary: isPrimary, configureBodyFontForTitle: configureBodyFontForTitle, accessibilityIdentifier: accessibilityIdentifier, callback: onTap) - } - - // MARK: Private properties - private let title: String - private let isPrimary: Bool - private let configureBodyFontForTitle: Bool - private let accessibilityIdentifier: String? - private let onTap: NUXButtonConfig.CallBackType - - init(stackView: StackView = .top, - title: String, - isPrimary: Bool = false, - configureBodyFontForTitle: Bool = false, - accessibilityIdentifier: String? = nil, - style: NUXButtonStyle?, - onTap: @escaping NUXButtonConfig.CallBackType) { - self.stackView = stackView - self.title = title - self.isPrimary = isPrimary - self.configureBodyFontForTitle = configureBodyFontForTitle - self.accessibilityIdentifier = accessibilityIdentifier - self.style = style - self.onTap = onTap - } - - // MARK: Initializers - - /// Initializes a new StackedButton instance using the properties from the provided `StackedButton` and the provided `stackView` - /// - /// Used to copy properties of a StackedButton and just change the stackView placement - /// - /// - Parameters: - /// - using: StackedButton to be copied. (Except the `stackView` property) - /// - stackView: StackView placement of the new StackedButton - init(using: StackedButton, - stackView: StackView) { - self.init(stackView: stackView, - title: using.title, - isPrimary: using.isPrimary, - configureBodyFontForTitle: using.configureBodyFontForTitle, - accessibilityIdentifier: using.accessibilityIdentifier, - style: using.style, - onTap: using.onTap) - } -} - -/// Used to create two stack views of NUXButtons optionally divided by a OR divider -/// -/// Created as a replacement for NUXButtonViewController -/// -open class NUXStackedButtonsViewController: UIViewController { - // MARK: - Properties - @IBOutlet private weak var buttonHolder: UIView? - - // Stack view - @IBOutlet private var topStackView: UIStackView? - @IBOutlet private var bottomStackView: UIStackView? - - // Divider line - @IBOutlet private weak var leadingDividerLine: UIView! - @IBOutlet private weak var leadingDividerLineHeight: NSLayoutConstraint! - @IBOutlet private weak var dividerStackView: UIStackView! - @IBOutlet private weak var dividerLabel: UILabel! - @IBOutlet private weak var trailingDividerLine: UIView! - @IBOutlet private weak var trailingDividerLineHeight: NSLayoutConstraint! - - // Shadow - @IBOutlet private weak var shadowView: UIImageView? - @IBOutlet private var shadowViewEdgeConstraints: [NSLayoutConstraint]! - - /// Used to constrain the shadow view outside of the - /// bounds of this view controller. - weak var shadowLayoutGuide: UILayoutGuide? { - didSet { - updateShadowViewEdgeConstraints() - } - } - - var backgroundColor: UIColor? - private var showDivider = true - private var buttons: [NUXButton] = [] - - private let style = WordPressAuthenticator.shared.style - - private var buttonConfigs = [StackedButton]() - - // MARK: - View - override open func viewDidLoad() { - super.viewDidLoad() - view.translatesAutoresizingMaskIntoConstraints = false - - shadowView?.image = style.buttonViewTopShadowImage - configureDivider() - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - reloadViews() - - buttonHolder?.backgroundColor = backgroundColor - } - - // MARK: public API - func setUpButtons(using config: [StackedButton], showDivider: Bool) { - self.buttonConfigs = config - self.showDivider = showDivider - createButtons() - } - - func hideShadowView() { - shadowView?.isHidden = true - } -} - -// MARK: Helpers -// -private extension NUXStackedButtonsViewController { - @objc func handleTap(_ sender: NUXButton) { - guard let index = buttons.firstIndex(of: sender), - let callback = buttonConfigs[index].config.callback else { - return - } - - callback() - } - - func reloadViews() { - for (index, button) in buttons.enumerated() { - button.configure(withConfig: buttonConfigs[index].config, and: buttonConfigs[index].style) - button.addTarget(self, action: #selector(handleTap), for: .touchUpInside) - } - dividerStackView.isHidden = !showDivider - } - - func createButtons() { - buttons = [] - topStackView?.arrangedSubviews.forEach({ $0.removeFromSuperview() }) - bottomStackView?.arrangedSubviews.forEach({ $0.removeFromSuperview() }) - for config in buttonConfigs { - let button = NUXButton() - switch config.stackView { - case .top: - topStackView?.addArrangedSubview(button) - case .bottom: - bottomStackView?.addArrangedSubview(button) - } - button.configure(withConfig: config.config, and: config.style) - buttons.append(button) - } - } - - func configureDivider() { - guard showDivider else { - return dividerStackView.isHidden = true - } - - leadingDividerLine.backgroundColor = style.orDividerSeparatorColor - leadingDividerLineHeight.constant = WPStyleGuide.hairlineBorderWidth - trailingDividerLine.backgroundColor = style.orDividerSeparatorColor - trailingDividerLineHeight.constant = WPStyleGuide.hairlineBorderWidth - dividerLabel.textColor = style.orDividerTextColor - dividerLabel.text = NSLocalizedString("Or", comment: "Divider on initial auth view separating auth options.").localizedUppercase - } - - func updateShadowViewEdgeConstraints() { - guard let layoutGuide = shadowLayoutGuide, - let shadowView else { - return - } - - NSLayoutConstraint.deactivate(shadowViewEdgeConstraints) - shadowView.translatesAutoresizingMaskIntoConstraints = false - - shadowViewEdgeConstraints = [ - layoutGuide.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor), - layoutGuide.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor), - ] - - NSLayoutConstraint.activate(shadowViewEdgeConstraints) - } - - // MARK: - Dynamic type - func didChangePreferredContentSize() { - reloadViews() - } -} - -extension NUXStackedButtonsViewController { - override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} - -extension NUXStackedButtonsViewController { - - /// Sets the parentViewControlleras the receiver instance's container. Plus: the containerView will also get the receiver's - /// view, attached to it's edges. This is effectively analog to using an Embed Segue with the NUXButtonViewController. - /// - public func move(to parentViewController: UIViewController, into containerView: UIView) { - containerView.translatesAutoresizingMaskIntoConstraints = false - containerView.addSubview(view) - containerView.pinSubviewToAllEdges(view) - - willMove(toParent: parentViewController) - parentViewController.addChild(self) - didMove(toParent: parentViewController) - } - - /// Returns a new NUXButtonViewController Instance - /// - public class func instance() -> NUXStackedButtonsViewController { - guard let buttonViewController = Storyboard.nuxButtonView.instantiateViewController(ofClass: NUXStackedButtonsViewController.self) else { - fatalError("Cannot instantiate initial NUXStackedButtonsViewController from NUXButtonView.storyboard") - } - - return buttonViewController - } -} - -private extension NUXButton { - func configure(withConfig buttonConfig: NUXButtonConfig?, and style: NUXButtonStyle?) { - guard let buttonConfig else { - isHidden = true - return - } - - if let attributedTitle = buttonConfig.attributedTitle { - setAttributedTitle(attributedTitle, for: .normal) - } else { - setTitle(buttonConfig.title, for: .normal) - } - - socialService = buttonConfig.socialService - accessibilityIdentifier = buttonConfig.accessibilityIdentifier ?? "\(buttonConfig.title ?? "") Button" - isPrimary = buttonConfig.isPrimary - - if buttonConfig.configureBodyFontForTitle == true { - customizeFont(WPStyleGuide.mediumWeightFont(forStyle: .body)) - } - - buttonStyle = style - - isHidden = false - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift b/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift deleted file mode 100644 index 280511bae000..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/ModalViewControllerPresenting.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -protocol ModalViewControllerPresenting { - func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) -} - -extension UIViewController: ModalViewControllerPresenting {} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift deleted file mode 100644 index 0bf3d386b098..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXKeyboardResponder.swift +++ /dev/null @@ -1,149 +0,0 @@ -import UIKit - -// The signin forms are centered, and then adjusted for the combined height of -// the status bar and navigation bar. -(20 + 44). -// If this value is changed be sure to update the storyboard for consistency. -let NUXKeyboardDefaultFormVerticalOffset: CGFloat = -64.0 - -/// A protocol and extension encapsulating common keyboard releated logic for -/// Signin controllers. -/// -public protocol NUXKeyboardResponder: AnyObject { - var bottomContentConstraint: NSLayoutConstraint? {get} - var verticalCenterConstraint: NSLayoutConstraint? {get} - - func signinFormVerticalOffset() -> CGFloat - func registerForKeyboardEvents(keyboardWillShowAction: Selector, keyboardWillHideAction: Selector) - func unregisterForKeyboardEvents() - func adjustViewForKeyboard(_ visibleKeyboard: Bool) - - func keyboardWillShow(_ notification: Foundation.Notification) - func keyboardWillHide(_ notification: Foundation.Notification) -} - -public extension NUXKeyboardResponder where Self: NUXViewController { - - /// Registeres the receiver for keyboard events using the passed selectors. - /// We pass the selectors this way so we can encapsulate functionality in a - /// Swift protocol extension and still play nice with Objective C code. - /// - /// - Parameters - /// - keyboardWillShowAction: A Selector to use for the UIKeyboardWillShowNotification observer. - /// - keyboardWillHideAction: A Selector to use for the UIKeyboardWillHideNotification observer. - /// - func registerForKeyboardEvents(keyboardWillShowAction: Selector, keyboardWillHideAction: Selector) { - NotificationCenter.default.addObserver(self, selector: keyboardWillShowAction, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: keyboardWillHideAction, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - /// Unregisters the receiver from keyboard events. - /// - func unregisterForKeyboardEvents() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - /// Returns the vertical offset to apply to the sign in form. - /// - /// - Returns: NUXKeyboardDefaultFormVerticalOffset unless a conforming controller provides its own implementation. - /// - func signinFormVerticalOffset() -> CGFloat { - return NUXKeyboardDefaultFormVerticalOffset - } - - /// Adjusts constraint constants to adapt the view for a visible keyboard. - /// - /// - Parameter visibleKeyboard: Whether to configure for a visible keyboard or without a keyboard. - /// - func adjustViewForKeyboard(_ visibleKeyboard: Bool) { - if visibleKeyboard && SigninEditingState.signinLastKeyboardHeightDelta > 0 { - bottomContentConstraint?.constant = SigninEditingState.signinLastKeyboardHeightDelta - verticalCenterConstraint?.constant = 0 - } else { - bottomContentConstraint?.constant = 0 - verticalCenterConstraint?.constant = signinFormVerticalOffset() - } - } - - /// Process the passed NSNotification from a UIKeyboardWillShowNotification. - /// - /// - Parameter notification: the NSNotification object from a UIKeyboardWillShowNotification. - /// - func keyboardWillShow(_ notification: Foundation.Notification) { - guard let keyboardInfo = keyboardFrameAndDurationFromNotification(notification) else { - return - } - - SigninEditingState.signinLastKeyboardHeightDelta = heightDeltaFromKeyboardFrame(keyboardInfo.keyboardFrame) - SigninEditingState.signinEditingStateActive = true - - if bottomContentConstraint?.constant == SigninEditingState.signinLastKeyboardHeightDelta { - return - } - - adjustViewForKeyboard(true) - UIView.animate(withDuration: keyboardInfo.animationDuration, - delay: 0, - options: .beginFromCurrentState, - animations: { - self.view.layoutIfNeeded() - }, - completion: nil) - } - - /// Process the passed NSNotification from a UIKeyboardWillHideNotification. - /// - /// - Parameter notification: the NSNotification object from a UIKeyboardWillHideNotification. - /// - func keyboardWillHide(_ notification: Foundation.Notification) { - guard let keyboardInfo = keyboardFrameAndDurationFromNotification(notification) else { - return - } - - SigninEditingState.signinEditingStateActive = false - - if bottomContentConstraint?.constant == 0 { - return - } - - adjustViewForKeyboard(false) - UIView.animate(withDuration: keyboardInfo.animationDuration, - delay: 0, - options: .beginFromCurrentState, - animations: { - self.view.layoutIfNeeded() - }, - completion: nil) - } - - /// Retrieves the keyboard frame and the animation duration from a keyboard - /// notificaiton. - /// - /// - Parameter notification: the NSNotification object from a keyboard notification. - /// - /// - Returns: An tupile optional containing the `keyboardFrame` and the `animationDuration`, or nil. - /// - func keyboardFrameAndDurationFromNotification(_ notification: Foundation.Notification) -> (keyboardFrame: CGRect, animationDuration: Double)? { - - guard let userInfo = notification.userInfo, - let frame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, - let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue - else { - return nil - } - return (keyboardFrame: frame, animationDuration: duration) - } - - func heightDeltaFromKeyboardFrame(_ keyboardFrame: CGRect) -> CGFloat { - // If an external keyboard is connected, the ending keyboard frame's maxY - // will exceed the height of the view controller's view. - // In these cases, just adjust the height by the amount of the keyboard visible. - if keyboardFrame.maxY > UIScreen.main.bounds.size.height { - return view.frame.height - keyboardFrame.minY - } - - // If the safe area has a bottom height, subtract that. - let bottomAdjust: CGFloat = view.safeAreaInsets.bottom - return keyboardFrame.height - bottomAdjust - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift deleted file mode 100644 index a212588773dd..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkAuthViewController.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit -import WordPressShared - -/// Handles the final step in the magic link auth process. At this point all the -/// necessary auth work should be done. We just need to create a WPAccount and to -/// sync account info and blog details. -/// The expectation is this controller will be momentarily visible when the app -/// is resumed/launched via the appropriate custom scheme, and quickly dismiss. -/// -class NUXLinkAuthViewController: LoginViewController { - @IBOutlet weak var statusLabel: UILabel? - - enum Flow { - case signup - case login - } - - /// Displays the specified text in the status label. - /// - /// - Parameter message: The text to display in the label. - /// - override func configureStatusLabel(_ message: String) { - statusLabel?.text = message - } - - func syncAndContinue(authToken: String, flow: Flow, isJetpackConnect: Bool) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackConnect, multifactor: false, siteURL: "https://wordpress.com") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - syncWPComAndPresentEpilogue(credentials: credentials) { - self.tracker.track(step: .success) - - switch flow { - case .signup: - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - WordPressAuthenticator.track(.createdAccount, properties: ["source": "email"]) - WordPressAuthenticator.track(.signupMagicLinkSucceeded) - case .login: - WordPressAuthenticator.track(.loginMagicLinkSucceeded) - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift deleted file mode 100644 index c91719e1ee5e..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXLinkMailViewController.swift +++ /dev/null @@ -1,128 +0,0 @@ -import UIKit -import WordPressShared - -/// Step two in the auth link flow. This VC prompts the user to open their email -/// app to look for the emailed authentication link. -/// -class NUXLinkMailViewController: LoginViewController { - @IBOutlet private weak var imageView: UIImageView! - @IBOutlet var label: UILabel? - @IBOutlet var openMailButton: NUXButton? - @IBOutlet var usePasswordButton: UIButton? - var emailMagicLinkSource: EmailMagicLinkSource? - override var sourceTag: WordPressSupportSourceTag { - get { - if let emailMagicLinkSource, - emailMagicLinkSource == .signup { - return .wpComSignupMagicLink - } - return .loginMagicLink - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - imageView.image = WordPressAuthenticator.shared.displayImages.magicLink - - let email = loginFields.username - if !email.isValidEmail() { - assert(email.isValidEmail(), "The value of loginFields.username was not a valid email address.") - } - - emailMagicLinkSource = loginFields.meta.emailMagicLinkSource - assert(emailMagicLinkSource != nil, "Must have an email link source.") - - styleUsePasswordButton() - localizeControls() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - } - - // MARK: - Configuration - - private func styleUsePasswordButton() { - guard let usePasswordButton else { - return - } - WPStyleGuide.configureTextButton(usePasswordButton) - } - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - - let openMailButtonTitle = NSLocalizedString("Open Mail", comment: "Title of a button. The text should be capitalized. Clicking opens the mail app in the user's iOS device.") - openMailButton?.setTitle(openMailButtonTitle, for: .normal) - openMailButton?.setTitle(openMailButtonTitle, for: .highlighted) - openMailButton?.accessibilityIdentifier = "Open Mail Button" - - let usePasswordTitle = NSLocalizedString("Enter your password instead.", comment: "Title of a button on the magic link screen.") - usePasswordButton?.setTitle(usePasswordTitle, for: .normal) - usePasswordButton?.setTitle(usePasswordTitle, for: .highlighted) - usePasswordButton?.titleLabel?.numberOfLines = 0 - usePasswordButton?.accessibilityIdentifier = "Use Password" - - guard let emailMagicLinkSource else { - return - } - - usePasswordButton?.isHidden = emailMagicLinkSource == .signup - - label?.text = NSLocalizedString("Check your email on this device, and tap the link in the email you received from WordPress.com.\n\nNot seeing the email? Check your Spam or Junk Mail folder.", comment: "Instructional text on how to open the email containing a magic link.") - - label?.textColor = WordPressAuthenticator.shared.style.instructionColor - } - - // MARK: - Dynamic type - override func didChangePreferredContentSize() { - label?.font = WPStyleGuide.fontForTextStyle(.headline) - } - - // MARK: - Actions - - @IBAction func handleOpenMailTapped(_ sender: UIButton) { - defer { - if let emailMagicLinkSource { - switch emailMagicLinkSource { - case .login: - WordPressAuthenticator.track(.loginMagicLinkOpenEmailClientViewed) - case .signup: - WordPressAuthenticator.track(.signupMagicLinkOpenEmailClientViewed) - } - } - } - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { - WordPressAuthenticator.track(.loginMagicLinkExited) - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginWPComViewController from NUXLinkMailViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -extension NUXLinkMailViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift deleted file mode 100644 index 7be8ebcd5bff..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXNavigationController.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -/// Simple subclass of UINavigationController to facilitate a customized -/// appearance as part of the sign in flow. -/// -public class NUXNavigationController: RotationAwareNavigationViewController { -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift deleted file mode 100644 index e7ccf579431e..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXTableViewController.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit - -// MARK: - NUXTableViewController -/// Base class to use for NUX view controllers that are also a table view controller -/// Note: shares most of its code with NUXViewController. -open class NUXTableViewController: UITableViewController, NUXViewControllerBase, UIViewControllerTransitioningDelegate { - // MARK: NUXViewControllerBase properties - /// these properties comply with NUXViewControllerBase and are duplicated with NUXViewController - public var helpButton = UIButton(type: .custom) - public var dismissBlock: ((_ cancelled: Bool) -> Void)? - public var loginFields = LoginFields() - open var sourceTag: WordPressSupportSourceTag { - get { - return .generalLogin - } - } - - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - // MARK: - Private - private var notificationObservers: [NSObjectProtocol] = [] - - override open func viewDidLoad() { - super.viewDidLoad() - setupHelpButtonIfNeeded() - setupCancelButtonIfNeeded() - } - - public func shouldShowCancelButton() -> Bool { - return shouldShowCancelButtonBase() - } - - // MARK: - Notification Observers - - public func addNotificationObserver(_ observer: NSObjectProtocol) { - notificationObservers.append(observer) - } - - deinit { - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } - notificationObservers.removeAll() - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift deleted file mode 100644 index 8758f7e04b83..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXViewController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import WordPressUI -import UIKit - -// MARK: - NUXViewController -/// Base class to use for NUX view controllers that aren't a table view -/// Note: shares most of its code with NUXTableViewController. Look to make -/// most changes in either the base protocol NUXViewControllerBase or further subclasses like LoginViewController -open class NUXViewController: UIViewController, NUXViewControllerBase, UIViewControllerTransitioningDelegate { - // MARK: NUXViewControllerBase properties - /// these properties comply with NUXViewControllerBase and are duplicated with NUXTableViewController - public var helpButton = UIButton(type: .custom) - public var dismissBlock: ((_ cancelled: Bool) -> Void)? - public var loginFields = LoginFields() - open var sourceTag: WordPressSupportSourceTag { - get { - return .generalLogin - } - } - - // MARK: - Private - private var notificationObservers: [NSObjectProtocol] = [] - - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - override open func viewDidLoad() { - super.viewDidLoad() - setupHelpButtonIfNeeded() - setupCancelButtonIfNeeded() - setupBackgroundTapGestureRecognizer() - } - - // properties specific to NUXViewController - @IBOutlet var submitButton: NUXButton? - @IBOutlet var errorLabel: UILabel? - - func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - submitButton?.isEnabled = enableSubmit(animating: animating) - } - - /// Localize the "Continue" button. - /// - func localizePrimaryButton() { - let primaryTitle = WordPressAuthenticator.shared.displayStrings.continueButtonTitle - submitButton?.setTitle(primaryTitle, for: .normal) - submitButton?.setTitle(primaryTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Continue Button" - } - - open func enableSubmit(animating: Bool) -> Bool { - return !animating - } - - public func shouldShowCancelButton() -> Bool { - return shouldShowCancelButtonBase() - } - - // MARK: - Notification Observers - - public func addNotificationObserver(_ observer: NSObjectProtocol) { - notificationObservers.append(observer) - } - - deinit { - for observer in notificationObservers { - NotificationCenter.default.removeObserver(observer) - } - notificationObservers.removeAll() - } -} - -extension NUXViewController { - // Required so that any FancyAlertViewControllers presented within the NUX - // use the correct dimmed backing view. - open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - if presented is FancyAlertViewController || - presented is LoginPrologueSignupMethodViewController || - presented is LoginPrologueLoginMethodViewController { - return FancyAlertPresentationController(presentedViewController: presented, presenting: presenting) - } - - return nil - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift b/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift deleted file mode 100644 index bf5b54360302..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/NUXViewControllerBase.swift +++ /dev/null @@ -1,223 +0,0 @@ -import Gridicons -import WordPressUI - -private enum Constants { - static let helpButtonInsets = UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 5.0) - // Button Item: Custom view wrapping the Help UIbutton - static let helpButtonItemMarginSpace = CGFloat(-8) - static let helpButtonItemMinimumSize = CGSize(width: 44.0, height: 44.0) - - static let notificationIndicatorCenterOffset = CGPoint(x: 5, y: 12) - static var notificationIndicatorSize = CGSize(width: 10, height: 10) -} - -/// base protocol for NUX view controllers -public protocol NUXViewControllerBase { - var sourceTag: WordPressSupportSourceTag { get } - var helpButton: UIButton { get } - var loginFields: LoginFields { get } - var dismissBlock: ((_ cancelled: Bool) -> Void)? { get } - - /// Checks if the signin vc modal should show a back button. The back button - /// visible when there is more than one child vc presented, and there is not - /// a case where a `SigninChildViewController.backButtonEnabled` in the stack - /// returns false. - /// - /// - Returns: True if the back button should be visible. False otherwise. - /// - func shouldShowCancelButton() -> Bool - func setupCancelButtonIfNeeded() - - /// Notification observers that can be tied to the lifecycle of the entities implementing the protocol - func addNotificationObserver(_ observer: NSObjectProtocol) -} - -/// extension for NUXViewControllerBase where the base class is UIViewController (and thus also NUXTableViewController) -extension NUXViewControllerBase where Self: UIViewController, Self: UIViewControllerTransitioningDelegate { - - /// Indicates if the Help Button should be displayed, or not. - /// - var shouldDisplayHelpButton: Bool { - return WordPressAuthenticator.shared.delegate?.supportActionEnabled ?? false - } - - /// Indicates if the Cancel button should be displayed, or not. - /// - func shouldShowCancelButtonBase() -> Bool { - return isCancellable() && navigationController?.viewControllers.first == self - } - - /// Sets up the cancel button for the navbar if its needed. - /// The cancel button is only shown when its appropriate to dismiss the modal view controller. - /// - public func setupCancelButtonIfNeeded() { - if !shouldShowCancelButton() { - return - } - - let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) - cancelButton.on { [weak self] (_: UIBarButtonItem) in - self?.handleCancelButtonTapped() - } - navigationItem.leftBarButtonItem = cancelButton - } - - /// Returns true whenever the current ViewController can be dismissed. - /// - func isCancellable() -> Bool { - return WordPressAuthenticator.shared.delegate?.dismissActionEnabled ?? true - } - - /// Displays a login error in an attractive dialog - /// - func displayError(_ error: Error, sourceTag: WordPressSupportSourceTag) { - let presentingController = navigationController ?? self - let controller = FancyAlertViewController.alertForError(error, loginFields: loginFields, sourceTag: sourceTag) - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = self - presentingController.present(controller, animated: true, completion: nil) - } - - /// Displays a login error message in an attractive dialog - /// - public func displayErrorAlert(_ message: String, sourceTag: WordPressSupportSourceTag, onDismiss: (() -> ())? = nil) { - let presentingController = navigationController ?? self - let controller = FancyAlertViewController.alertForGenericErrorMessageWithHelpButton(message, loginFields: loginFields, sourceTag: sourceTag, onDismiss: onDismiss) - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = self - presentingController.present(controller, animated: true, completion: nil) - } - - /// It is assumed that NUX view controllers are always presented modally. - /// - func dismiss() { - dismiss(cancelled: false) - } - - /// It is assumed that NUX view controllers are always presented modally. - /// This method dismisses the view controller - /// - /// - Parameters: - /// - cancelled: Should be passed true only when dismissed by a tap on the cancel button. - /// - fileprivate func dismiss(cancelled: Bool) { - dismissBlock?(cancelled) - self.dismiss(animated: true, completion: nil) - } - - // MARK: - Actions - - func handleBackgroundTapGesture() { - view.endEditing(true) - } - - func setupBackgroundTapGestureRecognizer() { - let tgr = UITapGestureRecognizer() - tgr.on { [weak self] _ in - self?.handleBackgroundTapGesture() - } - view.addGestureRecognizer(tgr) - } - - func handleCancelButtonTapped() { - dismiss(cancelled: true) - NotificationCenter.default.post(name: .wordpressLoginCancelled, object: nil) - } - - // Handle the help button being tapped - // - func handleHelpButtonTapped(_ sender: AnyObject) { - AuthenticatorAnalyticsTracker.shared.track(click: .showHelp) - - displaySupportViewController(from: sourceTag) - } - - /// Add/remove the nav bar app logo. - /// - func setupNavBarIcon(showIcon: Bool = true) { - showIcon ? addAppLogoToNavController() : removeAppLogoFromNavController() - } - - /// Adds the app logo to the nav controller - /// - public func addAppLogoToNavController() { - let image = WordPressAuthenticator.shared.style.navBarImage - let imageView = UIImageView(image: image.imageWithTintColor(UIColor.white)) - navigationItem.titleView = imageView - } - - /// Removes the app logo from the nav controller - /// - public func removeAppLogoFromNavController() { - navigationItem.titleView = nil - } - - /// Whenever the WordPressAuthenticator Delegate returns true, when `shouldDisplayHelpButton` is queried, we'll proceed - /// and attach the Help Button to the navigationController. - /// - func setupHelpButtonIfNeeded() { - guard shouldDisplayHelpButton else { - return - } - - addHelpButtonToNavController() - } - - // MARK: - Helpers - - /// Adds the Help Button to the nav controller - /// - private func addHelpButtonToNavController() { - let barButtonView = createBarButtonView() - addHelpButton(to: barButtonView) - addRightBarButtonItem(with: barButtonView) - } - - private func addRightBarButtonItem(with customView: UIView) { - let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) - spacer.width = Constants.helpButtonItemMarginSpace - - let barButton = UIBarButtonItem(customView: customView) - navigationItem.rightBarButtonItems = [spacer, barButton] - } - - private func createBarButtonView() -> UIView { - let customView = UIView(frame: .zero) - customView.translatesAutoresizingMaskIntoConstraints = false - customView.heightAnchor.constraint(equalToConstant: Constants.helpButtonItemMinimumSize.height).isActive = true - customView.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.helpButtonItemMinimumSize.width).isActive = true - - return customView - } - - private func addHelpButton(to superView: UIView) { - helpButton.setTitle(NSLocalizedString("Help", comment: "Help button"), for: .normal) - helpButton.setTitleColor(.label, for: []) - helpButton.accessibilityIdentifier = "authenticator-help-button" - - helpButton.on(.touchUpInside) { [weak self] control in - self?.handleHelpButtonTapped(control) - } - - superView.addSubview(helpButton) - helpButton.translatesAutoresizingMaskIntoConstraints = false - - helpButton.leadingAnchor.constraint(equalTo: superView.leadingAnchor, constant: Constants.helpButtonInsets.left).isActive = true - helpButton.trailingAnchor.constraint(equalTo: superView.trailingAnchor, constant: -Constants.helpButtonInsets.right).isActive = true - helpButton.topAnchor.constraint(equalTo: superView.topAnchor).isActive = true - helpButton.bottomAnchor.constraint(equalTo: superView.bottomAnchor).isActive = true - } - - // MARK: - UIViewControllerTransitioningDelegate - - /// Displays the support vc. - /// - func displaySupportViewController(from source: WordPressSupportSourceTag) { - guard let navigationController else { - fatalError() - } - - let state = AuthenticatorAnalyticsTracker.shared.state - WordPressAuthenticator.shared.delegate?.presentSupport(from: navigationController, sourceTag: source, lastStep: state.lastStep, lastFlow: state.lastFlow) - } -} diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h b/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h deleted file mode 100644 index f747f6035aad..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.h +++ /dev/null @@ -1,8 +0,0 @@ -#import - -@interface WPNUXMainButton : UIButton - -- (void)showActivityIndicator:(BOOL)show; -- (void)setColor:(UIColor *)color; - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m b/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m deleted file mode 100644 index 070f4a93bdf5..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPNUXMainButton.m +++ /dev/null @@ -1,81 +0,0 @@ -#import "WPNUXMainButton.h" - -@import WordPressShared; - -@implementation WPNUXMainButton { - UIActivityIndicatorView *activityIndicator; -} - -- (id)initWithFrame:(CGRect)frame -{ - self = [super initWithFrame:frame]; - if (self) { - [self configureButton]; - } - return self; -} - -- (id)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - [self configureButton]; - } - return self; -} - -- (void)layoutSubviews -{ - - [super layoutSubviews]; - if ([activityIndicator isAnimating]) { - - // hide the title label when the activity indicator is visible - self.titleLabel.frame = CGRectZero; - activityIndicator.frame = CGRectMake((self.frame.size.width - activityIndicator.frame.size.width) / 2.0, (self.frame.size.height - activityIndicator.frame.size.height) / 2.0, activityIndicator.frame.size.width, activityIndicator.frame.size.height); - } -} - -- (void)configureButton -{ - [self setTitle:NSLocalizedString(@"Log In", nil) forState:UIControlStateNormal]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.9] forState:UIControlStateNormal]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4] forState:UIControlStateDisabled]; - [self setTitleColor:[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:0.4] forState:UIControlStateHighlighted]; - self.titleLabel.font = [WPFontManager systemRegularFontOfSize:18.0]; - [self setColor:[UIColor colorWithRed:0/255.0f green:116/255.0f blue:162/255.0f alpha:1.0f]]; - - activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; - activityIndicator.hidesWhenStopped = YES; - [self addSubview:activityIndicator]; -} - -- (void)showActivityIndicator:(BOOL)show -{ - if (show) { - [activityIndicator startAnimating]; - } else { - [activityIndicator stopAnimating]; - } - [self setNeedsLayout]; -} - -- (void)setColor:(UIColor *)color -{ - CGRect fillRect = CGRectMake(0, 0, 11.0, 40.0); - UIEdgeInsets capInsets = UIEdgeInsetsMake(4, 4, 4, 4); - UIImage *mainImage; - - UIGraphicsBeginImageContextWithOptions(fillRect.size, NO, [[UIScreen mainScreen] scale]); - CGContextRef context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, color.CGColor); - CGContextAddPath(context, [UIBezierPath bezierPathWithRoundedRect:fillRect cornerRadius:3.0].CGPath); - CGContextClip(context); - CGContextFillRect(context, fillRect); - mainImage = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - [self setBackgroundImage:[mainImage resizableImageWithCapInsets:capInsets] forState:UIControlStateNormal]; -} - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h b/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h deleted file mode 100644 index c70d572c4b56..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.h +++ /dev/null @@ -1,43 +0,0 @@ -#import - -IB_DESIGNABLE -@interface WPWalkthroughTextField : UITextField - -@property (nonatomic) IBInspectable BOOL showTopLineSeparator; -@property (nonatomic) IBInspectable BOOL showSecureTextEntryToggle; -@property (nonatomic) IBInspectable UIImage *leftViewImage; -@property (nonatomic) IBInspectable UIColor *secureTextEntryImageColor; - -/// Width for the left view. Set to 0 to use the given frame in the view. -/// Default is: 30 -/// -@property (nonatomic) CGFloat leadingViewWidth; - -/// Width for the right view. Set to 0 to use the given frame in the view. -/// Default is: 40 -/// -@property (nonatomic) CGFloat trailingViewWidth; - -/// Insets around the text area. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets textInsets; - -/// Insets around the leading (left) view. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets leadingViewInsets; - -/// Insets around the trailing (right) view. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets trailingViewInsets; - -/// Insets around the whole content of the textfield. -/// This value is mirrored in Right-to-Left layout -/// -@property (nonatomic) UIEdgeInsets contentInsets; - -- (instancetype)initWithLeftViewImage:(UIImage *)image; - -@end diff --git a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m b/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m deleted file mode 100644 index 5a1d2afa039a..000000000000 --- a/Sources/WordPressAuthenticator/Features/NUX/WPWalkthroughTextField.m +++ /dev/null @@ -1,297 +0,0 @@ -#import "WPWalkthroughTextField.h" - -@import WordPressShared; - -NSInteger const LeftImageSpacing = 8; - -@import Gridicons; - -@interface WPWalkthroughTextField () -@property (nonatomic, strong) UIButton *secureTextEntryToggle; -@property (nonatomic, strong) UIImage *secureTextEntryImageVisible; -@property (nonatomic, strong) UIImage *secureTextEntryImageHidden; -@end - -@implementation WPWalkthroughTextField - -- (instancetype)init -{ - self = [super init]; - if (self) { - [self commonInit]; - } - return self; -} - -- (instancetype)initWithLeftViewImage:(UIImage *)image -{ - self = [self init]; - if (self) { - self.leftViewImage = image; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder -{ - self = [super initWithCoder:aDecoder]; - if (self) { - [self commonInit]; - } - return self; -} - -- (void)setLeftViewImage:(UIImage *)leftViewImage -{ - if (leftViewImage) { - _leftViewImage = leftViewImage; - UIImageView *imageView = [[UIImageView alloc] initWithImage:leftViewImage]; - if (self.leadingViewWidth > 0) { - imageView.frame = [self frameForLeadingView]; - imageView.contentMode = [self isLayoutLeftToRight] ? UIViewContentModeLeft : UIViewContentModeRight; - } else { - [imageView sizeToFit]; - } - self.leftView = imageView; - self.leftViewMode = UITextFieldViewModeAlways; - } else { - self.leftView = nil; - } -} - --(void)setRightView:(UIView *)rightView -{ - if (self.trailingViewWidth > 0) { - rightView.frame = [self frameForTrailingView]; - rightView.contentMode = [self isLayoutLeftToRight] ? UIViewContentModeRight : UIViewContentModeLeft; - if ([rightView isKindOfClass:[UIButton class]]) { - UIButton *button = (UIButton *)rightView; - if ([self isLayoutLeftToRight]) { - [button setContentHorizontalAlignment:UIControlContentHorizontalAlignmentRight]; - } else { - [button setContentHorizontalAlignment:UIControlContentHorizontalAlignmentLeft]; - } - } - } - [super setRightView:rightView]; -} - -- (void)setShowSecureTextEntryToggle:(BOOL)showSecureTextEntryToggle -{ - _showSecureTextEntryToggle = showSecureTextEntryToggle; - [self configureSecureTextEntryToggle]; -} - -- (void)commonInit -{ - self.leadingViewWidth = 30.f; - self.trailingViewWidth = 40.f; - - self.layer.cornerRadius = 0.0; - self.clipsToBounds = YES; - self.showTopLineSeparator = NO; - self.showSecureTextEntryToggle = NO; - - // Apply styles to the placeholder if one was set in IB. - if (self.placeholder) { - // colors here are overridden in LoginTextField - NSDictionary *attributes = @{ - NSForegroundColorAttributeName : WPStyleGuide.greyLighten10, - NSFontAttributeName : self.font, - }; - self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder attributes:attributes]; - } - - self.leadingViewInsets = UIEdgeInsetsMake(0, 0, 0, LeftImageSpacing); -} - -- (void)awakeFromNib { - [super awakeFromNib]; - [self configureSecureTextEntryToggle]; -} - -- (void)configureSecureTextEntryToggle { - if (self.showSecureTextEntryToggle == NO) { - return; - } - self.secureTextEntryImageVisible = [UIImage gridiconOfType:GridiconTypeVisible]; - self.secureTextEntryImageHidden = [UIImage gridiconOfType:GridiconTypeNotVisible]; - - self.secureTextEntryToggle = [UIButton buttonWithType:UIButtonTypeCustom]; - self.secureTextEntryToggle.clipsToBounds = true; - - // Tint color changes set in LoginTextField. - - [self.secureTextEntryToggle addTarget:self action:@selector(secureTextEntryToggleAction:) forControlEvents:UIControlEventTouchUpInside]; - - [self updateSecureTextEntryToggleImage]; - [self updateSecureTextEntryForAccessibility]; - - self.rightView = self.secureTextEntryToggle; - self.rightViewMode = UITextFieldViewModeAlways; -} - -- (CGSize)intrinsicContentSize -{ - return CGSizeMake(0.0, 44.0); -} - -- (void)drawRect:(CGRect)rect -{ - // Draw top border - if (!self.showTopLineSeparator) { - return; - } - - CGContextRef context = UIGraphicsGetCurrentContext(); - - UIBezierPath *path = [UIBezierPath bezierPath]; - CGFloat emptySpace = self.contentInsets.left; - if ([self isLayoutLeftToRight]) { - [path moveToPoint:CGPointMake(CGRectGetMinX(rect) + emptySpace, CGRectGetMinY(rect))]; - [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect))]; - } else { - [path moveToPoint:CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect))]; - [path addLineToPoint:CGPointMake(CGRectGetMaxX(rect) - emptySpace, CGRectGetMinY(rect))]; - } - - [path setLineWidth:[[UIScreen mainScreen] scale] / 2.0]; - CGContextAddPath(context, path.CGPath); - CGContextSetStrokeColorWithColor(context, [UIColor colorWithWhite:0.87 alpha:1.0].CGColor); - CGContextStrokePath(context); -} - - -/// Returns the drawing rectangle for the text field’s text. -/// -- (CGRect)textRectForBounds:(CGRect)bounds -{ - CGRect rect = [super textRectForBounds:bounds]; - return [self textAreaRectForProposedRect:rect]; -} - -/// Returns the rectangle in which editable text can be displayed. -/// -- (CGRect)editingRectForBounds:(CGRect)bounds -{ - CGRect rect = [super editingRectForBounds:bounds]; - return [self textAreaRectForProposedRect:rect]; -} - -/// Returns the drawing rectangle of the receiver’s left overlay view. -/// This value is always the view seen at the left side, independently of the layout direction. -/// -- (CGRect)leftViewRectForBounds:(CGRect)bounds -{ - CGRect rect = [super leftViewRectForBounds:bounds]; - if ([self isLayoutLeftToRight]) { - rect.origin.x += self.leadingViewInsets.left + self.contentInsets.left; - } else { - rect.origin.x += self.trailingViewInsets.right + self.contentInsets.right; - } - return rect; -} - -/// Returns the drawing location of the receiver’s right overlay view. -/// This value is always the view seen at the right side, independently of the layout direction. -/// -- (CGRect)rightViewRectForBounds:(CGRect)bounds -{ - CGRect rect = [super rightViewRectForBounds:bounds]; - if ([self isLayoutLeftToRight]) { - rect.origin.x -= self.trailingViewInsets.right + self.contentInsets.right; - } else { - rect.origin.x -= self.leadingViewInsets.left + self.contentInsets.left; - } - return rect; -} - -#pragma mark - Helpers - -- (BOOL)isLayoutLeftToRight -{ - return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute] == UIUserInterfaceLayoutDirectionLeftToRight; -} - -/// Returns the rectangle in which both editable text and the placeholder can be displayed. -/// -- (CGRect)textAreaRectForProposedRect:(CGRect)rect -{ - rect.size.width -= self.textInsets.left + self.textInsets.right; - if ([self isLayoutLeftToRight]) { - rect.origin.x += self.textInsets.left + self.leadingViewInsets.right; - rect.size.width -= self.leadingViewInsets.right + self.contentInsets.right; - if (self.leftView == nil) { - rect.origin.x += self.contentInsets.left; - rect.size.width -= self.contentInsets.right; - } - } else { - rect.origin.x += self.textInsets.right + self.trailingViewInsets.left; - rect.size.width -= self.leadingViewInsets.right + self.trailingViewInsets.left; - if (self.rightView == nil) { - rect.origin.x += self.contentInsets.right; - rect.size.width -= self.contentInsets.left; - } - if (self.leftView == nil) { - rect.size.width -= self.contentInsets.left; - } - } - return rect; -} - -- (CGRect)frameForTrailingView -{ - return CGRectMake(0, 0, self.trailingViewWidth, CGRectGetHeight(self.bounds)); -} - -- (CGRect)frameForLeadingView -{ - return CGRectMake(0, 0, self.leadingViewWidth, CGRectGetHeight(self.bounds)); -} - -#pragma mark - Secure Text Entry - -- (void)setSecureTextEntry:(BOOL)secureTextEntry -{ - // This is a fix for a bug where the text field reverts to a system - // serif font if you disable secure text entry while it contains text. - self.font = nil; - self.font = [WPFontManager systemRegularFontOfSize:16.0]; - - [super setSecureTextEntry:secureTextEntry]; - [self updateSecureTextEntryToggleImage]; - [self updateSecureTextEntryForAccessibility]; -} - -- (void)secureTextEntryToggleAction:(id)sender -{ - self.secureTextEntry = !self.secureTextEntry; - - // Save and re-apply the current selection range to save the cursor position - UITextRange *currentTextRange = self.selectedTextRange; - [self becomeFirstResponder]; - [self setSelectedTextRange:currentTextRange]; -} - -- (void)updateSecureTextEntryToggleImage -{ - UIImage *image = self.isSecureTextEntry ? self.secureTextEntryImageHidden : self.secureTextEntryImageVisible; - [self.secureTextEntryToggle setImage:image forState:UIControlStateNormal]; - [self.secureTextEntryToggle sizeToFit]; - self.secureTextEntryToggle.tintColor = self.secureTextEntryImageColor; -} - -- (void)updateSecureTextEntryForAccessibility -{ - self.secureTextEntryToggle.accessibilityLabel = NSLocalizedString(@"Show password", @"Accessibility label for the “Show password“ button in the login page's password field."); - - NSString *accessibilityValue; - if (self.isSecureTextEntry) { - accessibilityValue = NSLocalizedString(@"Hidden", "Accessibility value if login page's password field is hiding the password (i.e. with asterisks)."); - } else { - accessibilityValue = NSLocalizedString(@"Shown", "Accessibility value if login page's password field is displaying the password."); - } - self.secureTextEntryToggle.accessibilityValue = accessibilityValue; -} - -@end diff --git a/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift b/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift deleted file mode 100644 index bf5af489264e..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/AppleAuthenticator.swift +++ /dev/null @@ -1,291 +0,0 @@ -import Foundation -import AuthenticationServices -import WordPressKit -import SVProgressHUD -import WordPressShared - -@objc protocol AppleAuthenticatorDelegate { - func showWPComLogin(loginFields: LoginFields) - func showApple2FA(loginFields: LoginFields) - func authFailedWithError(message: String) -} - -class AppleAuthenticator: NSObject { - - // MARK: - Properties - - static var sharedInstance = AppleAuthenticator() - private var showFromViewController: UIViewController? - private let loginFields = LoginFields() - weak var delegate: AppleAuthenticatorDelegate? - let signupService: SocialUserCreating - - init(signupService: SocialUserCreating = SignupService()) { - self.signupService = signupService - super.init() - } - - static let credentialRevokedNotification = ASAuthorizationAppleIDProvider.credentialRevokedNotification - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - private var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - } - - // MARK: - Start Authentication - - func showFrom(viewController: UIViewController) { - loginFields.meta.socialService = SocialServiceName.apple - showFromViewController = viewController - requestAuthorization() - } -} - -// MARK: - Tracking - -private extension AppleAuthenticator { - func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any] = [:]) { - var trackProperties = properties - trackProperties["source"] = "apple" - WordPressAuthenticator.track(event, properties: trackProperties) - } -} - -// MARK: - Authentication Flow - -private extension AppleAuthenticator { - - func requestAuthorization() { - let provider = ASAuthorizationAppleIDProvider() - let request = provider.createRequest() - request.requestedScopes = [.fullName, .email] - - let controller = ASAuthorizationController(authorizationRequests: [request]) - controller.delegate = self - - controller.presentationContextProvider = self - controller.performRequests() - } - - /// Creates a WordPress.com account with the Apple ID - /// - func createWordPressComUser(appleCredentials: ASAuthorizationAppleIDCredential) { - guard let identityToken = appleCredentials.identityToken, - let token = String(data: identityToken, encoding: .utf8) else { - WPLogError("Apple Authenticator: invalid Apple credentials.") - return - } - - createWordPressComUser( - appleUserId: appleCredentials.user, - email: appleCredentials.email ?? "", - name: fullName(from: appleCredentials.fullName), - token: token - ) - } - - func signupSuccessful(with credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - track(.createdAccount) - - tracker.track(step: .success) { - track(.signupSocialSuccess) - } - - showSignupEpilogue(for: credentials) - } - - func loginSuccessful(with credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .success) { - track(.loginSocialSuccess) - } - - showLoginEpilogue(for: credentials) - } - - func showLoginEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController = showFromViewController?.navigationController else { - fatalError() - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource) {} - } - - func signupFailed(with error: Error) { - WPLogError("Apple Authenticator: Signup failed. error: \(error.localizedDescription)") - - let errorMessage = error.localizedDescription - - tracker.track(failure: errorMessage) { - let properties = ["error": errorMessage] - track(.signupSocialFailure, properties: properties) - } - - delegate?.authFailedWithError(message: error.localizedDescription) - } - - func logInInstead() { - tracker.set(flow: .loginWithApple) - tracker.track(step: .start) { - track(.signupSocialToLogin) - track(.loginSocialSuccess) - } - - delegate?.showWPComLogin(loginFields: loginFields) - } - - func show2FA() { - if tracker.shouldUseLegacyTracker() { - track(.signupSocialToLogin) - } - - delegate?.showApple2FA(loginFields: loginFields) - } - - // MARK: - Helpers - - func fullName(from components: PersonNameComponents?) -> String { - guard let name = components else { - return "" - } - return PersonNameComponentsFormatter().string(from: name) - } - - func updateLoginFields(email: String, fullName: String, token: String) { - updateLoginEmail(email) - loginFields.meta.socialServiceIDToken = token - loginFields.meta.socialUser = SocialUser(email: email, fullName: fullName, service: .apple) - } - - func updateLoginEmail(_ email: String) { - loginFields.emailAddress = email - loginFields.username = email - } -} - -extension AppleAuthenticator: ASAuthorizationControllerDelegate { - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - switch authorization.credential { - case let credentials as ASAuthorizationAppleIDCredential: - createWordPressComUser(appleCredentials: credentials) - default: - break - } - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - - // Don't show error if user cancelled authentication. - if let authorizationError = error as? ASAuthorizationError, - authorizationError.code == .canceled { - return - } - - WPLogError("Apple Authenticator: didCompleteWithError: \(error.localizedDescription)") - let message = NSLocalizedString("Apple authentication failed.\nPlease make sure you are signed in to iCloud with an Apple ID that uses two-factor authentication.", comment: "Message shown when Apple authentication fails.") - delegate?.authFailedWithError(message: message) - } -} - -extension AppleAuthenticator: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return showFromViewController?.view.window ?? UIWindow() - } -} - -extension AppleAuthenticator { - func getAppleIDCredentialState(for userID: String, - completion: @escaping (ASAuthorizationAppleIDProvider.CredentialState, Error?) -> Void) { - ASAuthorizationAppleIDProvider().getCredentialState(forUserID: userID, completion: completion) - } -} - -// This needs to be internal, at this point in time, to allow testing. -// -// Notice that none of this code was previously tested. A small encapsulation breach like this is -// worth the testability we gain from it. -extension AppleAuthenticator { - - func showSignupEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController = showFromViewController?.navigationController else { - fatalError() - } - - authenticationDelegate.presentSignupEpilogue( - in: navigationController, - for: credentials, - socialUser: loginFields.meta.socialUser - ) - } - - func createWordPressComUser(appleUserId: String, email: String, name: String, token: String) { - tracker.set(flow: .signupWithApple) - tracker.track(step: .start) { - track(.createAccountInitiated) - } - - SVProgressHUD.show( - withStatus: NSLocalizedString( - "Continuing with Apple", - comment: "Shown while logging in with Apple and the app waits for the site creation process to complete." - ) - ) - - updateLoginFields(email: email, fullName: name, token: token) - - signupService.createWPComUserWithApple( - token: token, - email: email, - fullName: name, - success: { [weak self] accountCreated, existingNonSocialAccount, existing2faAccount, wpcomUsername, wpcomToken in - SVProgressHUD.dismiss() - - // Notify host app of successful Apple authentication - self?.authenticationDelegate.userAuthenticatedWithAppleUserID(appleUserId) - - guard !existingNonSocialAccount else { - self?.tracker.set(flow: .loginWithApple) - - if existing2faAccount { - self?.show2FA() - return - } - - self?.updateLoginEmail(wpcomUsername) - self?.logInInstead() - return - } - - let wpcom = WordPressComCredentials(authToken: wpcomToken, isJetpackLogin: false, multifactor: false, siteURL: self?.loginFields.siteAddress ?? "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - if accountCreated { - self?.authenticationDelegate.createdWordPressComAccount(username: wpcomUsername, authToken: wpcomToken) - self?.signupSuccessful(with: credentials) - } else { - self?.authenticationDelegate.sync(credentials: credentials) { - self?.loginSuccessful(with: credentials) - } - } - }, - failure: { [weak self] error in - SVProgressHUD.dismiss() - self?.signupFailed(with: error) - } - ) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard b/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard deleted file mode 100644 index 505e6e7f0b0d..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/EmailMagicLink.storyboard +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard b/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard deleted file mode 100644 index 0b039b2f923b..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/Login.storyboard +++ /dev/null @@ -1,1481 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift deleted file mode 100644 index f8cb96047ae6..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/Login2FAViewController.swift +++ /dev/null @@ -1,326 +0,0 @@ -import UIKit -import SVProgressHUD -import WordPressShared -import WordPressKit - -/// Provides a form and functionality for entering a two factor auth code and -/// signing into WordPress.com -/// -class Login2FAViewController: LoginViewController, NUXKeyboardResponder, UITextFieldDelegate { - - @IBOutlet weak var verificationCodeField: LoginTextField! - @IBOutlet weak var sendCodeButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - - private var pasteboardChangeCountBeforeBackground: Int = 0 - override var sourceTag: WordPressSupportSourceTag { - get { - return .login2FA - } - } - - private enum Constants { - static let headsUpDismissDelay = TimeInterval(1) - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - configureTextFields() - configureSubmitButton(animating: false) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configureViewForEditingIfNeeded() - styleSendCodeButton() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(applicationBecameInactive), name: UIApplication.willResignActiveNotification, object: nil) - nc.addObserver(self, selector: #selector(applicationBecameActive), name: UIApplication.didBecomeActiveNotification, object: nil) - - WordPressAuthenticator.track(.loginTwoFactorFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - NotificationCenter.default.removeObserver(self) - - // Multifactor codes are time sensitive, so clear the stored code if the - // user dismisses the view. They'll need to reentered it upon return. - loginFields.multifactorCode = "" - verificationCodeField.text = "" - } - - // MARK: Dynamic Type - override func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - styleSendCodeButton() - } - - private func styleSendCodeButton() { - sendCodeButton.titleLabel?.adjustsFontForContentSizeCategory = true - sendCodeButton.titleLabel?.adjustsFontSizeToFitWidth = true - WPStyleGuide.configureTextButton(sendCodeButton) - } - - // MARK: Configuration Methods - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = NSLocalizedString("Almost there! Please enter the verification code from your authenticator app.", comment: "Instructions for users with two-factor authentication enabled.") - - verificationCodeField.placeholder = NSLocalizedString("Verification code", comment: "two factor code placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - sendCodeButton.setTitle(NSLocalizedString("Text me a code instead", comment: "Button title"), - for: .normal) - sendCodeButton.titleLabel?.numberOfLines = 0 - } - - /// configures the text fields - /// - @objc func configureTextFields() { - verificationCodeField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - verificationCodeField.textContentType = .oneTimeCode - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - let isNumeric = loginFields.multifactorCode.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - let isValidLength = SocialLogin2FANonceInfo.TwoFactorTypeLengths(rawValue: loginFields.multifactorCode.count) != nil - - submitButton?.isEnabled = ( - !animating && - isNumeric && - isValidLength - ) - } - - /// Configure the view's loading state. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - verificationCodeField.enablesReturnKeyAutomatically = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - verificationCodeField.becomeFirstResponder() - } - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - if let nonce = loginFields.nonceInfo { - loginWithNonce(info: nonce) - return - } - validateFormAndLogin() - } - - private func loginWithNonce(info nonceInfo: SocialLogin2FANonceInfo) { - let code = loginFields.multifactorCode - let (authType, nonce) = nonceInfo.authTypeAndNonce(for: code) - loginFacade.loginToWordPressDotCom(withUser: loginFields.nonceUserID, authType: authType, twoStepCode: code, twoStepNonce: nonce) - } - - func finishedLogin(withNonceAuthToken authToken: String) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: true, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - syncWPComAndPresentEpilogue(credentials: credentials) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - WordPressAuthenticator.track(.signedIn) - - var properties = [AnyHashable: Any]() - if let service = loginFields.meta.socialService?.rawValue { - properties["source"] = service - } - - WordPressAuthenticator.track(.loginSocialSuccess, properties: properties) - } - - /// Only allow digits in the 2FA text field - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString: String) -> Bool { - guard let fieldText = textField.text as NSString? else { - return true - } - let resultString = fieldText.replacingCharacters(in: range, with: replacementString) - - switch isValidCode(code: resultString) { - case .valid(let cleanedCode): - displayError(message: "") - - // because the string was stripped of whitespace, we can't return true and we update the textfield ourselves - textField.text = cleanedCode - handleTextFieldDidChange(textField) - case .invalid(nonNumbers: true): - displayError(message: NSLocalizedString("A verification code will only contain numbers.", comment: "Shown when a user types a non-number into the two factor field.")) - default: - if let pasteString = UIPasteboard.general.string, pasteString == replacementString { - displayError(message: NSLocalizedString("That doesn't appear to be a valid verification code.", comment: "Shown when a user pastes a code into the two factor field that contains letters or is the wrong length")) - } - } - - return false - } - - private enum CodeValidation { - case invalid(nonNumbers: Bool) - case valid(String) - } - - private func isValidCode(code: String) -> CodeValidation { - let codeStripped = code.components(separatedBy: .whitespacesAndNewlines).joined() - let allowedCharacters = CharacterSet.decimalDigits - let resultCharacterSet = CharacterSet(charactersIn: codeStripped) - let isOnlyNumbers = allowedCharacters.isSuperset(of: resultCharacterSet) - let isShortEnough = codeStripped.count <= SocialLogin2FANonceInfo.TwoFactorTypeLengths.backup.rawValue - - if isOnlyNumbers && isShortEnough { - return .valid(codeStripped) - } else if isOnlyNumbers { - return .invalid(nonNumbers: false) - } else { - return .invalid(nonNumbers: true) - } - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - validateForm() - return false - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.multifactorCode = verificationCodeField.nonNilTrimmedText() - configureSubmitButton(animating: false) - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - validateForm() - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - tracker.track(click: .submit) - - validateForm() - } - - @IBAction func handleSendVerificationButtonTapped(_ sender: UIButton) { - self.tracker.track(click: .sendCodeWithText) - - let message = NSLocalizedString("SMS Sent", comment: "One Time Code has been sent via SMS") - SVProgressHUD.showSuccess(withStatus: message) - SVProgressHUD.dismiss(withDelay: Constants.headsUpDismissDelay) - - if let _ = loginFields.nonceInfo { - // social login - loginFacade.requestSocial2FACode(with: loginFields) - } else { - loginFacade.requestOneTimeCode(with: loginFields) - } - } - - // MARK: - Handle application state changes. - - @objc func applicationBecameInactive() { - pasteboardChangeCountBeforeBackground = UIPasteboard.general.changeCount - } - - @objc func applicationBecameActive() { - let emptyField = verificationCodeField.text?.isEmpty ?? true - guard emptyField, - pasteboardChangeCountBeforeBackground != UIPasteboard.general.changeCount else { - return - } - - UIPasteboard.general.detectAuthenticatorCode { [weak self] result in - switch result { - case .success(let authenticatorCode): - self?.handle(code: authenticatorCode) - case .failure: - break - } - } - } - - private func handle(code: String) { - switch isValidCode(code: code) { - case .valid(let cleanedCode): - displayError(message: "") - verificationCodeField.text = cleanedCode - handleTextFieldDidChange(verificationCodeField) - default: - break - } - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension Login2FAViewController { - - override func displayRemoteError(_ error: Error) { - displayError(message: "") - - configureViewLoading(false) - let bad2FAMessage = NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.") - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidOneTimePassword { - // Invalid verification code. - displayError(message: bad2FAMessage) - } else if case let .endpointError(authenticationFailure) = (error as? WordPressComOAuthError), authenticationFailure.kind == .invalidTwoStepCode { - // Invalid 2FA during social login - if let newNonce = authenticationFailure.newNonce { - loginFields.nonceInfo?.updateNonce(with: newNonce) - } - displayError(message: bad2FAMessage) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift deleted file mode 100644 index d9dd2782191e..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginEmailViewController.swift +++ /dev/null @@ -1,631 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -/// This is the first screen following the log in prologue screen if the user chooses to log in. -/// -open class LoginEmailViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var emailTextField: WPWalkthroughTextField! - @IBOutlet open var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet open var verticalCenterConstraint: NSLayoutConstraint? - @IBOutlet var inputStack: UIStackView? - @IBOutlet var alternativeLoginLabel: UILabel? - @IBOutlet var hiddenPasswordField: WPWalkthroughTextField? - - var googleLoginButton: UIButton? - var selfHostedLoginButton: UIButton? - - // This signup button isn't for the main flow; it's only shown during Jetpack installation - var wpcomSignupButton: UIButton? - - override open var sourceTag: WordPressSupportSourceTag { - get { - return .loginEmail - } - } - - var didFindSafariSharedCredentials = false - var didRequestSafariSharedCredentials = false - open var offerSignupOption = false - private let showLoginOptions = WordPressAuthenticator.shared.configuration.showLoginOptions - - private struct Constants { - static let alternativeLogInAnimationDuration: TimeInterval = 0.33 - static let keyboardThreshold: CGFloat = 100.0 - } - - // MARK: Lifecycle Methods - - override open func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - - alternativeLoginLabel?.isHidden = showLoginOptions - if !showLoginOptions { - addGoogleButton() - } - - addSelfHostedLogInButton() - addSignupButton() - } - - override open func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - configureEmailField() - configureAlternativeLabel() - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // The old create account vc hides the nav bar, so make sure its always visible. - navigationController?.setNavigationBarHidden(false, animated: false) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureEmailField() - configureAlternativeLabel() - configureSubmitButton() - configureViewForEditingIfNeeded() - configureForWPComOnlyIfNeeded() - } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginEmailFormViewed) - - hiddenPasswordField?.text = nil - errorToPresent = nil - } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - /// Displays the self-hosted login form. - /// - override func loginToSelfHostedSite() { - guard let vc = LoginSiteAddressViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSiteAddressViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Setup and Configuration - - /// Hides the self-hosted login option. - /// - func configureForWPComOnlyIfNeeded() { - wpcomSignupButton?.isHidden = !offerSignupOption - selfHostedLoginButton?.isHidden = loginFields.restrictToWPCom - } - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - func localizeControls() { - if loginFields.meta.jetpackLogin { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.jetpackLoginInstructions - } else { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.emailLoginInstructions - } - emailTextField.placeholder = NSLocalizedString("Email address", comment: "Placeholder for a textfield. The user may enter their email address.") - emailTextField.accessibilityIdentifier = "Login Email Address" - - alternativeLoginLabel?.text = NSLocalizedString("Alternatively:", comment: "String displayed before offering alternative login methods") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Login Email Next Button" - } - - /// Add the log in with Google button to the view - /// - func addGoogleButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.googleLoginButton() - stackView.addArrangedSubview(button) - button.addTarget(self, action: #selector(googleTapped), for: .touchUpInside) - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - googleLoginButton = button - } - - /// Add the log in with site address button to the view - /// - func addSelfHostedLogInButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.selfHostedLoginButton() - stackView.addArrangedSubview(button) - button.addTarget(self, action: #selector(handleSelfHostedButtonTapped), for: .touchUpInside) - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - selfHostedLoginButton = button - } - - /// Add the sign up button - /// - /// Note: This is only used during Jetpack setup, not the normal flows - /// - func addSignupButton() { - guard let instructionLabel, - let stackView = inputStack else { - return - } - - let button = WPStyleGuide.wpcomSignupButton() - stackView.addArrangedSubview(button) - - // Tapping the Sign up text link in "Don't have an account? _Sign up_" - // will present the 3 button view for signing up. - button.on(.touchUpInside) { [weak self] _ in - guard let vc = LoginPrologueSignupMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueSignupMethodViewController") - return - } - - guard let self else { return } - - vc.loginFields = self.loginFields - vc.dismissBlock = self.dismissBlock - vc.transitioningDelegate = self - vc.modalPresentationStyle = .custom - - // Don't forget to handle the button taps! - vc.emailTapped = { [weak self] in - guard let toVC = SignupEmailViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate from LoginEmailViewController to SignupEmailViewController") - return - } - - self?.navigationController?.pushViewController(toVC, animated: true) - } - - vc.googleTapped = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .signupWithGoogle) - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - self.presentGoogleSignupView() - return - } - - self.presentUnifiedGoogleView() - } - - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - self.navigationController?.present(vc, animated: true, completion: nil) - } - - stackView.addConstraints([ - button.leadingAnchor.constraint(equalTo: instructionLabel.leadingAnchor), - button.trailingAnchor.constraint(equalTo: instructionLabel.trailingAnchor) - ]) - - wpcomSignupButton = button - } - - /// Configures the email text field, updating its text based on what's stored - /// in `loginFields`. - /// - func configureEmailField() { - emailTextField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - emailTextField.text = loginFields.username - emailTextField.adjustsFontForContentSizeCategory = true - hiddenPasswordField?.isAccessibilityElement = false - } - - private func configureAlternativeLabel() { - alternativeLoginLabel?.font = WPStyleGuide.fontForTextStyle(.subheadline) - alternativeLoginLabel?.textColor = WordPressAuthenticator.shared.style.subheadlineColor - } - - /// Configures whether appearance of the submit button. - /// - func configureSubmitButton() { - submitButton?.isEnabled = canSubmit() - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override open func configureViewLoading(_ loading: Bool) { - emailTextField.isEnabled = !loading - googleLoginButton?.isEnabled = !loading - - submitButton?.isEnabled = !loading - submitButton?.showActivityIndicator(loading) - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - emailTextField.becomeFirstResponder() - } - } - - // MARK: - Instance Methods - - /// Makes the call to retrieve Safari shared credentials if they exist. - /// - func fetchSharedWebCredentialsIfAvailable() { - didRequestSafariSharedCredentials = true - SafariCredentialsService.requestSharedWebCredentials { [weak self] found, username, password in - self?.handleFetchedWebCredentials(found, username: username, password: password) - } - } - - /// Handles Safari shared credentials if any where found. - /// - /// - Parameters: - /// - found: True if credentails were found. - /// - username: The selected username or nil. - /// - password: The selected password or nil. - /// - func handleFetchedWebCredentials(_ found: Bool, username: String?, password: String?) { - didFindSafariSharedCredentials = found - - guard let username, let password else { - return - } - - // Update the login fields - loginFields.username = username - loginFields.password = password - - // Persist credentials as autofilled credentials so we can update them later if needed. - loginFields.setStoredCredentials(usernameHash: username.hash, passwordHash: password.hash) - - loginWithUsernamePassword(immediately: true) - - WordPressAuthenticator.track(.loginAutoFillCredentialsFilled) - } - - /// Displays the wpcom sign in form, optionally telling it to immedately make - /// the call to authenticate with the available credentials. - /// - /// - Parameters: - /// - immediately: True if the newly loaded controller should immedately attempt - /// to authenticate the user with the available credentails. Default is `false`. - /// - func loginWithUsernamePassword(immediately: Bool = false) { - if immediately { - validateFormAndLogin() - } else { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginWPComViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - } - - /// Proceeds along the "magic link" sign-in flow, showing a form that lets - /// the user request a magic link. - /// - func requestLink() { - guard let vc = LoginLinkRequestViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginLinkRequestViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. Empties loginFields.meta.socialService as - /// social signin does not require form validation. - /// - func validateForm() { - loginFields.meta.socialService = nil - displayError(message: "") - - guard EmailFormatValidator.validate(string: loginFields.username) else { - assertionFailure("Form should not be submitted unless there is a valid looking email entered.") - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.isPasswordlessAccount(username: loginFields.username, - success: { [weak self] passwordless in - self?.configureViewLoading(false) - self?.loginFields.meta.passwordless = passwordless - self?.requestLink() - }, - failure: { [weak self] error in - WordPressAuthenticator.track(.loginFailed, error: error) - WPLogError(error.localizedDescription) - guard let strongSelf = self else { - return - } - strongSelf.configureViewLoading(false) - - let userInfo = (error as NSError).userInfo - let errorCode = userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String - if errorCode == "unknown_user" { - let msg = NSLocalizedString("This email address is not registered on WordPress.com.", - comment: "An error message informing the user the email address they entered did not match a WordPress.com account.") - strongSelf.displayError(message: msg) - } else if errorCode == "email_login_not_allowed" { - // If we get this error, we know we have a WordPress.com user but their - // email address is flagged as suspicious. They need to login via their - // username instead. - strongSelf.showSelfHostedUsernamePasswordAndError(error) - } else { - strongSelf.displayError(error, sourceTag: strongSelf.sourceTag) - } - }) - } - - /// When password autofill has entered a password on this screen, attempt to login immediately - func attemptAutofillLogin() { - loginFields.password = hiddenPasswordField?.text ?? "" - loginFields.meta.socialService = nil - displayError(message: "") - - loginWithUsernamePassword(immediately: true) - } - - /// Configures loginFields to log into wordpress.com and - /// navigates to the selfhosted username/password form. - /// Displays the specified error message when the new - /// view controller appears. - /// - @objc func showSelfHostedUsernamePasswordAndError(_ error: Error) { - loginFields.siteAddress = "https://wordpress.com" - errorToPresent = error - - guard let vc = LoginSelfHostedViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSelfHostedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return EmailFormatValidator.validate(string: loginFields.username) - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - if canSubmit() { - validateForm() - } - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleSelfHostedButtonTapped(_ sender: UIButton) { - loginToSelfHostedSite() - } - - private func appleTapped() { - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - @objc func googleTapped() { - self.tracker.track(click: .loginWithGoogle) - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - GoogleAuthenticator.sharedInstance.loginDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .login) - return - } - - presentUnifiedGoogleView() - } - - // Shows the VC that handles both Google login & signup. - private func presentUnifiedGoogleView() { - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles only Google signup. - private func presentGoogleSignupView() { - guard let toVC = SignupGoogleViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupGoogleViewController from LoginEmailVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - switch sender { - case emailTextField: - loginFields.username = emailTextField.nonNilTrimmedText() - configureSubmitButton() - case hiddenPasswordField: - attemptAutofillLogin() - default: - break - } - } - - @IBAction func handleTextFieldEditingDidBegin(_ sender: UITextField) { - if !didRequestSafariSharedCredentials { - fetchSharedWebCredentialsIfAvailable() - } - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - - adjustAlternativeLogInElementsVisibility(true) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - - adjustAlternativeLogInElementsVisibility(false) - } - - func adjustAlternativeLogInElementsVisibility(_ visible: Bool) { - let errorLength = errorLabel?.text?.count ?? 0 - let keyboardTallEnough = SigninEditingState.signinLastKeyboardHeightDelta > Constants.keyboardThreshold - let keyboardVisible = visible && keyboardTallEnough - - let baseAlpha: CGFloat = errorLength > 0 ? 0.0 : 1.0 - let newAlpha: CGFloat = keyboardVisible ? baseAlpha : 1.0 - - UIView.animate(withDuration: Constants.alternativeLogInAnimationDuration) { [weak self] in - self?.alternativeLoginLabel?.alpha = newAlpha - self?.googleLoginButton?.alpha = newAlpha - if let selfHostedLoginButton = self?.selfHostedLoginButton, - selfHostedLoginButton.isEnabled { - selfHostedLoginButton.alpha = newAlpha - } - } - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension LoginEmailViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginWPComViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - } -} - -// MARK: - GoogleAuthenticatorLoginDelegate - -extension LoginEmailViewController: GoogleAuthenticatorLoginDelegate { - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from Google Login to LoginWPComViewController (password VC)") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) { - self.loginFields = loginFields - configureViewLoading(false) - - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift deleted file mode 100644 index aa1c8042a0d4..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginLinkRequestViewController.swift +++ /dev/null @@ -1,188 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import GravatarUI - -/// Step one in the auth link flow. This VC displays a form to request a "magic" -/// authentication link be emailed to the user. Allows the user to signin via -/// email instead of their password. -/// -class LoginLinkRequestViewController: LoginViewController { - @IBOutlet var gravatarView: UIImageView? - @IBOutlet var label: UILabel? - @IBOutlet var sendLinkButton: NUXButton? - @IBOutlet var usePasswordButton: UIButton? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginMagicLink - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - configureUsePasswordButton() - - let email = loginFields.username - if !email.isValidEmail() { - assert(email.isValidEmail(), "The value of loginFields.username was not a valid email address.") - } - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let email = loginFields.username - if email.isValidEmail() { - Task { - try await downloadAvatar() - } - } else { - gravatarView?.isHidden = true - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - WordPressAuthenticator.track(.loginMagicLinkRequestFormViewed) - } - - private func downloadAvatar(forceRefresh: Bool = false) async throws { - let email = loginFields.username - try await gravatarView?.setGravatarImage(with: email, rating: .x, forceRefresh: forceRefresh) - } - - @objc private func refreshAvatar(_ notification: Foundation.Notification) { - guard notification.userInfoHasEmail(loginFields.username) else { return } - Task { - try await downloadAvatar(forceRefresh: true) - } - } - - // MARK: - Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - let format = NSLocalizedString("We'll email you a magic link that'll log you in instantly, no password needed. Hunt and peck no more!", comment: "Instructional text for the magic link login flow.") - label?.text = NSString(format: format as NSString, loginFields.username) as String - label?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - label?.textColor = WordPressAuthenticator.shared.style.instructionColor - label?.adjustsFontForContentSizeCategory = true - - let sendLinkButtonTitle = NSLocalizedString("Send Link", comment: "Title of a button. The text should be uppercase. Clicking requests a hyperlink be emailed ot the user.") - sendLinkButton?.setTitle(sendLinkButtonTitle, for: .normal) - sendLinkButton?.setTitle(sendLinkButtonTitle, for: .highlighted) - sendLinkButton?.accessibilityIdentifier = "Send Link Button" - - let usePasswordTitle = NSLocalizedString("Enter your password instead.", comment: "Title of a button. ") - usePasswordButton?.setTitle(usePasswordTitle, for: .normal) - usePasswordButton?.setTitle(usePasswordTitle, for: .highlighted) - usePasswordButton?.titleLabel?.numberOfLines = 0 - usePasswordButton?.titleLabel?.textAlignment = .center - usePasswordButton?.accessibilityIdentifier = "Use Password" - } - - @objc func configureLoading(_ animating: Bool) { - sendLinkButton?.showActivityIndicator(animating) - - sendLinkButton?.isEnabled = !animating - } - - private func configureUsePasswordButton() { - guard let usePasswordButton else { - return - } - WPStyleGuide.configureTextButton(usePasswordButton) - } - - // MARK: - Instance Methods - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - @objc func requestAuthenticationLink() { - - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - // This is a bit of paranoia as in practice it should never happen. - // However, let's make sure we give the user some useful feedback just in case. - WPLogError("Attempted to request authentication link, but the email address did not appear valid.") - let alert = UIAlertController(title: NSLocalizedString("Can Not Request Link", comment: "Title of an alert letting the user know"), message: NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", comment: "An error message."), preferredStyle: .alert) - alert.addActionWithTitle(NSLocalizedString("Need help?", comment: "Takes the user to get help"), style: .cancel, handler: { _ in WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) }) - alert.addActionWithTitle(NSLocalizedString("OK", comment: "Dismisses the alert"), style: .default, handler: nil) - self.present(alert, animated: true, completion: nil) - return - } - - configureLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureLoading(false) - }, failure: { [weak self] (error: Error) in - WordPressAuthenticator.track(.loginMagicLinkFailed) - WordPressAuthenticator.track(.loginFailed, error: error) - guard let strongSelf = self else { - return - } - strongSelf.displayError(error, sourceTag: strongSelf.sourceTag) - strongSelf.configureLoading(false) - }) - } - - // MARK: - Dynamic type - override func didChangePreferredContentSize() { - label?.font = WPStyleGuide.fontForTextStyle(.headline) - } - - // MARK: - Actions - - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginLinkRequestViewController to LoginWPComViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - WordPressAuthenticator.track(.loginMagicLinkExited) - } - - @IBAction func handleSendLinkTapped(_ sender: UIButton) { - requestAuthenticationLink() - } - - @objc func didRequestAuthenticationLink() { - WordPressAuthenticator.track(.loginMagicLinkRequested) - - guard let vc = NUXLinkMailViewController.instantiate(from: .emailMagicLink) else { - WPLogError("Failed to navigate to NUXLinkMailViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } -} - -extension LoginLinkRequestViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift deleted file mode 100644 index ae36a3ecd556..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginNavigationController.swift +++ /dev/null @@ -1,39 +0,0 @@ -import UIKit -import WordPressShared - -public class LoginNavigationController: RotationAwareNavigationViewController { - - public override var preferredStatusBarStyle: UIStatusBarStyle { - return topViewController?.preferredStatusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - public override func pushViewController(_ viewController: UIViewController, animated: Bool) { - // By default, the back button label uses the previous view's title. - // To override that, reset the label when pushing a new view controller. - self.viewControllers.last?.navigationItem.backButtonDisplayMode = .minimal - - super.pushViewController(viewController, animated: animated) - } -} - -// MARK: - RotationAwareNavigationViewController -// -public class RotationAwareNavigationViewController: UINavigationController { - - /// Should Autorotate: Respect the top child's orientation prefs. - /// - override open var shouldAutorotate: Bool { - return topViewController?.shouldAutorotate ?? super.shouldAutorotate - } - - /// Supported Orientations: Respect the top child's orientation prefs. - /// - override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { - if let supportedOrientations = topViewController?.supportedInterfaceOrientations { - return supportedOrientations - } - - let isPad = UIDevice.current.userInterfaceIdiom == .pad - return isPad ? .all : .allButUpsideDown - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift deleted file mode 100644 index 9be4d611c1fc..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueLoginMethodViewController.swift +++ /dev/null @@ -1,132 +0,0 @@ -import WordPressUI -import WordPressShared - -/// This class houses the "3 button view": -/// Continue with WordPress.com, Continue with Google, Continue with Apple -/// and a text link - Or log in by entering your site address. -/// -class LoginPrologueLoginMethodViewController: NUXViewController { - /// Buttons at bottom of screen - private var buttonViewController: NUXButtonViewController? - - /// Gesture recognizer for taps on the dialog if no buttons are present - fileprivate var dismissGestureRecognizer: UITapGestureRecognizer? - - open var emailTapped: (() -> Void)? - open var googleTapped: (() -> Void)? - open var selfHostedTapped: (() -> Void)? - open var appleTapped: (() -> Void)? - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - /// The big transparent (dismiss) button behind the buttons - @IBOutlet private weak var dismissButton: UIButton! - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureButtonVC() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - - private func configureButtonVC() { - guard let buttonViewController else { - return - } - - let wordpressTitle = NSLocalizedString("Log in or sign up with WordPress.com", comment: "Button title. Tapping begins our normal log in process.") - buttonViewController.setupTopButton(title: wordpressTitle, isPrimary: false, accessibilityIdentifier: "Log in with Email Button") { [weak self] in - - guard let self else { - return - } - - self.tracker.set(flow: .wpCom) - self.dismiss(animated: true) - self.emailTapped?() - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.handleGoogleButtonTapped() - } - - if !LoginFields().restrictToWPCom && selfHostedTapped != nil { - let selfHostedLoginButton = WPStyleGuide.selfHostedLoginButton(alignment: .center) - buttonViewController.stackView?.addArrangedSubview(selfHostedLoginButton) - selfHostedLoginButton.addTarget(self, action: #selector(handleSelfHostedButtonTapped), for: .touchUpInside) - } - - if WordPressAuthenticator.shared.configuration.enableSignInWithApple { - buttonViewController.setupTertiaryButtonFor(socialService: .apple) { [weak self] in - self?.handleAppleButtonTapped() - } - } - - buttonViewController.backgroundColor = WordPressAuthenticator.shared.style.buttonViewBackgroundColor - } - - @IBAction func dismissTapped() { - dismiss(animated: true) - } - - @IBAction func handleSelfHostedButtonTapped(_ sender: UIButton) { - dismiss(animated: true) - - tracker.set(flow: .loginWithSiteAddress) - tracker.track(click: .loginWithSiteAddress) - - selfHostedTapped?() - } - - @objc func handleAppleButtonTapped() { - tracker.set(flow: .loginWithApple) - tracker.track(click: .loginWithApple, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.loginSocialButtonClick, properties: ["source": "apple"]) - }) - - dismiss(animated: true) - appleTapped?() - } - - @objc func handleGoogleButtonTapped() { - tracker.set(flow: .loginWithGoogle) - tracker.track(click: .loginWithGoogle) - - dismiss(animated: true) - googleTapped?() - } - - // MARK: - Accessibility - - private func configureForAccessibility() { - dismissButton.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for the transparent space above the login dialog which acts as a button to dismiss the dialog.") - - // Ensure that the first button (in buttonViewController) is automatically selected by - // VoiceOver instead of the dismiss button. - if buttonViewController?.isViewLoaded == true, let buttonsView = buttonViewController?.view { - view.accessibilityElements = [ - buttonsView, - dismissButton as Any - ] - } - } - - override func accessibilityPerformEscape() -> Bool { - dismiss(animated: true) - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift deleted file mode 100644 index 1e9875e32d82..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginProloguePageViewController.swift +++ /dev/null @@ -1,98 +0,0 @@ -import UIKit -import WordPressShared - -class LoginProloguePageViewController: UIPageViewController { - // This property is a legacy of the previous UX iteration. It ought to be removed, but that's - // out of scope at the time of writing. It's now `private` to prevent using it within the - // library in the meantime - @objc private var pages: [UIViewController] = [] - - fileprivate var pageControl: UIPageControl? - fileprivate var bgAnimation: UIViewPropertyAnimator? - fileprivate struct Constants { - static let pagerPadding: CGFloat = 9.0 - static let pagerHeight: CGFloat = 0.13 - } - - override func viewDidLoad() { - super.viewDidLoad() - dataSource = self - delegate = self - - view.backgroundColor = WordPressAuthenticator.shared.style.prologueBackgroundColor - - addPageControl() - } - - @objc func addPageControl() { - let newControl = UIPageControl() - - newControl.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(newControl) - - newControl.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.pagerPadding).isActive = true - newControl.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - newControl.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - newControl.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: Constants.pagerHeight).isActive = true - - newControl.numberOfPages = pages.count - newControl.addTarget(self, action: #selector(handlePageControlValueChanged(sender:)), for: .valueChanged) - pageControl = newControl - } - - @objc func handlePageControlValueChanged(sender: UIPageControl) { - guard let currentPage = viewControllers?.first, - let currentIndex = pages.firstIndex(of: currentPage) else { - return - } - - let direction: UIPageViewController.NavigationDirection = sender.currentPage > currentIndex ? .forward : .reverse - setViewControllers([pages[sender.currentPage]], direction: direction, animated: true) - WordPressAuthenticator.track(.loginProloguePaged) - } -} - -extension LoginProloguePageViewController: UIPageViewControllerDataSource { - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let index = pages.firstIndex(of: viewController) else { - return nil - } - if index > 0 { - return pages[index - 1] - } - return nil - } - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let index = pages.firstIndex(of: viewController) else { - return nil - } - if index < pages.count - 1 { - return pages[index + 1] - } - return nil - } -} - -extension LoginProloguePageViewController: UIPageViewControllerDelegate { - func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - let toVC = previousViewControllers[0] - guard let index = pages.firstIndex(of: toVC) else { - return - } - if !completed { - pageControl?.currentPage = index - } else { - WordPressAuthenticator.track(.loginProloguePaged) - } - } - - func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { - let toVC = pendingViewControllers[0] - guard let index = pages.firstIndex(of: toVC) else { - return - } - pageControl?.currentPage = index - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift deleted file mode 100644 index 65279d774bb1..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueSignupMethodViewController.swift +++ /dev/null @@ -1,134 +0,0 @@ -import SafariServices -import WordPressUI -import WordPressShared - -class LoginPrologueSignupMethodViewController: NUXViewController { - /// Buttons at bottom of screen - private var buttonViewController: NUXButtonViewController? - - /// Gesture recognizer for taps on the dialog if no buttons are present - fileprivate var dismissGestureRecognizer: UITapGestureRecognizer? - - open var emailTapped: (() -> Void)? - open var googleTapped: (() -> Void)? - open var appleTapped: (() -> Void)? - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - /// The big transparent (dismiss) button behind the buttons - @IBOutlet private weak var dismissButton: UIButton! - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func viewDidLoad() { - super.viewDidLoad() - configureButtonVC() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(true, animated: false) - } - - private func configureButtonVC() { - guard let buttonViewController else { - return - } - - let loginTitle = NSLocalizedString("Sign up with Email", comment: "Button title. Tapping begins our normal sign up process.") - buttonViewController.setupTopButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Sign up with Email Button") { [weak self] in - - self?.tracker.set(flow: .wpCom) - - defer { - WordPressAuthenticator.track(.signupEmailButtonTapped) - } - self?.dismiss(animated: true) - self?.emailTapped?() - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.handleGoogleButtonTapped() - } - - let termsButton = WPStyleGuide.termsButton() - termsButton.on(.touchUpInside) { [weak self] _ in - defer { - self?.tracker.track(click: .termsOfService, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupTermsButtonTapped) - }) - } - - let safariViewController = SFSafariViewController(url: WordPressAuthenticator.shared.configuration.wpcomTermsOfServiceURL) - safariViewController.modalPresentationStyle = .pageSheet - self?.present(safariViewController, animated: true, completion: nil) - } - buttonViewController.stackView?.insertArrangedSubview(termsButton, at: 0) - - if WordPressAuthenticator.shared.configuration.enableSignInWithApple { - buttonViewController.setupTertiaryButtonFor(socialService: .apple) { [weak self] in - self?.handleAppleButtonTapped() - } - } - - buttonViewController.backgroundColor = WordPressAuthenticator.shared.style.buttonViewBackgroundColor - } - - @IBAction func dismissTapped() { - trackCancellationAndThenDismiss() - } - - @objc func handleAppleButtonTapped() { - tracker.set(flow: .signupWithApple) - tracker.track(click: .signupWithApple, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupSocialButtonTapped, properties: ["source": "apple"]) - }) - - dismiss(animated: true) - appleTapped?() - } - - @objc func handleGoogleButtonTapped() { - tracker.set(flow: .signupWithGoogle) - tracker.track(click: .signupWithGoogle, ifTrackingNotEnabled: { - WordPressAuthenticator.track(.signupSocialButtonTapped, properties: ["source": "google"]) - }) - - dismiss(animated: true) - googleTapped?() - } - - private func trackCancellationAndThenDismiss() { - WordPressAuthenticator.track(.signupCancelled) - dismiss(animated: true) - } - - // MARK: - Accessibility - - private func configureForAccessibility() { - dismissButton.accessibilityLabel = NSLocalizedString("Dismiss", comment: "Accessibility label for the transparent space above the signup dialog which acts as a button to dismiss the dialog.") - - // Ensure that the first button (in buttonViewController) is automatically selected by - // VoiceOver instead of the dismiss button. - if buttonViewController?.isViewLoaded == true, let buttonsView = buttonViewController?.view { - view.accessibilityElements = [ - buttonsView, - dismissButton as Any - ] - } - } - - override func accessibilityPerformEscape() -> Bool { - trackCancellationAndThenDismiss() - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift deleted file mode 100644 index e25b3a8fe1f8..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginPrologueViewController.swift +++ /dev/null @@ -1,739 +0,0 @@ -import UIKit -import WordPressShared -import WordPressUI -import WordPressKit - -class LoginPrologueViewController: LoginViewController { - - @IBOutlet private weak var topContainerView: UIView! - @IBOutlet private weak var buttonBlurEffectView: UIVisualEffectView! - @IBOutlet private weak var buttonBackgroundView: UIView! - private var buttonViewController: NUXButtonViewController? - private var stackedButtonsViewController: NUXStackedButtonsViewController? - var showCancel = false - var continueWithDotComOverwrite: ((UIViewController) -> Bool)? = nil - var selfHostedSiteLoginOverwrite: ((UIViewController) -> Bool)? = nil - - @IBOutlet private weak var buttonContainerView: UIView! - /// Blur effect on button container view - /// - private var blurEffect: UIBlurEffect.Style { - return .systemChromeMaterial - } - - /// Constraints on the button view container. - /// Used to adjust the button width in unified views. - @IBOutlet private weak var buttonViewLeadingConstraint: NSLayoutConstraint? - @IBOutlet private weak var buttonViewTrailingConstraint: NSLayoutConstraint? - private var defaultButtonViewMargin: CGFloat = 0 - - // Called when login button is tapped - var onLoginButtonTapped: (() -> Void)? - - private let configuration = WordPressAuthenticator.shared.configuration - private let style = WordPressAuthenticator.shared.style - - /// We can't rely on `isMovingToParent` to know if we need to track the `.prologue` step - /// because for the root view in an App, it's always `false`. We're relying this variable - /// instead, since the `.prologue` step only needs to be tracked once. - /// - private var prologueFlowTracked = false - - /// Return`true` to use new `NUXStackedButtonsViewController` instead of `NUXButtonViewController` to create buttons - /// - private var useStackedButtonsViewController: Bool { - configuration.enableWPComLoginOnlyInPrologue || - configuration.enableSiteCreation || - configuration.enableSiteAddressLoginOnlyInPrologue || - configuration.enableSiteCreationGuide - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - if let topContainerChildViewController = style.prologueTopContainerChildViewController() { - topContainerView.subviews.forEach { $0.removeFromSuperview() } - addChild(topContainerChildViewController) - topContainerView.addSubview(topContainerChildViewController.view) - topContainerChildViewController.didMove(toParent: self) - - topContainerChildViewController.view.translatesAutoresizingMaskIntoConstraints = false - topContainerView.pinSubviewToAllEdges(topContainerChildViewController.view) - } - - createButtonViewController() - - defaultButtonViewMargin = buttonViewLeadingConstraint?.constant ?? 0 - if let backgroundImage = WordPressAuthenticator.shared.unifiedStyle?.prologueBackgroundImage { - view.layer.contents = backgroundImage.cgImage - } - } - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - configureButtonVC() - navigationController?.setNavigationBarHidden(true, animated: animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // We've found some instances where the iCloud Keychain login flow was being started - // when the device was idle and the app was logged out and in the background. I couldn't - // find precise reproduction steps for this issue but my guess is that some background - // operation is triggering a call to this method while the app is in the background. - // The proposed solution is based off this StackOverflow reply: - // - // https://stackoverflow.com/questions/30584356/viewdidappear-is-called-when-app-is-started-due-to-significant-location-change - // - guard UIApplication.shared.applicationState != .background else { - return - } - - WordPressAuthenticator.track(.loginPrologueViewed) - - tracker.set(flow: .prologue) - - if !prologueFlowTracked { - tracker.track(step: .prologue) - prologueFlowTracked = true - } else { - tracker.set(step: .prologue) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - self.navigationController?.setNavigationBarHidden(false, animated: animated) - } - - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return UIDevice.isPad() ? .all : .portrait - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - setButtonViewMargins(forWidth: view.frame.width) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setButtonViewMargins(forWidth: size.width) - } - - private func configureButtonVC() { - guard configuration.enableUnifiedAuth else { - buildPrologueButtons() - return - } - - if useStackedButtonsViewController { - buildPrologueButtonsUsingStackedButtonsViewController() - } else { - buildUnifiedPrologueButtons() - } - - if let buttonViewController { - buttonViewController.shadowLayoutGuide = view.safeAreaLayoutGuide - buttonViewController.topButtonStyle = WordPressAuthenticator.shared.style.prologuePrimaryButtonStyle - buttonViewController.bottomButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - buttonViewController.tertiaryButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - } else if let stackedButtonsViewController { - stackedButtonsViewController.shadowLayoutGuide = view.safeAreaLayoutGuide - } - } - - /// Displays the old UI prologue buttons. - /// - private func buildPrologueButtons() { - guard let buttonViewController else { - return - } - - let loginTitle = NSLocalizedString("Log In", comment: "Button title. Tapping takes the user to the login form.") - let createTitle = NSLocalizedString("Sign up for WordPress.com", comment: "Button title. Tapping begins the process of creating a WordPress.com account.") - - buttonViewController.setupTopButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Prologue Log In Button") { [weak self] in - self?.onLoginButtonTapped?() - self?.loginTapped() - } - - if configuration.enableSignUp { - buttonViewController.setupBottomButton(title: createTitle, isPrimary: true, accessibilityIdentifier: "Prologue Signup Button") { [weak self] in - self?.signupTapped() - } - } - - if showCancel { - let cancelTitle = NSLocalizedString("Cancel", comment: "Button title. Tapping it cancels the login flow.") - buttonViewController.setupTertiaryButton(title: cancelTitle, isPrimary: false) { [weak self] in - self?.dismiss(animated: true, completion: nil) - } - } - - buttonViewController.backgroundColor = style.buttonViewBackgroundColor - buttonBlurEffectView.isHidden = true - } - - /// Displays the Unified prologue buttons. - /// - private func buildUnifiedPrologueButtons() { - guard let buttonViewController else { - return - } - - let displayStrings = WordPressAuthenticator.shared.displayStrings - let loginTitle = displayStrings.continueWithWPButtonTitle - let siteAddressTitle = displayStrings.enterYourSiteAddressButtonTitle - - if configuration.continueWithSiteAddressFirst { - buildUnifiedPrologueButtonsWithSiteAddressFirst(buttonViewController, loginTitle: loginTitle, siteAddressTitle: siteAddressTitle) - return - } - - buildDefaultUnifiedPrologueButtons(buttonViewController, loginTitle: loginTitle, siteAddressTitle: siteAddressTitle) - } - - private func buildDefaultUnifiedPrologueButtons(_ buttonViewController: NUXButtonViewController, loginTitle: String, siteAddressTitle: String) { - - setButtonViewMargins(forWidth: view.frame.width) - - buttonViewController.setupTopButton(title: loginTitle, isPrimary: true, configureBodyFontForTitle: true, accessibilityIdentifier: "Prologue Continue Button", onTap: loginTapCallback()) - - if configuration.enableUnifiedAuth { - buttonViewController.setupBottomButton(title: siteAddressTitle, isPrimary: false, configureBodyFontForTitle: true, accessibilityIdentifier: "Prologue Self Hosted Button", onTap: siteAddressTapCallback()) - } - - showCancelIfNeccessary(buttonViewController) - - setButtonViewControllerBackground() - } - - private func buildUnifiedPrologueButtonsWithSiteAddressFirst(_ buttonViewController: NUXButtonViewController, loginTitle: String, siteAddressTitle: String) { - guard configuration.enableUnifiedAuth == true else { - return - } - - setButtonViewMargins(forWidth: view.frame.width) - - buttonViewController.setupTopButton(title: siteAddressTitle, isPrimary: true, accessibilityIdentifier: "Prologue Self Hosted Button", onTap: siteAddressTapCallback()) - - buttonViewController.setupBottomButton(title: loginTitle, isPrimary: false, accessibilityIdentifier: "Prologue Continue Button", onTap: loginTapCallback()) - - showCancelIfNeccessary(buttonViewController) - - setButtonViewControllerBackground() - } - - private func buildPrologueButtonsUsingStackedButtonsViewController() { - guard let stackedButtonsViewController else { - return - } - - let primaryButtonStyle = WordPressAuthenticator.shared.style.prologuePrimaryButtonStyle - let secondaryButtonStyle = WordPressAuthenticator.shared.style.prologueSecondaryButtonStyle - - setButtonViewMargins(forWidth: view.frame.width) - let displayStrings = WordPressAuthenticator.shared.displayStrings - let buttons: [StackedButton] - - let continueWithWPButton: StackedButton? = { - guard !configuration.enableSiteAddressLoginOnlyInPrologue else { - return nil - } - return StackedButton(title: displayStrings.continueWithWPButtonTitle, - isPrimary: true, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Continue Button", - style: primaryButtonStyle, - onTap: loginTapCallback()) - }() - - let enterYourSiteAddressButton: StackedButton? = { - guard !configuration.enableWPComLoginOnlyInPrologue else { - return nil - } - let isPrimary = configuration.enableSiteAddressLoginOnlyInPrologue && !configuration.enableSiteCreation - return StackedButton(title: displayStrings.enterYourSiteAddressButtonTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Self Hosted Button", - style: secondaryButtonStyle, - onTap: siteAddressTapCallback()) - }() - - let createSiteButton: StackedButton? = { - guard configuration.enableSiteCreation else { - return nil - } - let isPrimary = configuration.enableSiteAddressLoginOnlyInPrologue - return StackedButton(title: displayStrings.siteCreationButtonTitle, - isPrimary: isPrimary, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Create Site Button", - style: secondaryButtonStyle, - onTap: simplifiedLoginSiteCreationCallback()) - }() - - let createSiteButtonForBottomStackView: StackedButton? = { - guard let createSiteButton else { - return nil - } - return StackedButton(using: createSiteButton, stackView: .bottom) - }() - - let siteCreationGuideButton: StackedButton? = { - guard configuration.enableSiteCreationGuide else { - return nil - } - return StackedButton(title: displayStrings.siteCreationGuideButtonTitle, - isPrimary: false, - configureBodyFontForTitle: true, - accessibilityIdentifier: "Prologue Site Creation Guide button", - style: NUXButtonStyle.linkButtonStyle, - onTap: siteCreationGuideCallback()) - }() - - let showBothLoginOptions = continueWithWPButton != nil && enterYourSiteAddressButton != nil - buttons = [ - continueWithWPButton, - !showBothLoginOptions ? createSiteButton : nil, - enterYourSiteAddressButton, - showBothLoginOptions ? createSiteButtonForBottomStackView : nil, - siteCreationGuideButton - ].compactMap { $0 } - - let showDivider = configuration.enableWPComLoginOnlyInPrologue == false && - configuration.enableSiteCreation == true && - configuration.enableSiteAddressLoginOnlyInPrologue == false - stackedButtonsViewController.setUpButtons(using: buttons, showDivider: showDivider) - setButtonViewControllerBackground() - } - - private func siteAddressTapCallback() -> NUXButtonViewController.CallBackType { - return { [weak self] in - self?.siteAddressTapped() - } - } - - private func loginTapCallback() -> NUXButtonViewController.CallBackType { - return { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .continueWithWordPressCom) - self.continueWithDotCom() - } - } - - private func simplifiedLoginSiteCreationCallback() -> NUXButtonViewController.CallBackType { - { [weak self] in - guard let self, let navigationController = self.navigationController else { return } - // triggers the delegate to ask the host app to handle site creation - WordPressAuthenticator.shared.delegate?.showSiteCreation(in: navigationController) - } - } - - private func siteCreationGuideCallback() -> NUXButtonViewController.CallBackType { - { [weak self] in - guard let self, let navigationController else { return } - // triggers the delegate to ask the host app to handle site creation guide - WordPressAuthenticator.shared.delegate?.showSiteCreationGuide(in: navigationController) - } - } - - private func showCancelIfNeccessary(_ buttonViewController: NUXButtonViewController) { - if showCancel { - let cancelTitle = NSLocalizedString("Cancel", comment: "Button title. Tapping it cancels the login flow.") - buttonViewController.setupTertiaryButton(title: cancelTitle, isPrimary: false) { [weak self] in - self?.dismiss(animated: true, completion: nil) - } - } - } - - private func setButtonViewControllerBackground() { - // Fallback to setting the button background color to clear so the blur effect blurs the Prologue background color. - let buttonsBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.prologueButtonsBackgroundColor ?? .clear - buttonViewController?.backgroundColor = buttonsBackgroundColor - buttonBackgroundView?.backgroundColor = buttonsBackgroundColor - stackedButtonsViewController?.backgroundColor = buttonsBackgroundColor - - /// If host apps provide a background color for the prologue buttons: - /// 1. Hide the blur effect - /// 2. Set the background color of the view controller to prologueViewBackgroundColor - let prologueViewBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.prologueViewBackgroundColor ?? .clear - - guard prologueViewBackgroundColor.cgColor == buttonsBackgroundColor.cgColor else { - buttonBlurEffectView.effect = UIBlurEffect(style: blurEffect) - return - } - // do not set background color if we've set a background image earlier - if WordPressAuthenticator.shared.unifiedStyle?.prologueBackgroundImage == nil { - view.backgroundColor = prologueViewBackgroundColor - } - // if a blur effect for the buttons was passed, use it; otherwise hide the view. - guard let blurEffect = WordPressAuthenticator.shared.unifiedStyle?.prologueButtonsBlurEffect else { - buttonBlurEffectView.isHidden = true - return - } - buttonBlurEffectView.effect = blurEffect - } - - // MARK: - Actions - - /// Old UI. "Log In" button action. - /// - private func loginTapped() { - tracker.set(source: .default) - - guard let vc = LoginPrologueLoginMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueLoginMethodViewController from LoginPrologueViewController") - return - } - - vc.transitioningDelegate = self - - // Continue with WordPress.com button action - vc.emailTapped = { [weak self] in - guard let self else { - return - } - - self.presentLoginEmailView() - } - - // Continue with Google button action - vc.googleTapped = { [weak self] in - self?.googleTapped() - } - - // Site address text link button action - vc.selfHostedTapped = { [weak self] in - self?.loginToSelfHostedSite() - } - - // Sign In With Apple (SIWA) button action - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - vc.modalPresentationStyle = .custom - navigationController?.present(vc, animated: true, completion: nil) - } - - /// Old UI. "Sign up with WordPress.com" button action. - /// - private func signupTapped() { - tracker.set(source: .default) - - // This stat is part of a funnel that provides critical information. - // Before making ANY modification to this stat please refer to: p4qSXL-35X-p2 - WordPressAuthenticator.track(.signupButtonTapped) - - guard let vc = LoginPrologueSignupMethodViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginPrologueSignupMethodViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.transitioningDelegate = self - vc.modalPresentationStyle = .custom - - vc.emailTapped = { [weak self] in - guard let self else { - return - } - - guard self.configuration.enableUnifiedAuth else { - self.presentSignUpEmailView() - return - } - - self.presentUnifiedSignupView() - } - - vc.googleTapped = { [weak self] in - guard let self else { - return - } - - guard self.configuration.enableUnifiedAuth else { - self.presentGoogleSignupView() - return - } - - self.presentUnifiedGoogleView() - } - - vc.appleTapped = { [weak self] in - self?.appleTapped() - } - - navigationController?.present(vc, animated: true, completion: nil) - } - - private func appleTapped() { - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - private func googleTapped() { - guard configuration.enableUnifiedAuth else { - GoogleAuthenticator.sharedInstance.loginDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .login) - return - } - - presentUnifiedGoogleView() - } - - /// Unified "Continue with WordPress.com" prologue button action. - /// - private func continueWithDotCom() { - if let continueWithDotComOverwrite, continueWithDotComOverwrite(self) { - return - } - - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - vc.source = .wpCom - - navigationController?.pushViewController(vc, animated: true) - } - - /// Unified "Enter your existing site address" prologue button action. - /// - private func siteAddressTapped() { - tracker.track(click: .loginWithSiteAddress) - - if let selfHostedSiteLoginOverwrite, selfHostedSiteLoginOverwrite(self) { - return - } - - loginToSelfHostedSite() - } - - private func presentSignUpEmailView() { - guard let toVC = SignupEmailViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupEmailViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentUnifiedSignupView() { - guard let toVC = UnifiedSignupViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate to UnifiedSignupViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentLoginEmailView() { - guard let toVC = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailVC from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles both Google login & signup. - private func presentUnifiedGoogleView() { - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - // Shows the VC that handles only Google signup. - private func presentGoogleSignupView() { - guard let toVC = SignupGoogleViewController.instantiate(from: .signup) else { - WPLogError("Failed to navigate to SignupGoogleViewController from LoginPrologueVC") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentWPLogin() { - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginPrologueViewController to LoginWPComViewController") - return - } - - vc.loginFields = self.loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - private func presentUnifiedPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate from LoginPrologueViewController to PasswordViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - private func createButtonViewController() { - if useStackedButtonsViewController { - let stackedButtonsViewController = NUXStackedButtonsViewController.instance() - self.stackedButtonsViewController = stackedButtonsViewController - stackedButtonsViewController.move(to: self, into: buttonContainerView) - } else { - let buttonViewController = NUXButtonViewController.instance() - self.buttonViewController = buttonViewController - buttonViewController.move(to: self, into: buttonContainerView) - } - view.bringSubviewToFront(buttonContainerView) - } -} - -// MARK: - LoginFacadeDelegate - -extension LoginPrologueViewController { - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension LoginPrologueViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentWPLogin() - return - } - - presentUnifiedPassword() - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - } -} - -// MARK: - GoogleAuthenticatorLoginDelegate - -extension LoginPrologueViewController: GoogleAuthenticatorLoginDelegate { - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from Google Login to LoginWPComViewController (password VC)") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) { - self.loginFields = loginFields - - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } -} - -// MARK: - Button View Sizing - -private extension LoginPrologueViewController { - - /// Resize the button view based on trait collection. - /// Used only in unified views. - /// - func setButtonViewMargins(forWidth viewWidth: CGFloat) { - - guard configuration.enableUnifiedAuth else { - return - } - - guard traitCollection.horizontalSizeClass == .regular && - traitCollection.verticalSizeClass == .regular else { - buttonViewLeadingConstraint?.constant = defaultButtonViewMargin - buttonViewTrailingConstraint?.constant = defaultButtonViewMargin - return - } - - let marginMultiplier = UIDevice.current.orientation.isLandscape ? - ButtonViewMarginMultipliers.ipadLandscape : - ButtonViewMarginMultipliers.ipadPortrait - - let margin = viewWidth * marginMultiplier - - buttonViewLeadingConstraint?.constant = margin - buttonViewTrailingConstraint?.constant = margin - } - - private enum ButtonViewMarginMultipliers { - static let ipadPortrait: CGFloat = 0.1667 - static let ipadLandscape: CGFloat = 0.25 - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift deleted file mode 100644 index 1ce4851a2c4c..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSelfHostedViewController.swift +++ /dev/null @@ -1,281 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow. Used by WPiOS and NiOS. -/// A valid site address should be acquired before presenting this view controller. -/// -class LoginSelfHostedViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var siteHeaderView: SiteInfoHeaderView! - @IBOutlet var usernameField: WPWalkthroughTextField! - @IBOutlet var passwordField: WPWalkthroughTextField! - @IBOutlet var forgotPasswordButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the username & password (if any) from LoginFields - loginFields.username = "" - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - configureHeader() - localizeControls() - displayLoginMessage("") - configureForAcessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = false - - configureTextFields() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - setupNavBarIcon() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginUsernamePasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - usernameField.placeholder = NSLocalizedString("Username", comment: "Username placeholder") - passwordField.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton.titleLabel?.numberOfLines = 0 - } - - /// Sets up necessary accessibility labels and attributes for the all the UI elements in self. - /// - private func configureForAcessibility() { - usernameField.accessibilityLabel = - NSLocalizedString("Username", comment: "Accessibility label for the username text field in the self-hosted login page.") - passwordField.accessibilityLabel = - NSLocalizedString("Password", comment: "Accessibility label for the password text field in the self-hosted login page.") - - if UIAccessibility.isVoiceOverRunning { - // Remove the placeholder if VoiceOver is running. VoiceOver speaks the label and the - // placeholder together. In this case, both labels and placeholders are the same so it's - // like VoiceOver is reading the same thing twice. - usernameField.placeholder = nil - passwordField.placeholder = nil - } - - forgotPasswordButton.accessibilityTraits = .link - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - usernameField.text = loginFields.username - passwordField.text = loginFields.password - passwordField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - usernameField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - } - - /// Configures the appearance and state of the forgot password button. - /// - @objc func configureForgotPasswordButton() { - forgotPasswordButton.isEnabled = enableSubmit(animating: false) - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField.isEnabled = !loading - passwordField.isEnabled = !loading - - configureSubmitButton(animating: loading) - configureForgotPasswordButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField.becomeFirstResponder() - } - } - - /// Configure the site header. - /// - @objc func configureHeader() { - if let siteInfo = loginFields.meta.siteInfo { - configureBlogDetailHeaderView(siteInfo: siteInfo) - } else { - configureSiteAddressHeader() - } - } - - /// Configure the site header to show the BlogDetailsHeaderView - /// - func configureBlogDetailHeaderView(siteInfo: WordPressComSiteInfo) { - let siteAddress = sanitizedSiteAddress(siteAddress: siteInfo.url) - siteHeaderView.title = siteInfo.name - siteHeaderView.subtitle = NSURL.idnDecodedURL(siteAddress) - siteHeaderView.subtitleIsHidden = false - - siteHeaderView.blavatarBorderIsHidden = false - siteHeaderView.downloadBlavatar(at: siteInfo.icon) - } - - /// Configure the site header to show the site address label. - /// - @objc func configureSiteAddressHeader() { - siteHeaderView.title = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - siteHeaderView.subtitleIsHidden = true - - siteHeaderView.blavatarBorderIsHidden = true - siteHeaderView.blavatarImage = .linkFieldImage - } - - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.username = usernameField.nonNilTrimmedText() - loginFields.password = passwordField.nonNilTrimmedText() - - configureForgotPasswordButton() - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension LoginSelfHostedViewController { - - func finishedLogin(withUsername username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - displayLoginMessage("") - - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - - let wporg = WordPressOrgCredentials(username: username, password: password, xmlrpc: xmlrpc, options: options) - let credentials = AuthenticatorCredentials(wporg: wporg) - delegate.sync(credentials: credentials) { [weak self] in - - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) - self?.showLoginEpilogue(for: credentials) - } - } - - func displayLoginMessage(_ message: String) { - configureForgotPasswordButton() - } - - override func displayRemoteError(_ error: Error) { - displayLoginMessage("") - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} - -extension LoginSelfHostedViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - passwordField.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift deleted file mode 100644 index 5936635c6db3..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSiteAddressViewController.swift +++ /dev/null @@ -1,345 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit -import WordPressUI - -class LoginSiteAddressViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet weak var siteURLField: WPWalkthroughTextField! - @IBOutlet var siteAddressHelpButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginSiteAddress - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the site url and site info (if any) from LoginFields - loginFields.siteAddress = "" - loginFields.meta.siteInfo = nil - } - } - - // MARK: - URL Validation - - private lazy var urlErrorDebouncer = Debouncer(delay: 2) { [weak self] in - let errorMessage = NSLocalizedString("Please enter a complete website address, like example.com.", comment: "Error message shown when a URL is invalid.") - - self?.displayError(message: errorMessage) - } - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - localizeControls() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = false - - configureTextFields() - configureSiteAddressHelpButton() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - navigationController?.setNavigationBarHidden(false, animated: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - WordPressAuthenticator.track(.loginURLFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.siteLoginInstructions - - siteURLField.placeholder = NSLocalizedString("example.com", comment: "Site Address placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Site Address Next Button" - - let siteAddressHelpTitle = NSLocalizedString("Need help finding your site address?", comment: "A button title.") - siteAddressHelpButton.setTitle(siteAddressHelpTitle, for: .normal) - siteAddressHelpButton.setTitle(siteAddressHelpTitle, for: .highlighted) - siteAddressHelpButton.titleLabel?.numberOfLines = 0 - } - - /// Sets up necessary accessibility labels and attributes for the all the UI elements in self. - /// - private func configureForAccessibility() { - siteURLField.accessibilityLabel = - NSLocalizedString("Site address", comment: "Accessibility label of the site address field shown when adding a self-hosted site.") - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - siteURLField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - siteURLField.text = loginFields.siteAddress - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && canSubmit() - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - siteURLField.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - siteURLField.becomeFirstResponder() - } - } - - private func configureSiteAddressHelpButton() { - WPStyleGuide.configureTextButton(siteAddressHelpButton) - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - view.endEditing(true) - displayError(message: "") - - // We need to to this here because before this point we need the URL to be pre-validated - // exactly as the user inputs it, and after this point we need the URL to be the base site URL. - // This isn't really great, but it's the only sane solution I could come up with given the current - // architecture of this pod. - loginFields.siteAddress = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - - configureViewLoading(true) - - let facade = WordPressXMLRPCAPIFacade() - facade.guessXMLRPCURL(forSite: loginFields.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 { - self?.loginFields.meta.xmlrpcURL = url as NSURL - } - // Let's try to grab site info in preparation for the next screen. - self?.fetchSiteInfo() - }, failure: { [weak self] error in - guard let error, let self else { - return - } - WPLogError(error.localizedDescription) - WordPressAuthenticator.track(.loginFailedToGuessXMLRPC, error: error) - WordPressAuthenticator.track(.loginFailed, error: error) - self.configureViewLoading(false) - - let err = self.originalErrorOrError(error: error as NSError) - - if let xmlrpcValidatorError = err as? WordPressOrgXMLRPCValidatorError { - self.displayError(message: xmlrpcValidatorError.localizedDescription, moveVoiceOverFocus: true) - } else if (err.domain == NSURLErrorDomain && err.code == NSURLErrorCannotFindHost) || - (err.domain == NSURLErrorDomain && err.code == NSURLErrorNetworkConnectionLost) { - // NSURLErrorNetworkConnectionLost can be returned when an invalid URL is entered. - let msg = NSLocalizedString( - "The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.", - comment: "Error message shown a URL does not point to an existing site.") - self.displayError(message: msg, moveVoiceOverFocus: true) - } else { - self.displayError(error, sourceTag: self.sourceTag) - } - }) - } - - @objc func fetchSiteInfo() { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let service = WordPressComBlogService() - let successBlock: (WordPressComSiteInfo) -> Void = { [weak self] siteInfo in - guard let self else { - return - } - self.configureViewLoading(false) - if siteInfo.isWPCom && WordPressAuthenticator.shared.delegate?.allowWPComLogin == false { - // Hey, you have to log out of your existing WP.com account before logging into another one. - self.promptUserToLogoutBeforeConnectingWPComSite() - return - } - self.presentNextControllerIfPossible(siteInfo: siteInfo) - } - service.fetchUnauthenticatedSiteInfoForAddress(for: baseSiteUrl, success: successBlock, failure: { [weak self] _ in - self?.configureViewLoading(false) - guard let self else { - return - } - self.presentNextControllerIfPossible(siteInfo: nil) - }) - } - - func presentNextControllerIfPossible(siteInfo: WordPressComSiteInfo?) { - WordPressAuthenticator.shared.delegate?.shouldPresentUsernamePasswordController(for: siteInfo, onCompletion: { result in - switch result { - case let .error(error): - self.displayError(message: error.localizedDescription) - case let .presentPasswordController(isSelfHosted): - if isSelfHosted { - self.showSelfHostedUsernamePassword() - } - - self.showWPUsernamePassword() - case .presentEmailController: - // This case is only used for UL&S - break - case .injectViewController: - // This case is only used for UL&S - break - } - }) - } - - @objc func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - return err - } - - /// Here we will continue with the self-hosted flow. - /// - @objc func showSelfHostedUsernamePassword() { - configureViewLoading(false) - guard let vc = LoginSelfHostedViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginEmailViewController to LoginSelfHostedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Break away from the self-hosted flow. - /// Display a username / password login screen for WP.com sites. - /// - @objc func showWPUsernamePassword() { - configureViewLoading(false) - - guard let vc = LoginUsernamePasswordViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginSiteAddressViewController to LoginUsernamePasswordViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - @objc func canSubmit() -> Bool { - return loginFields.validateSiteForSignin() - } - - @objc private func promptUserToLogoutBeforeConnectingWPComSite() { - let acceptActionTitle = NSLocalizedString("OK", comment: "Alert dismissal title") - let message = NSLocalizedString("Please log out before connecting to a different wordpress.com site", comment: "Message for alert to prompt user to logout before connecting to a different wordpress.com site.") - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(acceptActionTitle) - present(alertController, animated: true) - } - - // MARK: - URL Validation - - /// Does a local / quick Site Address validation and refreshes the UI with an error - /// if necessary. - /// - /// - Returns: `true` if the Site Address contains a valid URL. `false` otherwise. - /// - private func refreshSiteAddressError(immediate: Bool) { - let showError = !loginFields.siteAddress.isEmpty && !loginFields.validateSiteForSignin() - - if showError { - urlErrorDebouncer.call(immediate: immediate) - } else { - urlErrorDebouncer.cancel() - displayError(message: "") - } - } - - // MARK: - Actions - - @IBAction func handleSubmitForm() { - if canSubmit() { - validateForm() - } - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleSiteAddressHelpButtonTapped(_ sender: UIButton) { - SiteAddressViewController.showSiteAddressHelpAlert(from: self, sourceTag: sourceTag) - WordPressAuthenticator.track(.loginURLHelpScreenViewed) - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - displayError(message: "") - loginFields.siteAddress = siteURLField.nonNilTrimmedText() - configureSubmitButton(animating: false) - refreshSiteAddressError(immediate: false) - } - - @IBAction func handleEditingDidEnd(_ sender: UITextField) { - refreshSiteAddressError(immediate: true) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift deleted file mode 100644 index b9f02c30ef4d..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorCell.swift +++ /dev/null @@ -1,94 +0,0 @@ -import WordPressShared - -open class LoginSocialErrorCell: UITableViewCell { - private let errorTitle: String - private let errorDescription: String - private var errorDescriptionStyled: NSAttributedString? - private let titleLabel: UILabel - private let descriptionLabel: UILabel - private let labelStack: UIStackView - - private var forUnified: Bool = false - - private struct Constants { - static let labelSpacing: CGFloat = 15.0 - static let labelVerticalMargin: CGFloat = 20.0 - static let descriptionMinHeight: CGFloat = 14.0 - } - - @objc public init(title: String, description: String, forUnified: Bool = false) { - errorTitle = title - errorDescription = description - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - self.forUnified = forUnified - - super.init(style: .default, reuseIdentifier: "LoginSocialErrorCell") - - layoutLabels() - } - - public init(title: String, description styledDescription: NSAttributedString) { - errorDescriptionStyled = styledDescription - errorDescription = "" - errorTitle = title - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - - super.init(style: .default, reuseIdentifier: "LoginSocialErrorCell") - - layoutLabels() - } - - required public init?(coder aDecoder: NSCoder) { - errorTitle = aDecoder.value(forKey: "errorTitle") as? String ?? "" - errorDescription = aDecoder.value(forKey: "errorDescription") as? String ?? "" - titleLabel = UILabel() - descriptionLabel = UILabel() - labelStack = UIStackView() - - super.init(coder: aDecoder) - - layoutLabels() - } - - private func layoutLabels() { - contentView.addSubview(labelStack) - labelStack.translatesAutoresizingMaskIntoConstraints = false - labelStack.addArrangedSubview(titleLabel) - labelStack.addArrangedSubview(descriptionLabel) - labelStack.axis = .vertical - labelStack.spacing = Constants.labelSpacing - - let style = WordPressAuthenticator.shared.style - titleLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - titleLabel.textColor = style.instructionColor - descriptionLabel.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - descriptionLabel.textColor = style.subheadlineColor - descriptionLabel.numberOfLines = 0 - descriptionLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: Constants.descriptionMinHeight).isActive = true - - contentView.addConstraints([ - contentView.topAnchor.constraint(equalTo: labelStack.topAnchor, constant: Constants.labelVerticalMargin * -1.0), - contentView.bottomAnchor.constraint(equalTo: labelStack.bottomAnchor, constant: Constants.labelVerticalMargin), - contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: labelStack.leadingAnchor), - contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: labelStack.trailingAnchor) - ]) - - titleLabel.text = errorTitle.localizedUppercase - if let styledDescription = errorDescriptionStyled { - descriptionLabel.attributedText = styledDescription - } else { - descriptionLabel.text = errorDescription - } - - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return - } - - backgroundColor = forUnified ? unifiedBackgroundColor : WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift deleted file mode 100644 index 9c7c9c25250b..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginSocialErrorViewController.swift +++ /dev/null @@ -1,217 +0,0 @@ -import Foundation -import Gridicons -import WordPressShared - -@objc -protocol LoginSocialErrorViewControllerDelegate { - func retryWithEmail() - func retryWithAddress() - func retryAsSignup() - func errorDismissed() -} - -/// ViewController for presenting recovery options when social login fails -class LoginSocialErrorViewController: NUXTableViewController { - fileprivate var errorTitle: String - fileprivate var errorDescription: String - @objc weak var delegate: LoginSocialErrorViewControllerDelegate? - - private var forUnified: Bool = false - private var actionButtonTapped: Bool = false - private let unifiedAuthEnabled = WordPressAuthenticator.shared.configuration.enableUnifiedAuth - - fileprivate enum Sections: Int { - case titleAndDescription = 0 - case buttons = 1 - - static var count: Int { - return buttons.rawValue + 1 - } - } - - fileprivate enum Buttons: Int { - case tryEmail = 0 - case tryAddress = 1 - case signup = 2 - - static var count: Int { - return signup.rawValue + 1 - } - } - - /// Create and instance of LoginSocialErrorViewController - /// - /// - Parameters: - /// - title: The title that will be shown on the error VC - /// - description: A brief explination of what failed during social login - @objc init(title: String, description: String) { - errorTitle = title - errorDescription = description - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder aDecoder: NSCoder) { - errorTitle = aDecoder.value(forKey: "errorTitle") as? String ?? "" - errorDescription = aDecoder.value(forKey: "errorDescription") as? String ?? "" - - super.init(coder: aDecoder) - } - - override func viewDidLoad() { - super.viewDidLoad() - - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - forUnified = unifiedGoogle || unifiedApple - - styleBackground() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if !actionButtonTapped { - delegate?.errorDismissed() - } - } - - private func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - view.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return - } - - view.backgroundColor = forUnified ? unifiedBackgroundColor : WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard indexPath.section == Sections.buttons.rawValue, - let delegate else { - return - } - - actionButtonTapped = true - - switch indexPath.row { - case Buttons.tryEmail.rawValue: - delegate.retryWithEmail() - case Buttons.tryAddress.rawValue: - if loginFields.restrictToWPCom { - fallthrough - } else { - delegate.retryWithAddress() - } - case Buttons.signup.rawValue: - fallthrough - default: - delegate.retryAsSignup() - } - } -} - -// MARK: UITableViewDelegate methods - -extension LoginSocialErrorViewController { - private struct RowHeightConstants { - static let estimate: CGFloat = 45.0 - static let automatic: CGFloat = UITableView.automaticDimension - } - - override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return RowHeightConstants.estimate - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return RowHeightConstants.automatic - } -} - -// MARK: UITableViewDataSource methods - -extension LoginSocialErrorViewController { - private func numberOfButtonsToShow() -> Int { - - var buttonCount = loginFields.restrictToWPCom ? Buttons.count - 1 : Buttons.count - - // Don't show the Signup Retry if showing unified social flows. - // At this point, we've already tried signup and are past it. - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - - if unifiedGoogle || unifiedApple { - buttonCount -= 1 - } - - return buttonCount - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return Sections.count - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch section { - case Sections.titleAndDescription.rawValue: - return 1 - case Sections.buttons.rawValue: - return numberOfButtonsToShow() - default: - return 0 - } - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - switch indexPath.section { - case Sections.titleAndDescription.rawValue: - cell = titleAndDescriptionCell() - case Sections.buttons.rawValue: - fallthrough - default: - cell = buttonCell(index: indexPath.row) - } - return cell - } - - override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - let footer = UIView() - footer.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - return footer - } - - override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return 0.5 - } - - private func titleAndDescriptionCell() -> UITableViewCell { - return LoginSocialErrorCell(title: errorTitle, description: errorDescription, forUnified: forUnified) - } - - private func buttonCell(index: Int) -> UITableViewCell { - let cell = UITableViewCell() - let buttonText: String - let buttonIcon: UIImage - switch index { - case Buttons.tryEmail.rawValue: - buttonText = NSLocalizedString("Try with another email", comment: "When social login fails, this button offers to let the user try again with a differen email address") - buttonIcon = .gridicon(.undo) - case Buttons.tryAddress.rawValue: - if loginFields.restrictToWPCom { - fallthrough - } else { - buttonText = NSLocalizedString("Try with the site address", comment: "When social login fails, this button offers to let them try tp login using a URL") - buttonIcon = .gridicon(.domains) - } - case Buttons.signup.rawValue: - fallthrough - default: - buttonText = NSLocalizedString("Sign up", comment: "When social login fails, this button offers to let them signup for a new WordPress.com account") - buttonIcon = .gridicon(.mySites) - } - cell.textLabel?.text = buttonText - cell.textLabel?.textColor = WPStyleGuide.darkGrey() - cell.imageView?.image = buttonIcon.imageWithTintColor(WPStyleGuide.grey()) - return cell - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift deleted file mode 100644 index 63afb958adfa..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginUsernamePasswordViewController.swift +++ /dev/null @@ -1,245 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow. For use by WCiOS only. -/// A valid site address should be acquired before presenting this view controller. -/// -class LoginUsernamePasswordViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet var siteHeaderView: SiteInfoHeaderView! - @IBOutlet var usernameField: WPWalkthroughTextField! - @IBOutlet var passwordField: WPWalkthroughTextField! - @IBOutlet var forgotPasswordButton: UIButton! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet var verticalCenterConstraint: NSLayoutConstraint? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the username & password (if any) from LoginFields - loginFields.username = "" - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - configureHeader() - localizeControls() - displayLoginMessage("") - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureTextFields() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - setupNavBarIcon() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - WordPressAuthenticator.track(.loginUsernamePasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Setup and Configuration - - /// Assigns localized strings to various UIControl defined in the storyboard. - /// - @objc func localizeControls() { - instructionLabel?.text = WordPressAuthenticator.shared.displayStrings.usernamePasswordInstructions - - usernameField.placeholder = NSLocalizedString("Username", comment: "Username placeholder") - passwordField.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton.titleLabel?.numberOfLines = 0 - } - - /// Configures the content of the text fields based on what is saved in `loginFields`. - /// - @objc func configureTextFields() { - usernameField.text = loginFields.username - passwordField.text = loginFields.password - passwordField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - usernameField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - } - - /// Configures the appearance and state of the forgot password button. - /// - @objc func configureForgotPasswordButton() { - forgotPasswordButton.isEnabled = enableSubmit(animating: false) - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField.isEnabled = !loading - passwordField.isEnabled = !loading - - configureSubmitButton(animating: loading) - configureForgotPasswordButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField.becomeFirstResponder() - } - } - - /// Configure the site header. - /// - @objc func configureHeader() { - if let siteInfo = loginFields.meta.siteInfo { - configureBlogDetailHeaderView(siteInfo: siteInfo) - } else { - configureSiteAddressHeader() - } - } - - /// Configure the site header to show the BlogDetailsHeaderView - /// - func configureBlogDetailHeaderView(siteInfo: WordPressComSiteInfo) { - let siteAddress = sanitizedSiteAddress(siteAddress: siteInfo.url) - siteHeaderView.title = siteInfo.name - siteHeaderView.subtitle = NSURL.idnDecodedURL(siteAddress) - siteHeaderView.subtitleIsHidden = false - - siteHeaderView.blavatarBorderIsHidden = false - siteHeaderView.downloadBlavatar(at: siteInfo.icon) - } - - /// Configure the site header to show the site address label. - /// - @objc func configureSiteAddressHeader() { - siteHeaderView.title = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - siteHeaderView.subtitleIsHidden = true - - siteHeaderView.blavatarBorderIsHidden = true - siteHeaderView.blavatarImage = .linkFieldImage - } - - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.username = usernameField.nonNilTrimmedText() - loginFields.password = passwordField.nonNilTrimmedText() - - configureForgotPasswordButton() - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -extension LoginUsernamePasswordViewController { - - func displayLoginMessage(_ message: String) { - configureForgotPasswordButton() - } - - override func displayRemoteError(_ error: Error) { - displayLoginMessage("") - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - displayError(message: NSLocalizedString("It looks like this username/password isn't associated with this site.", comment: "An error message shown during log in when the username or password is incorrect.")) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} - -extension LoginUsernamePasswordViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - passwordField.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift deleted file mode 100644 index a94e0aa98bda..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginViewController.swift +++ /dev/null @@ -1,539 +0,0 @@ -import WordPressShared -import WordPressKit - -/// View Controller for login-specific screens -open class LoginViewController: NUXViewController, LoginFacadeDelegate { - @IBOutlet var instructionLabel: UILabel? - @objc var errorToPresent: Error? - - let tracker = AuthenticatorAnalyticsTracker.shared - - /// Constraints on the table view container. - /// Used to adjust the table width in unified views. - @IBOutlet var tableViewLeadingConstraint: NSLayoutConstraint? - @IBOutlet var tableViewTrailingConstraint: NSLayoutConstraint? - var defaultTableViewMargin: CGFloat = 0 - - lazy var loginFacade: LoginFacade = { - let configuration = WordPressAuthenticator.shared.configuration - let facade = LoginFacade(dotcomClientID: configuration.wpcomClientId, - dotcomSecret: configuration.wpcomSecret, - userAgent: configuration.userAgent) - facade.delegate = self - return facade - }() - - var isJetpackLogin: Bool { - return loginFields.meta.jetpackLogin - } - - private var isSignUp: Bool { - return loginFields.meta.emailMagicLinkSource == .signup - } - - var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - - return delegate - } - - open override var preferredStatusBarStyle: UIStatusBarStyle { - // Set to the old style as the default. - // Each VC in the unified flows needs to override this to use the unified style. - return WordPressAuthenticator.shared.style.statusBarStyle - } - - // MARK: Lifecycle Methods - - override open func viewDidLoad() { - super.viewDidLoad() - - displayError(message: "") - styleBackground() - styleInstructions() - - if let error = errorToPresent { - displayRemoteError(error) - errorToPresent = nil - } - } - - override open func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if isBeingDismissedInAnyWay { - tracker.track(click: .dismiss) - } - } - - func didChangePreferredContentSize() { - styleInstructions() - } - - // MARK: - Setup and Configuration - - /// Styles the view's background color. Defaults to WPStyleGuide.lightGrey() - /// - func styleBackground() { - view.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } - - /// Configures instruction label font - /// - func styleInstructions() { - instructionLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - instructionLabel?.adjustsFontForContentSizeCategory = true - instructionLabel?.textColor = WordPressAuthenticator.shared.style.instructionColor - } - - func configureViewLoading(_ loading: Bool) { - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Sets the text of the error label. - /// - /// - Parameter message: The message to display in the `errorLabel`. If empty, the `errorLabel` - /// will be hidden. - /// - Parameter moveVoiceOverFocus: If `true`, moves the VoiceOver focus to the `errorLabel`. - /// You will want to set this to `true` if the error was caused after pressing a button - /// (e.g. Next button). - func displayError(message: String, moveVoiceOverFocus: Bool = false) { - guard !message.isEmpty else { - errorLabel?.isHidden = true - return - } - - tracker.track(failure: message) - - errorLabel?.isHidden = false - errorLabel?.text = message - errorToPresent = nil - - if moveVoiceOverFocus, let errorLabel { - UIAccessibility.post(notification: .layoutChanged, argument: errorLabel) - } - } - - private func mustShowLoginEpilogue() -> Bool { - return isSignUp == false && authenticationDelegate.shouldPresentLoginEpilogue(isJetpackLogin: isJetpackLogin) - } - - private func mustShowSignupEpilogue() -> Bool { - return isSignUp && authenticationDelegate.shouldPresentSignupEpilogue() - } - - // MARK: - Epilogue - - func showSignupEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController else { - fatalError() - } - - authenticationDelegate.presentSignupEpilogue( - in: navigationController, - for: credentials, - socialUser: loginFields.meta.socialUser - ) - } - - func showLoginEpilogue(for credentials: AuthenticatorCredentials) { - guard let navigationController else { - fatalError() - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource) { [weak self] in - self?.dismissBlock?(false) - } - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with login. - /// - func validateFormAndLogin() { - view.endEditing(true) - displayError(message: "") - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = LocalizedText.missingInfoError - displayError(message: errorMsg) - - return - } - - configureViewLoading(true) - - loginFacade.signIn(with: loginFields) - } - - // MARK: SigninWPComSyncHandler methods - dynamic open func finishedLogin(withAuthToken authToken: String, requiredMultifactorCode: Bool) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: requiredMultifactorCode, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - syncWPComAndPresentEpilogue(credentials: credentials) - - linkSocialServiceIfNeeded(loginFields: loginFields, wpcomAuthToken: authToken) - } - - func configureStatusLabel(_ message: String) { - // this is now a no-op, unless status labels return - } - - /// Overridden here to direct these errors to the login screen's error label - dynamic open func displayRemoteError(_ error: Error) { - configureViewLoading(false) - let err = error as NSError - guard err.code != 403 else { - let message = LocalizedText.loginError - displayError(message: message) - return - } - - displayError(err, sourceTag: sourceTag) - } - - open func needsMultifactorCode() { - displayError(message: "") - configureViewLoading(false) - - if tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorCodeRequested) - } - - let unifiedAuthEnabled = WordPressAuthenticator.shared.configuration.enableUnifiedAuth - let unifiedGoogle = unifiedAuthEnabled && loginFields.meta.socialService == .google - let unifiedApple = unifiedAuthEnabled && loginFields.meta.socialService == .apple - let unifiedSiteAddress = unifiedAuthEnabled && !loginFields.siteAddress.isEmpty - let unifiedWordPress = unifiedAuthEnabled && loginFields.meta.userIsDotCom - - guard unifiedGoogle || unifiedApple || unifiedSiteAddress || unifiedWordPress else { - presentLogin2FA() - return - } - - // Make sure we don't provide any old nonce information when we are required to present only the multi-factor code option. - loginFields.nonceInfo = nil - loginFields.nonceUserID = 0 - - presentUnified2FA() - } - - private enum LocalizedText { - static let loginError = NSLocalizedString("Whoops, something went wrong and we couldn't log you in. Please try again!", comment: "An error message shown when a wpcom user provides the wrong password.") - static let missingInfoError = NSLocalizedString("Please fill out all the fields", comment: "A short prompt asking the user to properly fill out all login fields.") - static let gettingAccountInfo = NSLocalizedString("Getting account information", comment: "Alerts the user that wpcom account information is being retrieved.") - } -} - -// MARK: - View FLow - -extension LoginViewController { - func presentEpilogue(credentials: AuthenticatorCredentials) { - if mustShowSignupEpilogue() { - showSignupEpilogue(for: credentials) - } else if mustShowLoginEpilogue() { - showLoginEpilogue(for: credentials) - } else { - dismiss() - } - } -} - -// MARK: - Sync Helpers - -extension LoginViewController { - - /// Signals the Main App to synchronize the specified WordPress.com account. On completion, the epilogue will be pushed (if needed). - /// - func syncWPComAndPresentEpilogue( - credentials: AuthenticatorCredentials, - completion: (() -> Void)? = nil) { - - configureStatusLabel(LocalizedText.gettingAccountInfo) - - syncWPCom(credentials: credentials) { [weak self] in - guard let self else { - return - } - - completion?() - - self.presentEpilogue(credentials: credentials) - self.configureStatusLabel("") - self.configureViewLoading(false) - self.trackSignIn(credentials: credentials) - } - } - - /// Signals the Main App to synchronize the specified WordPress.com account. - /// - func syncWPCom(credentials: AuthenticatorCredentials, completion: (() -> Void)? = nil) { - authenticationDelegate.sync(credentials: credentials) { - completion?() - } - } - - /// Tracks the SignIn Event - /// - func trackSignIn(credentials: AuthenticatorCredentials) { - var properties = [String: String]() - - if let wpcom = credentials.wpcom { - properties = [ - "multifactor": wpcom.multifactor.description, - "dotcom_user": true.description - ] - } - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - WordPressAuthenticator.track(.signedIn, properties: properties) - tracker.track(step: .success) - } - - /// Links the current WordPress Account to a Social Service (if possible!!). - /// - func linkSocialServiceIfNeeded(loginFields: LoginFields, wpcomAuthToken: String) { - guard let serviceName = loginFields.meta.socialService, let serviceToken = loginFields.meta.socialServiceIDToken else { - return - } - - linkSocialService(serviceName: serviceName, - serviceToken: serviceToken, - wpcomAuthToken: wpcomAuthToken, - appleConnectParameters: loginFields.parametersForSignInWithApple) - } - - /// Links the current WordPress Account to a Social Service. - /// - func linkSocialService(serviceName: SocialServiceName, - serviceToken: String, - wpcomAuthToken: String, - appleConnectParameters: [String: AnyObject]? = nil) { - let service = WordPressComAccountService() - service.connect(wpcomAuthToken: wpcomAuthToken, - serviceName: serviceName, - serviceToken: serviceToken, - connectParameters: appleConnectParameters, - success: { - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - let source = appleConnectParameters != nil ? "apple" : "google" - WordPressAuthenticator.track(.signedIn, properties: ["source": source]) - - if AuthenticatorAnalyticsTracker.shared.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.loginSocialConnectSuccess) - WordPressAuthenticator.track(.loginSocialSuccess) - } - }, failure: { error in - WPLogError("Social Link Error: \(error)") - WordPressAuthenticator.track(.loginSocialConnectFailure, error: error) - // We're opting to let this call fail silently. - // Our user has already successfully authenticated and can use the app -- - // connecting the social service isn't critical. There's little to - // be gained by displaying an error that can not currently be resolved - // in the app and doing so might tarnish an otherwise satisfying login - // experience. - // If/when we add support for manually connecting/disconnecting services - // we can revisit. - }) - } -} - -// MARK: - Handle View Changes -// -extension LoginViewController { - - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // Update Dynamic Type - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - - // Update Table View size - setTableViewMargins(forWidth: view.frame.width) - } - - open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - setTableViewMargins(forWidth: size.width) - } - - /// Resize the table view based on trait collection. - /// Used only in unified views. - /// - func setTableViewMargins(forWidth viewWidth: CGFloat) { - guard let tableViewLeadingConstraint, - let tableViewTrailingConstraint else { - return - } - - guard traitCollection.horizontalSizeClass == .regular && - traitCollection.verticalSizeClass == .regular else { - tableViewLeadingConstraint.constant = defaultTableViewMargin - tableViewTrailingConstraint.constant = defaultTableViewMargin - return - } - - let marginMultiplier = UIDevice.current.orientation.isLandscape ? - TableViewMarginMultipliers.ipadLandscape : - TableViewMarginMultipliers.ipadPortrait - - let margin = viewWidth * marginMultiplier - - tableViewLeadingConstraint.constant = margin - tableViewTrailingConstraint.constant = margin - } - - private enum TableViewMarginMultipliers { - static let ipadPortrait: CGFloat = 0.1667 - static let ipadLandscape: CGFloat = 0.25 - } -} - -// MARK: - Social Sign In Handling - -extension LoginViewController { - - func removeGoogleWaitingView() { - // Remove the Waiting for Google view so it doesn't reappear when backing through the navigation stack. - navigationController?.viewControllers.removeAll(where: { $0 is GoogleAuthViewController }) - } - - func signInAppleAccount() { - guard let token = loginFields.meta.socialServiceIDToken else { - WordPressAuthenticator.track(.loginSocialButtonFailure, properties: ["source": SocialServiceName.apple.rawValue]) - configureViewLoading(false) - return - } - - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.apple.rawValue) - } - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func socialNeedsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - loginFields.nonceInfo = nonceInfo - loginFields.nonceUserID = userID - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentLogin2FA() - return - } - - presentUnified2FA() - } - - private func presentLogin2FA() { - var properties = [AnyHashable: Any]() - if let service = loginFields.meta.socialService?.rawValue { - properties["source"] = service - } - - if tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.loginSocial2faNeeded, properties: properties) - } - - guard let vc = Login2FAViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to Login2FAViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - private func presentUnified2FA() { - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from LoginViewController to TwoFAViewController") - return - } - - vc.dismissBlock = dismissBlock - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - LoginSocialError delegate methods - -extension LoginViewController: LoginSocialErrorViewControllerDelegate { - - func retryWithEmail() { - loginFields.username = "" - cleanupAfterSocialErrors() - navigationController?.popToRootViewController(animated: true) - } - - func retryWithAddress() { - cleanupAfterSocialErrors() - loginToSelfHostedSite() - } - - func retryAsSignup() { - cleanupAfterSocialErrors() - - if let controller = SignupEmailViewController.instantiate(from: .signup) { - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } - } - - func errorDismissed() { - loginFields.username = "" - navigationController?.popToRootViewController(animated: true) - } - - private func cleanupAfterSocialErrors() { - dismiss(animated: true) {} - } - - /// Displays the self-hosted login form. - /// - @objc func loginToSelfHostedSite() { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - presentSelfHostedView() - return - } - - presentUnifiedSiteAddressView() - } - - /// Navigates to the unified site address login flow. - /// - func presentUnifiedSiteAddressView() { - guard let vc = SiteAddressViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from LoginViewController to SiteAddressViewController") - return - } - - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates to the old self-hosted login flow. - /// - func presentSelfHostedView() { - guard let vc = LoginSiteAddressViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from LoginViewController to LoginSiteAddressViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift b/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift deleted file mode 100644 index 78faf7464bc7..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/LoginWPComViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -/// Provides a form and functionality for signing a user in to WordPress.com -/// -class LoginWPComViewController: LoginViewController, NUXKeyboardResponder { - @IBOutlet weak var passwordField: WPWalkthroughTextField? - @IBOutlet weak var forgotPasswordButton: UIButton? - @IBOutlet weak var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet weak var verticalCenterConstraint: NSLayoutConstraint? - @IBOutlet var emailIcon: UIImageView? - @IBOutlet var emailLabel: UITextField? - @IBOutlet var emailStackView: UIStackView? - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComPassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the password (if any) from LoginFields. - loginFields.password = "" - } - } - - // MARK: - Lifecycle Methods - - override func viewDidLoad() { - super.viewDidLoad() - - localizeControls() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // Update special case login fields. - loginFields.meta.userIsDotCom = true - - configureTextFields() - configureEmailIcon() - configureForgotPasswordButton() - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - passwordField?.becomeFirstResponder() - WordPressAuthenticator.track(.loginPasswordFormViewed) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - - if isMovingFromParent { - // There was a bug that was causing iOS's update password prompt to come up - // when this VC was being dismissed pressing the "< Back" button. The following - // line ensures that such prompt doesn't come up anymore. - // - // More information can be found in the PR where this workaround is introduced: - // https://git.io/JUkak - // - passwordField?.text = "" - } - } - - // MARK: Setup and Configuration - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - submitButton?.isEnabled = enableSubmit(animating: animating) - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - } - - /// Configure the view's loading state. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - passwordField?.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editiing state should be assumed. - // Check the helper to determine whether an editiing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - passwordField?.becomeFirstResponder() - } - } - - @objc func configureTextFields() { - passwordField?.text = loginFields.password - passwordField?.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - emailLabel?.text = loginFields.username - emailLabel?.textColor = WordPressAuthenticator.shared.style.subheadlineColor - } - - func configureEmailIcon() { - guard let image = emailIcon?.image else { - return - } - emailIcon?.image = image.imageWithTintColor(WordPressAuthenticator.shared.style.subheadlineColor) - } - - private func configureForgotPasswordButton() { - guard let forgotPasswordButton else { - return - } - WPStyleGuide.configureTextButton(forgotPasswordButton) - } - - @objc func localizeControls() { - - instructionLabel?.text = { - guard let service = loginFields.meta.socialService else { - return NSLocalizedString("Enter the password for your WordPress.com account.", comment: "Instructional text shown when requesting the user's password for login.") - } - - if service == SocialServiceName.google { - return NSLocalizedString("To proceed with this Google account, please first log in with your WordPress.com password. This will only be asked once.", comment: "") - } - - return NSLocalizedString( - "Please enter the password for your WordPress.com account to log in with your Apple ID.", - comment: "Instructional text shown when requesting the user's password for a login initiated via Sign In with Apple" - ) - }() - - passwordField?.placeholder = NSLocalizedString("Password", comment: "Password placeholder") - passwordField?.accessibilityIdentifier = "Password" - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Password Next Button" - - let forgotPasswordTitle = NSLocalizedString("Lost your password?", comment: "Title of a button. ") - forgotPasswordButton?.setTitle(forgotPasswordTitle, for: .normal) - forgotPasswordButton?.setTitle(forgotPasswordTitle, for: .highlighted) - forgotPasswordButton?.titleLabel?.numberOfLines = 0 - } - - // MARK: - Instance Methods - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - validateFormAndLogin() - } - - // MARK: - Actions - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - switch sender { - case passwordField: - loginFields.password = sender.nonNilTrimmedText() - case emailLabel: - // The email can only be changed via a password manager. - // In this case, don't update username for social accounts. - // This prevents inadvertent account linking. - // Ref: https://git.io/JJSUM - if loginFields.meta.socialService != nil { - emailLabel?.text = loginFields.username - } else { - loginFields.username = sender.nonNilTrimmedText() - } - default: - break - } - - configureSubmitButton(animating: false) - } - - @IBAction func handleSubmitButtonTapped(_ sender: UIButton) { - validateForm() - } - - @IBAction func handleForgotPasswordButtonTapped(_ sender: UIButton) { - WordPressAuthenticator.openForgotPasswordURL(loginFields) - WordPressAuthenticator.track(.loginForgotPasswordClicked) - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidRequest { - let message = NSLocalizedString("It seems like you've entered an incorrect password. Want to give it another try?", comment: "An error message shown when a wpcom user provides the wrong password.") - displayError(message: message) - } else { - super.displayRemoteError(error) - } - } - - // MARK: - Dynamic type - - override func didChangePreferredContentSize() { - super.didChangePreferredContentSize() - emailLabel?.font = WPStyleGuide.fontForTextStyle(.body) - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } - - // MARK: Keyboard Events - - @objc func signinFormVerticalOffset() -> CGFloat { - // the stackview-based layout shifts fine with this adjustment - return 0 - } -} - -extension LoginWPComViewController: UITextFieldDelegate { - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if enableSubmit(animating: false) { - validateForm() - } - return true - } -} - -extension LoginWPComViewController { - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory { - didChangePreferredContentSize() - } - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift b/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift deleted file mode 100644 index e2ecf4f4aa7a..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/SigninEditingState.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -open class SigninEditingState { - public static var signinEditingStateActive = false - public static var signinLastKeyboardHeightDelta: CGFloat = 0 -} diff --git a/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift b/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift deleted file mode 100644 index 02c99799b714..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignIn/UIImageView+Additions.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit -import WordPressUI -import GravatarUI - -extension UIImageView { - func setGravatarImage(with email: String, placeholder: UIImage = .gravatarPlaceholderImage, rating: Rating = .general, preferredSize: CGSize? = nil, forceRefresh: Bool = false) async throws { - listenForGravatarChanges(forEmail: email) - var options: [ImageSettingOption] = [] - if forceRefresh { - options.append(.forceRefresh) - } - try await gravatar.setImage(avatarID: .email(email), - placeholder: placeholder, - rating: rating, - preferredSize: preferredSize, - defaultAvatarOption: .status404, - options: options) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard b/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard deleted file mode 100644 index 94c12288ea9f..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/Signup.storyboard +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift deleted file mode 100644 index 0cd864b91f16..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupEmailViewController.swift +++ /dev/null @@ -1,239 +0,0 @@ -import UIKit -import WordPressShared -import WordPressKit - -class SignupEmailViewController: LoginViewController, NUXKeyboardResponder { - - // MARK: - NUXKeyboardResponder Properties - - @IBOutlet weak var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet weak var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - Properties - - @IBOutlet weak var emailField: LoginTextField! - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupEmail - } - } - - private enum ErrorMessage: String { - case invalidEmail = "invalid_email" - case availabilityCheckFail = "availability_check_fail" - case emailUnavailable = "email_unavailable" - case magicLinkRequestFail = "magic_link_request_fail" - - func description() -> String { - switch self { - case .invalidEmail: - return NSLocalizedString("Please enter a valid email address.", comment: "Error message displayed when the user attempts use an invalid email address.") - case .availabilityCheckFail: - return NSLocalizedString("Unable to verify the email address. Please try again later.", comment: "Error message displayed when an error occurred checking for email availability.") - case .emailUnavailable: - return NSLocalizedString("Sorry, that email address is already being used!", comment: "Error message displayed when the entered email is not available.") - case .magicLinkRequestFail: - return NSLocalizedString("We were unable to send you an email at this time. Please try again later.", comment: "Error message displayed when an error occurred sending the magic link email.") - } - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - localizeControls() - WordPressAuthenticator.track(.createAccountInitiated) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - configureViewForEditingIfNeeded() - - // If email address already exists, pre-populate it. - emailField.text = loginFields.emailAddress - - configureSubmitButton(animating: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - private func localizeControls() { - instructionLabel?.text = NSLocalizedString("To create your new WordPress.com account, please enter your email address.", comment: "Text instructing the user to enter their email address.") - - emailField.placeholder = NSLocalizedString("Email address", comment: "Placeholder for a textfield. The user may enter their email address.") - emailField.accessibilityIdentifier = "Signup Email Address" - emailField.contentInsets = WPStyleGuide.edgeInsetForLoginTextFields() - - let submitButtonTitle = NSLocalizedString("Next", comment: "Title of a button. The text should be capitalized.").localizedCapitalized - submitButton?.setTitle(submitButtonTitle, for: .normal) - submitButton?.setTitle(submitButtonTitle, for: .highlighted) - submitButton?.accessibilityIdentifier = "Signup Email Next Button" - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - private func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - emailField.becomeFirstResponder() - } - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && validEmail() - } - - // MARK: - Keyboard Notifications - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } - - // MARK: - Email Validation - - private func validateForm() { - - // Hide the error label. - displayError(message: "") - - // If the email address is invalid, display appropriate message. - if !validEmail() { - displayError(message: ErrorMessage.invalidEmail.description()) - configureSubmitButton(animating: false) - return - } - - checkEmailAvailability { available in - if available { - self.loginFields.username = self.loginFields.emailAddress - self.loginFields.meta.emailMagicLinkSource = .signup - self.requestAuthenticationLink() - } - self.configureSubmitButton(animating: false) - } - } - - private func validEmail() -> Bool { - return EmailFormatValidator.validate(string: loginFields.emailAddress) - } - - // MARK: - Email Availability - - private func checkEmailAvailability(completion: @escaping (Bool) -> Void) { - - let remote = AccountServiceRemoteREST( - wordPressComRestApi: WordPressComRestApi(baseURL: WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL)) - - remote.isEmailAvailable(loginFields.emailAddress, success: { [weak self] available in - if !available { - defer { - WordPressAuthenticator.track(.signupEmailToLogin) - } - // If the user has already signed up redirect to the Login flow - guard let vc = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailViewController from SignupEmailViewController") - return - } - - guard let self else { - return - } - - vc.loginFields.restrictToWPCom = true - vc.loginFields.username = self.loginFields.emailAddress - - self.navigationController?.pushViewController(vc, animated: true) - } - completion(available) - }, failure: { error in - guard let error else { - self.displayError(message: ErrorMessage.availabilityCheckFail.description()) - completion(false) - return - } - - WPLogError("Error checking email availability: \(error.localizedDescription)") - - switch error { - case AccountServiceRemoteError.emailAddressInvalid: - self.displayError(message: error.localizedDescription) - completion(false) - default: - self.displayError(message: ErrorMessage.availabilityCheckFail.description()) - completion(false) - } - }) - } - - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - private func requestAuthenticationLink() { - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (_: Error) in - WPLogError("Request for signup link email failed.") - WordPressAuthenticator.track(.signupMagicLinkFailed) - self?.displayError(message: ErrorMessage.magicLinkRequestFail.description()) - self?.configureSubmitButton(animating: false) - }) - } - - private func didRequestSignupLink() { - WordPressAuthenticator.track(.signupMagicLinkRequested) - - guard let vc = NUXLinkMailViewController.instantiate(from: .emailMagicLink) else { - WPLogError("Failed to navigate to NUXLinkMailViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Action Handling - - @IBAction func handleSubmit() { - displayError(message: "") - configureSubmitButton(animating: true) - validateForm() - } - - @IBAction func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.emailAddress = emailField.nonNilTrimmedText() - configureSubmitButton(animating: false) - } - - // MARK: - Misc - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift deleted file mode 100644 index f6072704b7f0..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupGoogleViewController.swift +++ /dev/null @@ -1,80 +0,0 @@ -import UIKit -import WordPressShared - -/// View controller that handles the google signup flow -/// -class SignupGoogleViewController: LoginViewController { - - // MARK: - Properties - - private var hasShownGoogle = false - @IBOutlet var titleLabel: UILabel? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupWaitingForGoogle - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - titleLabel?.text = LocalizedText.waitingForGoogle - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - showGoogleScreenIfNeeded() - } -} - -// MARK: - Private Methods - -private extension SignupGoogleViewController { - - func showGoogleScreenIfNeeded() { - guard !hasShownGoogle else { - return - } - - // Flag this as a social sign in. - loginFields.meta.socialService = .google - - GoogleAuthenticator.sharedInstance.signupDelegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields, for: .signup) - - hasShownGoogle = true - } - - enum LocalizedText { - static let waitingForGoogle = NSLocalizedString("Waiting for Google to complete…", comment: "Message shown on screen while waiting for Google to finish its signup process.") - static let signupFailed = NSLocalizedString("Google sign up failed.", comment: "Message shown on screen after the Google sign up process failed.") - } -} - -// MARK: - GoogleAuthenticatorSignupDelegate - -extension SignupGoogleViewController: GoogleAuthenticatorSignupDelegate { - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showSignupEpilogue(for: credentials) - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showLoginEpilogue(for: credentials) - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - self.loginFields = loginFields - titleLabel?.textColor = .systemRed - titleLabel?.text = LocalizedText.signupFailed - displayError(error, sourceTag: .wpComSignup) - } - - func googleSignupCancelled() { - navigationController?.popViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift b/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift deleted file mode 100644 index 2bdab164f574..000000000000 --- a/Sources/WordPressAuthenticator/Features/SignUp/SignupNavigationController.swift +++ /dev/null @@ -1,8 +0,0 @@ -import UIKit -import WordPressUI - -class SignupNavigationController: RotationAwareNavigationViewController { - override func viewDidLoad() { - super.viewDidLoad() - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift b/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift deleted file mode 100644 index 438c873079d0..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Analytics/AuthenticatorAnalyticsTracker.swift +++ /dev/null @@ -1,513 +0,0 @@ -import Foundation -import WordPressShared - -/// Implements the analytics tracking logic for our sign in flow. -/// -public class AuthenticatorAnalyticsTracker { - - private static let defaultSource: Source = .default - private static let defaultFlow: Flow = .prologue - private static let defaultStep: Step = .prologue - - /// The method used for analytics tracking. Useful for overriding in automated tests. - /// - typealias TrackerMethod = (_ event: AnalyticsEvent) -> Void - - public enum EventType: String { - case step = "unified_login_step" - case interaction = "unified_login_interaction" - case failure = "unified_login_failure" - } - - public enum Property: String { - case failure - case flow - case click - case source - case step - } - - public enum Source: String { - /// Starts when the user logs in / sign up from the prologue screen - /// - case `default` - - case jetpack - case share - case deeplink - case reauthentication - - /// Starts when the used adds a site from the site picker - /// - case selfHosted = "self_hosted" - } - - public enum Flow: String { - /// The initial flow before we decide whether the user is logging in or signing up - /// - case wpCom = "wordpress_com" - - /// Flow for Google login - /// - case loginWithGoogle = "google_login" - - /// Flow for Google signup - /// - case signupWithGoogle = "google_signup" - - /// Flow for Apple login - /// - case loginWithApple = "siwa_login" - - /// Flow for Apple signup - /// - case signupWithApple = "siwa_signup" - - /// Flow for iCloud Keychain login - /// - case loginWithiCloudKeychain = "icloud_keychain_login" - - /// The flow that starts when we offer the user the magic link login - /// - case loginWithMagicLink = "login_magic_link" - - /// This flow starts when the user decides to login with a password instead - /// - case loginWithPassword = "login_password" - - /// This flow starts when the user decides to login with a password instead, with magic link logic emphasis - /// where the CTA is a secondary CTA instead of a table view row - /// - case loginWithPasswordWithMagicLinkEmphasis = "login_password_magic_link_emphasis" - - /// This flow starts when the user decides to log in with their site address - /// - 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 - - /// This flow represents the prologue screen. - /// - case prologue - } - - public enum Step: String { - /// Gets shown on the Prologue screen - /// - case prologue - - /// Triggered when a flow is started - /// - case start - - /// Triggered when a user requests a magic link and sees the screen with the “Open mail” button - /// - case magicLinkRequested = "magic_link_requested" - - /// This represents the user opening their mail. It’s not strictly speaking an in-app screen but for the user it is part of the flow. - case emailOpened = "email_opened" - - /// Represents the screen or step in which WPCOM account email is entered by the user - /// - case enterEmailAddress = "enter_email_address" - - /// The screen with a username and password visible - /// - case usernamePassword = "username_password" - - /// The screen that requests the password - /// - case passwordChallenge = "password_challenge" - - /// Triggered on the epilogue screen - /// - case success - - /// Triggered on the help screen - /// - case help - - /// When we ask user to input the code from the 2 factor authentication - case twoFactorAuthentication = "2fa" - - /// Triggered when a user enters site credentials and sees the screen with instructions to verify email. (`VerifyEmailViewController`) - /// - case verifyEmailInstructions = "instructions_to_verify_email" - - /// Triggered when a magic link is automatically requested after filling in email address and the requested screen is shown - /// - case magicLinkAutoRequested = "magic_link_auto_requested" - } - - public enum ClickTarget: String { - /// Tracked when submitting the email form, the email & password form, site address form, - /// username & password form and signup email form - /// - case submit - - /// Tracked when the user clicks on continue in the login/signup epilogue - /// - case `continue` - - /// Tracked when the post signup interstitial screen is dismissed, when the - /// login signup help dialog is dismissed and when the email hint dialog is dismissed - /// - case dismiss - - /// Tracked when the user clicks “Continue with WordPress.com” on the Prologue screen - /// - case continueWithWordPressCom = "continue_with_wordpress_com" - - /// Tracked when the user clicks “What is WordPress.com?" button on the WordPress.com flow screen - /// - case whatIsWPCom = "what_is_wordpress_com" - - /// Tracked when the user clicks “Login with site address” on the Prologue screen - /// - case loginWithSiteAddress = "login_with_site_address" - - /// When the user tries to login with Apple from the confirmation screen - /// - case loginWithApple = "login_with_apple" - - /// Tracked when the user clicks “Login with Google” on the WordPress.com flow screen - /// - case loginWithGoogle = "login_with_google" - - /// When the user clicks on “Forgotten password” on one of the screens that show the password field - /// - case forgottenPassword = "forgotten_password" - - /// When the user clicks on terms of service anywhere - /// - case termsOfService = "terms_of_service_clicked" - - /// When the user tries to sign up with email from the confirmation screen - /// - case signupWithEmail = "signup_with_email" - - /// When the user tries to sign up with Apple from the confirmation screen - /// - case signupWithApple = "signup_with_apple" - - /// When the user tries to sign up with Google from the confirmation screen - /// - case signupWithGoogle = "signup_with_google" - - /// When the user opens the email client from the magic link screen - /// - case openEmailClient = "open_email_client" - - /// Any time the user clicks on the help icon in the login flow - /// - case showHelp = "show_help" - - /// Used on the 2FA screen to send code with a text instead of using the authenticator app - /// - case sendCodeWithText = "send_code_with_text" - - /// Used on the 2FA screen to use a security key instead of using the authenticator app - /// - case enterSecurityKey = "enter_security_key" - - /// Used on the 2FA screen to submit authentication code - /// - case submitTwoFactorCode = "submit_2fa_code" - - /// When the user requests a magic link after filling in email address - /// - case requestMagicLink = "request_magic_link" - - /// Click on “Create new site” button after a successful signup - /// - case createNewSite = "create_new_site" - - /// Adding a self-hosted site from the epilogue - /// - case addSelfHostedSite = "add_self_hosted_site" - - /// Connecting a site from the epilogue - /// - case connectSite = "connect_site" - - /// Picking an avatar from the epilogue after a successful signup - /// - case selectAvatar = "select_avatar" - - /// Editing the username from the epilogue after a successful signup - /// - case editUsername = "edit_username" - - /// Clicking on “Need help finding site address” from a dialog - /// - case helpFindingSiteAddress = "help_finding_site_address" - - /// When the user clicks on the email field to log in, this triggers the hint dialog to show up - /// - case selectEmailField = "select_email_field" - - /// When the user selects an email from the hint dialog - /// - case pickEmailFromHint = "pick_email_from_hint" - - /// When the user clicks on “Create account” on the signup confirmation screen - /// - case createAccount = "create_account" - - /// When the user taps of "Sign in with site credentials" button in `GetStartedViewController` - /// - case signInWithSiteCredentials = "sign_in_with_site_credentials" - - /// When the user clicks on “Login with account password” on `VerifyEmailViewController` - /// - case loginWithAccountPassword = "login_with_password" - } - - public enum Failure: String { - /// Failure to guess XMLRPC URL - /// - case loginFailedToGuessXMLRPC = "login_failed_to_guess_xmlrpc_url" - } - - /// Shared Instance. - /// - public static var shared: AuthenticatorAnalyticsTracker = { - return AuthenticatorAnalyticsTracker() - }() - - /// State for the analytics tracker. - /// - public class State { - internal(set) public var lastFlow: Flow - internal(set) public var lastSource: Source - internal(set) public var lastStep: Step - - init(lastFlow: Flow = AuthenticatorAnalyticsTracker.defaultFlow, lastSource: Source = AuthenticatorAnalyticsTracker.defaultSource, lastStep: Step = AuthenticatorAnalyticsTracker.defaultStep) { - self.lastFlow = lastFlow - self.lastSource = lastSource - self.lastStep = lastStep - } - } - - /// The state of this tracker. - /// - public let state = State() - - /// The backing analytics tracking method. Can be overridden for testing purposes. - /// - let track: TrackerMethod - - /// Whether tracking is enabled or not. This is just a convenience configuration to enable this tracker to be turned on and off - /// using a feature flag. It should go away once we remove the legacy tracking. - /// - let enabled: Bool - - // MARK: - Initializers - - init(enabled: Bool = WordPressAuthenticator.shared.configuration.enableUnifiedAuth, track: @escaping TrackerMethod = WPAnalytics.track) { - self.enabled = enabled - self.track = track - } - - /// Resets the flow and step to the defaults. The source is left untouched, and should only be set explicitly. - /// - func resetState() { - set(flow: Self.defaultFlow) - set(step: Self.defaultStep) - } - - // MARK: - Legacy vs Unified tracking - - /// This method will reply whether, for the current flow in the state, tracking is enabled. - /// - /// It's the responsibility of the class calling the tracking methods to check this before attempting to actually do the tracking. - /// - /// - Returns: `true` if we can track using the state machine. - /// - public func canTrack() -> Bool { - return enabled - } - - /// This is a convenience method, that's useful for cases where we simply want to check if the legacy tracking should be - /// enabled. It can be particularly useful in cases where we don't have a matching tracking call in the new flow. - /// - /// - Returns: `true` if we must use legacy tracking, `false` otherwise. - /// - public func shouldUseLegacyTracker() -> Bool { - return !canTrack() - } - - // MARK: - Tracking - - /// Track a step within a flow. - /// - public func track(step: Step) { - guard canTrack() else { - return - } - - track(event(step: step)) - } - - /// Track a click interaction. - /// - public func track(click: ClickTarget) { - guard canTrack() else { - return - } - - track(event(click: click)) - } - - /// Track a predefined failure enum. - /// - public func track(failure: Failure) { - track(failure: failure.rawValue) - } - - /// Track a failure. - /// - public func track(failure: String) { - guard canTrack() else { - return - } - - track(event(failure: failure)) - } - - // MARK: - Tracking: Legacy Tracking Support - - /// Tracks a step within a flow if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(step: Step, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(step: step) - } - - /// Track a click interaction if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(click: ClickTarget, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(event(click: click)) - } - - /// Track a failure if tracking is enabled for that flow, or executes the specified block if tracking is not enabled - /// for the flow. - /// - public func track(failure: String, ifTrackingNotEnabled legacyTracking: () -> Void) { - guard canTrack() else { - legacyTracking() - return - } - - track(event(failure: failure)) - } - - // MARK: - Event Construction & Context Updating - - /// Creates an event for a step. Updates the state machine. - /// - /// - Parameters: - /// - step: the step we're tracking. - /// - flow: the flow that the step belongs to. - /// - /// - Returns: an analytics event representing the step. - /// - private func event(step: Step) -> AnalyticsEvent { - let event = AnalyticsEvent( - name: EventType.step.rawValue, - properties: properties(step: step)) - - state.lastStep = step - - return event - } - - /// Creates an event for a failure. Loads the properties from the state machine. - /// - /// - Parameters: - /// - failure: the error message we want to track. - /// - /// - Returns: an analytics event representing the failure. - /// - private func event(failure: String) -> AnalyticsEvent { - var properties = lastProperties() - properties[Property.failure.rawValue] = failure - - return AnalyticsEvent( - name: EventType.failure.rawValue, - properties: properties) - } - - /// Creates an event for a click interaction. Loads the properties from the state machine. - /// - /// - Parameters: - /// - click: the target of the click interaction. - /// - /// - Returns: an analytics event representing the click interaction. - /// - private func event(click: ClickTarget) -> AnalyticsEvent { - var properties = lastProperties() - properties[Property.click.rawValue] = click.rawValue - - return AnalyticsEvent( - name: EventType.interaction.rawValue, - properties: properties) - } - - // MARK: - Source & Flow - - /// Allows the caller to set the flow without tracking. - /// - public func set(flow: Flow) { - state.lastFlow = flow - } - - /// Allows the caller to set the source without tracking. - /// - public func set(source: Source) { - state.lastSource = source - } - - /// Allows the caller to set the step without tracking. - /// - public func set(step: Step) { - state.lastStep = step - } - - // MARK: - Properties - - private func properties(step: Step) -> [String: String] { - return properties(step: step, flow: state.lastFlow, source: state.lastSource) - } - - private func properties(step: Step, flow: Flow, source: Source) -> [String: String] { - return [ - Property.flow.rawValue: flow.rawValue, - Property.source.rawValue: source.rawValue, - Property.step.rawValue: step.rawValue - ] - } - - /// Retrieve the last step, flow and source stored in the state machine. - /// - private func lastProperties() -> [String: String] { - return properties(step: state.lastStep, flow: state.lastFlow, source: state.lastSource) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift deleted file mode 100644 index 7d86cb7b6b28..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Errors.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -// MARK: - WordPressAuthenticator Error Constants. Once the entire code is Swifted, let's *PLEASE* have a -// beautiful Error `Swift Enum`. -// -extension WordPressAuthenticator { - - /// Error Domain for Authentication issues. - /// - @objc public static let errorDomain = "org.wordpress.ios.authenticator" - - /// "Invalid Version" Error Code. Used whenever the remote WordPress.org endpoint is below the supported version. - /// - @objc public static let invalidVersionErrorCode = 5000 -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift deleted file mode 100644 index 774056aaaf77..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Events.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - Authentication Flow Event. Useful to relay internal Auth events over to activity trackers. -// -extension WordPressAuthenticator { - - /// Tracks the specified event. - /// - @objc - public static func track(_ event: WPAnalyticsStat) { - WordPressAuthenticator.shared.delegate?.track(event: event) - } - - /// Tracks the specified event, with the specified properties. - /// - @objc - public static func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any]) { - WordPressAuthenticator.shared.delegate?.track(event: event, properties: properties) - } - - /// Tracks the specified event, with the associated Error. - /// - /// Note: Ideally speaking... `Error` is not optional. *However* this method is to be used in the ObjC realm, where not everything - /// has it's nullability specifier set. We're just covering unexpected scenarios. - /// - @objc - public static func track(_ event: WPAnalyticsStat, error: Error?) { - guard let error else { - track(event) - return - } - - WordPressAuthenticator.shared.delegate?.track(event: event, error: error) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift deleted file mode 100644 index 7c2c25cc8718..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator+Notifications.swift +++ /dev/null @@ -1,11 +0,0 @@ -// MARK: - WordPressAuthenticator-Y Notifications -// -extension NSNotification.Name { - /// Posted whenever the Login Flow has been cancelled. - /// - public static let wordpressLoginCancelled = Foundation.Notification.Name(rawValue: "WordPressLoginCancelled") - - /// Posted whenever a Jetpack Login was successfully performed. - /// - public static let wordpressLoginFinishedJetpackLogin = Foundation.Notification.Name(rawValue: "WordPressLoginFinishedJetpackLogin") -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift deleted file mode 100644 index be3086998416..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticator.swift +++ /dev/null @@ -1,547 +0,0 @@ -import AuthenticationServices -import NSURL_IDN -import UIKit -import WordPressShared -import WordPressKit - -// MARK: - WordPressAuthenticator: Public API to deal with WordPress.com and WordPress.org authentication. -// -@objc public class WordPressAuthenticator: NSObject { - - /// (Private) Shared Instance. - /// - private static var privateInstance: WordPressAuthenticator? - - /// Observer for AppleID Credential State - /// - private var appleIDCredentialObserver: NSObjectProtocol? - - /// Optional sign in source that could be from the login prologue or the host app to track the entry point - /// for customizations in the epilogue handling. - var signInSource: SignInSource? - - /// Shared Instance. - /// - @objc public static var shared: WordPressAuthenticator { - guard let privateInstance else { - fatalError("WordPressAuthenticator wasn't initialized") - } - - return privateInstance - } - - /// Authenticator's Delegate. - /// - public weak var delegate: WordPressAuthenticatorDelegate? - - /// Authenticator's Configuration. - /// - public let configuration: WordPressAuthenticatorConfiguration - - /// Authenticator's Styles. - /// - public let style: WordPressAuthenticatorStyle - - /// Authenticator's Styles for unified flows. - /// - public let unifiedStyle: WordPressAuthenticatorUnifiedStyle? - - /// Authenticator's Display Images. - /// - public let displayImages: WordPressAuthenticatorDisplayImages - - /// Authenticator's Display Texts. - /// - public let displayStrings: WordPressAuthenticatorDisplayStrings - - /// Notification to be posted whenever the signing flow completes. - /// - @objc public static let WPSigninDidFinishNotification = "WPSigninDidFinishNotification" - - /// The host name that identifies magic link URLs - /// - private static let magicLinkUrlHostname = "magic-login" - - // MARK: - Initialization - - /// Designated Initializer - /// - init(configuration: WordPressAuthenticatorConfiguration, - style: WordPressAuthenticatorStyle, - unifiedStyle: WordPressAuthenticatorUnifiedStyle?, - displayImages: WordPressAuthenticatorDisplayImages, - displayStrings: WordPressAuthenticatorDisplayStrings) { - self.configuration = configuration - self.style = style - self.unifiedStyle = unifiedStyle - self.displayImages = displayImages - self.displayStrings = displayStrings - } - - /// Initializes the WordPressAuthenticator with the specified Configuration. - /// - public static func initialize(configuration: WordPressAuthenticatorConfiguration, - style: WordPressAuthenticatorStyle, - unifiedStyle: WordPressAuthenticatorUnifiedStyle?, - displayImages: WordPressAuthenticatorDisplayImages = .defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings = .defaultStrings) { - privateInstance = WordPressAuthenticator(configuration: configuration, - style: style, - unifiedStyle: unifiedStyle, - displayImages: displayImages, - displayStrings: displayStrings) - } - - // MARK: - Testing Support - - class func isInitialized() -> Bool { - return privateInstance != nil - } - - // MARK: - Public Methods - - /// Indicates if the specified ViewController belongs to the Authentication Flow, or not. - /// - public class func isAuthenticationViewController(_ viewController: UIViewController) -> Bool { - return viewController is NUXViewControllerBase - } - - /// Indicates if the received URL is a Google Authentication Callback. - /// - @objc public func isGoogleAuthUrl(_ url: URL) -> Bool { - return url.absoluteString.hasPrefix(configuration.googleLoginScheme) - } - - /// Indicates if the received URL is a WordPress.com Authentication Callback. - /// - @objc public func isWordPressAuthUrl(_ url: URL) -> Bool { - let expectedPrefix = configuration.wpcomScheme + "://" + Self.magicLinkUrlHostname - return url.absoluteString.hasPrefix(expectedPrefix) - } - - /// Attempts to process the specified URL as a WordPress Authentication Link. Returns *true* on success. - /// - @objc public func handleWordPressAuthUrl(_ url: URL, rootViewController: UIViewController, automatedTesting: Bool = false) -> Bool { - return WordPressAuthenticator.openAuthenticationURL(url, fromRootViewController: rootViewController, automatedTesting: automatedTesting) - } - - // MARK: - Helpers for presenting the login flow - - /// Used to present the new login flow from the app delegate - @objc public class func showLoginFromPresenter(_ presenter: UIViewController, animated: Bool) { - showLogin(from: presenter, animated: animated) - } - - /// Shows login UI from the given presenter view controller. - /// - /// - Parameters: - /// - presenter: The view controller that presents the login UI. - /// - animated: Whether the login UI is presented with animation. - /// - showCancel: Whether a cancel CTA is shown on the login prologue screen. - /// - restrictToWPCom: Whether only WordPress.com login is enabled. - /// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped. - /// - onCompletion: Called when the login UI presentation completes. - public class func showLogin(from presenter: UIViewController, animated: Bool, showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, onCompletion: (() -> Void)? = nil) { - guard let loginViewController = loginUI(showCancel: showCancel, restrictToWPCom: restrictToWPCom, onLoginButtonTapped: onLoginButtonTapped) else { - return - } - presenter.present(loginViewController, animated: animated, completion: onCompletion) - trackOpenedLogin() - } - - /// Returns the view controller for the login flow. - /// The caller is responsible for tracking `.openedLogin` event when displaying the view controller as in `showLogin`. - /// - /// - Parameters: - /// - showCancel: Whether a cancel CTA is shown on the login prologue screen. - /// - restrictToWPCom: Whether only WordPress.com login is enabled. - /// - onLoginButtonTapped: Called when the login button on the prologue screen is tapped. - /// - Returns: The root view controller for the login flow. - public class func loginUI(showCancel: Bool = false, restrictToWPCom: Bool = false, onLoginButtonTapped: (() -> Void)? = nil, continueWithDotCom: ((UIViewController) -> Bool)? = nil, selfHostedSiteLogin: ((UIViewController) -> Bool)? = nil) -> UIViewController? { - let storyboard = Storyboard.login.instance - guard let controller = storyboard.instantiateInitialViewController() else { - assertionFailure("Cannot instantiate initial login controller from Login.storyboard") - return nil - } - - if let loginNavController = controller as? LoginNavigationController, let loginPrologueViewController = loginNavController.viewControllers.first as? LoginPrologueViewController { - loginPrologueViewController.showCancel = showCancel - loginPrologueViewController.continueWithDotComOverwrite = continueWithDotCom - loginPrologueViewController.selfHostedSiteLoginOverwrite = selfHostedSiteLogin - } - - controller.modalPresentationStyle = .fullScreen - return controller - } - - /// Used to present the new wpcom-only login flow from the app delegate - @objc public class func showLoginForJustWPCom(from presenter: UIViewController, jetpackLogin: Bool = false, connectedEmail: String? = nil, siteURL: String? = nil) { - defer { - trackOpenedLogin() - } - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - showEmailLogin(from: presenter, jetpackLogin: jetpackLogin, connectedEmail: connectedEmail, siteURL: siteURL) - return - } - - showGetStarted(from: presenter, jetpackLogin: jetpackLogin, connectedEmail: connectedEmail, siteURL: siteURL) - } - - /// Used to present the Verify Email flow from the app delegate. - /// - /// - Parameters: - /// - presenter: The view controller that presents the Verify Email view. - /// - xmlrpc: The URL to reach the XMLRPC file of the site to log in to. - /// - connectedEmail: The email address used to authorized Jetpack connection with the site. - /// - siteURL: The URL of the site to log in to. - /// - @objc public class func showVerifyEmailForWPCom(from presenter: UIViewController, xmlrpc: String, connectedEmail: String, siteURL: String) { - let loginFields = LoginFields() - loginFields.meta.xmlrpcURL = NSURL(string: xmlrpc) - loginFields.username = connectedEmail - loginFields.siteAddress = siteURL - - guard let vc = VerifyEmailViewController.instantiate(from: .verifyEmail) else { - WPLogError("Failed to navigate to VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - let navController = LoginNavigationController(rootViewController: vc) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Used to present the site credential login flow directly from the delegate. - /// - /// - Parameters: - /// - presenter: The view controller that presents the site credential login flow. - /// - siteURL: The URL of the site to log in to. - /// - onCompletion: The closure to be trigged when the login succeeds with the input credentials. - /// - public class func showSiteCredentialLogin(from presenter: UIViewController, siteURL: String, onCompletion: @escaping (WordPressOrgCredentials) -> Void) { - let controller = SiteCredentialsViewController.instantiate(from: .siteAddress) { coder in - SiteCredentialsViewController(coder: coder, isDismissible: true, onCompletion: onCompletion) - } - guard let controller else { - WPLogError("Failed to navigate from GetStartedViewController to SiteCredentialsViewController") - return - } - - let loginFields = LoginFields() - loginFields.siteAddress = siteURL - controller.loginFields = loginFields - controller.dismissBlock = { _ in - controller.navigationController?.dismiss(animated: true) - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// A helper method to fetch site info for a given URL. - /// - Parameters: - /// - siteURL: The URL of the site to fetch information for. - /// - onCompletion: The closure to be triggered when fetching site info is done. - /// - public class func fetchSiteInfo(for siteURL: String, onCompletion: @escaping (Result) -> Void) { - let service = WordPressComBlogService() - service.fetchUnauthenticatedSiteInfoForAddress(for: siteURL, success: { siteInfo in - onCompletion(.success(siteInfo)) - }, failure: { error in - onCompletion(.failure(error)) - }) - } - - /// Shows the unified Login/Signup flow. - /// - private class func showGetStarted(from presenter: UIViewController, jetpackLogin: Bool, connectedEmail: String? = nil, siteURL: String? = nil) { - guard let controller = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - - controller.loginFields.restrictToWPCom = true - controller.loginFields.username = connectedEmail ?? String() - controller.loginFields.meta.jetpackLogin = jetpackLogin - if let siteURL { - controller.loginFields.siteAddress = siteURL - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Shows the Email Login view with Signup option. - /// - private class func showEmailLogin(from presenter: UIViewController, jetpackLogin: Bool, connectedEmail: String? = nil, siteURL: String? = nil) { - guard let controller = LoginEmailViewController.instantiate(from: .login) else { - return - } - - controller.loginFields.restrictToWPCom = true - controller.loginFields.meta.jetpackLogin = jetpackLogin - if let siteURL { - controller.loginFields.siteAddress = siteURL - } - - if let email = connectedEmail { - controller.loginFields.username = email - } else { - controller.offerSignupOption = true - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - @objc public class func showLoginForSelfHostedSite(_ presenter: UIViewController) { - defer { - trackOpenedLogin() - } - - AuthenticatorAnalyticsTracker.shared.set(source: .selfHosted) - - guard let controller = signinForWPOrg() else { - WPLogError("WordPressAuthenticator: Failed to instantiate Site Address view controller.") - return - } - - let navController = LoginNavigationController(rootViewController: controller) - navController.modalPresentationStyle = .fullScreen - presenter.present(navController, animated: true, completion: nil) - } - - /// Returns a Site Address view controller: allows the user to log into a WordPress.org website. - /// - @objc public class func signinForWPOrg() -> UIViewController? { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - return LoginSiteAddressViewController.instantiate(from: .login) - } - - 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) - } - } - - @objc - public class func signinForWPCom(dotcomEmailAddress: String?, dotcomUsername: String?, onDismissed: ((_ cancelled: Bool) -> Void)? = nil) -> UIViewController { - let loginFields = LoginFields() - loginFields.emailAddress = dotcomEmailAddress ?? String() - loginFields.username = dotcomUsername ?? String() - - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth else { - guard let controller = LoginWPComViewController.instantiate(from: .login) else { - WPLogError("WordPressAuthenticator: Failed to instantiate LoginWPComViewController") - return UIViewController() - } - - controller.loginFields = loginFields - controller.dismissBlock = onDismissed - - return NUXNavigationController(rootViewController: controller) - } - - AuthenticatorAnalyticsTracker.shared.set(source: .reauthentication) - AuthenticatorAnalyticsTracker.shared.set(flow: .loginWithPassword) - - guard let controller = PasswordViewController.instantiate(from: .password) else { - WPLogError("WordPressAuthenticator: Failed to instantiate PasswordViewController") - return UIViewController() - } - - controller.loginFields = loginFields - controller.dismissBlock = onDismissed - controller.trackAsPasswordChallenge = false - - return NUXNavigationController(rootViewController: controller) - } - - /// Returns an instance of LoginEmailViewController. - /// This allows the host app to configure the controller's features. - /// - public class func signinForWPCom() -> LoginEmailViewController { - guard let controller = LoginEmailViewController.instantiate(from: .login) else { - fatalError() - } - - return controller - } - - private class func trackOpenedLogin() { - WordPressAuthenticator.track(.openedLogin) - } - - // MARK: - Authentication Link Helpers - - /// Present a signin view controller to handle an authentication link. - /// - /// - Parameters: - /// - url: The authentication URL - /// - rootViewController: The view controller to act as the presenter for the signin view controller. By convention this is the app's root vc. - /// - automatedTesting: for calling this method for automated testing. It won't sync the account or load any other VCs. - /// - @objc public class func openAuthenticationURL( - _ url: URL, - fromRootViewController rootViewController: UIViewController, - automatedTesting: Bool = false) -> Bool { - - guard let queryDictionary = url.query?.dictionaryFromQueryString() else { - WPLogError("Magic link error: we couldn't retrieve the query dictionary from the sign-in URL.") - return false - } - - guard let authToken = queryDictionary["token"] as? String else { - WPLogError("Magic link error: we couldn't retrieve the authentication token from the sign-in URL.") - return false - } - - guard let flowRawValue = queryDictionary["flow"] as? String else { - WPLogError("Magic link error: we couldn't retrieve the flow from the sign-in URL.") - return false - } - - let loginFields = LoginFields() - - if url.isJetpackConnect { - loginFields.meta.jetpackLogin = true - } - - // We could just use the flow, but since `MagicLinkFlow` is an ObjC enum, it always - // allows a `default` value. By mapping the ObjC enum to a Swift enum we can avoid that afterwards. - let flow: NUXLinkAuthViewController.Flow - - switch MagicLinkFlow(rawValue: flowRawValue) { - case .signup: - flow = .signup - loginFields.meta.emailMagicLinkSource = .signup - Self.track(.signupMagicLinkOpened) - case .login: - flow = .login - loginFields.meta.emailMagicLinkSource = .login - Self.track(.loginMagicLinkOpened) - default: - WPLogError("Magic link error: the flow should be either `signup` or `login`. We can't handle an unsupported flow.") - return false - } - - if !automatedTesting { - let storyboard = Storyboard.emailMagicLink.instance - guard let loginVC = storyboard.instantiateViewController(withIdentifier: "LinkAuthView") as? NUXLinkAuthViewController else { - WPLogInfo("App opened with authentication link but couldn't create login screen.") - return false - } - loginVC.loginFields = loginFields - - let navController = LoginNavigationController(rootViewController: loginVC) - navController.modalPresentationStyle = .fullScreen - - // The way the magic link flow works some view controller might - // still be presented when the app is resumed by tapping on the auth link. - // We need to do a little work to present the SigninLinkAuth controller - // from the right place. - // - If the rootViewController is not presenting another vc then just - // present the auth controller. - // - If the rootViewController is presenting another NUX vc, dismiss the - // NUX vc then present the auth controller. - // - If the rootViewController is presenting *any* other vc, present the - // auth controller from the presented vc. - let presenter = rootViewController.topmostPresentedViewController - if presenter.isKind(of: NUXNavigationController.self) || presenter.isKind(of: LoginNavigationController.self), - let parent = presenter.presentingViewController { - parent.dismiss(animated: false, completion: { - parent.present(navController, animated: false, completion: nil) - }) - } else { - presenter.present(navController, animated: false, completion: nil) - } - - loginVC.syncAndContinue(authToken: authToken, flow: flow, isJetpackConnect: url.isJetpackConnect) - } - - return true - } - - // MARK: - Site URL helper - - /// The base site URL path derived from `loginFields.siteUrl` - /// - /// - Parameter string: The source URL as a string. - /// - /// - Returns: The base URL or an empty string. - /// - public class func baseSiteURL(string: String) -> String { - - guard !string.isEmpty, - let siteURL = NSURL(string: NSURL.idnEncodedURL(string)), - var path = siteURL.absoluteString else { - return "" - } - - let isSiteURLSchemeEmpty = siteURL.scheme == nil || siteURL.scheme!.isEmpty - - if isSiteURLSchemeEmpty { - path = "https://\(path)" - } else if path.isWordPressComPath() && path.contains("http://") { - path = path.replacingOccurrences(of: "http://", with: "https://") - } - - path.removeSuffix("/wp-login.php") - - // Remove wp-admin and everything after it. - try? path.removeSuffix(pattern: "/wp-admin(.*)") - - path.removeSuffix("/") - - return path - } - - // MARK: - Other Helpers - - /// Opens Safari to display the forgot password page for a wpcom or self-hosted - /// based on the passed LoginFields instance. - /// - /// - Parameter loginFields: A LoginFields instance. - /// - public class func openForgotPasswordURL(_ loginFields: LoginFields) { - let baseURL = loginFields.meta.userIsDotCom ? "https://wordpress.com" : WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let forgotPasswordURL = URL(string: baseURL + "/wp-login.php?action=lostpassword&redirect_to=wordpress%3A%2F%2F")! - UIApplication.shared.open(forgotPasswordURL) - } - - public class var bundle: Bundle { - Bundle(for: WordPressAuthenticator.self) - } -} - -public extension WordPressAuthenticator { - - func getAppleIDCredentialState(for userID: String, completion: @escaping (ASAuthorizationAppleIDProvider.CredentialState, Error?) -> Void) { - AppleAuthenticator.sharedInstance.getAppleIDCredentialState(for: userID) { state, error in - // If credentialState == .notFound, error will have a value. - completion(state, error) - } - } - - func startObservingAppleIDCredentialRevoked(completion: @escaping () -> Void) { - appleIDCredentialObserver = NotificationCenter.default.addObserver(forName: AppleAuthenticator.credentialRevokedNotification, object: nil, queue: nil) { _ in - completion() - } - } - - func stopObservingAppleIDCredentialRevoked() { - if let observer = appleIDCredentialObserver { - NotificationCenter.default.removeObserver(observer) - } - appleIDCredentialObserver = nil - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift deleted file mode 100644 index 681f0d5fab3e..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorConfiguration.swift +++ /dev/null @@ -1,262 +0,0 @@ -import WordPressKit - -// MARK: - WordPressAuthenticator Configuration -// -public struct WordPressAuthenticatorConfiguration { - - /// WordPress.com Client ID - /// - let wpcomClientId: String - - /// WordPress.com Secret - /// - let wpcomSecret: String - - /// Client App: Used for Magic Link purposes. - /// - let wpcomScheme: String - - /// WordPress.com Terms of Service URL - /// - let wpcomTermsOfServiceURL: URL - - /// WordPress.com Base URL for OAuth - /// - let wpcomBaseURL: URL - - /// WordPress.com API Base URL - /// - let wpcomAPIBaseURL: URL - - /// The URL of a webpage which has details about What is WordPress.com?. - /// - /// Displayed in the WordPress.com login page. The button/link will not be displayed if this value is nil. - /// - let whatIsWPComURL: URL? - - /// GoogleLogin Client ID - /// - let googleLoginClientId: String - - /// GoogleLogin ServerClient ID - /// - let googleLoginServerClientId: String - - /// GoogleLogin Callback Scheme - /// - let googleLoginScheme: String - - internal var googleClientId: GoogleClientId { - guard let clientId = GoogleClientId(string: googleLoginClientId) else { - fatalError("Could not init GoogleClientId from developer provided value.") - } - - return clientId - } - - /// UserAgent - /// - let userAgent: String - - /// Flag indicating which Log In flow to display. - /// If enabled, when Log In is selected, a button view is displayed with options. - /// If disabled, when Log In is selected, the email login view is displayed with alternative options. - /// - let showLoginOptions: Bool - - /// Flag indicating if Sign Up UX is enabled for all services. - /// - let enableSignUp: Bool - - /// Hint buttons help users complete a step in the unified auth flow. Enabled by default. - /// If enabled, "Find your site address", "Reset your password", and others will be displayed. - /// If disabled, none of the hint buttons will appear on the unified auth flows. - let displayHintButtons: Bool - - /// Flag indicating if the Sign In With Apple option should be displayed. - /// - let enableSignInWithApple: Bool - - /// Flag indicating if signing up via Google is enabled. - /// This only applies to the unified Google flow. - /// When a user attempts to log in with a nonexistent account: - /// If enabled, the user will be redirected to Google signup. - /// If disabled, a view is displayed providing the user with other options. - /// - let enableSignupWithGoogle: Bool - - /// Flag for the unified login/signup flows. - /// If disabled, none of the unified flows will display. - /// If enabled, all unified flows will display. - /// - let enableUnifiedAuth: Bool - - /// Flag for the new prologue carousel. - /// If disabled, displays the old carousel. - /// If enabled, displays the new carousel. - let enableUnifiedCarousel: Bool - - /// Flag for the Passkeys, or WebAuthn, login. - let enablePasskeys: Bool - - /// Flag for the unified login/signup flows. - /// If disabled, the "Continue With WordPress" button in the login prologue is shown first. - /// If enabled, the "Enter your existing site address" button in the login prologue is shown first. - /// Default value is disabled - let continueWithSiteAddressFirst: Bool - - /// If enabled shows a "Sign in with site credentials" button in `GetStartedViewController` when landing in the screen after entering site address - /// Used to enable sign-in to self-hosted sites using WordPress.org credentials. - /// Disabled by default - let enableSiteCredentialsLoginForSelfHostedSites: Bool - - /// If enabled, we will ask for WPCOM login after signing in using .org site credentials. - /// Disabled by default - let isWPComLoginRequiredForSiteCredentialsLogin: Bool - - /// If enabled, a magic link is sent automatically in place of password then fall back to password. - /// If disabled, password is shown by default with an option to send a magic link. - let isWPComMagicLinkPreferredToPassword: Bool - - /// If enabled, the alternative magic link action on the password screen is shown as a secondary call-to-action at the bottom. - /// If disabled, the alternative magic link action on the password screen is shown below the reset password action. - let isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool - - /// If enabled, the Prologue screen will display only the entry point for WPCom login and no site address login. - /// - let enableWPComLoginOnlyInPrologue: Bool - - /// If enabled, an entry point to the site creation flow will be added to the bottom button of the prologue screen of simplified login. - /// - let enableSiteCreation: Bool - - /// If enabled, social login will be display at the bottom of the WPCom login screen. - /// - let enableSocialLogin: Bool - - /// If enabled, there will be a border around the email label on the WPCom password screen. - /// - let emphasizeEmailForWPComPassword: Bool - - /// The optional instructions for WPCom password. - /// - let wpcomPasswordInstructions: String? - - /// If enabled, site discovery will not check for XMLRPC URL. - /// - let skipXMLRPCCheckForSiteDiscovery: Bool - - /// If enabled, site address login will not check for XMLRPC URL. - /// - let skipXMLRPCCheckForSiteAddressLogin: Bool - - /// If enabled, the library will trigger the delegate method `handleSiteCredentialLogin` - /// instead of using the XMLRPC API for handling site credential login. - let enableManualSiteCredentialLogin: Bool - - /// If enabled, the library will not show any alert or inline error message - /// when site credential login fails. - /// Instead, the delegate method `handleSiteCredentialLoginFailure` will be called. - /// - let enableManualErrorHandlingForSiteCredentialLogin: Bool - - /// Used to determine the `step` value for `unified_login_step` analytics event in `GetStartedViewController` - /// - /// - If disabled `start` will be used as `step` value - /// - Disabled by default - /// - If enabled, `enter_email_address` will be used as `step` value - /// - Custom step value is used because `start` is used in other VCs as well, which doesn't allow us to differentiate between screens. - /// - i.e. Some screens have the same `step` and `flow` value. `GetStartedViewController` and `SiteAddressViewController` for example. - /// - let useEnterEmailAddressAsStepValueForGetStartedVC: Bool - - /// If enabled, the prologue screen should hide the WPCom login CTA and show only the entry point to site address login. - /// - let enableSiteAddressLoginOnlyInPrologue: Bool - - /// If enabled, the prologue screen would display a link for site creation guide. - /// - let enableSiteCreationGuide: Bool - - let disableAutofill: Bool - - /// Designated Initializer - /// - public init (wpcomClientId: String, - wpcomSecret: String, - wpcomScheme: String, - wpcomTermsOfServiceURL: URL, - wpcomBaseURL: URL = WordPressComOAuthClient.WordPressComOAuthDefaultBaseURL, - wpcomAPIBaseURL: URL = WordPressComOAuthClient.WordPressComOAuthDefaultApiBaseURL, - whatIsWPComURL: URL? = nil, - googleLoginClientId: String, - googleLoginServerClientId: String, - googleLoginScheme: String, - userAgent: String, - showLoginOptions: Bool = false, - enableSignUp: Bool = true, - enableSignInWithApple: Bool = false, - enableSignupWithGoogle: Bool = false, - enableUnifiedAuth: Bool = false, - enableUnifiedCarousel: Bool = false, - enablePasskeys: Bool = true, - displayHintButtons: Bool = true, - continueWithSiteAddressFirst: Bool = false, - enableSiteCredentialsLoginForSelfHostedSites: Bool = false, - isWPComLoginRequiredForSiteCredentialsLogin: Bool = false, - isWPComMagicLinkPreferredToPassword: Bool = false, - isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen: Bool = false, - enableWPComLoginOnlyInPrologue: Bool = false, - enableSiteCreation: Bool = false, - enableSocialLogin: Bool = false, - emphasizeEmailForWPComPassword: Bool = false, - wpcomPasswordInstructions: String? = nil, - skipXMLRPCCheckForSiteDiscovery: Bool = false, - skipXMLRPCCheckForSiteAddressLogin: Bool = false, - enableManualSiteCredentialLogin: Bool = false, - enableManualErrorHandlingForSiteCredentialLogin: Bool = false, - useEnterEmailAddressAsStepValueForGetStartedVC: Bool = false, - enableSiteAddressLoginOnlyInPrologue: Bool = false, - enableSiteCreationGuide: Bool = false, - disableAutofill: Bool = false - ) { - - self.wpcomClientId = wpcomClientId - self.wpcomSecret = wpcomSecret - self.wpcomScheme = wpcomScheme - self.wpcomTermsOfServiceURL = wpcomTermsOfServiceURL - self.wpcomBaseURL = wpcomBaseURL - self.wpcomAPIBaseURL = wpcomAPIBaseURL - self.whatIsWPComURL = whatIsWPComURL - self.googleLoginClientId = googleLoginClientId - self.googleLoginServerClientId = googleLoginServerClientId - self.googleLoginScheme = googleLoginScheme - self.userAgent = userAgent - self.showLoginOptions = showLoginOptions - self.enableSignUp = enableSignUp - self.enableSignInWithApple = enableSignInWithApple - self.enableUnifiedAuth = enableUnifiedAuth - self.enableUnifiedCarousel = enableUnifiedCarousel - self.enablePasskeys = enablePasskeys - self.displayHintButtons = displayHintButtons - self.enableSignupWithGoogle = enableSignupWithGoogle - self.continueWithSiteAddressFirst = continueWithSiteAddressFirst - self.enableSiteCredentialsLoginForSelfHostedSites = enableSiteCredentialsLoginForSelfHostedSites - self.isWPComLoginRequiredForSiteCredentialsLogin = isWPComLoginRequiredForSiteCredentialsLogin - self.isWPComMagicLinkPreferredToPassword = isWPComMagicLinkPreferredToPassword - self.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen = isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen - self.enableWPComLoginOnlyInPrologue = enableWPComLoginOnlyInPrologue - self.enableSiteCreation = enableSiteCreation - self.enableSocialLogin = enableSocialLogin - self.emphasizeEmailForWPComPassword = emphasizeEmailForWPComPassword - self.wpcomPasswordInstructions = wpcomPasswordInstructions - self.skipXMLRPCCheckForSiteDiscovery = skipXMLRPCCheckForSiteDiscovery - self.skipXMLRPCCheckForSiteAddressLogin = skipXMLRPCCheckForSiteAddressLogin - self.enableManualSiteCredentialLogin = enableManualSiteCredentialLogin - self.enableManualErrorHandlingForSiteCredentialLogin = enableManualErrorHandlingForSiteCredentialLogin - self.useEnterEmailAddressAsStepValueForGetStartedVC = useEnterEmailAddressAsStepValueForGetStartedVC - self.enableSiteAddressLoginOnlyInPrologue = enableSiteAddressLoginOnlyInPrologue - self.enableSiteCreationGuide = enableSiteCreationGuide - self.disableAutofill = disableAutofill - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift deleted file mode 100644 index 5c2724a8cd6c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDelegateProtocol.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - WordPressAuthenticator Delegate Protocol -// -public protocol WordPressAuthenticatorDelegate: AnyObject { - - /// Indicates if the active Authenticator can be dismissed, or not. - /// - var dismissActionEnabled: Bool { get } - - /// Indicates if the Support button action should be enabled, or not. - /// - var supportActionEnabled: Bool { get } - - /// Indicates if the WordPress.com's Terms of Service should be enabled, or not. - /// - var wpcomTermsOfServiceEnabled: Bool { get } - - /// Indicates if Support is available or not. - /// - var supportEnabled: Bool { get } - - /// Returns true if there isn't a default WordPress.com account connected in the app. - var allowWPComLogin: Bool { get } - - /// Signals the Host App that a new WordPress.com account has just been created. - /// - /// - Parameters: - /// - username: WordPress.com Username. - /// - authToken: WordPress.com Bearer Token. - /// - func createdWordPressComAccount(username: String, authToken: String) - - /// Signals the Host App that the user has successfully authenticated with an Apple account. - /// - /// - Parameters: - /// - appleUserID: User ID received in the Apple credentials. - /// - func userAuthenticatedWithAppleUserID(_ appleUserID: String) - - /// Presents the Support new request, from a given ViewController, with a specified SourceTag. - /// - func presentSupportRequest(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag) - - /// Signals to the Host App that a WordPress site is available and needs validated - /// before presenting the username and password view controller. - /// - Parameters: - /// - site: passes in the site information to the delegate method. - /// - onCompletion: Closure to be executed on completion. - /// - func shouldPresentUsernamePasswordController(for siteInfo: WordPressComSiteInfo?, onCompletion: @escaping (WordPressAuthenticatorResult) -> Void) - - /// Presents the Login Epilogue, in the specified NavigationController. - /// - /// - Parameters: - /// - navigationController: navigation stack for any epilogue views to be shown on. - /// - credentials: WPCOM or WPORG credentials. - /// - source: an optional identifier of the login flow, can be from the login prologue or provided by the host app. - /// - onDismiss: called when the auth flow is dismissed. - func presentLoginEpilogue(in navigationController: UINavigationController, for credentials: AuthenticatorCredentials, source: SignInSource?, onDismiss: @escaping () -> Void) - - /// Presents the Login Epilogue, in the specified NavigationController. - /// - func presentSignupEpilogue( - in navigationController: UINavigationController, - for credentials: AuthenticatorCredentials, - socialUser: SocialUser? - ) - - /// Presents the Support Interface from a given ViewController. - /// - /// - Parameters: - /// - from: ViewController from which to present the support interface from - /// - sourceTag: Support source tag of the view controller. - /// - lastStep: Last `Step` tracked in `AuthenticatorAnalyticsTracker` - /// - lastFlow: Last `Flow` tracked in `AuthenticatorAnalyticsTracker` - /// - func presentSupport(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag, lastStep: AuthenticatorAnalyticsTracker.Step, lastFlow: AuthenticatorAnalyticsTracker.Flow) - - /// Indicates if the Login Epilogue should be displayed. - /// - /// - Parameter isJetpackLogin: Indicates if we've just logged into a WordPress.com account for Jetpack purposes!. - /// - func shouldPresentLoginEpilogue(isJetpackLogin: Bool) -> Bool - - /// Indicates the Host app wants to handle and display a given error. - /// - func shouldHandleError(_ error: Error) -> Bool - - /// Signals the Host app that there is an error that needs to be handled. - /// - func handleError(_ error: Error, onCompletion: @escaping (UIViewController) -> Void) - - /// Indicates if the Signup Epilogue should be displayed. - /// - func shouldPresentSignupEpilogue() -> Bool - - /// Signals the Host App that a WordPress Site (wpcom or wporg) is available with the specified credentials. - /// - /// - Parameters: - /// - credentials: WordPress Site Credentials. - /// - onCompletion: Closure to be executed on completion. - /// - 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?) - - /// Sends site credentials to the host app so that it can handle login locally. - /// This method is only triggered when the config `skipXMLRPCCheckForSiteAddressLogin` is enabled. - /// - /// - Parameters: - /// - credentials: WordPress.org credentials submitted in the site credentials form. - /// - onLoading: the block to update the loading state on the site credentials form when necessary. - /// - onSuccess: the block to finish the login flow after login succeeds. - /// - onFailure: the block to trigger error handling. The closure accepts an error and a boolean indicating if the login failed with incorrect credentials. - /// - func handleSiteCredentialLogin(credentials: WordPressOrgCredentials, - onLoading: @escaping (Bool) -> Void, - onSuccess: @escaping () -> Void, - onFailure: @escaping (Error, Bool) -> Void) - - /// Signals to the Host App to handle an error for site credential login. - /// - /// - Parameters: - /// - error: The site credential login failure. - /// - siteURL: The site URL of the login failure. - /// - viewController: the view controller containing the site credential input. - /// - func handleSiteCredentialLoginFailure(error: Error, - for siteURL: String, - in viewController: UIViewController) - - /// Signals to the Host App to navigate to the site creation flow. - /// This method is currently used only in the simplified login flow - /// when the configs `enableSimplifiedLoginI1` and `enableSiteCreationForSimplifiedLoginI1` is enabled - /// - /// - Parameters: - /// - navigationController: the current navigation stack of the login flow. - /// - func showSiteCreation(in navigationController: UINavigationController) - - /// Signals to the Host App to navigate to the site creation guide. - /// This method triggered only if `enableSiteCreationGuide` config is enabled. - /// - /// - Parameters: - /// - navigationController: the current navigation stack of the login flow. - /// - func showSiteCreationGuide(in navigationController: UINavigationController) - - /// Signals the Host App that a given Analytics Event has occurred. - /// - func track(event: WPAnalyticsStat) - - /// Signals the Host App that a given Analytics Event (with the specified properties) has occurred. - /// - func track(event: WPAnalyticsStat, properties: [AnyHashable: Any]) - - /// Signals the Host App that a given Analytics Event (with an associated Error) has occurred. - /// - 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 - } - - func showSiteCreation(in navigationController: UINavigationController) { - // No-op - } - - func showSiteCreationGuide(in navigationController: UINavigationController) { - // No-op - } - - func handleSiteCredentialLogin(credentials: WordPressOrgCredentials, - onLoading: @escaping (Bool) -> Void, - onSuccess: @escaping () -> Void, - onFailure: @escaping (Error, Bool) -> Void) { - // No-op - } - - func handleSiteCredentialLoginFailure(error: Error, - for siteURL: String, - in viewController: UIViewController) { - // No-op - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift deleted file mode 100644 index 97c38fd3ab7f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayImages.swift +++ /dev/null @@ -1,19 +0,0 @@ -// MARK: - WordPress Authenticator Display Images -// -public struct WordPressAuthenticatorDisplayImages { - public let magicLink: UIImage - - /// Designated initializer. - /// - public init(magicLink: UIImage) { - self.magicLink = magicLink - } -} - -public extension WordPressAuthenticatorDisplayImages { - static var defaultImages: WordPressAuthenticatorDisplayImages { - return WordPressAuthenticatorDisplayImages( - magicLink: .magicLinkImage - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift deleted file mode 100644 index a160b331fee9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorDisplayStrings.swift +++ /dev/null @@ -1,258 +0,0 @@ -import Foundation - -// MARK: - WordPress Authenticator Display Strings -// -public struct WordPressAuthenticatorDisplayStrings { - /// Strings: Login instructions. - /// - public let emailLoginInstructions: String - public let getStartedInstructions: String - public let jetpackLoginInstructions: String - public let siteLoginInstructions: String - public let siteCredentialInstructions: String - public let usernamePasswordInstructions: String - public let twoFactorInstructions: String - public let twoFactorOtherFormsInstructions: String - public let magicLinkSignupInstructions: String - public let openMailSignupInstructions: String - public let openMailLoginInstructions: String - public let verifyMailLoginInstructions: String - public let alternativelyEnterPasswordInstructions: String - public let checkSpamInstructions: String - public let oopsInstructions: String - public let googleSignupInstructions: String - public let googlePasswordInstructions: String - public let applePasswordInstructions: String - - /// Strings: primary call-to-action button titles. - /// - public let continueButtonTitle: String - public let magicLinkButtonTitle: String - public let openMailButtonTitle: String - public let createAccountButtonTitle: String - public let continueWithWPButtonTitle: String - public let enterYourSiteAddressButtonTitle: String - public let signInWithSiteCredentialsButtonTitle: String - public let sendEmailVerificationLinkButtonTitle: String - public let loginWithAccountPasswordButtonTitle: String - - /// Large titles displayed in unified auth flows. - /// - public let getStartedTitle: String - public let logInTitle: String - public let signUpTitle: String - public let waitingForGoogleTitle: String - - /// Strings: secondary call-to-action button titles. - /// - public let findSiteButtonTitle: String - public let resetPasswordButtonTitle: String - public let getLoginLinkButtonTitle: String - public let textCodeButtonTitle: String - public let securityKeyButtonTitle: String - public let loginTermsOfService: String - public let signupTermsOfService: String - public let whatIsWPComLinkTitle: String - public let siteCreationButtonTitle: String - public let siteCreationGuideButtonTitle: String - - /// Placeholder text for textfields. - /// - public let usernamePlaceholder: String - public let passwordPlaceholder: String - public let siteAddressPlaceholder: String - public let twoFactorCodePlaceholder: String - public let emailAddressPlaceholder: String - - /// Designated initializer. - /// - public init(emailLoginInstructions: String = defaultStrings.emailLoginInstructions, - getStartedInstructions: String = defaultStrings.getStartedInstructions, - jetpackLoginInstructions: String = defaultStrings.jetpackLoginInstructions, - siteLoginInstructions: String = defaultStrings.siteLoginInstructions, - siteCredentialInstructions: String = defaultStrings.siteCredentialInstructions, - usernamePasswordInstructions: String = defaultStrings.usernamePasswordInstructions, - twoFactorInstructions: String = defaultStrings.twoFactorInstructions, - twoFactorOtherFormsInstructions: String = defaultStrings.twoFactorOtherFormsInstructions, - magicLinkSignupInstructions: String = defaultStrings.magicLinkSignupInstructions, - openMailSignupInstructions: String = defaultStrings.openMailSignupInstructions, - openMailLoginInstructions: String = defaultStrings.openMailLoginInstructions, - verifyMailLoginInstructions: String = defaultStrings.verifyMailLoginInstructions, - alternativelyEnterPasswordInstructions: String = defaultStrings.alternativelyEnterPasswordInstructions, - checkSpamInstructions: String = defaultStrings.checkSpamInstructions, - oopsInstructions: String = defaultStrings.oopsInstructions, - googleSignupInstructions: String = defaultStrings.googleSignupInstructions, - googlePasswordInstructions: String = defaultStrings.googlePasswordInstructions, - applePasswordInstructions: String = defaultStrings.applePasswordInstructions, - continueButtonTitle: String = defaultStrings.continueButtonTitle, - magicLinkButtonTitle: String = defaultStrings.magicLinkButtonTitle, - openMailButtonTitle: String = defaultStrings.openMailButtonTitle, - createAccountButtonTitle: String = defaultStrings.createAccountButtonTitle, - continueWithWPButtonTitle: String = defaultStrings.continueWithWPButtonTitle, - enterYourSiteAddressButtonTitle: String = defaultStrings.enterYourSiteAddressButtonTitle, - signInWithSiteCredentialsButtonTitle: String = defaultStrings.signInWithSiteCredentialsButtonTitle, - sendEmailVerificationLinkButtonTitle: String = defaultStrings.sendEmailVerificationLinkButtonTitle, - loginWithAccountPasswordButtonTitle: String = defaultStrings.loginWithAccountPasswordButtonTitle, - findSiteButtonTitle: String = defaultStrings.findSiteButtonTitle, - resetPasswordButtonTitle: String = defaultStrings.resetPasswordButtonTitle, - getLoginLinkButtonTitle: String = defaultStrings.getLoginLinkButtonTitle, - textCodeButtonTitle: String = defaultStrings.textCodeButtonTitle, - securityKeyButtonTitle: String = defaultStrings.securityKeyButtonTitle, - loginTermsOfService: String = defaultStrings.loginTermsOfService, - signupTermsOfService: String = defaultStrings.signupTermsOfService, - whatIsWPComLinkTitle: String = defaultStrings.whatIsWPComLinkTitle, - siteCreationButtonTitle: String = defaultStrings.siteCreationButtonTitle, - getStartedTitle: String = defaultStrings.getStartedTitle, - logInTitle: String = defaultStrings.logInTitle, - signUpTitle: String = defaultStrings.signUpTitle, - waitingForGoogleTitle: String = defaultStrings.waitingForGoogleTitle, - usernamePlaceholder: String = defaultStrings.usernamePlaceholder, - passwordPlaceholder: String = defaultStrings.passwordPlaceholder, - siteAddressPlaceholder: String = defaultStrings.siteAddressPlaceholder, - twoFactorCodePlaceholder: String = defaultStrings.twoFactorCodePlaceholder, - emailAddressPlaceholder: String = defaultStrings.emailAddressPlaceholder, - siteCreationGuideButtonTitle: String = defaultStrings.siteCreationGuideButtonTitle) { - self.emailLoginInstructions = emailLoginInstructions - self.getStartedInstructions = getStartedInstructions - self.jetpackLoginInstructions = jetpackLoginInstructions - self.siteLoginInstructions = siteLoginInstructions - self.siteCredentialInstructions = siteCredentialInstructions - self.usernamePasswordInstructions = usernamePasswordInstructions - self.twoFactorInstructions = twoFactorInstructions - self.twoFactorOtherFormsInstructions = twoFactorOtherFormsInstructions - self.magicLinkSignupInstructions = magicLinkSignupInstructions - self.openMailSignupInstructions = openMailSignupInstructions - self.openMailLoginInstructions = openMailLoginInstructions - self.verifyMailLoginInstructions = verifyMailLoginInstructions - self.alternativelyEnterPasswordInstructions = alternativelyEnterPasswordInstructions - self.checkSpamInstructions = checkSpamInstructions - self.oopsInstructions = oopsInstructions - self.googleSignupInstructions = googleSignupInstructions - self.googlePasswordInstructions = googlePasswordInstructions - self.applePasswordInstructions = applePasswordInstructions - self.continueButtonTitle = continueButtonTitle - self.magicLinkButtonTitle = magicLinkButtonTitle - self.openMailButtonTitle = openMailButtonTitle - self.createAccountButtonTitle = createAccountButtonTitle - self.continueWithWPButtonTitle = continueWithWPButtonTitle - self.enterYourSiteAddressButtonTitle = enterYourSiteAddressButtonTitle - self.signInWithSiteCredentialsButtonTitle = signInWithSiteCredentialsButtonTitle - self.sendEmailVerificationLinkButtonTitle = sendEmailVerificationLinkButtonTitle - self.loginWithAccountPasswordButtonTitle = loginWithAccountPasswordButtonTitle - self.findSiteButtonTitle = findSiteButtonTitle - self.resetPasswordButtonTitle = resetPasswordButtonTitle - self.getLoginLinkButtonTitle = getLoginLinkButtonTitle - self.textCodeButtonTitle = textCodeButtonTitle - self.securityKeyButtonTitle = securityKeyButtonTitle - self.loginTermsOfService = loginTermsOfService - self.signupTermsOfService = signupTermsOfService - self.whatIsWPComLinkTitle = whatIsWPComLinkTitle - self.siteCreationButtonTitle = siteCreationButtonTitle - self.getStartedTitle = getStartedTitle - self.logInTitle = logInTitle - self.signUpTitle = signUpTitle - self.waitingForGoogleTitle = waitingForGoogleTitle - self.usernamePlaceholder = usernamePlaceholder - self.passwordPlaceholder = passwordPlaceholder - self.siteAddressPlaceholder = siteAddressPlaceholder - self.twoFactorCodePlaceholder = twoFactorCodePlaceholder - self.emailAddressPlaceholder = emailAddressPlaceholder - self.siteCreationGuideButtonTitle = siteCreationGuideButtonTitle - } -} - -public extension WordPressAuthenticatorDisplayStrings { - static var defaultStrings: WordPressAuthenticatorDisplayStrings { - return WordPressAuthenticatorDisplayStrings( - emailLoginInstructions: NSLocalizedString("Log in to your WordPress.com account with your email address.", - comment: "Instruction text on the login's email address screen."), - getStartedInstructions: NSLocalizedString("Enter your email address to log in or create a WordPress.com account.", - comment: "Instruction text on the initial email address entry screen."), - jetpackLoginInstructions: NSLocalizedString("Log in to the WordPress.com account you used to connect Jetpack.", - comment: "Instruction text on the login's email address screen."), - siteLoginInstructions: NSLocalizedString("Enter the address of the WordPress site you'd like to connect.", - comment: "Instruction text on the login's site addresss screen."), - siteCredentialInstructions: NSLocalizedString("Enter your account information for %@.", - comment: "Enter your account information for {site url}. Asks the user to enter a username and password for their self-hosted site."), - usernamePasswordInstructions: NSLocalizedString("Log in with your WordPress.com username and password.", - comment: "Instructions on the WordPress.com username / password log in form."), - twoFactorInstructions: NSLocalizedString("login.twoFactorInstructions.details", value: "Please enter the verification code from your authentication app for your WordPress.com account.", comment: "Instruction label in the two-factor authorization screen WordPress.com login authentication. Note: it has to mention that it's for a WordPress.com account."), - twoFactorOtherFormsInstructions: NSLocalizedString("Or choose another form of authentication.", - comment: "Instruction text for other forms of two-factor auth methods."), - magicLinkSignupInstructions: NSLocalizedString("We'll email you a signup link to create your new WordPress.com account.", - comment: "Instruction text on the Sign Up screen."), - openMailSignupInstructions: NSLocalizedString("We've emailed you a signup link to create your new WordPress.com account. Check your email on this device, and tap the link in the email you receive from WordPress.com.", - comment: "Instruction text after a signup Magic Link was requested."), - openMailLoginInstructions: NSLocalizedString("Check your email on this device, and tap the link in the email you receive from WordPress.com.", - comment: "Instruction text after a login Magic Link was requested."), - verifyMailLoginInstructions: NSLocalizedString("A WordPress.com account is connected to your store credentials. To continue, we will send a verification link to the email address above.", - comment: "Instruction text to explain magic link login step."), - alternativelyEnterPasswordInstructions: NSLocalizedString("Alternatively, you may enter the password for this account.", - comment: "Instruction text to explain to help users type their password instead of using magic link login option."), - checkSpamInstructions: NSLocalizedString("Not seeing the email? Check your Spam or Junk Mail folder.", comment: "Instructions after a Magic Link was sent, but the email can't be found in their inbox."), - oopsInstructions: NSLocalizedString("Didn't mean to create a new account? Go back to re-enter your email address.", comment: "Instructions after a Magic Link was sent, but email is incorrect."), - googleSignupInstructions: NSLocalizedString("We'll use this email address to create your new WordPress.com account.", comment: "Text confirming email address to be used for new account."), - googlePasswordInstructions: NSLocalizedString("To proceed with this Google account, please first log in with your WordPress.com password. This will only be asked once.", - comment: "Instructional text shown when requesting the user's password for Google login."), - applePasswordInstructions: NSLocalizedString("To proceed with this Apple ID, please first log in with your WordPress.com password. This will only be asked once.", - comment: "Instructional text shown when requesting the user's password for Apple login."), - continueButtonTitle: NSLocalizedString("Continue", - comment: "The button title text when there is a next step for logging in or signing up."), - magicLinkButtonTitle: NSLocalizedString("Send Link by Email", - comment: "The button title text for sending a magic link."), - openMailButtonTitle: NSLocalizedString("Open Mail", - comment: "The button title text for opening the user's preferred email app."), - createAccountButtonTitle: NSLocalizedString("Create Account", - comment: "The button title text for creating a new account."), - continueWithWPButtonTitle: NSLocalizedString("Log in or sign up with WordPress.com", - comment: "Button title. Takes the user to the login by email flow."), - enterYourSiteAddressButtonTitle: NSLocalizedString("Enter your existing site address", - comment: "Button title. Takes the user to the login by site address flow."), - signInWithSiteCredentialsButtonTitle: NSLocalizedString("Sign in with site credentials", - comment: "Button title. Takes the user the Enter site credentials screen."), - sendEmailVerificationLinkButtonTitle: NSLocalizedString("Send email verification link", - comment: "Button title. Sends a email verification link (Magin link) for signing in."), - loginWithAccountPasswordButtonTitle: NSLocalizedString("Login with account password", - comment: "Button title. Takes the user to the Enter account password screen."), - findSiteButtonTitle: NSLocalizedString("Find your site address", - comment: "The hint button's title text to help users find their site address."), - resetPasswordButtonTitle: NSLocalizedString("Reset your password", - comment: "The button title for a secondary call-to-action button. When the user can't remember their password."), - getLoginLinkButtonTitle: NSLocalizedString("Get a login link by email", - comment: "The button title for a secondary call-to-action button. When the user wants to try sending a magic link instead of entering a password."), - textCodeButtonTitle: NSLocalizedString("Text me a code via SMS", - comment: "The button's title text to send a 2FA code via SMS text message."), - securityKeyButtonTitle: NSLocalizedString("Use a security key", - comment: "The button's title text to use a security key."), - loginTermsOfService: NSLocalizedString("By continuing, you agree to our _Terms of Service_.", comment: "Legal disclaimer for logging in. The underscores _..._ denote underline."), - signupTermsOfService: NSLocalizedString("If you continue with Apple or Google and don't already have a WordPress.com account, you are creating an account and you agree to our _Terms of Service_.", comment: "Legal disclaimer for signing up. The underscores _..._ denote underline."), - whatIsWPComLinkTitle: NSLocalizedString("What is WordPress.com?", - comment: "Navigates to page with details about What is WordPress.com."), - siteCreationButtonTitle: NSLocalizedString("Create a Site", - comment: "Navigates to a new flow for site creation."), - getStartedTitle: NSLocalizedString("Get Started", - comment: "View title for initial auth views."), - logInTitle: NSLocalizedString("Log In", - comment: "View title during the log in process."), - signUpTitle: NSLocalizedString("Sign Up", - comment: "View title during the sign up process."), - waitingForGoogleTitle: NSLocalizedString("Waiting...", - comment: "View title during the Google auth process."), - usernamePlaceholder: NSLocalizedString("Username", - comment: "Placeholder for the username textfield."), - passwordPlaceholder: NSLocalizedString("Password", - comment: "Placeholder for the password textfield."), - siteAddressPlaceholder: NSLocalizedString("example.com", - comment: "Placeholder for the site url textfield."), - twoFactorCodePlaceholder: NSLocalizedString("Authentication code", - comment: "Placeholder for the 2FA code textfield."), - emailAddressPlaceholder: NSLocalizedString("Email address", - comment: "Placeholder for the email address textfield."), - siteCreationGuideButtonTitle: NSLocalizedString( - "wordPressAuthenticatorDisplayStrings.default.siteCreationGuideButtonTitle", - value: "Starting a new site?", - comment: "Title for the link for site creation guide." - ) - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift deleted file mode 100644 index e8497064271a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorResult.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -/// Provides options for clients of WordPressAuthenticator -/// to signal what they expect WPAuthenticator to do in response to -/// `shouldPresentUsernamePasswordController` -/// -/// @see WordPressAuthenticatorDelegate.shouldPresentUsernamePasswordController -public enum WordPressAuthenticatorResult { - - /// An error - /// - case error(value: Error) - - /// Boolean flag to indicate if UI providing entry for username and passsword - /// should be presented - /// - case presentPasswordController(value: Bool) - - /// Present the view controller requesting the email address - /// associated to the user's wordpress.com account - /// - case presentEmailController - - /// A view controller to be inserted into the navigation stack - /// - case injectViewController(value: UIViewController) -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift deleted file mode 100644 index 3d9214100967..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressAuthenticatorStyles.swift +++ /dev/null @@ -1,305 +0,0 @@ -import UIKit -import Gridicons -import WordPressShared - -// MARK: - WordPress Authenticator Styles -// -public struct WordPressAuthenticatorStyle { - /// Style: Primary + Normal State - /// - public let primaryNormalBackgroundColor: UIColor - - public let primaryNormalBorderColor: UIColor? - - /// Style: Primary + Highlighted State - /// - public let primaryHighlightBackgroundColor: UIColor - - public let primaryHighlightBorderColor: UIColor? - - /// Style: Secondary - /// - public let secondaryNormalBackgroundColor: UIColor - - public let secondaryNormalBorderColor: UIColor - - public let secondaryHighlightBackgroundColor: UIColor - - public let secondaryHighlightBorderColor: UIColor - - /// Style: Disabled State - /// - public let disabledBackgroundColor: UIColor - - public let disabledBorderColor: UIColor - - public let primaryTitleColor: UIColor - - public let secondaryTitleColor: UIColor - - public let disabledTitleColor: UIColor - - /// Color of the spinner that is shown when a button is disabled. - public let disabledButtonActivityIndicatorColor: UIColor - - /// Style: Text Buttons - /// - public let textButtonColor: UIColor - - public let textButtonHighlightColor: UIColor - - /// Style: Labels - /// - public let instructionColor: UIColor - - public let subheadlineColor: UIColor - - public let placeholderColor: UIColor - - /// Style: Login screen background colors - /// - public let viewControllerBackgroundColor: UIColor - - public let textFieldBackgroundColor: UIColor - - // If not specified, falls back to viewControllerBackgroundColor. - public let buttonViewBackgroundColor: UIColor - - /// Style: shadow image view on top of the button view like a divider. - /// If not specified, falls back to image "darkgrey-shadow". - /// - public let buttonViewTopShadowImage: UIImage? - - /// Style: nav bar - /// - public let navBarImage: UIImage - - public let navBarBadgeColor: UIColor - - public let navBarBackgroundColor: UIColor - - public let navButtonTextColor: UIColor - - /// Style: prologue background colors - /// - public let prologueBackgroundColor: UIColor - - /// Style: optional prologue background image - /// - public let prologueBackgroundImage: UIImage? - - /// Style: prologue background colors - /// - public let prologueTitleColor: UIColor - - /// Style: optional prologue buttons blur effect - public let prologueButtonsBlurEffect: UIBlurEffect? - - /// Style: primary button on the prologue view (continue) - /// When `nil` it will use the primary styles defined here - /// Defaults to `nil` - /// - public let prologuePrimaryButtonStyle: NUXButtonStyle? - - /// Style: secondary button on the prologue view (site address) - /// When `nil` it will use the secondary styles defined here - /// Defaults to `nil` - /// - public let prologueSecondaryButtonStyle: NUXButtonStyle? - - /// Style: prologue top container child view controller - /// When nil, `LoginProloguePageViewController` is displayed in the top container - /// - public let prologueTopContainerChildViewController: () -> UIViewController? - - /// Style: status bar style - /// - public let statusBarStyle: UIStatusBarStyle - - /// Style: OR divider separator color - /// - /// Used in `NUXStackedButtonsViewController` - /// - public let orDividerSeparatorColor: UIColor - - /// Style: OR divider text color - /// - /// Used in `NUXStackedButtonsViewController` - /// - public let orDividerTextColor: UIColor - - /// Designated initializer - /// - public init(primaryNormalBackgroundColor: UIColor, - primaryNormalBorderColor: UIColor?, - primaryHighlightBackgroundColor: UIColor, - primaryHighlightBorderColor: UIColor?, - secondaryNormalBackgroundColor: UIColor, - secondaryNormalBorderColor: UIColor, - secondaryHighlightBackgroundColor: UIColor, - secondaryHighlightBorderColor: UIColor, - disabledBackgroundColor: UIColor, - disabledBorderColor: UIColor, - primaryTitleColor: UIColor, - secondaryTitleColor: UIColor, - disabledTitleColor: UIColor, - disabledButtonActivityIndicatorColor: UIColor, - textButtonColor: UIColor, - textButtonHighlightColor: UIColor, - instructionColor: UIColor, - subheadlineColor: UIColor, - placeholderColor: UIColor, - viewControllerBackgroundColor: UIColor, - textFieldBackgroundColor: UIColor, - buttonViewBackgroundColor: UIColor? = nil, - buttonViewTopShadowImage: UIImage? = UIImage(named: "darkgrey-shadow"), - navBarImage: UIImage, - navBarBadgeColor: UIColor, - navBarBackgroundColor: UIColor, - navButtonTextColor: UIColor = .white, - prologueBackgroundColor: UIColor = WPStyleGuide.wordPressBlue(), - prologueBackgroundImage: UIImage? = nil, - prologueTitleColor: UIColor = .white, - prologueButtonsBlurEffect: UIBlurEffect? = nil, - prologuePrimaryButtonStyle: NUXButtonStyle? = nil, - prologueSecondaryButtonStyle: NUXButtonStyle? = nil, - prologueTopContainerChildViewController: @autoclosure @escaping () -> UIViewController? = nil, - statusBarStyle: UIStatusBarStyle = .lightContent, - orDividerSeparatorColor: UIColor = .tertiaryLabel, - orDividerTextColor: UIColor = .secondaryLabel) { - self.primaryNormalBackgroundColor = primaryNormalBackgroundColor - self.primaryNormalBorderColor = primaryNormalBorderColor - self.primaryHighlightBackgroundColor = primaryHighlightBackgroundColor - self.primaryHighlightBorderColor = primaryHighlightBorderColor - self.secondaryNormalBackgroundColor = secondaryNormalBackgroundColor - self.secondaryNormalBorderColor = secondaryNormalBorderColor - self.secondaryHighlightBackgroundColor = secondaryHighlightBackgroundColor - self.secondaryHighlightBorderColor = secondaryHighlightBorderColor - self.disabledBackgroundColor = disabledBackgroundColor - self.disabledBorderColor = disabledBorderColor - self.primaryTitleColor = primaryTitleColor - self.secondaryTitleColor = secondaryTitleColor - self.disabledTitleColor = disabledTitleColor - self.disabledButtonActivityIndicatorColor = disabledButtonActivityIndicatorColor - self.textButtonColor = textButtonColor - self.textButtonHighlightColor = textButtonHighlightColor - self.instructionColor = instructionColor - self.subheadlineColor = subheadlineColor - self.placeholderColor = placeholderColor - self.viewControllerBackgroundColor = viewControllerBackgroundColor - self.textFieldBackgroundColor = textFieldBackgroundColor - self.buttonViewBackgroundColor = buttonViewBackgroundColor ?? viewControllerBackgroundColor - self.buttonViewTopShadowImage = buttonViewTopShadowImage - self.navBarImage = navBarImage - self.navBarBadgeColor = navBarBadgeColor - self.navBarBackgroundColor = navBarBackgroundColor - self.navButtonTextColor = navButtonTextColor - self.prologueBackgroundColor = prologueBackgroundColor - self.prologueBackgroundImage = prologueBackgroundImage - self.prologueTitleColor = prologueTitleColor - self.prologueButtonsBlurEffect = prologueButtonsBlurEffect - self.prologuePrimaryButtonStyle = prologuePrimaryButtonStyle - self.prologueSecondaryButtonStyle = prologueSecondaryButtonStyle - self.prologueTopContainerChildViewController = prologueTopContainerChildViewController - self.statusBarStyle = statusBarStyle - self.orDividerSeparatorColor = orDividerSeparatorColor - self.orDividerTextColor = orDividerTextColor - } -} - -// MARK: - WordPress Unified Authenticator Styles -// -// Styles specifically for the unified auth flows. -// -public struct WordPressAuthenticatorUnifiedStyle { - - /// Style: Auth view border colors - /// - public let borderColor: UIColor - - /// Style Auth default error color - /// - public let errorColor: UIColor - - /// Style: Auth default text color - /// - public let textColor: UIColor - - /// Style: Auth subtle text color - /// - public let textSubtleColor: UIColor - - /// Style: Auth plain text button normal state color - /// - public let textButtonColor: UIColor - - /// Style: Auth plain text button highlight state color - /// - public let textButtonHighlightColor: UIColor - - /// Style: Auth view background colors - /// - public let viewControllerBackgroundColor: UIColor - - /// Style: Auth Prologue buttons background color - public let prologueButtonsBackgroundColor: UIColor - - /// Style: Auth Prologue view background color - public let prologueViewBackgroundColor: UIColor - - /// Style: optional auth Prologue view background image - public let prologueBackgroundImage: UIImage? - - /// Style: optional blur effect for the buttons view - public let prologueButtonsBlurEffect: UIBlurEffect? - - /// Style: Status bar style. Defaults to `default`. - /// - public let statusBarStyle: UIStatusBarStyle - - /// Style: Navigation bar. - /// - public let navBarBackgroundColor: UIColor - public let navButtonTextColor: UIColor - public let navTitleTextColor: UIColor - - /// Style: Text color to be used for email in `GravatarEmailTableViewCell` - /// - public let gravatarEmailTextColor: UIColor? - - /// Designated initializer - /// - public init(borderColor: UIColor, - errorColor: UIColor, - textColor: UIColor, - textSubtleColor: UIColor, - textButtonColor: UIColor, - textButtonHighlightColor: UIColor, - viewControllerBackgroundColor: UIColor, - prologueButtonsBackgroundColor: UIColor = .clear, - prologueViewBackgroundColor: UIColor? = nil, - prologueBackgroundImage: UIImage? = nil, - prologueButtonsBlurEffect: UIBlurEffect? = nil, - statusBarStyle: UIStatusBarStyle = .default, - navBarBackgroundColor: UIColor, - navButtonTextColor: UIColor, - navTitleTextColor: UIColor, - gravatarEmailTextColor: UIColor? = nil) { - self.borderColor = borderColor - self.errorColor = errorColor - self.textColor = textColor - self.textSubtleColor = textSubtleColor - self.textButtonColor = textButtonColor - self.textButtonHighlightColor = textButtonHighlightColor - self.viewControllerBackgroundColor = viewControllerBackgroundColor - self.prologueButtonsBackgroundColor = prologueButtonsBackgroundColor - self.prologueViewBackgroundColor = prologueViewBackgroundColor ?? viewControllerBackgroundColor - self.prologueBackgroundImage = prologueBackgroundImage - self.prologueButtonsBlurEffect = prologueButtonsBlurEffect - self.statusBarStyle = statusBarStyle - self.navBarBackgroundColor = navBarBackgroundColor - self.navButtonTextColor = navButtonTextColor - self.navTitleTextColor = navTitleTextColor - self.gravatarEmailTextColor = gravatarEmailTextColor - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift b/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift deleted file mode 100644 index 6df18dee9e76..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Authenticator/WordPressSupportSourceTag.swift +++ /dev/null @@ -1,84 +0,0 @@ -import Foundation - -// MARK: - Authentication Flow Event. Useful to relay internal Auth events over to activity trackers. -// -public struct WordPressSupportSourceTag { - public let name: String - public let origin: String? - - public init(name: String, origin: String? = nil) { - self.name = name - self.origin = origin - } -} - -func ==(lhs: WordPressSupportSourceTag, rhs: WordPressSupportSourceTag) -> Bool { - return lhs.name == rhs.name -} - -extension WordPressSupportSourceTag { - public static var generalLogin: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "generalLogin", origin: "origin:login-screen") - } - public static var jetpackLogin: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "jetpackLogin", origin: "origin:jetpack-login-screen") - } - public static var loginEmail: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginEmail", origin: "origin:login-email") - } - public static var loginApple: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginApple", origin: "origin:login-apple") - } - public static var login2FA: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "login2FA", origin: "origin:login-2fa") - } - public static var loginWebauthn: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWebauthn", origin: "origin:login-webauthn") - } - public static var loginMagicLink: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginMagicLink", origin: "origin:login-magic-link") - } - public static var loginSiteAddress: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginSiteAddress", origin: "origin:login-site-address") - } - - /// For `VerifyEmailViewController` - public static var verifyEmailInstructions: WordPressSupportSourceTag { - WordPressSupportSourceTag(name: "verifyEmailInstructions", origin: "origin:login-site-address") - } - - public static var loginUsernamePassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginUsernamePassword", origin: "origin:login-username-password") - } - public static var loginWPComUsernamePassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWPComUsernamePassword", origin: "origin:wpcom-login-username-password") - } - public static var loginWPComPassword: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "loginWPComPassword", origin: "origin:login-wpcom-password") - } - public static var wpComSignupEmail: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupEmail", origin: "origin:wpcom-signup-email-entry") - } - public static var wpComSignup: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignup", origin: "origin:signup-screen") - } - public static var wpComSignupWaitingForGoogle: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupWaitingForGoogle", origin: "origin:signup-waiting-for-google") - } - public static var wpComAuthWaitingForGoogle: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComAuthWaitingForGoogle", origin: "origin:auth-waiting-for-google") - } - public static var wpComAuthGoogleSignupConfirmation: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComAuthGoogleSignupConfirmation", origin: "origin:auth-google-signup-confirmation") - } - public static var wpComSignupMagicLink: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupMagicLink", origin: "origin:signup-magic-link") - } - public static var wpComSignupApple: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComSignupApple", origin: "origin:signup-apple") - } - - public static var wpComLoginMagicLinkAutoRequested: WordPressSupportSourceTag { - return WordPressSupportSourceTag(name: "wpComLoginMagicLinkAutoRequested", origin: "origin:login-email") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift deleted file mode 100644 index 0a9963e6118a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/AuthenticatorCredentials.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -// MARK: - Authenticator Credentials -// -public struct AuthenticatorCredentials { - /// WordPress.com credentials - /// - public let wpcom: WordPressComCredentials? - - /// Self-hosted site credentials - /// - public let wporg: WordPressOrgCredentials? - - /// Designated initializer - /// - public init(wpcom: WordPressComCredentials? = nil, wporg: WordPressOrgCredentials? = nil) { - self.wpcom = wpcom - self.wporg = wporg - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift deleted file mode 100644 index 205a42191344..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressComCredentials.swift +++ /dev/null @@ -1,42 +0,0 @@ -import Foundation - -// MARK: - WordPress.com Credentials -// -public struct WordPressComCredentials: Equatable { - - /// WordPress.com authentication token - /// - public let authToken: String - - /// Is this a Jetpack-connected site? - /// - public let isJetpackLogin: Bool - - /// Is 2-factor Authentication Enabled? - /// - public let multifactor: Bool - - /// The site address used during login - /// - public var siteURL: String - - private let wpComURL = "https://wordpress.com" - - /// Legacy initializer, for backwards compatibility - /// - public init(authToken: String, - isJetpackLogin: Bool, - multifactor: Bool, - siteURL: String = "https://wordpress.com") { - self.authToken = authToken - self.isJetpackLogin = isJetpackLogin - self.multifactor = multifactor - self.siteURL = !siteURL.isEmpty ? siteURL : wpComURL - } -} - -// MARK: - Equatable Conformance -// -public func ==(lhs: WordPressComCredentials, rhs: WordPressComCredentials) -> Bool { - return lhs.authToken == rhs.authToken && lhs.siteURL == rhs.siteURL -} diff --git a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift deleted file mode 100644 index 6d951c14cd7a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Credentials/WordPressOrgCredentials.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -// MARK: - WordPress.org (aka self-hosted site) Credentials -// -public struct WordPressOrgCredentials: Equatable { - /// Self-hosted login username. - /// The one used in the /wp-admin/ panel. - /// - public let username: String - - /// Self-hosted login password. - /// The one used in the /wp-admin/ panel. - /// - public let password: String - - /// The URL to reach the XMLRPC file. - /// e.g.: https://exmaple.com/xmlrpc.php - /// - public let xmlrpc: String - - /// Self-hosted site options - /// - public let options: [AnyHashable: Any] - - /// Designated initializer - /// - public init(username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - self.username = username - self.password = password - self.xmlrpc = xmlrpc - self.options = options - } - - /// Returns site URL by stripping "/xmlrpc.php" from `xmlrpc` String property - /// - public var siteURL: String { - xmlrpc.removingSuffix("/xmlrpc.php") - } -} - -// MARK: - Equatable Conformance -// -public func ==(lhs: WordPressOrgCredentials, rhs: WordPressOrgCredentials) -> Bool { - return lhs.username == rhs.username && lhs.password == rhs.password && lhs.xmlrpc == rhs.xmlrpc -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift deleted file mode 100644 index 2d4891a5848c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ASWebAuthenticationSession+Utils.swift .swift +++ /dev/null @@ -1,20 +0,0 @@ -import AuthenticationServices - -extension ASWebAuthenticationSession { - - /// Wrapper around the default `init(url:, callbackULRScheme:, completionHandler:)` where the - /// `completionHandler` argument is a `Result` instead of a `URL` and `Error` pair. - convenience init(url: URL, callbackURLScheme: String, completionHandler: @escaping (Result) -> Void) { - self.init(url: url, callbackURLScheme: callbackURLScheme) { callbackURL, error in - completionHandler( - Result( - value: callbackURL, - error: error, - // Unfortunately we cannot exted `ASWebAuthenticationSessionError.Code` to add - // a custom error for this scenario, so we're left to use a "generic" one. - inconsistentStateError: OAuthError.inconsistentWebAuthenticationSessionCompletion - ) - ) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift deleted file mode 100644 index 63d7bccc5264..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Character+URLSafe.swift +++ /dev/null @@ -1,10 +0,0 @@ -extension Character { - - // From the docs: using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" - // That is, URL safe characters. - // - // Notice that Swift offers `CharacterSet.urlQueryAllowed` to represent this set of characters. - // However, there is no straightforward way to convert a `CharacterSet` to a `Set`. - // See for example https://nshipster.com/characterset/. - static let urlSafeCharacters = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~") -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift deleted file mode 100644 index 30ed99509535..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+Base64URL.swift +++ /dev/null @@ -1,34 +0,0 @@ -public extension Data { - - /// "base64url" is an encoding that is safe to use with URLs. - /// It is defined in RFC 4648, section 5. - /// - /// See: - /// - https://tools.ietf.org/html/rfc4648#section-5 - /// - https://tools.ietf.org/html/rfc7515#appendix-C - init?(base64URLEncoded: String) { - let base64 = base64URLEncoded - .replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - - let length = Double(base64.lengthOfBytes(using: String.Encoding.utf8)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = requiredLength - length - if paddingLength > 0 { - let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - self.init(base64Encoded: base64 + padding, options: .ignoreUnknownCharacters) - } else { - self.init(base64Encoded: base64, options: .ignoreUnknownCharacters) - } - } - - /// See https://tools.ietf.org/html/rfc4648#section-5 - /// - /// Function name to match the standard library's `base64EncodedString()`. - func base64URLEncodedString() -> String { - base64EncodedString() - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "=", with: "") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift deleted file mode 100644 index 82f14056f7ba..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Data+SHA256.swift +++ /dev/null @@ -1,12 +0,0 @@ -import CryptoKit - -extension Data { - - func sha256Hashed() -> Data { - Data(SHA256.hash(data: self)) - } - - func sha256Hashed() -> String { - SHA256.hash(data: self).map { String(format: "%02hhx", $0) }.joined() - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift deleted file mode 100644 index f817743a93f9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/DataGetting.swift +++ /dev/null @@ -1,4 +0,0 @@ -protocol DataGetting { - - func data(for request: URLRequest) async throws -> Data -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift deleted file mode 100644 index 4adb4cc29014..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleClientId.swift +++ /dev/null @@ -1,26 +0,0 @@ -public struct GoogleClientId { - - let value: String - - public init?(string: String) { - guard string.split(separator: ".").count > 1 else { - return nil - } - self.value = string - } - - /// See https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier - func redirectURI(path: String?) -> String { - let root = value.split(separator: ".").reversed().joined(separator: ".") - - guard let path else { - return root - } - - return "\(root):/\(path)" - } - - var defaultRedirectURI: String { - redirectURI(path: "oauth2callback") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift deleted file mode 100644 index abfd0210c0f9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetter.swift +++ /dev/null @@ -1,28 +0,0 @@ -class GoogleOAuthTokenGetter: GoogleOAuthTokenGetting { - - let dataGetter: DataGetting - - init(dataGetter: DataGetting = URLSession.shared) { - self.dataGetter = dataGetter - } - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody { - let request = try URLRequest.googleSignInTokenRequest( - body: .googleSignInRequestBody( - clientId: clientId, - audience: audience, - authCode: authCode, - pkce: pkce - ) - ) - - let data = try await dataGetter.data(for: request) - - return try JSONDecoder().decode(OAuthTokenResponseBody.self, from: data) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift deleted file mode 100644 index c505494a3533..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/GoogleOAuthTokenGetting.swift +++ /dev/null @@ -1,9 +0,0 @@ -protocol GoogleOAuthTokenGetting { - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift deleted file mode 100644 index 706a9015e9f6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/IDToken.swift +++ /dev/null @@ -1,23 +0,0 @@ -/// See https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo -public struct IDToken { - - public let token: JSONWebToken - public let name: String - public let email: String - - // TODO: Validate token! – https://developers.google.com/identity/openid-connect/openid-connect#validatinganidtoken - init?(jwt: JSONWebToken) { - // Name and email might not be part of the JWT Google sent us if the scope used for the - // request didn't include them - guard let email = jwt.payload["email"] as? String else { - return nil - } - guard let name = jwt.payload["name"] as? String else { - return nil - } - - self.token = jwt - self.name = name - self.email = email - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift deleted file mode 100644 index a45156756430..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/JSONWebToken.swift +++ /dev/null @@ -1,47 +0,0 @@ -/// Represents a JSON Web Token (JWT) -/// -/// See https://jwt.io/introduction -public struct JSONWebToken { - let rawValue: String - - let header: [String: Any] - let payload: [String: Any] - let signature: String - - init?(encodedString: String) { - let segments = encodedString.components(separatedBy: ".") - - // JWT has three segments: header, payload, and signature - guard segments.count == 3 else { - return nil - } - - // Notice that JWT uses base64url encoding, not base64. - // - // See: - // - https://tools.ietf.org/html/rfc7515#appendix-C - // - https://jwt.io/introduction - - // Note: Splitting the guards is useful to know which one fails - guard let headerData = Data(base64URLEncoded: segments[0]) else { - return nil - } - - guard let payloadData = Data(base64URLEncoded: segments[1]) else { - return nil - } - - guard let header = try? JSONSerialization.jsonObject(with: headerData, options: []) as? [String: Any] else { - return nil - } - - guard let payload = try? JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any] else { - return nil - } - - self.rawValue = encodedString - self.header = header - self.payload = payload - self.signature = segments[2] - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift deleted file mode 100644 index f6b728d57d64..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/NewGoogleAuthenticator.swift +++ /dev/null @@ -1,123 +0,0 @@ -@preconcurrency import AuthenticationServices - -public class NewGoogleAuthenticator: NSObject { - - let clientId: GoogleClientId - let scheme: String - let audience: String - let oauthTokenGetter: GoogleOAuthTokenGetting - - public convenience init( - clientId: GoogleClientId, - scheme: String, - audience: String, - urlSession: URLSession - ) { - self.init( - clientId: clientId, - scheme: scheme, - audience: audience, - oautTokenGetter: GoogleOAuthTokenGetter(dataGetter: urlSession) - ) - } - - init( - clientId: GoogleClientId, - scheme: String, - audience: String, - oautTokenGetter: GoogleOAuthTokenGetting - ) { - self.clientId = clientId - self.scheme = scheme - self.audience = audience - self.oauthTokenGetter = oautTokenGetter - } - - /// Get the user's OAuth token from their Google account. This token can be used to authenticate with the WordPress backend. - /// - /// The app will present the browser to hand over authentication to Google from the given `UIViewController`. - public func getOAuthToken(from viewController: UIViewController) async throws -> IDToken { - return try await getOAuthToken( - from: WebAuthenticationPresentationContext(viewController: viewController) - ) - } - - /// Get the user's OAuth token from their Google account. This token can be used to authenticate with the WordPress backend. - /// - /// The app will present the browser to hand over authentication to Google using the given - /// `ASWebAuthenticationPresentationContextProviding`. - public func getOAuthToken( - from contextProvider: ASWebAuthenticationPresentationContextProviding - ) async throws -> IDToken { - let pkce = try ProofKeyForCodeExchange() - let url = try await getURL( - clientId: clientId, - scheme: scheme, - pkce: pkce, - contextProvider: contextProvider - ) - return try await requestOAuthToken(url: url, clientId: clientId, audience: audience, pkce: pkce) - } - - func getURL( - clientId: GoogleClientId, - scheme: String, - pkce: ProofKeyForCodeExchange, - contextProvider: ASWebAuthenticationPresentationContextProviding - ) async throws -> URL { - let url = try URL.googleSignInAuthURL(clientId: clientId, pkce: pkce) - return try await withCheckedThrowingContinuation { continuation in - let session = ASWebAuthenticationSession( - url: url, - callbackURLScheme: scheme, - completionHandler: { result in - continuation.resume(with: result) - } - ) - - session.presentationContextProvider = contextProvider - // At this point in time, we don't see the need to make the session ephemeral. - // - // Additionally, from a user's perspective, it would be frustrating to have to - // authenticate with Google again unless necessary—it certainly would be when testing - // the app. - session.prefersEphemeralWebBrowserSession = false - - // It feels inappropriate to force a dispatch on the main queue deep within the library. - // However, this is required to ensure `session` accesses the view it needs for the presentation on the right thread. - // - // See tradeoffs consideration at: - // https://github.com/wordpress-mobile/WordPressAuthenticator-iOS/pull/743#discussion_r1109325159 - DispatchQueue.main.async { - session.start() - } - } - } - - func requestOAuthToken( - url: URL, - clientId: GoogleClientId, - audience: String, - pkce: ProofKeyForCodeExchange - ) async throws -> IDToken { - guard let authCode = URLComponents(url: url, resolvingAgainstBaseURL: false)? - .queryItems? - .first(where: { $0.name == "code" })? - .value else { - throw OAuthError.urlDidNotContainCodeParameter(url: url) - } - - let response = try await oauthTokenGetter.getToken( - clientId: clientId, - audience: audience, - authCode: authCode, - pkce: pkce - ) - - guard let idToken = response.idToken else { - throw OAuthError.tokenResponseDidNotIncludeIdToken - } - - return idToken - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift deleted file mode 100644 index 329d3c3dd19d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthError.swift +++ /dev/null @@ -1,24 +0,0 @@ -public enum OAuthError: LocalizedError { - - // ASWebAuthenticationSession - case inconsistentWebAuthenticationSessionCompletion - - case failedToGenerateSecureRandomCodeVerifier(status: Int32) - - // OAuth token response - case urlDidNotContainCodeParameter(url: URL) - case tokenResponseDidNotIncludeIdToken - - public var errorDescription: String { - switch self { - case .inconsistentWebAuthenticationSessionCompletion: - return "ASWebAuthenticationSession authentication finished with neither a callback URL nor error" - case .failedToGenerateSecureRandomCodeVerifier(let status): - return "Could not generate a cryptographically secure random PKCE code verifier value. Underlying error code \(status)" - case .urlDidNotContainCodeParameter(let url): - return "Could not find 'code' parameter in URL '\(url)'" - case .tokenResponseDidNotIncludeIdToken: - return "OAuth token response did not include idToken" - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift deleted file mode 100644 index 7f3df6b157b5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthRequestBody+GoogleSignIn.swift +++ /dev/null @@ -1,26 +0,0 @@ -extension OAuthTokenRequestBody { - - static func googleSignInRequestBody( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) -> Self { - .init( - clientId: clientId.value, - // "The client secret obtained from the API Console Credentials page." - // - https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server - // - // There doesn't seem to be any secret for iOS app credentials. - // The process works with an empty string... - clientSecret: "", - audience: audience, - code: authCode, - codeVerifier: pkce.codeVerifier, - // As defined in the OAuth 2.0 specification, this field's value must be set to authorization_code. - // – https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code - grantType: "authorization_code", - redirectURI: clientId.defaultRedirectURI - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift deleted file mode 100644 index 49cd824b4009..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenRequestBody.swift +++ /dev/null @@ -1,45 +0,0 @@ -/// Models the request to send for an OAuth token -/// -/// - Note: See documentation at https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code -struct OAuthTokenRequestBody { - let clientId: String - let clientSecret: String - let audience: String - let code: String - let codeVerifier: ProofKeyForCodeExchange.CodeVerifier - let grantType: String - let redirectURI: String - - enum CodingKeys: String, CodingKey { - case clientId = "client_id" - case clientSecret = "client_secret" - case audience - case code - case codeVerifier = "code_verifier" - case grantType = "grant_type" - case redirectURI = "redirect_uri" - } - - func asURLEncodedData() throws -> Data { - let params = [ - (CodingKeys.clientId.rawValue, clientId), - (CodingKeys.clientSecret.rawValue, clientSecret), - (CodingKeys.code.rawValue, code), - (CodingKeys.codeVerifier.rawValue, codeVerifier.rawValue), - (CodingKeys.grantType.rawValue, grantType), - (CodingKeys.redirectURI.rawValue, redirectURI), - // This is not in the spec at - // https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server - // but we'll get an idToken that our backend considers invalid if omitted. - (CodingKeys.audience.rawValue, audience), - ] - - let items = params.map { URLQueryItem(name: $0.0, value: $0.1) } - - var components = URLComponents() - components.queryItems = items - - // We can assume `query` to never be nil because we set `queryItems` in the line above. - return Data(components.query!.utf8) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift deleted file mode 100644 index 145df1691d72..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/OAuthTokenResponseBody.swift +++ /dev/null @@ -1,35 +0,0 @@ -/// Models the response to an OAuth token request. -/// -/// - Note: See documentation at https://developers.google.com/identity/protocols/oauth2/native-app#exchange-authorization-code -struct OAuthTokenResponseBody: Codable, Equatable { - let accessToken: String - let expiresIn: Int - /// This value is only returned if the request included an identity scope, such as openid, profile, or email. - /// The value is a JSON Web Token (JWT) that contains digitally signed identity information about the user. - let rawIDToken: String? - let refreshToken: String? - let scope: String - /// The type of token returned. At this time, this field's value is always set to Bearer. - let tokenType: String - - var idToken: IDToken? { - guard let rawIDToken else { - return nil - } - - guard let jwt = JSONWebToken(encodedString: rawIDToken) else { - return nil - } - - return IDToken(jwt: jwt) - } - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case rawIDToken = "id_token" - case refreshToken = "refresh_token" - case scope - case tokenType = "token_type" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift deleted file mode 100644 index 84d8f6ad2818..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/ProofKeyForCodeExchange.swift +++ /dev/null @@ -1,120 +0,0 @@ -// See: -// - https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier -// - https://www.rfc-editor.org/rfc/rfc7636 -// -// Note: The common abbreviation of "Proof Key for Code Exchange" is PKCE and is pronounced "pixy". -struct ProofKeyForCodeExchange: Equatable { - - enum Method: Equatable { - case s256 - case plain - - var urlQueryParameterValue: String { - switch self { - case .plain: return "plain" - case .s256: return "S256" - } - } - } - - let codeVerifier: CodeVerifier - let method: Method - - init() throws { - self.codeVerifier = try .makeRandomCodeVerifier() - self.method = .s256 - } - - init(codeVerifier: CodeVerifier, method: Method) { - self.codeVerifier = codeVerifier - self.method = method - } - - var codeChallenge: String { - codeVerifier.codeChallenge(using: method) - } -} - -extension ProofKeyForCodeExchange { - - // A code_verifier is a high-entropy cryptographic random string using the unreserved - // characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a minimum length of 43 - // characters and a maximum length of 128 characters. - // - // The code verifier should have enough entropy to make it impractical to guess the value. - // - // See: - // - https://www.rfc-editor.org/rfc/rfc7636#section-4.1 - // - https://developers.google.com/identity/protocols/oauth2/native-app#step1-code-verifier - struct CodeVerifier: Equatable { - - let rawValue: String - - static let allowedCharacters = Character.urlSafeCharacters - static let allowedLengthRange = (43...128) - - /// Generates a random code verifier according to the PKCE RFC. - /// - /// - Note: This method name is more verbose than the recommended "make" for this factory to communicate the randomness component. - static func makeRandomCodeVerifier() throws -> Self { - let value = try randomSecureCodeVerifier() - - // It's appropriate to force unwrap here because a `nil` value could only result from - // a developer error—either wrong coding of the constrained length or of the allowed - // characters. - return .init(value: value)! - } - - init?(value: String) { - guard CodeVerifier.allowedLengthRange.contains(value.count) else { return nil } - - guard Set(value).isSubset(of: CodeVerifier.allowedCharacters) else { return nil } - - self.rawValue = value - } - - func codeChallenge(using method: Method) -> String { - switch method { - case .s256: - // The spec defines code_challenge for the s256 mode as: - // - // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) - // - // We don't need the ASCII conversion, because we build `CodeVerifier` from URL safe - // characters. - let rawData = Data(rawValue.utf8) - let hashedData: Data = rawData.sha256Hashed() - return hashedData.base64URLEncodedString() - case .plain: - return rawValue - } - } - } - - /// Generates a random code verifier according to the PKCE RFC. - /// - /// The RFC states: - /// - /// > It is RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet sequence. - /// > The octet sequence is then base64url-encoded to produce a 43-octet URL safe string to use as the code verifier. - static func randomSecureCodeVerifier() throws -> String { - let byteCount = 32 - var bytes = [UInt8](repeating: 0, count: byteCount) - let result = SecRandomCopyBytes(kSecRandomDefault, byteCount, &bytes) - - guard result == errSecSuccess else { - throw OAuthError.failedToGenerateSecureRandomCodeVerifier(status: result) - } - - let data = Data(bytes) - - // Base64url-encoding a 32-octect sequence should always result in a 43-length string, - // string, but let's cap it just in case. - // - // Also notice that by base64url-encoding, we ensure the characters are in the allowed - // set. - // - // 43 is also the minimum length for a code verifier, hence the `allowedLengthRange.lowerBound` usage. - return String(data.base64URLEncodedString().prefix(CodeVerifier.allowedLengthRange.lowerBound)) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift deleted file mode 100644 index 2ea91504226a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/Result+ConvenienceInit.swift +++ /dev/null @@ -1,15 +0,0 @@ -extension Swift.Result { - - /// Convenience init to lift the values in an Objective-C style callback, where both success and failure parameters can be nil, to - /// a domain where at least one is not nil. - /// - /// If both values are nil, it will create a `failure` instance wrapping the given `inconsistentStateError`. - init(value: Success?, error: Failure?, inconsistentStateError: Failure) { - switch (value, error) { - case (.some(let value), .none): self = .success(value) - case (.some, .some(let error)): self = .failure(error) - case (.none, .some(let error)): self = .failure(error) - case (.none, .none): self = .failure(inconsistentStateError) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift deleted file mode 100644 index 3aea2b9418bd..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URL+GoogleSignIn.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -// It's acceptable to force-unwrap here because, for this call to fail we'd need a developer error, -// which we would catch because the unit tests would crash. -extension URL { - - static var googleSignInBaseURL = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")! - - static var googleSignInOAuthTokenURL = URL(string: "https://oauth2.googleapis.com/token")! -} - -extension URL { - - static func googleSignInAuthURL(clientId: GoogleClientId, pkce: ProofKeyForCodeExchange) throws -> URL { - let queryItems = [ - ("client_id", clientId.value), - ("code_challenge", pkce.codeChallenge), - ("code_challenge_method", pkce.method.urlQueryParameterValue), - ("redirect_uri", clientId.defaultRedirectURI), - ("response_type", "code"), - // See what the Google SDK does: - // https://github.com/google/GoogleSignIn-iOS/blob/7.0.0/GoogleSignIn/Sources/GIDScopes.m#L58-L61 - ("scope", "profile email") - ].map { URLQueryItem(name: $0.0, value: $0.1) } - - return googleSignInBaseURL.appending(queryItems: queryItems) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift deleted file mode 100644 index 5bd8c78275e1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLRequest+GoogleSignIn.swift +++ /dev/null @@ -1,18 +0,0 @@ -extension URLRequest { - - static func googleSignInTokenRequest( - body: OAuthTokenRequestBody - ) throws -> URLRequest { - var request = URLRequest(url: URL.googleSignInOAuthTokenURL) - request.httpMethod = "POST" - - request.setValue( - "application/x-www-form-urlencoded; charset=UTF-8", - forHTTPHeaderField: "Content-Type" - ) - - request.httpBody = try body.asURLEncodedData() - - return request - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift b/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift deleted file mode 100644 index fef3acbc528a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/GoogleSignIn/URLSesison+DataGetting.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -// In other projects, we avoid extending `URLSession` to conform to "-getting" protocols shaped -// like `DataGetting` and prefer composition instead, creating an object conforming to the protocol -// and holding an `URLSession` reference. -// -// The concern with extending a Foundation type with special-purpose domain object getting ability -// is that it would pollute the namespace, offering the protocol methods as an option everywhere -// `URLSession` is used, even in part of the app that are unrelated with the resource. -// -// But since the type `DataGetting` revolves around is `Data` and `URLSession` already exposes -// methods returning `Data`, this feels more like a syntax-sugar extension rather than one adding -// whole new domain-specific APIs to the type. -extension URLSession: DataGetting { - - func data(for request: URLRequest) async throws -> Data { - let (data, _) = try await data(for: request) - return data - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.h b/Sources/WordPressAuthenticator/Helpers/LoginFacade.h deleted file mode 100644 index 01a7aa40206f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.h +++ /dev/null @@ -1,176 +0,0 @@ -#import - -// This is a strange hack to make the `-[LoginFacadeDelegate needsMultifactorCodeForUserID:andNonceInfo:]` -// available in Objective-C runtime. -@import WordPressKit; - -NS_ASSUME_NONNULL_BEGIN - -@class LoginFields; -@class SocialLogin2FANonceInfo; -@protocol WordPressComOAuthClientFacadeProtocol; -@protocol WordPressXMLRPCAPIFacade; -@protocol LoginFacadeDelegate; - - -/** - * This protocol represents a class that handles the signing in to a self hosted/.com site. - */ -@protocol LoginFacade - -/** - * This method initializes the LoginFacade instance. - * - * @param dotcomClientID WordPress.com Client ID. - * @param dotcomSecret WordPress.com Secret. - * @param userAgent UserAgent string to be used interacting with a remote endpoint. - * - * @note When this class is Swifted, we can (probably) just query WordPressAuthenticator. - */ -- (instancetype)initWithDotcomClientID:(NSString *)dotcomClientID dotcomSecret:(NSString *)dotcomSecret userAgent:(NSString *)userAgent; - -/** - * This method will attempt to sign in to a self hosted/.com site. - * XMLRPC endpoint discover is performed. - * - * @param loginFields the fields representing the site we are attempting to login to. - */ -- (void)signInWithLoginFields:(LoginFields *)loginFields; - -/** - * This method will attempt to sign in to a self hosted/.com site. - * The XML-RPC endpoint should be present in the loginFields.siteUrl field. - * - * @param loginFields the fields representing the site we are attempting to login to. - */ -- (void)loginToSelfHosted:(LoginFields *)loginFields; - -/** - * Social login. - * - * @param token Social id token. - */ -- (void)loginToWordPressDotComWithSocialIDToken:(NSString *)token - service:(NSString *)service; - -/** - * Social login via a social account with 2FA using a nonce. - * - * @param userID WordPress.com User ID - * @param authType Indicates the Kind of Authentication we're performing (sms / authenticator / etc). - * @param twoStepCode Multifactor Code. - * @param twoStepNonce Two Step Nonce. - */ -- (void)loginToWordPressDotComWithUser:(NSInteger)userID - authType:(NSString *)authType - twoStepCode:(NSString *)twoStepCode - twoStepNonce:(NSString *)twoStepNonce; - - -/** - * A delegate with a few methods that indicate various aspects of the login process - */ -@property (nonatomic, weak) id delegate; - -/** - * A class that handles the login to sites requiring oauth(primarily .com sites) - */ -@property (nonatomic, strong) id wordpressComOAuthClientFacade; - -/** - * A class that handles the login to self hosted sites - */ -@property (nonatomic, strong) id wordpressXMLRPCAPIFacade; - -@end - -/** - * This class handles the signing in to a self hosted/.com site. - */ -@interface LoginFacade : NSObject - -@end - -/** - * Protocol with a few methods that indicate various aspects of the login process. - */ -@protocol LoginFacadeDelegate - -@optional - -/** - * This is called when we need to indicate to the a messagea about the current login (e.g. "Signing In", "Authenticating", "Syncing", etc.) - * - * @param message the message to display to the user. - */ -- (void)displayLoginMessage:(NSString *)message; - -/** - * This is called when the initial login failed because we need a 2fa code. - */ -- (void)needsMultifactorCode; - -/** - * This is called when the initial login failed because we need a 2fa code for a social login. - * - * @param userID the WPCom userID of the user logging in. - * @param nonceInfo an object containing information about available 2fa nonce options. - */ -- (void)needsMultifactorCodeForUserID:(NSInteger)userID andNonceInfo:(SocialLogin2FANonceInfo *)nonceInfo; - -/** - * This is called when there's been an error and we want to inform the user. - * - * @param error the error in question. - */ -- (void)displayRemoteError:(NSError *)error; - -/** - * Called when finished logging into a self hosted site - * - * @param username username of the site - * @param password password of the site - * @param xmlrpc the xmlrpc url of the site - * @param options the options dictionary coming back from the `wp.getOptions` method. - */ -- (void)finishedLoginWithUsername:(NSString *)username password:(NSString *)password xmlrpc:(NSString *)xmlrpc options:(NSDictionary * )options; - - -/** - * Called when finished logging in to a WordPress.com site - * - * @param authToken authToken to be used to access the site - * @param requiredMultifactorCode whether the login required a 2fa code - */ -- (void)finishedLoginWithAuthToken:(NSString *)authToken requiredMultifactorCode:(BOOL)requiredMultifactorCode; - - -/** - * Called when finished logging in to a WordPress.com site via a Google token. - * - * @param googleIDToken the token used - * @param authToken authToken to be used to access the site - */ -- (void)finishedLoginWithGoogleIDToken:(NSString *)googleIDToken authToken:(NSString *)authToken; - - -/** - * Called when finished logging in to a WordPress.com site via a 2FA Nonce. - * - * @param authToken authToken to be used to access the site - */ -- (void)finishedLoginWithNonceAuthToken:(NSString *)authToken; - - -/** - * Lets the delegate know that a social login attempt found a matching user, but - * their account has not been connected to the social service previously. - * - * @param email The email address that was matched. - */ -- (void)existingUserNeedsConnection:(NSString *)email; - -@end - -NS_ASSUME_NONNULL_END - diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.m b/Sources/WordPressAuthenticator/Helpers/LoginFacade.m deleted file mode 100644 index 333865b345d6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.m +++ /dev/null @@ -1,179 +0,0 @@ -#import "LoginFacade.h" -#import "WordPressXMLRPCAPIFacade.h" -#import -@import NSURL_IDN; -@import WordPressKit; - -@implementation LoginFacade - -@synthesize delegate; -@synthesize wordpressComOAuthClientFacade = _wordpressComOAuthClientFacade; -@synthesize wordpressXMLRPCAPIFacade = _wordpressXMLRPCAPIFacade; - -- (instancetype)initWithDotcomClientID:(NSString *)dotcomClientID dotcomSecret:(NSString *)dotcomSecret userAgent:(NSString *)userAgent -{ - self = [super init]; - if (self) { - _wordpressComOAuthClientFacade = [[WordPressComOAuthClientFacade alloc] initWithClient:dotcomClientID secret:dotcomSecret]; - _wordpressXMLRPCAPIFacade = [[WordPressXMLRPCAPIFacade alloc] initWithUserAgent:userAgent]; - } - return self; -} - -- (void)signInWithLoginFields:(LoginFields *)loginFields -{ - NSAssert(self.delegate != nil, @"Must set delegate to use service"); - - if (loginFields.userIsDotCom || loginFields.siteAddress.isWordPressComPath) { - [self signInToWordpressDotCom:loginFields]; - } else { - [self signInToSelfHosted:loginFields]; - } -} - -- (void)loginToWordPressDotComWithSocialIDToken:(NSString *)token - service:(NSString *)service -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithSocialIDToken:token - service:service - success:^(NSString *authToken) { - if ([service isEqualToString:@"google"] && [self.delegate respondsToSelector:@selector(finishedLoginWithGoogleIDToken:authToken:)]) { - // Apple is handled in AppleAuthenticator - [self.delegate finishedLoginWithGoogleIDToken:token authToken:authToken]; - } - [self trackSuccess]; - } needsMultifactor:^(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo){ - if ([self.delegate respondsToSelector:@selector(needsMultifactorCodeForUserID:andNonceInfo:)]) { - [self.delegate needsMultifactorCodeForUserID:userID andNonceInfo:nonceInfo]; - } - } existingUserNeedsConnection: ^(NSString *email) { - // Apple is handled in AppleAuthenticator - if ([self.delegate respondsToSelector:@selector(existingUserNeedsConnection:)]) { - [self.delegate existingUserNeedsConnection: email]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - [self track:WPAnalyticsStatLoginSocialFailure error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)loginToWordPressDotComWithUser:(NSInteger)userID - authType:(NSString *)authType - twoStepCode:(NSString *)twoStepCode - twoStepNonce:(NSString *)twoStepNonce -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithSocialLoginUser:userID - authType:authType - twoStepCode:twoStepCode - twoStepNonce:twoStepNonce - success:^(NSString *authToken) { - if ([self.delegate respondsToSelector:@selector(finishedLoginWithNonceAuthToken:)]) { - [self.delegate finishedLoginWithNonceAuthToken:authToken]; - } - [self trackSuccess]; - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)signInToWordpressDotCom:(LoginFields *)loginFields -{ - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - } - - [self.wordpressComOAuthClientFacade authenticateWithUsername:loginFields.username password:loginFields.password multifactorCode:loginFields.multifactorCode success:^(NSString *authToken) { - if ([self.delegate respondsToSelector:@selector(finishedLoginWithAuthToken:requiredMultifactorCode:)]) { - [self.delegate finishedLoginWithAuthToken:authToken requiredMultifactorCode:loginFields.requiredMultifactor]; - } - [self trackSuccess]; - } needsMultifactor:^(NSInteger userID, SocialLogin2FANonceInfo *nonceInfo) { - if (nonceInfo == nil && [self.delegate respondsToSelector:@selector(needsMultifactorCode)]) { - [self.delegate needsMultifactorCode]; - } else if (nonceInfo != nil && [self.delegate respondsToSelector:@selector(needsMultifactorCodeForUserID:andNonceInfo:)]) { - [self.delegate needsMultifactorCodeForUserID:userID andNonceInfo:nonceInfo]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - if ([self.delegate respondsToSelector:@selector(displayRemoteError:)]) { - [self.delegate displayRemoteError:error]; - } - }]; -} - -- (void)signInToSelfHosted:(LoginFields *)loginFields -{ - void (^guessXMLRPCURLSuccess)(NSURL *) = ^(NSURL *xmlRPCURL) { - loginFields.xmlrpcURL = xmlRPCURL; - [self loginToSelfHosted:loginFields]; - }; - - void (^guessXMLRPCURLFailure)(NSError *) = ^(NSError *error){ - [self track:WPAnalyticsStatLoginFailedToGuessXMLRPC error:error]; - [self track:WPAnalyticsStatLoginFailed error:error]; - [self.delegate displayRemoteError:error]; - }; - - if ([self.delegate respondsToSelector:@selector(displayLoginMessage:)]) { - [self.delegate displayLoginMessage:NSLocalizedString(@"Authenticating", nil)]; - } - - NSString *siteUrl = [NSURL IDNEncodedURL: loginFields.siteAddress]; - [self.wordpressXMLRPCAPIFacade guessXMLRPCURLForSite:siteUrl success:guessXMLRPCURLSuccess failure:guessXMLRPCURLFailure]; -} - -- (void)loginToSelfHosted:(LoginFields *)loginFields -{ - NSURL *xmlRPCURL = loginFields.xmlrpcURL; - [self.wordpressXMLRPCAPIFacade getBlogOptionsWithEndpoint:xmlRPCURL username:loginFields.username password:loginFields.password success:^(id options) { - if ([options objectForKey:@"wordpress.com"] != nil) { - [self signInToWordpressDotCom:loginFields]; - } else { - NSString *versionString = options[@"software_version"][@"value"]; - NSString *minimumSupported = [WordPressOrgXMLRPCApi minimumSupportedVersion]; - CGFloat version = [versionString floatValue]; - - if (version > 0 && version < [minimumSupported floatValue]) { - NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"WordPress version too old. The site at %@ uses WordPress %@. We recommend to update to the latest version, or at least %@", nil), [xmlRPCURL host], versionString, minimumSupported]; - NSError *versionError = [NSError errorWithDomain:WordPressAuthenticator.errorDomain - code:WordPressAuthenticator.invalidVersionErrorCode - userInfo:@{NSLocalizedDescriptionKey:errorMessage}]; - [self track:WPAnalyticsStatLoginFailed error:versionError]; - [self.delegate displayRemoteError:versionError]; - return; - } - NSString *xmlrpc = [xmlRPCURL absoluteString]; - [self.delegate finishedLoginWithUsername:loginFields.username password:loginFields.password xmlrpc:xmlrpc options:options]; - [self trackSuccess]; - } - } failure:^(NSError *error) { - [self track:WPAnalyticsStatLoginFailed error:error]; - [self.delegate displayRemoteError:error]; - }]; -} - -- (void)track:(WPAnalyticsStat)stat -{ - [WordPressAuthenticator track:stat]; -} - -- (void)track:(WPAnalyticsStat)stat error:(NSError *)error -{ - [WordPressAuthenticator track:stat error:error]; -} - -@end diff --git a/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift b/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift deleted file mode 100644 index c2117e235cda..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/LoginFacade.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation -import WordPressKit -import WordPressShared - -/// Extension for handling 2FA authentication. -/// -public extension LoginFacade { - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - func requestOneTimeCode(with loginFields: LoginFields) { - wordpressComOAuthClientFacade.requestOneTimeCode( - username: loginFields.username, - password: loginFields.password, - success: { [weak self] in - guard let self else { - return - } - - if self.tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorSentSMS) - } - }) { _ in - WPLogError("Failed to request one time code") - } - } - - func requestSocial2FACode(with loginFields: LoginFields) { - guard let nonce = loginFields.nonceInfo?.nonceSMS else { - return - } - - wordpressComOAuthClientFacade.requestSocial2FACode( - userID: loginFields.nonceUserID, - nonce: nonce, - success: { [weak self] newNonce in - guard let self else { - return - } - - loginFields.nonceInfo?.nonceSMS = newNonce - - if self.tracker.shouldUseLegacyTracker() { - WordPressAuthenticator.track(.twoFactorSentSMS) - } - }) { _, newNonce in - if let newNonce { - loginFields.nonceInfo?.nonceSMS = newNonce - } - WPLogError("Failed to request one time code") - } - } - - /// Async function that returns the necessary `WebauthnChallengeInfo` to start allow for a security key log in. - /// - func requestWebauthnChallenge(userID: Int, twoStepNonce: String) async -> WebauthnChallengeInfo? { - - delegate?.displayLoginMessage?(NSLocalizedString("Waiting for security key", comment: "Text while waiting for a security key challenge")) - - return await withCheckedContinuation { continuation in - wordpressComOAuthClientFacade.requestWebauthnChallenge(userID: Int64(userID), twoStepNonce: twoStepNonce, success: { challengeInfo in - continuation.resume(returning: challengeInfo) - }, failure: { [weak self] error in - guard let self else { return } - - WPLogError("Failed to request webauthn challenge \(error)") - WordPressAuthenticator.track(.loginFailed, error: error) - continuation.resume(returning: nil) - - DispatchQueue.main.async { - self.delegate?.displayRemoteError?(error) - } - }) - } - } - - /// Forwards the authentication signature message and updates delegates accordingly. - /// - func authenticateWebauthnSignature(userID: Int, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data) { - - delegate?.displayLoginMessage?(NSLocalizedString("Waiting for security key", comment: "Text while the webauthn signature is being verified")) - - wordpressComOAuthClientFacade.authenticateWebauthnSignature(userID: Int64(userID), - twoStepNonce: twoStepNonce, - credentialID: credentialID, - clientDataJson: clientDataJson, - authenticatorData: authenticatorData, - signature: signature, - userHandle: userHandle, - success: { [weak self] accessToken in - self?.delegate?.finishedLogin?(withNonceAuthToken: accessToken) - self?.trackSuccess() - }, failure: { [weak self] error in - WPLogError("Failed to verify webauthn signature \(error)") - WordPressAuthenticator.track(.loginFailed, error: error) - self?.delegate?.displayRemoteError?(error) - }) - } - - @objc - func trackSuccess() { - tracker.track(step: .success) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift deleted file mode 100644 index a22cd6cb1692..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields+Validation.swift +++ /dev/null @@ -1,47 +0,0 @@ -// MARK: - LoginFields Validation Methods -// -extension LoginFields { - - /// Returns *true* if the fields required for SignIn have been populated. - /// Note: that loginFields.emailAddress is not checked. Use loginFields.username instead. - /// - func validateFieldsPopulatedForSignin() -> Bool { - return !username.isEmpty && - !password.isEmpty && - (meta.userIsDotCom || !siteAddress.isEmpty) - } - - /// Returns *true* if the siteURL contains a valid URL. False otherwise. - /// - func validateSiteForSignin() -> Bool { - guard let url = URL(string: NSURL.idnEncodedURL(siteAddress)) else { - return false - } - - return !url.absoluteString.isEmpty - } - - /// Returns *true* if the credentials required for account creation have been provided. - /// - func validateFieldsPopulatedForCreateAccount() -> Bool { - return !emailAddress.isEmpty && - !username.isEmpty && - !password.isEmpty && - !siteAddress.isEmpty - } - - /// Returns *true* if no spaces have been used in [email, username, address] - /// - func validateFieldsForSigninContainNoSpaces() -> Bool { - let space = " " - return !emailAddress.contains(space) && - !username.contains(space) && - !siteAddress.contains(space) - } - - /// Returns *true* if the username is 50 characters or less. - /// - func validateUsernameMaxLength() -> Bool { - return username.count <= 50 - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift deleted file mode 100644 index db3bbaf4e2b1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFields.swift +++ /dev/null @@ -1,143 +0,0 @@ -import Foundation -import WordPressKit - -/// LoginFields is a state container for user textfield input on the login screens -/// as well as other meta data regarding the nature of a login attempt. -/// -@objc -public class LoginFields: NSObject { - // These fields store user input from text fields. - - /// Stores the user's account identifier (either email address or username) that is - /// entered in the login flow. By convention, even if the user is logging in - /// via an email address this field should store that value. - @objc public var username = "" - - /// The user's password. - @objc public var password = "" - - /// The site address if logging in via the self-hosted flow. - @objc public var siteAddress = "" - - /// The two factor code entered by a user. - @objc public var multifactorCode = "" // 2fa code - - /// Nonce info in the event of a social login with 2fa - @objc public var nonceInfo: SocialLogin2FANonceInfo? - - /// User ID for use with the nonce for social login - @objc public var nonceUserID: Int = 0 - - /// Used to restrict login to WordPress.com - public var restrictToWPCom = false - - /// Used on the webauthn/security key flow. - public var webauthnChallengeInfo: WebauthnChallengeInfo? - - /// Used by the SignupViewController. Signup currently asks for both a - /// username and an email address. This can be factored away when we revamp - /// the signup flow. - @objc public var emailAddress = "" - - var meta = LoginFieldsMeta() - - @objc public var userIsDotCom: Bool { - get { meta.userIsDotCom } - set { meta.userIsDotCom = newValue } - } - - @objc public var requiredMultifactor: Bool { - meta.requiredMultifactor - } - - @objc public var xmlrpcURL: NSURL? { - get { meta.xmlrpcURL } - set { meta.xmlrpcURL = newValue } - } - - var storedCredentials: SafariStoredCredentials? - - /// Convenience method for persisting stored credentials. - /// - @objc func setStoredCredentials(usernameHash: Int, passwordHash: Int) { - storedCredentials = SafariStoredCredentials() - storedCredentials?.storedUserameHash = usernameHash - storedCredentials?.storedPasswordHash = passwordHash - } - - class func makeForWPCom(username: String, password: String) -> LoginFields { - let loginFields = LoginFields() - - loginFields.username = username - loginFields.password = password - - return loginFields - } - - /// Using a convenience initializer for its Objective-C usage in unit tests. - convenience init(username: String, - password: String, - siteAddress: String, - multifactorCode: String, - nonceInfo: SocialLogin2FANonceInfo?, - nonceUserID: Int, - restrictToWPCom: Bool, - emailAddress: String, - meta: LoginFieldsMeta, - storedCredentials: SafariStoredCredentials?) { - self.init() - self.username = username - self.password = password - self.siteAddress = siteAddress - self.multifactorCode = multifactorCode - self.nonceInfo = nonceInfo - self.nonceUserID = nonceUserID - self.restrictToWPCom = restrictToWPCom - self.emailAddress = emailAddress - self.meta = meta - self.storedCredentials = storedCredentials - } -} - -extension LoginFields { - func copy() -> LoginFields { - .init(username: username, - password: password, - siteAddress: siteAddress, - multifactorCode: multifactorCode, - nonceInfo: nonceInfo, - nonceUserID: nonceUserID, - restrictToWPCom: restrictToWPCom, - emailAddress: emailAddress, - meta: meta.copy(), - storedCredentials: storedCredentials) - } -} - -extension LoginFields { - - var parametersForSignInWithApple: [String: AnyObject]? { - guard let user = meta.socialUser, case .apple = user.service else { - return nil - } - - return AccountServiceRemoteREST.appleSignInParameters( - email: user.email, - fullName: user.fullName - ) - } -} - -/// A helper class for storing safari saved password information. -/// -class SafariStoredCredentials { - var storedUserameHash = 0 - var storedPasswordHash = 0 -} - -/// An enum to indicate where the Magic Link Email was sent from. -/// -enum EmailMagicLinkSource: Int { - case login = 1 - case signup = 2 -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift b/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift deleted file mode 100644 index 18dd5c49be5d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/LoginFieldsMeta.swift +++ /dev/null @@ -1,83 +0,0 @@ -import WordPressKit - -class LoginFieldsMeta { - - /// Indicates where the Magic Link Email was sent from. - /// - var emailMagicLinkSource: EmailMagicLinkSource? - - /// Indicates whether a self-hosted user is attempting to log in to Jetpack - /// - var jetpackLogin: Bool - - /// Indicates whether a user is logging in via the wpcom flow or a self-hosted flow. Used by the - /// the LoginFacade in its branching logic. - /// This is a good candidate to refactor out and call the proper login method directly. - /// - var userIsDotCom: Bool - - /// Indicates a wpcom account created via social sign up that requires either a magic link, or a social log in option. - /// If a user signed up via social sign up and subsequently reset their password this field will be false. - /// - var passwordless: Bool - - /// Should point to the site's xmlrpc.php for a self-hosted log in. Should be the value returned via XML-RPC discovery. - /// - var xmlrpcURL: NSURL? - - /// Meta data about a site. This information is fetched and then displayed on the login epilogue. - /// - var siteInfo: WordPressComSiteInfo? - - /// Flags whether a 2FA challenge had to be satisfied before a log in could be complete. - /// Included in analytics after a successful login. - /// - /// A `false` value means that a 2FA prompt was needed. - /// - var requiredMultifactor: Bool - - /// Identifies a social login and the service used. - /// - var socialService: SocialServiceName? - - var socialServiceIDToken: String? - - var socialUser: SocialUser? - - init(emailMagicLinkSource: EmailMagicLinkSource? = nil, - jetpackLogin: Bool = false, - userIsDotCom: Bool = true, - passwordless: Bool = false, - xmlrpcURL: NSURL? = nil, - siteInfo: WordPressComSiteInfo? = nil, - requiredMultifactor: Bool = false, - socialService: SocialServiceName? = nil, - socialServiceIDToken: String? = nil, - socialUser: SocialUser? = nil) { - self.emailMagicLinkSource = emailMagicLinkSource - self.jetpackLogin = jetpackLogin - self.userIsDotCom = userIsDotCom - self.passwordless = passwordless - self.xmlrpcURL = xmlrpcURL - self.siteInfo = siteInfo - self.requiredMultifactor = requiredMultifactor - self.socialService = socialService - self.socialServiceIDToken = socialServiceIDToken - self.socialUser = socialUser - } -} - -extension LoginFieldsMeta { - func copy() -> LoginFieldsMeta { - .init(emailMagicLinkSource: emailMagicLinkSource, - jetpackLogin: jetpackLogin, - userIsDotCom: userIsDotCom, - passwordless: passwordless, - xmlrpcURL: xmlrpcURL, - siteInfo: siteInfo, - requiredMultifactor: requiredMultifactor, - socialService: socialService, - socialServiceIDToken: socialServiceIDToken, - socialUser: socialUser) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift b/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift deleted file mode 100644 index 2be773ff7711..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Model/WordPressComSiteInfo.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Foundation - -// MARK: - WordPress.com Site Info -// -public class WordPressComSiteInfo { - - /// Site's Name! - /// - public let name: String - - /// Tagline. - /// - public let tagline: String - - /// Public URL. - /// - public let url: String - - /// Indicates if Jetpack is available, or not. - /// - public let hasJetpack: Bool - - /// Indicates if Jetpack is active, or not. - /// - public let isJetpackActive: Bool - - /// Indicates if Jetpack is connected, or not. - /// - public let isJetpackConnected: Bool - - /// URL of the Site's Blavatar. - /// - public let icon: String - - /// Indicates whether the site is WordPressDotCom, or not. - /// - public let isWPCom: Bool - - /// Inidcates wheter the site is WordPress, or not. - /// - public let isWP: Bool - - /// Inidcates whether the site exists, or not. - /// - public let exists: Bool - - /// Initializes the current SiteInfo instance with a raw dictionary. - /// - public init(remote: [AnyHashable: Any]) { - name = remote["name"] as? String ?? "" - tagline = remote["description"] as? String ?? "" - url = remote["urlAfterRedirects"] as? String ?? "" - hasJetpack = remote["hasJetpack"] as? Bool ?? false - isJetpackActive = remote["isJetpackActive"] as? Bool ?? false - isJetpackConnected = remote["isJetpackConnected"] as? Bool ?? false - icon = remote["icon.img"] as? String ?? "" - isWPCom = remote["isWordPressDotCom"] as? Bool ?? false - isWP = remote["isWordPress"] as? Bool ?? false - exists = remote["exists"] as? Bool ?? false - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift deleted file mode 100644 index 0608c0da09c4..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateBack.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Navigates back one step. -/// -public struct NavigateBack: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - pop(navigationController: from?.navigationController) - } -} - -private extension NavigateBack { - func pop(navigationController: UINavigationController?) { - navigationController?.popViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift deleted file mode 100644 index 7c889da602cd..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterAccount.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the unified "Continue with WordPress.com" flow. -/// -public struct NavigateToEnterAccount: NavigationCommand { - private let signInSource: SignInSource - private let email: String? - - public init(signInSource: SignInSource, email: String? = nil) { - self.signInSource = signInSource - self.email = email - } - - public func execute(from: UIViewController?) { - continueWithDotCom(email: email, navigationController: from?.navigationController) - } -} - -private extension NavigateToEnterAccount { - private func continueWithDotCom(email: String? = nil, navigationController: UINavigationController?) { - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from LoginPrologueViewController to GetStartedViewController") - return - } - vc.source = signInSource - vc.loginFields.username = email ?? "" - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift deleted file mode 100644 index b40742f6105d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSite.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the unified site address login flow. -/// -public struct NavigateToEnterSite: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - presentUnifiedSiteAddressView(navigationController: from?.navigationController) - } -} - -private extension NavigateToEnterSite { - func presentUnifiedSiteAddressView(navigationController: UINavigationController?) { - guard let vc = SiteAddressViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from LoginViewController to SiteAddressViewController") - return - } - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift deleted file mode 100644 index e2eb6285bbbb..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterSiteCredentials.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the wp-admin site credentials flow. -/// -public struct NavigateToEnterSiteCredentials: NavigationCommand { - private let loginFields: LoginFields - - public init(loginFields: LoginFields) { - self.loginFields = loginFields - } - public func execute(from: UIViewController?) { - let navigationController = (from as? UINavigationController) ?? from?.navigationController - presentSiteCredentialsView(navigationController: navigationController, - loginFields: loginFields) - } -} - -private extension NavigateToEnterSiteCredentials { - func presentSiteCredentialsView(navigationController: UINavigationController?, loginFields: LoginFields) { - guard let controller = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate to SiteCredentialsViewController") - return - } - - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift deleted file mode 100644 index 174b36dbe50e..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToEnterWPCOMPassword.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import WordPressShared - -/// Navigates to the WPCOM password flow. -/// -public struct NavigateToEnterWPCOMPassword: NavigationCommand { - private let loginFields: LoginFields - - public init(loginFields: LoginFields) { - self.loginFields = loginFields - } - public func execute(from: UIViewController?) { - let navigationController = (from as? UINavigationController) ?? from?.navigationController - presentPasswordView(navigationController: navigationController, - loginFields: loginFields) - } -} - -private extension NavigateToEnterWPCOMPassword { - func presentPasswordView(navigationController: UINavigationController?, loginFields: LoginFields) { - guard let controller = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - return - } - - controller.loginFields = loginFields - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift deleted file mode 100644 index a4c6fd48e89d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigateToRoot.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -/// Navigates to the root of the unified login flow. -/// -public struct NavigateToRoot: NavigationCommand { - public init() {} - public func execute(from: UIViewController?) { - presentUnifiedSiteAddressView(navigationController: from?.navigationController) - } -} - -private extension NavigateToRoot { - func presentUnifiedSiteAddressView(navigationController: UINavigationController?) { - navigationController?.popToRootViewController(animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift b/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift deleted file mode 100644 index f6653c571d69..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/Navigation/NavigationCommand.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// NavigationCommand abstracts logic necessary provide clients of this library -/// with a way to navigate to a particular location in the UL navigation flow. -/// -/// Concrete implementations of this protocol will decide what that means -/// -public protocol NavigationCommand { - func execute(from: UIViewController?) -} diff --git a/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift b/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift deleted file mode 100644 index 5cee52fae30f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SafariCredentialsService.swift +++ /dev/null @@ -1,92 +0,0 @@ -import Foundation -import WordPressShared - -// MARK: - SafariCredentialsService -// -class SafariCredentialsService { - - @objc static let LoginSharedWebCredentialFQDN: CFString = "wordpress.com" as CFString - typealias SharedWebCredentialsCallback = (_ credentialsFound: Bool, _ username: String?, _ password: String?) -> Void - - /// Update safari stored credentials. - /// - /// - Parameter loginFields: An instance of LoginFields - /// - class func updateSafariCredentialsIfNeeded(with loginFields: LoginFields) { - // Paranioa. Don't try and update credentials for self-hosted. - if !loginFields.meta.userIsDotCom { - return - } - - // If the user changed screen names, don't try and update/create a new shared web credential. - // We'll let Safari handle creating newly saved usernames/passwords. - if loginFields.storedCredentials?.storedUserameHash != loginFields.username.hash { - return - } - - // If the user didn't change the password from previousl filled password no update is needed. - if loginFields.storedCredentials?.storedPasswordHash == loginFields.password.hash { - return - } - - // Update the shared credential - let username: CFString = loginFields.username as CFString - let password: CFString = loginFields.password as CFString - - SecAddSharedWebCredential(LoginSharedWebCredentialFQDN, username, password, { (error: CFError?) in - guard error == nil else { - let err = error - WPLogError("Error occurred updating shared web credential: \(String(describing: err?.localizedDescription))") - return - } - DispatchQueue.main.async(execute: { - WordPressAuthenticator.track(.loginAutoFillCredentialsUpdated) - }) - }) - } - - /// Request shared safari credentials if they exist. - /// - /// - Parameter completion: A completion block. - /// - class func requestSharedWebCredentials(_ completion: @escaping SharedWebCredentialsCallback) { - SecRequestSharedWebCredential(LoginSharedWebCredentialFQDN, nil, { (credentials: CFArray?, error: CFError?) in - WPLogInfo("Completed requesting shared web credentials") - guard error == nil else { - let err = error as Error? - if let error = err as NSError?, error.code == -25300 { - // An OSStatus of -25300 is expected when no saved credentails are found. - WPLogInfo("No shared web credenitals found.") - } else { - WPLogError("Error requesting shared web credentials: \(String(describing: err?.localizedDescription))") - } - DispatchQueue.main.async { - completion(false, nil, nil) - } - return - } - - guard let credentials, CFArrayGetCount(credentials) > 0 else { - // Saved credentials exist but were not selected. - DispatchQueue.main.async(execute: { - completion(true, nil, nil) - }) - return - } - - // What a chore! - let unsafeCredentials = CFArrayGetValueAtIndex(credentials, 0) - let credentialsDict = unsafeBitCast(unsafeCredentials, to: CFDictionary.self) - - let unsafeUsername = CFDictionaryGetValue(credentialsDict, Unmanaged.passUnretained(kSecAttrAccount).toOpaque()) - let usernameStr = unsafeBitCast(unsafeUsername, to: CFString.self) as String - - let unsafePassword = CFDictionaryGetValue(credentialsDict, Unmanaged.passUnretained(kSecSharedPassword).toOpaque()) - let passwordStr = unsafeBitCast(unsafePassword, to: CFString.self) as String - - DispatchQueue.main.async { - completion(true, usernameStr, passwordStr) - } - }) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/SignupService.swift b/Sources/WordPressAuthenticator/Helpers/SignupService.swift deleted file mode 100644 index 1ea1498b6339..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SignupService.swift +++ /dev/null @@ -1,134 +0,0 @@ -import Foundation -import WordPressShared -import WordPressKit - -/// SignupService: Responsible for creating a new WPCom user and blog. -/// -class SignupService: SocialUserCreating { - - /// Create a new WPcom account using Google signin token - /// - /// - Parameters: - /// - googleToken: the token from a successful Google login - /// - success: block called when account is created successfully - /// - failure: block called when account creation fails - /// - func createWPComUserWithGoogle(token: String, - success: @escaping (_ newAccount: Bool, _ username: String, _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void) { - - let remote = WordPressComServiceRemote(wordPressComRestApi: anonymousAPI) - - remote.createWPComAccount(withGoogle: token, - andClientID: configuration.wpcomClientId, - andClientSecret: configuration.wpcomSecret, - success: { response in - - guard let username = response?[ResponseKeys.username] as? String, - let bearer_token = response?[ResponseKeys.bearerToken] as? String else { - failure(SignupError.unknown) - return - } - - let createdAccount = (response?[ResponseKeys.createdAccount] as? Int ?? 0) == 1 - success(createdAccount, username, bearer_token) - }, failure: { error in - failure(error ?? SignupError.unknown) - }) - } - - /// Create a new WPcom account using Apple ID - /// - /// - Parameters: - /// - token: Token provided by Apple. - /// - email: Email provided by Apple. - /// - fullName: Formatted full name provided by Apple. - /// - success: Block called when account is created successfully. - /// - failure: Block called when account creation fails. - /// - func createWPComUserWithApple(token: String, - email: String, - fullName: String?, - success: @escaping (_ newAccount: Bool, - _ existingNonSocialAccount: Bool, - _ existing2faAccount: Bool, - _ username: String, - _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void) { - let remote = WordPressComServiceRemote(wordPressComRestApi: anonymousAPI) - - remote.createWPComAccount(withApple: token, - andEmail: email, - andFullName: fullName, - andClientID: configuration.wpcomClientId, - andClientSecret: configuration.wpcomSecret, - success: { response in - guard let username = response?[ResponseKeys.username] as? String, - let bearer_token = response?[ResponseKeys.bearerToken] as? String else { - failure(SignupError.unknown) - return - } - - let createdAccount = (response?[ResponseKeys.createdAccount] as? Int ?? 0) == 1 - success(createdAccount, false, false, username, bearer_token) - }, failure: { error in - if let error = (error as NSError?) { - - if (error.userInfo[ErrorKeys.errorCode] as? String ?? "") == ErrorKeys.twoFactorEnabled { - success(false, true, true, "", "") - return - } - - if (error.userInfo[ErrorKeys.errorCode] as? String ?? "") == ErrorKeys.existingNonSocialUser { - - // If an account already exists, the account email should be returned in the Error response. - // Extract it and return it. - var existingEmail = "" - if let errorData = error.userInfo[WordPressComRestApi.ErrorKeyErrorData] as? [String: String] { - let emailDict = errorData.first { $0.key == WordPressComRestApi.ErrorKeyErrorDataEmail } - let email = emailDict?.value ?? "" - existingEmail = email - } - - success(false, true, false, existingEmail, "") - return - } - } - - failure(error ?? SignupError.unknown) - }) - } -} - -// MARK: - Private -// -private extension SignupService { - - var anonymousAPI: WordPressComRestApi { - return WordPressComRestApi(oAuthToken: nil, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - } - - var configuration: WordPressAuthenticatorConfiguration { - return WordPressAuthenticator.shared.configuration - } - - struct ResponseKeys { - static let bearerToken = "bearer_token" - static let username = "username" - static let createdAccount = "created_account" - } - - struct ErrorKeys { - static let errorCode = "WordPressComRestApiErrorCodeKey" - static let existingNonSocialUser = "user_exists" - static let twoFactorEnabled = "2FA_enabled" - } -} - -// MARK: - Errors -// -enum SignupError: Error { - case unknown -} diff --git a/Sources/WordPressAuthenticator/Helpers/SocialUser.swift b/Sources/WordPressAuthenticator/Helpers/SocialUser.swift deleted file mode 100644 index 372287ddde87..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SocialUser.swift +++ /dev/null @@ -1,8 +0,0 @@ -import WordPressKit - -public struct SocialUser { - - public let email: String - public let fullName: String - public let service: SocialServiceName -} diff --git a/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift b/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift deleted file mode 100644 index 63b1b6942460..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/SocialUserCreating.swift +++ /dev/null @@ -1,23 +0,0 @@ -/// A type that can create WordPress.com users given a social users, either coming from Google or Apple. -protocol SocialUserCreating: AnyObject { - - func createWPComUserWithGoogle( - token: String, - success: @escaping (_ newAccount: Bool, _ username: String, _ wpcomToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func createWPComUserWithApple( - token: String, - email: String, - fullName: String?, - success: @escaping ( - _ newAccount: Bool, - _ existingNonSocialAccount: Bool, - _ existing2faAccount: Bool, - _ username: String, - _ wpcomToken: String - ) -> Void, - failure: @escaping (_ error: Error) -> Void - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift deleted file mode 100644 index 28d5b498e220..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/GoogleAuthenticator.swift +++ /dev/null @@ -1,442 +0,0 @@ -import Foundation -import SVProgressHUD -import WordPressKit -import WordPressShared - -/// Contains delegate methods for Google authentication unified auth flow. -/// Both Login and Signup are handled via this delegate. -/// -protocol GoogleAuthenticatorDelegate: AnyObject { - // Google account login was successful. - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account login was successful, but a WP 2FA code is required. - func googleNeedsMultifactorCode(loginFields: LoginFields) - - // Google account login was successful, but a WP password is required. - func googleExistingUserNeedsConnection(loginFields: LoginFields) - - // Google account login failed. - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) - - // Google account selection cancelled by user. - func googleAuthCancelled() - - // Google account signup was successful. - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup redirected to login was successful. - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup failed. - func googleSignupFailed(error: Error, loginFields: LoginFields) -} - -/// Indicate which type of authentication is initiated. -/// Utilized by ViewControllers that handle separate Google Login and Signup flows. -/// This is needed as long as: -/// Separate Google Login and Signup flows are utilized. -/// Tracking is specific to separate Login and Signup flows. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -enum GoogleAuthType { - case login - case signup -} - -/// Contains delegate methods for Google login specific flow. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -protocol GoogleAuthenticatorLoginDelegate: AnyObject { - // Google account login was successful. - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account login was successful, but a WP 2FA code is required. - func googleNeedsMultifactorCode(loginFields: LoginFields) - - // Google account login was successful, but a WP password is required. - func googleExistingUserNeedsConnection(loginFields: LoginFields) - - // Google account login failed. - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields) -} - -/// Contains delegate methods for Google signup specific flow. -/// When separate Google Login and Signup flows are no longer used, this no longer needed. -/// -protocol GoogleAuthenticatorSignupDelegate: AnyObject { - // Google account signup was successful. - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup redirected to login was successful. - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) - - // Google account signup failed. - func googleSignupFailed(error: Error, loginFields: LoginFields) - - // Google account signup cancelled by user. - func googleSignupCancelled() -} - -class GoogleAuthenticator: NSObject { - - // MARK: - Properties - - static var sharedInstance = GoogleAuthenticator() - weak var loginDelegate: GoogleAuthenticatorLoginDelegate? - weak var signupDelegate: GoogleAuthenticatorSignupDelegate? - weak var delegate: GoogleAuthenticatorDelegate? - - private var loginFields = LoginFields() - private let authConfig = WordPressAuthenticator.shared.configuration - private var authType: GoogleAuthType = .login - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - private lazy var loginFacade: LoginFacade = { - let facade = LoginFacade(dotcomClientID: authConfig.wpcomClientId, - dotcomSecret: authConfig.wpcomSecret, - userAgent: authConfig.userAgent) - facade.delegate = self - return facade - }() - - private weak var authenticationDelegate: WordPressAuthenticatorDelegate? = { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - }() - - // MARK: - Start Authentication - - /// Public method to initiate the Google auth process. - /// - Parameters: - /// - viewController: The UIViewController that Google is being presented from. - /// Required by Google SDK. - /// - loginFields: LoginFields from the calling view controller. - /// The values are updated during the Google process, - /// and returned to the calling view controller via delegate methods. - /// - authType: Indicates the type of authentication (login or signup) - func showFrom( - viewController: UIViewController, - loginFields: LoginFields, - for authType: GoogleAuthType = .login - ) { - // The fact that we set `loginFields`, then reset its `meta.socialService` property doesn't - // seem ideal... - self.loginFields = loginFields - self.loginFields.meta.socialService = SocialServiceName.google - self.authType = authType - - Task { @MainActor in - do { - let token = try await requestAuthorization( - for: authType, - from: viewController, - loginFields: loginFields - ) - - didSignIn(token: token.token.rawValue, email: token.email, fullName: token.name) - } catch { - failedToSignIn(error: error) - } - } - } - - /// Public method to create a WP account with a Google account. - /// - Parameters: - /// - loginFields: LoginFields from the calling view controller. - /// The values are updated during the Google process, - /// and returned to the calling view controller via delegate methods. - func createGoogleAccount(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let token = loginFields.meta.socialServiceIDToken else { - WPLogError("GoogleAuthenticator - createGoogleAccount: Failed to get Google account information.") - return - } - - createWordPressComUser(token: token, email: loginFields.emailAddress) - } -} - -// MARK: - Private Extension - -private extension GoogleAuthenticator { - - private func trackRequestAuthorizitation(type: GoogleAuthType) { - switch type { - case .login: - tracker.set(flow: .loginWithGoogle) - tracker.track(step: .start) { - track(.loginSocialButtonClick) - } - case .signup: - track(.createAccountInitiated) - } - } - - func track(_ event: WPAnalyticsStat, properties: [AnyHashable: Any] = [:]) { - var trackProperties = properties - trackProperties["source"] = "google" - WordPressAuthenticator.track(event, properties: trackProperties) - } - - private func failedToSignIn(error: Error?) { - // The Google SignIn may have been cancelled. - // - // FIXME: Is `error == .none` how we distinguish between user cancellation and legit error? - let failure = error?.localizedDescription ?? "Unknown error" - - tracker.track(failure: failure, ifTrackingNotEnabled: { - let properties = ["error": failure] - - switch authType { - case .login: - track(.loginSocialButtonFailure, properties: properties) - case .signup: - track(.signupSocialButtonFailure, properties: properties) - } - }) - - // Notify the delegates so the Google Auth view can be dismissed. - // - // FIXME: Shouldn't we be calling a method to report error, if there was one? - signupDelegate?.googleSignupCancelled() - delegate?.googleAuthCancelled() - } - - private func didSignIn(token: String, email: String, fullName: String) { - // Save account information to pass back to delegate later. - loginFields.emailAddress = email - loginFields.username = email - loginFields.meta.socialServiceIDToken = token - loginFields.meta.socialUser = SocialUser(email: email, fullName: fullName, service: .google) - - guard authConfig.enableUnifiedAuth else { - // Initiate separate WP login / signup paths. - switch authType { - case .login: - SVProgressHUD.show() - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.google.rawValue) - case .signup: - createWordPressComUser(token: token, email: email) - } - - return - } - - // Initiate unified path by attempting to login first. - // - // `SVProgressHUD.show()` will crash in an app that doesn't have a window property in its - // `UIApplicationDelegate`, such as those created via the Xcode templates circa version 12 - // onwards. - SVProgressHUD.show() - loginFacade.loginToWordPressDotCom(withSocialIDToken: token, service: SocialServiceName.google.rawValue) - } - - enum LocalizedText { - static let googleConnected = NSLocalizedString("Connected But…", comment: "Title shown when a user logs in with Google but no matching WordPress.com account is found") - static let googleConnectedError = NSLocalizedString("The Google account \"%@\" doesn't match any account on WordPress.com", comment: "Description shown when a user logs in with Google but no matching WordPress.com account is found") - static let googleUnableToConnect = NSLocalizedString("Unable To Connect", comment: "Shown when a user logs in with Google but it subsequently fails to work as login to WordPress.com") - } -} - -// MARK: - SDK-less flow - -extension GoogleAuthenticator { - - private func requestAuthorization( - for authType: GoogleAuthType, - from viewController: UIViewController, - loginFields: LoginFields - ) async throws -> IDToken { - // Intentionally duplicated from the callsite, so we don't forget about this when removing - // the SDK. - // - // The fact that we set `loginFields`, then reset its `meta.socialService` property doesn't - // seem ideal... - self.loginFields = loginFields - self.loginFields.meta.socialService = SocialServiceName.google - self.authType = authType - - trackRequestAuthorizitation(type: authType) - - let sdkLessGoogleAuthenticator = NewGoogleAuthenticator( - clientId: authConfig.googleClientId, - scheme: authConfig.googleLoginScheme, - audience: authConfig.googleLoginServerClientId, - urlSession: .shared - ) - - await SVProgressHUD.show() - return try await sdkLessGoogleAuthenticator.getOAuthToken(from: viewController) - } -} - -// MARK: - LoginFacadeDelegate - -extension GoogleAuthenticator: LoginFacadeDelegate { - - // Google account login was successful. - func finishedLogin(withGoogleIDToken googleIDToken: String, authToken: String) { - SVProgressHUD.dismiss() - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialSuccess) - } - - let wpcom = WordPressComCredentials(authToken: authToken, - isJetpackLogin: loginFields.meta.jetpackLogin, - multifactor: false, - siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - loginDelegate?.googleFinishedLogin(credentials: credentials, loginFields: loginFields) - delegate?.googleFinishedLogin(credentials: credentials, loginFields: loginFields) - } - - // Google account login was successful, but a WP 2FA code is required. - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - SVProgressHUD.dismiss() - - loginFields.nonceInfo = nonceInfo - loginFields.nonceUserID = userID - - if tracker.shouldUseLegacyTracker() { - track(.loginSocial2faNeeded) - } - - loginDelegate?.googleNeedsMultifactorCode(loginFields: loginFields) - delegate?.googleNeedsMultifactorCode(loginFields: loginFields) - } - - // Google account login was successful, but a WP password is required. - func existingUserNeedsConnection(_ email: String) { - SVProgressHUD.dismiss() - - loginFields.username = email - loginFields.emailAddress = email - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialAccountsNeedConnecting) - } - - loginDelegate?.googleExistingUserNeedsConnection(loginFields: loginFields) - delegate?.googleExistingUserNeedsConnection(loginFields: loginFields) - } - - // Google account login failed. - func displayRemoteError(_ error: Error) { - SVProgressHUD.dismiss() - - var errorTitle = LocalizedText.googleUnableToConnect - var errorDescription = error.localizedDescription - let unknownUser = (error as? WordPressComOAuthError)?.authenticationFailureKind == .unknownUser - - if unknownUser { - errorTitle = LocalizedText.googleConnected - errorDescription = String(format: LocalizedText.googleConnectedError, loginFields.username) - - if tracker.shouldUseLegacyTracker() { - track(.loginSocialErrorUnknownUser) - } - } else { - // Don't track unknown user for unified Auth. - tracker.track(failure: errorDescription) - } - - loginDelegate?.googleLoginFailed(errorTitle: errorTitle, errorDescription: errorDescription, loginFields: loginFields) - delegate?.googleLoginFailed(errorTitle: errorTitle, errorDescription: errorDescription, loginFields: loginFields, unknownUser: unknownUser) - } -} - -// MARK: - Sign Up Methods - -private extension GoogleAuthenticator { - - /// Creates a WordPress.com account with the associated Google token and email. - /// - func createWordPressComUser(token: String, email: String) { - SVProgressHUD.show() - let service = SignupService() - - service.createWPComUserWithGoogle(token: token, success: { [weak self] accountCreated, wpcomUsername, wpcomToken in - - let wpcom = WordPressComCredentials(authToken: wpcomToken, isJetpackLogin: false, multifactor: false, siteURL: self?.loginFields.siteAddress ?? "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - // New Account - if accountCreated { - SVProgressHUD.dismiss() - // Notify the host app - self?.authenticationDelegate?.createdWordPressComAccount(username: wpcomUsername, authToken: wpcomToken) - // Notify the delegate - self?.accountCreated(credentials: credentials) - - return - } - - // Existing Account - // Sync host app - self?.authenticationDelegate?.sync(credentials: credentials) { - SVProgressHUD.dismiss() - // Notify delegate - self?.logInInstead(credentials: credentials) - } - }, failure: { [weak self] error in - SVProgressHUD.dismiss() - // Notify delegate - self?.signupFailed(error: error) - }) - } - - func accountCreated(credentials: AuthenticatorCredentials) { - // This stat is part of a funnel that provides critical information. Before - // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - track(.createdAccount) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .success, ifTrackingNotEnabled: { - track(.signupSocialSuccess) - }) - - signupDelegate?.googleFinishedSignup(credentials: credentials, loginFields: loginFields) - delegate?.googleFinishedSignup(credentials: credentials, loginFields: loginFields) - } - - func logInInstead(credentials: AuthenticatorCredentials) { - tracker.set(flow: .loginWithGoogle) - - // This stat is part of a funnel that provides critical information. Please - // consult with your lead before removing this event. - track(.signedIn) - - tracker.track(step: .start) { - track(.signupSocialToLogin) - track(.loginSocialSuccess) - } - - signupDelegate?.googleLoggedInInstead(credentials: credentials, loginFields: loginFields) - delegate?.googleLoggedInInstead(credentials: credentials, loginFields: loginFields) - } - - func signupFailed(error: Error) { - tracker.track(failure: error.localizedDescription, ifTrackingNotEnabled: { - track(.signupSocialFailure, properties: ["error": error.localizedDescription]) - }) - - signupDelegate?.googleSignupFailed(error: error, loginFields: loginFields) - delegate?.googleSignupFailed(error: error, loginFields: loginFields) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift deleted file mode 100644 index 6d01ca653687..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsAuthenticator.swift +++ /dev/null @@ -1,247 +0,0 @@ -import Foundation -import AuthenticationServices -import SVProgressHUD -import WordPressKit -import WordPressShared - -/// The authorization flow handled by this class starts by showing Apple's `ASAuthorizationController` -/// through our class `StoredCredentialsPicker`. This controller lets the user pick the credentials they -/// want to login with. This class handles both showing that controller and executing the remaining flow to -/// complete the login process. -/// -class StoredCredentialsAuthenticator: NSObject { - - // MARK: - Delegates - - private var authenticationDelegate: WordPressAuthenticatorDelegate { - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError() - } - return delegate - } - - // MARK: - Configuration - - private var authConfig: WordPressAuthenticatorConfiguration { - WordPressAuthenticator.shared.configuration - } - - // MARK: - Login Support - - private lazy var loginFacade: LoginFacade = { - let facade = LoginFacade(dotcomClientID: authConfig.wpcomClientId, - dotcomSecret: authConfig.wpcomSecret, - userAgent: authConfig.userAgent) - facade.delegate = self - return facade - }() - - // MARK: - Cancellation - - private let onCancel: (() -> Void)? - - // MARK: - UI - - private let picker = StoredCredentialsPicker() - private weak var navigationController: UINavigationController? - - // MARK: - Tracking Support - - private var tracker: AuthenticatorAnalyticsTracker { - AuthenticatorAnalyticsTracker.shared - } - - // MARK: - Login Fields - - private var loginFields: LoginFields? - - // MARK: - Initialization - - init(onCancel: (() -> Void)? = nil) { - self.onCancel = onCancel - } - - // MARK: - Picker - - /// Shows the UI for picking stored credentials for the user to log into their account. - /// - func showPicker(from navigationController: UINavigationController) { - self.navigationController = navigationController - - guard let window = navigationController.view.window else { - WPLogError("Can't obtain window for navigation controller") - return - } - - picker.show(in: window) { [weak self] result in - guard let self else { - return - } - - switch result { - case .success(let authorization): - self.pickerSuccess(authorization) - case .failure(let error): - self.pickerFailure(error) - } - } - } - - /// The selection of credentials and subsequent authorization by the OS succeeded. This method processes the credentials - /// and proceeds with the login operation. - /// - /// - Parameters: - /// - authorization: The authorization by the OS, containing the credentials picked by the user. - /// - private func pickerSuccess(_ authorization: ASAuthorization) { - tracker.track(step: .start) - tracker.set(flow: .loginWithiCloudKeychain) - SVProgressHUD.show() - - switch authorization.credential { - case _ as ASAuthorizationAppleIDCredential: - // No-op for now, but we can decide to implement AppleID login through this authenticator - // by implementing the logic here. - break - case let credential as ASPasswordCredential: - let loginFields = LoginFields.makeForWPCom(username: credential.user, password: credential.password) - loginFacade.signIn(with: loginFields) - self.loginFields = loginFields - default: - // There aren't any other known methods for us to handle here, but we still need to complete the switch - // statement. - break - } - } - - /// The selection of credentials or the subsequent authorization by the OS failed. This method processes the failure. - /// - /// - Parameters: - /// - error: The error detailing what failed. - /// - private func pickerFailure(_ error: Error) { - let authError = ASAuthorizationError(_nsError: error as NSError) - - switch authError.code { - case .canceled: - // The user cancelling the flow is not really an error, so we're not reporting or tracking - // this as an error. - // - // We're not tracking this either, since the Android App doesn't for SmartLock. The reason is - // that it's not trivial to know when the credentials picker UI is shown to the user, so knowing - // it's being dismissed is also not trivial. This was decided during the Unified Login & Signup - // project in a conversation between myself (Diego Rey Mendez) and Renan Ferrari. - break - default: - tracker.track(failure: authError.localizedDescription) - WPLogError("ASAuthorizationError: \(authError.localizedDescription)") - } - } -} - -extension StoredCredentialsAuthenticator: LoginFacadeDelegate { - func displayRemoteError(_ error: Error) { - tracker.track(failure: error.localizedDescription) - SVProgressHUD.dismiss() - - guard authConfig.enableUnifiedAuth else { - presentLoginEmailView(error: error) - return - } - - presentGetStartedView(error: error) - } - - func needsMultifactorCode() { - SVProgressHUD.dismiss() - presentTwoFactorAuthenticationView() - } - - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - loginFields?.nonceInfo = nonceInfo - loginFields?.nonceUserID = userID - - needsMultifactorCode() - } - - func finishedLogin(withAuthToken authToken: String, requiredMultifactorCode: Bool) { - let wpcom = WordPressComCredentials( - authToken: authToken, - isJetpackLogin: false, - multifactor: requiredMultifactorCode, - siteURL: "") - let credentials = AuthenticatorCredentials(wpcom: wpcom) - - authenticationDelegate.sync(credentials: credentials) { [weak self] in - SVProgressHUD.dismiss() - self?.presentLoginEpilogue(credentials: credentials) - } - } -} - -// MARK: - UI Flow - -extension StoredCredentialsAuthenticator { - private func presentLoginEpilogue(credentials: AuthenticatorCredentials) { - guard let navigationController = self.navigationController else { - WPLogError("No navigation controller to present the login epilogue from") - return - } - - authenticationDelegate.presentLoginEpilogue(in: navigationController, - for: credentials, - source: WordPressAuthenticator.shared.signInSource, - onDismiss: {}) - } - - /// Presents the login email screen, displaying the specified error. This is useful - /// for example for iCloud Keychain in the case where there's an error logging the user - /// in with the stored credentials for whatever reason. - /// - private func presentLoginEmailView(error: Error) { - guard let toVC = LoginEmailViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate to LoginEmailVC from LoginPrologueVC") - return - } - - if let loginFields { - toVC.loginFields = loginFields - } - toVC.errorToPresent = error - - navigationController?.pushViewController(toVC, animated: true) - } - - /// Presents the get started screen, displaying the specified error. This is useful - /// for example for iCloud Keychain in the case where there's an error logging the user - /// in with the stored credentials for whatever reason. - /// - private func presentGetStartedView(error: Error) { - guard let toVC = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate to GetStartedViewController") - return - } - - if let loginFields { - toVC.loginFields = loginFields - } - - toVC.errorMessage = error.localizedDescription - navigationController?.pushViewController(toVC, animated: true) - } - - private func presentTwoFactorAuthenticationView() { - guard let loginFields else { - return - } - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from LoginViewController to TwoFAViewController") - return - } - - vc.loginFields = loginFields - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift deleted file mode 100644 index a0668f5ab977..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/StoredCredentialsPicker.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import AuthenticationServices - -/// Thin wrapper around `ASAuthorizationController` to avoid having to set delegate methods in the VC -/// and to modularize / abstract the logic to show Apple's UI for picking the stored credentials. -/// -/// This picker takes care of returning the credentials that were picked (and authorized by the iOS) through a closure. -/// It's not within the scope of this class to take care of what happens after the credentials are picked. -/// -class StoredCredentialsPicker: NSObject { - - typealias CompletionClosure = (Result) -> Void - - /// The closure that will be executed once the credentials are picked and returned by the OS, - /// or once there's an Error. - /// - private var onComplete: CompletionClosure! - - /// The window where the quick authentication flow will be shown. - /// - private var window: UIWindow! - - func show(in window: UIWindow, onComplete: @escaping CompletionClosure) { - - self.onComplete = onComplete - self.window = window - - let requests = [ASAuthorizationPasswordProvider().createRequest()] - let controller = ASAuthorizationController(authorizationRequests: requests) - - controller.delegate = self - controller.presentationContextProvider = self - controller.performRequests() - } -} - -// MARK: - ASAuthorizationControllerDelegate - -extension StoredCredentialsPicker: ASAuthorizationControllerDelegate { - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - onComplete(.success(authorization)) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - onComplete(.failure(error)) - } -} - -// MARK: - ASAuthorizationControllerPresentationContextProviding - -extension StoredCredentialsPicker: ASAuthorizationControllerPresentationContextProviding { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return window - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard deleted file mode 100644 index 19b8e002cb02..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFA.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift deleted file mode 100644 index 0e95674e4f7f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/2FA/TwoFAViewController.swift +++ /dev/null @@ -1,694 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared -import SVProgressHUD -import AuthenticationServices - -/// TwoFAViewController: view to enter 2FA code. -/// -final class TwoFAViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - private weak var codeField: UITextField? - - private var rows = [Row]() - private var errorMessage: String? - private var pasteboardChangeCountBeforeBackground: Int? - private var shouldChangeVoiceOverFocus: Bool = false - - /// Tracks when the initial challenge request was made. - private var initialChallengeRequestTime: Date? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .login2FA - } - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .twoFactorAuthentication) - } else { - tracker.set(step: .twoFactorAuthentication) - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - configureSubmitButton(animating: false) - configureViewForEditingIfNeeded() - - let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(applicationBecameInactive), name: UIApplication.willResignActiveNotification, object: nil) - nc.addObserver(self, selector: #selector(applicationBecameActive), name: UIApplication.didBecomeActiveNotification, object: nil) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - - // Multifactor codes are time sensitive, so clear the stored code if the - // user dismisses the view. They'll need to reenter it upon return. - loginFields.multifactorCode = "" - codeField?.text = "" - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - let isNumeric = loginFields.multifactorCode.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil - let isValidLength = SocialLogin2FANonceInfo.TwoFactorTypeLengths(rawValue: loginFields.multifactorCode.count) != nil - - submitButton?.isEnabled = ( - !animating && - isNumeric && - isValidLength - ) - } - - override func configureViewLoading(_ loading: Bool) { - super.configureViewLoading(loading) - codeField?.isEnabled = !loading - initialChallengeRequestTime = nil - } - - override func displayRemoteError(_ error: Error) { - displayError(message: "") - - let err = error as NSError - - // If the error happened because the security key challenge request started more than 1 minute ago, show a timeout error. - // This check is needed because the server sends a generic error. - if let initialChallengeRequestTime, Date().timeIntervalSince(initialChallengeRequestTime) >= 60, err.code == .zero { - return displaySecurityKeyErrorMessageAndExitFlow(message: LocalizedText.timeoutError) - } - - configureViewLoading(false) - if (error as? WordPressComOAuthError)?.authenticationFailureKind == .invalidOneTimePassword { - // Invalid verification code. - displayError(message: LocalizedText.bad2FAMessage, moveVoiceOverFocus: true) - } else if case let .endpointError(authenticationFailure) = (error as? WordPressComOAuthError), authenticationFailure.kind == .invalidTwoStepCode { - // Invalid 2FA during social login - if let newNonce = authenticationFailure.newNonce { - loginFields.nonceInfo?.updateNonce(with: newNonce) - } - displayError(message: LocalizedText.bad2FAMessage, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - Validation and Login - -private extension TwoFAViewController { - - // MARK: - Button Actions - - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submitTwoFactorCode) - validateForm() - } - - func requestCode() { - SVProgressHUD.showSuccess(withStatus: LocalizedText.smsSent) - SVProgressHUD.dismiss(withDelay: TimeInterval(1)) - - if loginFields.nonceInfo != nil { - // social login - loginFacade.requestSocial2FACode(with: loginFields) - } else { - loginFacade.requestOneTimeCode(with: loginFields) - } - } - - // MARK: - Login - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - func validateForm() { - guard let nonceInfo = loginFields.nonceInfo else { - return validateFormAndLogin() - } - - let (authType, nonce) = nonceInfo.authTypeAndNonce(for: loginFields.multifactorCode) - if nonce.isEmpty { - return validateFormAndLogin() - } - - loginWithNonce(nonce, authType: authType, code: loginFields.multifactorCode) - } - - func loginWithNonce(_ nonce: String, authType: String, code: String) { - configureViewLoading(true) - loginFacade.loginToWordPressDotCom(withUser: loginFields.nonceUserID, authType: authType, twoStepCode: code, twoStepNonce: nonce) - } - - func finishedLogin(withNonceAuthToken authToken: String) { - let wpcom = WordPressComCredentials(authToken: authToken, isJetpackLogin: isJetpackLogin, multifactor: true, siteURL: loginFields.siteAddress) - let credentials = AuthenticatorCredentials(wpcom: wpcom) - syncWPComAndPresentEpilogue(credentials: credentials) - } - - // MARK: - Security Keys - - func loginWithSecurityKeys() { - - guard let twoStepNonce = loginFields.nonceInfo?.nonceWebauthn else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - configureViewLoading(true) - initialChallengeRequestTime = Date() - - Task { @MainActor in - guard let challengeInfo = await loginFacade.requestWebauthnChallenge(userID: loginFields.nonceUserID, twoStepNonce: twoStepNonce) else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - signChallenge(challengeInfo) - } - } - - func signChallenge(_ challengeInfo: WebauthnChallengeInfo) { - - loginFields.nonceInfo?.updateNonce(with: challengeInfo.twoStepNonce) - loginFields.webauthnChallengeInfo = challengeInfo - - let challenge = Data(base64URLEncoded: challengeInfo.challenge) ?? Data() - let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: challengeInfo.rpID) - let platformKeyRequest = platformProvider.createCredentialAssertionRequest(challenge: challenge) - - let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest]) - authController.delegate = self - authController.presentationContextProvider = self - authController.performRequests() - } - - // When an security key error occurs, we need to restart the flow to regenerate the necessary nonces. - func displaySecurityKeyErrorMessageAndExitFlow(message: String = LocalizedText.unknownError) { - configureViewLoading(false) - displayErrorAlert(message, sourceTag: .loginWebauthn, onDismiss: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - } - - // MARK: - Code Validation - - enum CodeValidation { - case invalid(nonNumbers: Bool) - case valid(String) - } - - func isValidCode(code: String) -> CodeValidation { - let codeStripped = code.components(separatedBy: .whitespacesAndNewlines).joined() - let allowedCharacters = CharacterSet.decimalDigits - let resultCharacterSet = CharacterSet(charactersIn: codeStripped) - let isOnlyNumbers = allowedCharacters.isSuperset(of: resultCharacterSet) - let isShortEnough = codeStripped.count <= SocialLogin2FANonceInfo.TwoFactorTypeLengths.backup.rawValue - - if isOnlyNumbers && isShortEnough { - return .valid(codeStripped) - } - - if isOnlyNumbers { - return .invalid(nonNumbers: false) - } - - return .invalid(nonNumbers: true) - } - - // MARK: - Text Field Handling - - func handleTextFieldDidChange(_ sender: UITextField) { - loginFields.multifactorCode = codeField?.nonNilTrimmedText() ?? "" - configureSubmitButton(animating: false) - } -} - -// MARK: - Security Keys -extension TwoFAViewController: ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - - // Validate necessary data - guard let credential = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion, - let challengeInfo = loginFields.webauthnChallengeInfo, - let clientDataJson = extractClientData(from: credential, challengeInfo: challengeInfo) else { - return displaySecurityKeyErrorMessageAndExitFlow() - } - - // Validate that the submitted passkey is allowed. - guard challengeInfo.allowedCredentialIDs.contains(credential.credentialID.base64URLEncodedString()) else { - return displaySecurityKeyErrorMessageAndExitFlow(message: LocalizedText.invalidKey) - } - - loginFacade.authenticateWebauthnSignature(userID: loginFields.nonceUserID, - twoStepNonce: challengeInfo.twoStepNonce, - credentialID: credential.credentialID, - clientDataJson: clientDataJson, - authenticatorData: credential.rawAuthenticatorData, - signature: credential.signature, - userHandle: credential.userID) - } - - // Some password managers(like 1P) don't deliver `rawClientDataJSON`. In those cases we need to assemble it manually. - func extractClientData(from credential: ASAuthorizationPlatformPublicKeyCredentialAssertion, challengeInfo: WebauthnChallengeInfo) -> Data? { - - if !credential.rawClientDataJSON.isEmpty { - return credential.rawClientDataJSON - } - - // We build this manually because we need to guarantee this exact element order. - let rawClientJSON = "{\"type\":\"webauthn.get\",\"challenge\":\"\(challengeInfo.challenge)\",\"origin\":\"https://\(challengeInfo.rpID)\"}" - return rawClientJSON.data(using: .utf8) - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - WPLogError("Error signing challenge: \(error.localizedDescription)") - displaySecurityKeyErrorMessageAndExitFlow() - } - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - view.window! - } -} - -// MARK: - UITextFieldDelegate - -extension TwoFAViewController: UITextFieldDelegate { - - /// Only allow digits in the 2FA text field - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString: String) -> Bool { - - guard let fieldText = textField.text as NSString? else { - return true - } - - let resultString = fieldText.replacingCharacters(in: range, with: replacementString) - - switch isValidCode(code: resultString) { - case .valid(let cleanedCode): - displayError(message: "") - - // because the string was stripped of whitespace, we can't return true and we update the textfield ourselves - textField.text = cleanedCode - handleTextFieldDidChange(textField) - case .invalid(nonNumbers: true): - displayError(message: LocalizedText.numericalCode) - default: - if let pasteString = UIPasteboard.general.string, pasteString == replacementString { - displayError(message: LocalizedText.invalidCode) - } - } - - return false - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - validateForm() - return true - } -} - -// MARK: - UITableViewDataSource - -extension TwoFAViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Keyboard Notifications - -extension TwoFAViewController: NUXKeyboardResponder { - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Application state changes - -private extension TwoFAViewController { - - @objc func applicationBecameInactive() { - pasteboardChangeCountBeforeBackground = UIPasteboard.general.changeCount - } - - @objc func applicationBecameActive() { - guard let codeField else { - return - } - - let emptyField = codeField.text?.isEmpty ?? true - guard emptyField, - pasteboardChangeCountBeforeBackground != UIPasteboard.general.changeCount else { - return - } - - UIPasteboard.general.detectAuthenticatorCode { [weak self] result in - switch result { - case .success(let authenticatorCode): - self?.handle(code: authenticatorCode, textField: codeField) - case .failure: - break - } - } - } - - private func handle(code: String, textField: UITextField) { - switch isValidCode(code: code) { - case .valid(let cleanedCode): - displayError(message: "") - textField.text = cleanedCode - handleTextFieldDidChange(textField) - default: - break - } - } -} - -// MARK: - Table Management - -private extension TwoFAViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - - tableView.register(SpacerTableViewCell.self, forCellReuseIdentifier: SpacerTableViewCell.reuseIdentifier) - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .code] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - rows.append(.spacer(20)) - rows.append(.alternateInstructions) - - rows.append(.spacer(4)) - rows.append(.sendCode) - - if WordPressAuthenticator.shared.configuration.enablePasskeys, loginFields.nonceInfo?.nonceWebauthn.isEmpty == false { - rows.append(.spacer(4)) - rows.append(.enterSecurityKey) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .alternateInstructions: - configureAlternateInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureTextField(cell) - case let cell as TextLinkButtonTableViewCell where row == .sendCode: - configureTextLinkButton(cell) - case let cell as TextLinkButtonTableViewCell where row == .enterSecurityKey: - configureEnterSecurityKeyLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - case let cell as SpacerTableViewCell: - if case let .spacer(spacing) = row { - configureSpacerCell(cell, spacing: spacing) - } - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.twoFactorInstructions) - } - - /// Configure the alternate instruction cell. - /// - func configureAlternateInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.twoFactorOtherFormsInstructions) - } - - /// Configure the textfield cell. - /// - func configureTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .numericCode, - placeholder: WordPressAuthenticator.shared.displayStrings.twoFactorCodePlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - codeField = cell.textField - cell.textField.delegate = self - - SigninEditingState.signinEditingStateActive = true - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive VoiceOver elements. - codeField?.placeholder = nil - } - } - - /// Configure the link cell. - /// - func configureTextLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.textCodeButtonTitle, icon: .phoneIcon) - - cell.actionHandler = { [weak self] in - guard let self else { return } - - self.tracker.track(click: .sendCodeWithText) - self.requestCode() - } - } - - /// Configure the security key link cell. - /// - func configureEnterSecurityKeyLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.securityKeyButtonTitle, - icon: .keyIcon, - accessibilityIdentifier: TextLinkButtonTableViewCell.Constants.passkeysID) - - cell.actionHandler = { [weak self] in - guard let self else { return } - - self.tracker.track(click: .enterSecurityKey) - self.loginWithSecurityKeys() - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the spacer cell. - /// - func configureSpacerCell(_ cell: SpacerTableViewCell, spacing: CGFloat) { - cell.spacing = spacing - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - codeField?.becomeFirstResponder() - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - codeField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: codeField) - } - - /// Rows listed in the order they were created. - /// - enum Row: Equatable { - case instructions - case code - case alternateInstructions - case sendCode - case enterSecurityKey - case errorMessage - case spacer(CGFloat) - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .code: - return TextFieldTableViewCell.reuseIdentifier - case .alternateInstructions: - return TextLabelTableViewCell.reuseIdentifier - case .sendCode: - return TextLinkButtonTableViewCell.reuseIdentifier - case .enterSecurityKey: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - case .spacer: - return SpacerTableViewCell.reuseIdentifier - } - } - } - - enum LocalizedText { - static let bad2FAMessage = NSLocalizedString("Whoops, that's not a valid two-factor verification code. Double-check your code and try again!", comment: "Error message shown when an incorrect two factor code is provided.") - static let numericalCode = NSLocalizedString("A verification code will only contain numbers.", comment: "Shown when a user types a non-number into the two factor field.") - static let invalidCode = NSLocalizedString("That doesn't appear to be a valid verification code.", comment: "Shown when a user pastes a code into the two factor field that contains letters or is the wrong length") - static let smsSent = NSLocalizedString("SMS Sent", comment: "One Time Code has been sent via SMS") - static let invalidKey = NSLocalizedString("Whoops, that security key does not seem valid. Please try again with another one", - comment: "Error when the uses chooses an invalid security key on the 2FA screen.") - static let timeoutError = NSLocalizedString("Time's up, but don't worry, your security is our priority. Please try again!", - comment: "Error when the uses takes more than 1 minute to submit a security key.") - static let unknownError = NSLocalizedString("Whoops, something went wrong. Please try again!", comment: "Generic error on the 2FA screen") - } -} - -private extension TwoFAViewController { - /// Simple spacer cell for a table view. - /// - final class SpacerTableViewCell: UITableViewCell { - - /// Static identifier - /// - static let reuseIdentifier = "SpacerTableViewCell" - - /// Gets or sets the desired vertical spacing. - /// - var spacing: CGFloat { - get { - heightConstraint.constant - } - set { - heightConstraint.constant = newValue - } - } - - /// Determines the view height internally - /// - private let heightConstraint: NSLayoutConstraint - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - - let spacerView = UIView() - spacerView.translatesAutoresizingMaskIntoConstraints = false - heightConstraint = spacerView.heightAnchor.constraint(equalToConstant: 0) - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - addSubview(spacerView) - NSLayoutConstraint.activate([ - spacerView.topAnchor.constraint(equalTo: topAnchor), - spacerView.bottomAnchor.constraint(equalTo: bottomAnchor), - spacerView.leadingAnchor.constraint(equalTo: leadingAnchor), - spacerView.trailingAnchor.constraint(equalTo: trailingAnchor), - heightConstraint - ]) - } - - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard deleted file mode 100644 index 36179af4040c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStarted.storyboard +++ /dev/null @@ -1,159 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift deleted file mode 100644 index 368d120bb14a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/GetStarted/GetStartedViewController.swift +++ /dev/null @@ -1,987 +0,0 @@ -import UIKit -import SafariServices -import WordPressKit -import WordPressShared - -/// The source for the sign in flow for external tracking. -public enum SignInSource: Equatable { - /// Initiated from the WP.com login CTA. - case wpCom - /// Initiated from the WP.com login flow that starts with site address. - case wpComSiteAddress - /// Other source identifier from the host app. - case custom(source: String) -} - -/// The error during the sign in flow. -public enum SignInError: Error { - case invalidWPComEmail(source: SignInSource) - case invalidWPComPassword(source: SignInSource) - - init?(error: Error, source: SignInSource?) { - // `WordPressComRestApi` currently may return an WordPressComRestApiEndpointError, but it will later be changed - // to return `WordPressAPIError`. We'll handle both cases for now. - var restApiError = error as? WordPressComRestApiEndpointError - - if restApiError == nil, - let apiError = error as? WordPressAPIError, - case let .endpointError(endpointError) = apiError { - restApiError = endpointError - } - - if let restApiError, restApiError.code == .unknown { - if let source, restApiError.apiErrorCode == "unknown_user" { - self = .invalidWPComEmail(source: source) - } else { - return nil - } - } - - return nil - } -} - -class GetStartedViewController: LoginViewController, NUXKeyboardResponder { - - private enum ScreenMode { - /// For signing in using .org site credentials - /// - case signInUsingSiteCredentials - - /// For signing in using WPCOM credentials or social accounts - case signInUsingWordPressComOrSocialAccounts - } - - // MARK: - NUXKeyboardResponder constraints - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - Properties - @IBOutlet private weak var tableView: UITableView! - @IBOutlet private weak var leadingDividerLine: UIView! - @IBOutlet private weak var leadingDividerLineWidth: NSLayoutConstraint! - @IBOutlet private weak var dividerStackView: UIStackView! - @IBOutlet private weak var dividerLabel: UILabel! - @IBOutlet private weak var trailingDividerLine: UIView! - @IBOutlet private weak var trailingDividerLineWidth: NSLayoutConstraint! - - private weak var emailField: UITextField? - // This is to contain the password selected by password auto-fill. - // When it is populated, login is attempted. - @IBOutlet private weak var hiddenPasswordField: UITextField? - - // This is public so it can be set from StoredCredentialsAuthenticator. - var errorMessage: String? - - var source: SignInSource? { - didSet { - WordPressAuthenticator.shared.signInSource = source - } - } - - private var rows = [Row]() - private var buttonViewController: NUXButtonViewController? - private let configuration = WordPressAuthenticator.shared.configuration - private var shouldChangeVoiceOverFocus: Bool = false - - private var passwordCoordinator: PasswordCoordinator? - - private lazy var storedCredentialsAuthenticator = StoredCredentialsAuthenticator(onCancel: { [weak self] in - // Since the authenticator has its own flow - self?.tracker.resetState() - }) - - /// Sign in with site credentials button will be displayed based on the `screenMode` - /// - private var screenMode: ScreenMode { - guard configuration.enableSiteCredentialsLoginForSelfHostedSites, - loginFields.siteAddress.isEmpty == false else { - return .signInUsingWordPressComOrSocialAccounts - } - return .signInUsingSiteCredentials - } - - // Submit button displayed in the table footer. - private lazy var continueButton: NUXButton = { - let button = NUXButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.isPrimary = true - button.isEnabled = false - button.addTarget(self, action: #selector(handleSubmitButtonTapped), for: .touchUpInside) - button.accessibilityIdentifier = ButtonConfiguration.Continue.accessibilityIdentifier - button.setTitle(ButtonConfiguration.Continue.title, for: .normal) - - return button - }() - - // "What is WordPress.com?" button - private lazy var whatisWPCOMButton: UIButton = { - let button = UIButton() - button.setTitle(WordPressAuthenticator.shared.displayStrings.whatIsWPComLinkTitle, for: .normal) - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - button.titleLabel?.font = WPStyleGuide.mediumWeightFont(forStyle: .subheadline) - button.setTitleColor(buttonTitleColor, for: .normal) - button.setTitleColor(buttonHighlightColor, for: .highlighted) - button.addTarget(self, action: #selector(whatIsWPComButtonTapped(_:)), for: .touchUpInside) - return button - }() - - private var showsContinueButtonAtTheBottom: Bool { - configuration.enableSocialLogin == false - } - - override open var sourceTag: WordPressSupportSourceTag { - get { - return .loginEmail - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - configureNavBar() - setupTable() - registerTableViewCells() - loadRows() - setupTableFooterView() - configureDivider() - - if screenMode == .signInUsingSiteCredentials { - configureButtonViewControllerForSiteCredentialsMode() - } else if configuration.enableSocialLogin == false { - configureButtonViewControllerWithoutSocialLogin() - } else { - configureSocialButtons() - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - refreshEmailField() - - // Ensure the continue button matches the validity of the email field - configureContinueButton(animating: false) - - if errorMessage != nil { - shouldChangeVoiceOverFocus = true - } - } - - override open func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - configureAnalyticsTracker() - - errorMessage = nil - hiddenPasswordField?.text = nil - hiddenPasswordField?.isAccessibilityElement = false - - if showsContinueButtonAtTheBottom { - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - } - - if screenMode == .signInUsingWordPressComOrSocialAccounts && isMovingToParent { - showiCloudKeychainLoginFlow() - } - } - - /// Starts the iCloud Keychain login flow if the conditions are given. - /// - private func showiCloudKeychainLoginFlow() { - guard WordPressAuthenticator.shared.configuration.enableUnifiedAuth, - let navigationController else { - return - } - - storedCredentialsAuthenticator.showPicker(from: navigationController) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - tableView.updateFooterHeight() - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - override func configureViewLoading(_ loading: Bool) { - configureContinueButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - override func enableSubmit(animating: Bool) -> Bool { - return !animating && canSubmit() - } - - private func refreshEmailField() { - // It's possible that the password screen could have changed the loginFields username, for example when using - // autofill from a password manager. Let's ensure the loginFields matches the email field. - loginFields.username = emailField?.nonNilTrimmedText() ?? loginFields.username - } -} - -// MARK: - UITableViewDataSource - -extension GetStartedViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Private methods - -private extension GetStartedViewController { - - // MARK: - Configuration - - func configureNavBar() { - navigationItem.title = WordPressAuthenticator.shared.displayStrings.getStartedTitle - } - - func setupTable() { - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - } - - func setupTableFooterView() { - let stackView = UIStackView() - stackView.axis = .vertical - stackView.alignment = .fill - stackView.spacing = Constants.FooterStackView.spacing - stackView.layoutMargins = Constants.FooterStackView.layoutMargins - stackView.isLayoutMarginsRelativeArrangement = true - - if showsContinueButtonAtTheBottom == false { - // Continue button will be added to `buttonViewController` along with sign in with site credentials button when `screenMode` is `signInUsingSiteCredentials` - // and simplified login flow is disabled. - stackView.addArrangedSubview(continueButton) - } - - if configuration.whatIsWPComURL != nil { - let stackViewWithCenterAlignment = UIStackView() - stackViewWithCenterAlignment.axis = .vertical - stackViewWithCenterAlignment.alignment = .center - - stackViewWithCenterAlignment.addArrangedSubview(whatisWPCOMButton) - - stackView.addArrangedSubview(stackViewWithCenterAlignment) - } - - tableView.tableFooterView = stackView - tableView.updateFooterHeight() - } - - /// Style the "OR" divider. - /// - func configureDivider() { - guard showsContinueButtonAtTheBottom == false else { - return dividerStackView.isHidden = true - } - let color = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - leadingDividerLine.backgroundColor = color - leadingDividerLineWidth.constant = WPStyleGuide.hairlineBorderWidth - trailingDividerLine.backgroundColor = color - trailingDividerLineWidth.constant = WPStyleGuide.hairlineBorderWidth - dividerLabel.textColor = color - dividerLabel.text = NSLocalizedString("Or", comment: "Divider on initial auth view separating auth options.").localizedUppercase - } - - // MARK: - Continue Button Action - - @objc func handleSubmitButtonTapped() { - tracker.track(click: .submit) - validateForm() - } - - // MARK: - Sign in with site credentials Button Action - @objc func handleSiteCredentialsButtonTapped() { - tracker.track(click: .signInWithSiteCredentials) - goToSiteCredentialsScreen() - } - - // MARK: - What is WordPress.com Button Action - - @IBAction func whatIsWPComButtonTapped(_ sender: UIButton) { - tracker.track(click: .whatIsWPCom) - guard let whatIsWPCom = configuration.whatIsWPComURL else { - return - } - UIApplication.shared.open(whatIsWPCom) - } - - // MARK: - Hidden Password Field Action - - @IBAction func handlePasswordFieldDidChange(_ sender: UITextField) { - attemptAutofillLogin() - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextWithLinkTableViewCell.reuseIdentifier: TextWithLinkTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .email] - - if let authenticationDelegate = WordPressAuthenticator.shared.delegate, authenticationDelegate.wpcomTermsOfServiceEnabled { - rows.append(.tos) - } - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureEmailField(cell) - case let cell as TextWithLinkTableViewCell: - configureTextWithLink(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.getStartedInstructions) - } - - /// Configure the email cell. - /// - func configureEmailField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .email, - placeholder: WordPressAuthenticator.shared.displayStrings.emailAddressPlaceholder, - text: loginFields.username) - cell.textField.delegate = self - emailField = cell.textField - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.configureContinueButton(animating: false) - } - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - emailField?.placeholder = nil - } - } - - /// Configure the link cell. - /// - func configureTextWithLink(_ cell: TextWithLinkTableViewCell) { - cell.configureButton(markedText: WordPressAuthenticator.shared.displayStrings.loginTermsOfService) - - cell.actionHandler = { [weak self] in - self?.termsTapped() - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case email - case tos - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions, .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - case .email: - return TextFieldTableViewCell.reuseIdentifier - case .tos: - return TextWithLinkTableViewCell.reuseIdentifier - } - } - } - - enum Constants { - enum FooterStackView { - static let spacing = 16.0 - static let layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16) - } - } - - // MARK: Analytics - // - func configureAnalyticsTracker() { - // Configure tracker flow based on screen mode. - switch screenMode { - case .signInUsingSiteCredentials: - tracker.set(flow: .loginWithSiteAddress) - case .signInUsingWordPressComOrSocialAccounts: - tracker.set(flow: .wpCom) - } - - let stepValue: AuthenticatorAnalyticsTracker.Step = configuration.useEnterEmailAddressAsStepValueForGetStartedVC ? .enterEmailAddress : .start - if isMovingToParent { - tracker.track(step: stepValue) - } else { - tracker.set(step: stepValue) - } - } -} - -// MARK: - Validation - -private extension GetStartedViewController { - - /// Configures appearance of the submit button. - /// - func configureContinueButton(animating: Bool) { - if showsContinueButtonAtTheBottom { - buttonViewController?.setTopButtonState(isLoading: animating, - isEnabled: enableSubmit(animating: animating)) - } else { - continueButton.showActivityIndicator(animating) - continueButton.isEnabled = enableSubmit(animating: animating) - } - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return EmailFormatValidator.validate(string: loginFields.username) - } - - /// Validates email address and proceeds with the submit action. - /// Empties loginFields.meta.socialService as - /// social signin does not require form validation. - /// - func validateForm() { - loginFields.meta.socialService = nil - displayError(message: "") - - guard EmailFormatValidator.validate(string: loginFields.username) else { - present(buildInvalidEmailAlertGeneric(), animated: true, completion: nil) - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.isPasswordlessAccount(username: loginFields.username, - success: { [weak self] passwordless in - self?.configureViewLoading(false) - self?.loginFields.meta.passwordless = passwordless - passwordless ? self?.requestAuthenticationLink() : self?.showPasswordOrMagicLinkView() - }, - failure: { [weak self] error in - WordPressAuthenticator.track(.loginFailed, error: error) - WPLogError(error.localizedDescription) - guard let self else { - return - } - self.configureViewLoading(false) - - self.handleLoginError(error) - }) - } - - /// Show the Password entry view. - /// - func showPasswordView() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - return - } - - vc.source = source - vc.loginFields = loginFields - vc.trackAsPasswordChallenge = false - - navigationController?.pushViewController(vc, animated: true) - } - - /// Show the password or magic link view based on the configuration. - /// - func showPasswordOrMagicLinkView() { - guard let navigationController else { - return - } - configureViewLoading(true) - let coordinator = PasswordCoordinator(navigationController: navigationController, - source: source, - loginFields: loginFields, - tracker: tracker, - configuration: configuration) - passwordCoordinator = coordinator - Task { @MainActor [weak self] in - guard let self else { return } - await coordinator.start() - self.configureViewLoading(false) - } - } - - /// Handle errors when attempting to log in with an email address - /// - func handleLoginError(_ error: Error) { - let userInfo = (error as NSError).userInfo - let errorCode = userInfo[WordPressComRestApi.ErrorKeyErrorCode] as? String - - if configuration.enableSignUp, errorCode == "unknown_user" { - self.sendEmail() - } else if errorCode == "email_login_not_allowed" { - // If we get this error, we know we have a WordPress.com user but their - // email address is flagged as suspicious. They need to login via their - // username instead. - self.showSelfHostedWithError(error) - } else { - let signInError = SignInError(error: error, source: source) ?? error - guard let authenticationDelegate = WordPressAuthenticator.shared.delegate, - authenticationDelegate.shouldHandleError(signInError) else { - displayError(error, sourceTag: sourceTag) - return - } - - /// Hand over control to the host app. - authenticationDelegate.handleError(signInError) { customUI in - // Setting the rightBarButtonItems of the custom UI before pushing the view controller - // and resetting the navigationController's navigationItem after the push seems to be the - // only combination that gets the Help button to show up. - customUI.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - self.navigationController?.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - - self.navigationController?.pushViewController(customUI, animated: true) - } - } - } - - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - private func sendEmail() { - tracker.set(flow: .signup) - loginFields.meta.emailMagicLinkSource = .signup - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (error: Error) in - WPLogError("Request for signup link email failed.") - - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - self.displayError(error, sourceTag: self.sourceTag) - self.configureSubmitButton(animating: false) - }) - } - - private func didRequestSignupLink() { - guard let vc = SignupMagicLinkViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate from UnifiedSignupViewController to SignupMagicLinkViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - guard email.isValidEmail() else { - present(buildInvalidEmailLinkAlert(), animated: true, completion: nil) - return - } - - configureViewLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureViewLoading(false) - }, failure: { [weak self] (error: Error) in - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - - self.displayError(error, sourceTag: self.sourceTag) - self.configureViewLoading(false) - }) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController from GetStartedViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Build the alert message when the email address is invalid - /// - private func buildInvalidEmailAlertGeneric() -> UIAlertController { - let title = NSLocalizedString("Invalid Email Address", - comment: "Title of an alert letting the user know the email address that they've entered isn't valid") - let message = NSLocalizedString("Please enter a valid email address for a WordPress.com account.", - comment: "An error message.") - - return buildInvalidEmailAlert(title: title, message: message) - } - - /// Build the alert message when the email address is invalid so a link cannot be requested - /// - private func buildInvalidEmailLinkAlert() -> UIAlertController { - let title = NSLocalizedString("Can Not Request Link", - comment: "Title of an alert letting the user know") - let message = NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", - comment: "An error message.") - - return buildInvalidEmailAlert(title: title, message: message) - } - - private func buildInvalidEmailAlert(title: String, message: String) -> UIAlertController { - - let helpActionTitle = NSLocalizedString("Need help?", - comment: "Takes the user to get help") - let okActionTitle = NSLocalizedString("OK", - comment: "Dismisses the alert") - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - alert.addActionWithTitle(helpActionTitle, - style: .cancel, - handler: { _ in - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) - }) - - alert.addActionWithTitle(okActionTitle, style: .default, handler: nil) - - return alert - } - - /// When password autofill has entered a password on this screen, attempt to login immediately - /// - func attemptAutofillLogin() { - // Even though there was no explicit submit action by the user, we'll interpret - // the credentials selection as such. - tracker.track(click: .submit) - - loginFields.password = hiddenPasswordField?.text ?? "" - loginFields.meta.socialService = nil - displayError(message: "") - validateFormAndLogin() - } - - /// Configures loginFields to log into wordpress.com and navigates to the selfhosted username/password form. - /// Displays the specified error message when the new view controller appears. - /// - func showSelfHostedWithError(_ error: Error) { - loginFields.siteAddress = "https://wordpress.com" - errorToPresent = error - - tracker.track(failure: error.localizedDescription) - - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate to SiteCredentialsViewController from GetStartedViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates to site credentials screen where .org site credentials can be entered - /// - func goToSiteCredentialsScreen() { - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from GetStartedViewController to SiteCredentialsViewController") - return - } - - vc.loginFields = loginFields.copy() - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - Social Button Management - -private extension GetStartedViewController { - - func configureSocialButtons() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if configuration.enableSignInWithApple { - buttonViewController.setupTopButtonFor(socialService: .apple) { [weak self] in - self?.appleTapped() - } - } - - buttonViewController.setupButtomButtonFor(socialService: .google) { [weak self] in - self?.googleTapped() - } - - let termsButton = WPStyleGuide.signupTermsButton() - buttonViewController.stackView?.addArrangedSubview(termsButton) - termsButton.addTarget(self, action: #selector(termsTapped), for: .touchUpInside) - } - - func configureButtonViewControllerForSiteCredentialsMode() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if configuration.enableSocialLogin { - configureSocialButtons() - - // Setup Sign in with site credentials button - buttonViewController.setupTertiaryButton(attributedTitle: WPStyleGuide.formattedSignInWithSiteCredentialsString(), - isPrimary: false, - accessibilityIdentifier: ButtonConfiguration.SignInWithSiteCredentials.accessibilityIdentifier) { [weak self] in - self?.handleSiteCredentialsButtonTapped() - } - } else { - // Add a "Continue" button here as the `continueButton` at the top will be hidden - // - if showsContinueButtonAtTheBottom { - buttonViewController.setupTopButton(title: ButtonConfiguration.Continue.title, - isPrimary: true, - accessibilityIdentifier: ButtonConfiguration.Continue.accessibilityIdentifier) { [weak self] in - self?.handleSubmitButtonTapped() - } - } - - // Setup Sign in with site credentials button - buttonViewController.setupBottomButton(attributedTitle: WPStyleGuide.formattedSignInWithSiteCredentialsString(), - isPrimary: false, - accessibilityIdentifier: ButtonConfiguration.SignInWithSiteCredentials.accessibilityIdentifier) { [weak self] in - self?.handleSiteCredentialsButtonTapped() - } - } - } - - func configureButtonViewControllerWithoutSocialLogin() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - if showsContinueButtonAtTheBottom { - // Add a "Continue" button here as the `continueButton` at the top will be hidden - // - buttonViewController.setupTopButton(title: ButtonConfiguration.Continue.title, - isPrimary: true, - accessibilityIdentifier: ButtonConfiguration.Continue.accessibilityIdentifier) { [weak self] in - self?.handleSubmitButtonTapped() - } - } - } - - @objc func appleTapped() { - tracker.track(click: .loginWithApple) - - AppleAuthenticator.sharedInstance.delegate = self - AppleAuthenticator.sharedInstance.showFrom(viewController: self) - } - - @objc func googleTapped() { - tracker.track(click: .loginWithGoogle) - - guard let toVC = GoogleAuthViewController.instantiate(from: .googleAuth) else { - WPLogError("Failed to navigate to GoogleAuthViewController from GetStartedViewController") - return - } - - navigationController?.pushViewController(toVC, animated: true) - } - - @objc func termsTapped() { - tracker.track(click: .termsOfService) - - UIApplication.shared.open(configuration.wpcomTermsOfServiceURL) - } -} - -// MARK: - SFSafariViewControllerDelegate - -extension GetStartedViewController: SFSafariViewControllerDelegate { - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - // This will only work when the user taps "Done" in the terms of service screen. - // It won't be executed if the user dismisses the terms of service VC by sliding it out of view. - // Unfortunately I haven't found a way to track that scenario. - // - tracker.track(click: .dismiss) - } -} - -// MARK: - AppleAuthenticatorDelegate - -extension GetStartedViewController: AppleAuthenticatorDelegate { - - func showWPComLogin(loginFields: LoginFields) { - self.loginFields = loginFields - showPasswordView() - } - - func showApple2FA(loginFields: LoginFields) { - self.loginFields = loginFields - signInAppleAccount() - } - - func authFailedWithError(message: String) { - displayErrorAlert(message, sourceTag: .loginApple) - tracker.set(flow: .wpCom) - } -} - -// MARK: - LoginFacadeDelegate - -extension GetStartedViewController { - - // Used by SIWA when logging with with a passwordless, 2FA account. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - UITextFieldDelegate - -extension GetStartedViewController: UITextFieldDelegate { - - func textFieldDidBeginEditing(_ textField: UITextField) { - tracker.track(click: .selectEmailField) - } - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if canSubmit() { - validateForm() - } - return true - } -} - -// MARK: - Keyboard Notifications - -extension GetStartedViewController { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Button configuration - -private extension GetStartedViewController { - enum ButtonConfiguration { - enum Continue { - static let title = WordPressAuthenticator.shared.displayStrings.continueButtonTitle - static let accessibilityIdentifier = "Get Started Email Continue Button" - } - - enum SignInWithSiteCredentials { - static let accessibilityIdentifier = "Sign in with site credentials Button" - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard deleted file mode 100644 index 38789068fba7..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuth.storyboard +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift deleted file mode 100644 index fa380cfce74a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleAuthViewController.swift +++ /dev/null @@ -1,165 +0,0 @@ -import UIKit -import SVProgressHUD -import WordPressShared - -/// View controller that handles the google authentication flow -/// -class GoogleAuthViewController: LoginViewController { - - // MARK: - Properties - - private var hasShownGoogle = false - @IBOutlet var titleLabel: UILabel? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComAuthWaitingForGoogle - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.waitingForGoogleTitle - - titleLabel?.text = NSLocalizedString("Waiting for Google to complete…", comment: "Message shown on screen while waiting for Google to finish its signup process.") - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - showGoogleScreenIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - if isMovingFromParent { - AuthenticatorAnalyticsTracker.shared.track(click: .dismiss) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } -} - -// MARK: - Private Methods - -private extension GoogleAuthViewController { - - func showGoogleScreenIfNeeded() { - guard !hasShownGoogle else { - return - } - - // Flag this as a social sign in. - loginFields.meta.socialService = .google - - GoogleAuthenticator.sharedInstance.delegate = self - GoogleAuthenticator.sharedInstance.showFrom(viewController: self, loginFields: loginFields) - hasShownGoogle = true - } - - func showLoginErrorView(errorTitle: String, errorDescription: String) { - let socialErrorVC = LoginSocialErrorViewController(title: errorTitle, description: errorDescription) - let socialErrorNav = LoginNavigationController(rootViewController: socialErrorVC) - socialErrorVC.delegate = self - socialErrorVC.loginFields = loginFields - socialErrorVC.modalPresentationStyle = .fullScreen - present(socialErrorNav, animated: true) - } - - func showSignupConfirmationView() { - guard let vc = GoogleSignupConfirmationViewController.instantiate(from: .googleSignupConfirmation) else { - WPLogError("Failed to navigate from GoogleAuthViewController to GoogleSignupConfirmationViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } -} - -// MARK: - GoogleAuthenticatorDelegate - -extension GoogleAuthViewController: GoogleAuthenticatorDelegate { - - // MARK: - Login - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - syncWPComAndPresentEpilogue(credentials: credentials) - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = TwoFAViewController.instantiate(from: .twoFA) else { - WPLogError("Failed to navigate from GoogleAuthViewController to TwoFAViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - self.loginFields = loginFields - - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate from GoogleAuthViewController to PasswordViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) { - self.loginFields = loginFields - - // If login failed because there is no existing account, redirect to signup. - // Otherwise, display the error. - let redirectToSignup = unknownUser && WordPressAuthenticator.shared.configuration.enableSignupWithGoogle - - redirectToSignup ? showSignupConfirmationView() : - showLoginErrorView(errorTitle: errorTitle, errorDescription: errorDescription) - } - - func googleAuthCancelled() { - SVProgressHUD.dismiss() - navigationController?.popViewController(animated: true) - } - - // MARK: - Signup - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - // Here for protocol compliance. - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard deleted file mode 100644 index ee7ca90ffd8f..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmation.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift deleted file mode 100644 index b05814fe81d5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Google/GoogleSignupConfirmationViewController.swift +++ /dev/null @@ -1,256 +0,0 @@ -import UIKit -import WordPressShared - -class GoogleSignupConfirmationViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComAuthGoogleSignupConfirmation - } - } - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .signupWithGoogle) - - if isBeingPresentedInAnyWay { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.createAccountButtonTitle, for: .normal) - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource - -extension GoogleSignupConfirmationViewController: UITableViewDataSource { - - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Extension - -private extension GoogleSignupConfirmationViewController { - - // MARK: - Button Handling - - @IBAction func handleSubmit() { - tracker.track(click: .submit) - tracker.track(click: .createAccount) - - configureSubmitButton(animating: true) - GoogleAuthenticator.sharedInstance.delegate = self - GoogleAuthenticator.sharedInstance.createGoogleAccount(loginFields: loginFields) - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail, .instructions] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.googleSignupInstructions, style: .body) - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: tableView) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - GoogleAuthenticatorDelegate - -extension GoogleSignupConfirmationViewController: GoogleAuthenticatorDelegate { - - // MARK: - Signup - - func googleFinishedSignup(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showSignupEpilogue(for: credentials) - } - - func googleLoggedInInstead(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - self.loginFields = loginFields - showLoginEpilogue(for: credentials) - } - - func googleSignupFailed(error: Error, loginFields: LoginFields) { - configureSubmitButton(animating: false) - self.loginFields = loginFields - displayError(message: error.localizedDescription, moveVoiceOverFocus: true) - } - - // MARK: - Login - - func googleFinishedLogin(credentials: AuthenticatorCredentials, loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleNeedsMultifactorCode(loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleExistingUserNeedsConnection(loginFields: LoginFields) { - // Here for protocol compliance. - } - - func googleLoginFailed(errorTitle: String, errorDescription: String, loginFields: LoginFields, unknownUser: Bool) { - // Here for protocol compliance. - } - - func googleAuthCancelled() { - // Here for protocol compliance. - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard deleted file mode 100644 index 9e7fdb1fb5ae..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLink.storyboard +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift deleted file mode 100644 index 761eaf44fd48..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/LoginMagicLinkViewController.swift +++ /dev/null @@ -1,180 +0,0 @@ -import UIKit -import WordPressShared - -/// Unified LoginMagicLinkViewController: login to .com with a magic link -/// -final class LoginMagicLinkViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginMagicLink - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .loginWithMagicLink) - - if isMovingToParent { - tracker.track(step: .magicLinkRequested) - } else { - tracker.set(step: .magicLinkRequested) - } - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(true) - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, for: .normal) - submitButton?.accessibilityIdentifier = "Open Mail Button" - } -} - -// MARK: - UITableViewDataSource -extension LoginMagicLinkViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension LoginMagicLinkViewController { - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.persona, .instructions, .checkSpam] - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .checkSpam: - configureCheckSpamLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.openMailLoginInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureCheckSpamLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.checkSpamInstructions, style: .body) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case persona - case instructions - case checkSpam - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .checkSpam: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift deleted file mode 100644 index 75aff3e9ca81..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.swift +++ /dev/null @@ -1,158 +0,0 @@ -import UIKit -import WordPressUI -import WordPressShared - -final class MagicLinkRequestedViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var subtitleLabel: UILabel! - @IBOutlet private weak var emailLabel: UILabel! - @IBOutlet private weak var cannotFindEmailLabel: UILabel! - @IBOutlet private weak var buttonContainerView: UIView! - @IBOutlet private weak var loginWithPasswordButton: UIButton! - - private let email: String - private let loginWithPassword: () -> Void - - private lazy var buttonViewController: NUXButtonViewController = .instance() - - init(email: String, loginWithPassword: @escaping () -> Void) { - self.email = email - self.loginWithPassword = loginWithPassword - super.init(nibName: "MagicLinkRequestedViewController", bundle: WordPressAuthenticator.bundle) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var sourceTag: WordPressSupportSourceTag { - .wpComLoginMagicLinkAutoRequested - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - setupButtons() - setupTitleLabel() - setupSubtitleLabel() - setupEmailLabel() - setupCannotFindEmailLabel() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .loginWithMagicLink) - - if isBeingPresentedInAnyWay { - tracker.track(step: .magicLinkAutoRequested) - } else { - tracker.set(step: .magicLinkAutoRequested) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - return super.styleBackground() - } - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } -} - -private extension MagicLinkRequestedViewController { - func setupButtons() { - setupContinueMailButton() - setupLoginWithPasswordButton() - } - - /// Configures the primary button using the shared NUXButton style without a Storyboard. - func setupContinueMailButton() { - buttonViewController.setupTopButton(title: WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, isPrimary: true, onTap: { [weak self] in - guard let self else { return } - guard let topButton = self.buttonViewController.topButton else { - return - } - self.openMail(sender: topButton) - }) - buttonViewController.move(to: self, into: buttonContainerView) - } - - /// Unfortunately, the plain text button style is not available in `NUXButton` as it currently supports primary or secondary. - /// The plain text button is configured manually here. - func setupLoginWithPasswordButton() { - loginWithPasswordButton.setTitle(Localization.loginWithPasswordAction, for: .normal) - loginWithPasswordButton.applyLinkButtonStyle() - loginWithPasswordButton.on(.touchUpInside) { [weak self] _ in - self?.loginWithPassword() - } - } - - func setupTitleLabel() { - titleLabel.text = Localization.title - titleLabel.font = WPStyleGuide.mediumWeightFont(forStyle: .title3) - titleLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - titleLabel.numberOfLines = 0 - } - - func setupSubtitleLabel() { - subtitleLabel.text = Localization.subtitle - subtitleLabel.font = WPStyleGuide.fontForTextStyle(.body) - subtitleLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - subtitleLabel.numberOfLines = 0 - } - - func setupEmailLabel() { - emailLabel.text = email - emailLabel.numberOfLines = 0 - emailLabel.font = WPStyleGuide.fontForTextStyle(.body, fontWeight: .bold) - emailLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor - } - - func setupCannotFindEmailLabel() { - cannotFindEmailLabel.text = Localization.cannotFindMailLoginInstructions - cannotFindEmailLabel.numberOfLines = 0 - cannotFindEmailLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - cannotFindEmailLabel.textColor = WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor - } -} - -private extension MagicLinkRequestedViewController { - func openMail(sender: UIView) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: email) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } -} - -private extension MagicLinkRequestedViewController { - enum Localization { - static let cannotFindMailLoginInstructions = NSLocalizedString("If you can’t find the email, please check your junk or spam email folder", - comment: "The instructions text about not being able to find the magic link email.") - static let title = NSLocalizedString("Check your email on this device!", - comment: "The title text on the magic link requested screen.") - static let subtitle = NSLocalizedString("We just sent a magic link to", - comment: "The subtitle text on the magic link requested screen followed by the email address.") - static let loginWithPasswordAction = NSLocalizedString("Use password to sign in", - comment: "The button title text for logging in with WP.com password instead of magic link.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib deleted file mode 100644 index fdcd6f348132..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequestedViewController.xib +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift deleted file mode 100644 index efb7409a98ab..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Login/MagicLinkRequester.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -/// Encapsulates the async request for a magic link and email validation for use cases that send a magic link. -struct MagicLinkRequester { - /// Makes the call to request a magic authentication link be emailed to the user if possible. - func requestMagicLink(email: String, jetpackLogin: Bool) async -> Result { - await withCheckedContinuation { continuation in - guard email.isValidEmail() else { - return continuation.resume(returning: .failure(MagicLinkRequestError.invalidEmail)) - } - - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: jetpackLogin, - success: { - continuation.resume(returning: .success(())) - }, failure: { error in - continuation.resume(returning: .failure(error)) - }) - } - } -} - -extension MagicLinkRequester { - enum MagicLinkRequestError: Error { - case invalidEmail - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard deleted file mode 100644 index 85cfde6f2711..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/Password.storyboard +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift deleted file mode 100644 index 01527194d607..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordCoordinator.swift +++ /dev/null @@ -1,71 +0,0 @@ -import UIKit -import WordPressShared - -/// Coordinates the navigation after entering WP.com username. -/// Based on the configuration, it could automatically send a magic link and proceed the magic link requested screen on success and fall back to password. -@MainActor -final class PasswordCoordinator { - private weak var navigationController: UINavigationController? - private let source: SignInSource? - private let loginFields: LoginFields - private let tracker: AuthenticatorAnalyticsTracker - private let configuration: WordPressAuthenticatorConfiguration - - init(navigationController: UINavigationController, - source: SignInSource?, - loginFields: LoginFields, - tracker: AuthenticatorAnalyticsTracker, - configuration: WordPressAuthenticatorConfiguration) { - self.navigationController = navigationController - self.source = source - self.loginFields = loginFields - self.tracker = tracker - self.configuration = configuration - } - - func start() async { - if configuration.isWPComMagicLinkPreferredToPassword { - let result = await requestMagicLink() - switch result { - case .success: - loginFields.restrictToWPCom = true - showMagicLinkRequested() - case .failure(let error): - // When magic link request fails, falls back to the password flow. - showPassword() - tracker.track(failure: error.localizedDescription) - } - } else { - showPassword() - } - } -} - -private extension PasswordCoordinator { - /// Makes the call to request a magic authentication link be emailed to the user. - func requestMagicLink() async -> Result { - loginFields.meta.emailMagicLinkSource = .login - return await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) - } - - /// After a magic link is successfully sent, navigates the user to the requested screen. - func showMagicLinkRequested() { - let vc = MagicLinkRequestedViewController(email: loginFields.username) { [weak self] in - self?.showPassword() - } - navigationController?.pushViewController(vc, animated: true) - } - - /// Navigates the user to enter WP.com password. - func showPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - return WPLogError("Failed to navigate to PasswordViewController from GetStartedViewController") - } - - vc.source = source - vc.loginFields = loginFields - vc.trackAsPasswordChallenge = false - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift deleted file mode 100644 index e35e87318c58..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/Password/PasswordViewController.swift +++ /dev/null @@ -1,614 +0,0 @@ -import UIKit -import WordPressKit -import WordPressShared - -/// PasswordViewController: view to enter WP account password. -/// -class PasswordViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - @IBOutlet private weak var secondaryButton: NUXButton! - - private weak var passwordField: UITextField? - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - private var loginLinkCell: TextLinkButtonTableViewCell? - - private let isMagicLinkShownAsSecondaryAction: Bool = WordPressAuthenticator.shared.configuration.isWPComMagicLinkShownAsSecondaryActionOnPasswordScreen - - private let configuration = WordPressAuthenticator.shared.configuration - - /// Depending on where we're coming from, this screen needs to track a password challenge - /// (if logging on with a Social account) or not (if logging in through WP.com). - /// - var trackAsPasswordChallenge = true - - var source: SignInSource? - - override var loginFields: LoginFields { - didSet { - loginFields.password = "" - } - } - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginWPComPassword - } - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - // MARK: - View - - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - configureLoginWithMagicLinkButton() - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - loginFields.meta.userIsDotCom = true - configureSubmitButton(animating: false) - loginLinkCell?.enableButton(true) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if trackAsPasswordChallenge { - if isMovingToParent { - tracker.track(step: .passwordChallenge) - } else { - tracker.set(step: .passwordChallenge) - } - } else { - tracker.set(flow: isMagicLinkShownAsSecondaryAction ? .loginWithPasswordWithMagicLinkEmphasis : .loginWithPassword) - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - - configureViewForEditingIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Overrides - - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? - WordPressAuthenticator.shared.style.statusBarStyle - } - - override func configureViewLoading(_ loading: Bool) { - super.configureViewLoading(loading) - passwordField?.isEnabled = !loading - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - - if let source, loginFields.meta.userIsDotCom { - let passwordError = SignInError.invalidWPComPassword(source: source) - if authenticationDelegate.shouldHandleError(passwordError) { - authenticationDelegate.handleError(passwordError) { _ in - // No custom navigation is expected in this case. - } - } - } - - if let oauthError = error as? WordPressComOAuthError, case let .endpointError(failure) = oauthError, failure.kind == .invalidRequest { - // The only difference between an incorrect password error and exceeded login limit error - // is the actual error string. So check for "password" in the error string, and show the custom - // error message. Otherwise, show the actual response error. - var displayMessage: String { - // swiftlint:disable localization_comment - if let msg = failure.localizedErrorMessage, msg.contains(NSLocalizedString("password", comment: "")) { - // swiftlint:enable localization_comment - return NSLocalizedString("It seems like you've entered an incorrect password. Want to give it another try?", comment: "An error message shown when a wpcom user provides the wrong password.") - } - if let msg = failure.localizedErrorMessage { - return msg - } - return oauthError.localizedDescription - } - displayError(message: displayMessage, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - // The reason why this check is necessary is that we're calling this method - // with an empty error message when setting up the VC. We don't want to track - // an empty error when that happens. - if !message.isEmpty { - tracker.track(failure: message) - } - - configureViewLoading(false) - - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } - - override func validateFormAndLogin() { - view.endEditing(true) - displayError(message: "", moveVoiceOverFocus: true) - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = Localization.missingInfoError - displayError(message: errorMsg, moveVoiceOverFocus: true) - - return - } - - configureViewLoading(true) - - loginFacade.signIn(with: loginFields) - } -} - -// MARK: - LoginFacadeDelegate - -extension PasswordViewController { - // Used when the account has support for security keys. - // - func needsMultifactorCode(forUserID userID: Int, andNonceInfo nonceInfo: SocialLogin2FANonceInfo) { - configureViewLoading(false) - socialNeedsMultifactorCode(forUserID: userID, andNonceInfo: nonceInfo) - } -} - -// MARK: - Validation and Continue - -private extension PasswordViewController { - - // MARK: - Button Actions - - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submit) - - configureViewLoading(true) - validateForm() - } - - func validateForm() { - validateFormAndLogin() - } -} - -// MARK: - UITextFieldDelegate - -extension PasswordViewController: UITextFieldDelegate { - - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if enableSubmit(animating: false) { - validateForm() - } - return true - } -} - -// MARK: - UITableViewDataSource - -extension PasswordViewController: UITableViewDataSource { - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - return cell - } -} - -// MARK: - Keyboard Notifications - -extension PasswordViewController: NUXKeyboardResponder { - - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - Magic Link - -private extension PasswordViewController { - func configureLoginWithMagicLinkButton() { - if isMagicLinkShownAsSecondaryAction { - secondaryButton.setTitle(Localization.loginWithMagicLink, for: .normal) - secondaryButton.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink - secondaryButton.on(.touchUpInside) { [weak self] _ in - Task { @MainActor [weak self] in - guard let self else { return } - self.secondaryButton.isEnabled = false - await self.loginWithMagicLink() - self.secondaryButton.isEnabled = true - } - } - } else { - secondaryButton.isHidden = true - } - } - - func loginWithMagicLink() async { - tracker.track(click: .requestMagicLink) - loginFields.meta.emailMagicLinkSource = .login - - updateLoadingUI(isRequestingMagicLink: true) - let result = await MagicLinkRequester().requestMagicLink(email: loginFields.username, jetpackLogin: loginFields.meta.jetpackLogin) - switch result { - case .success: - didRequestAuthenticationLink() - case .failure(let error): - switch error { - case MagicLinkRequester.MagicLinkRequestError.invalidEmail: - WPLogError("Attempted to request authentication link, but the email address did not appear valid.") - let alert = buildInvalidEmailAlert() - present(alert, animated: true, completion: nil) - default: - tracker.track(failure: error.localizedDescription) - displayError(error, sourceTag: sourceTag) - } - } - updateLoadingUI(isRequestingMagicLink: false) - } - - func updateLoadingUI(isRequestingMagicLink: Bool) { - if isRequestingMagicLink { - if isMagicLinkShownAsSecondaryAction { - submitButton?.isEnabled = false - secondaryButton.showActivityIndicator(true) - } else { - configureViewLoading(true) - } - } else { - if isMagicLinkShownAsSecondaryAction { - submitButton?.isEnabled = true - secondaryButton.showActivityIndicator(false) - } else { - configureViewLoading(false) - } - } - } -} - -// MARK: - Table Management - -private extension PasswordViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail] - - // Instructions only for social accounts and simplified WPCom login flow - if loginFields.meta.socialService != nil || - configuration.wpcomPasswordInstructions != nil { - rows.append(.instructions) - } - - rows.append(.password) - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - rows.append(.forgotPassword) - - if !isMagicLinkShownAsSecondaryAction { - rows.append(.sendMagicLink) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell where row == .password: - configurePasswordTextField(cell) - case let cell as TextLinkButtonTableViewCell where row == .forgotPassword: - configureForgotPasswordButton(cell) - case let cell as TextLinkButtonTableViewCell where row == .sendMagicLink: - configureSendMagicLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username, hasBorders: configuration.emphasizeEmailForWPComPassword) - - cell.onChangeSelectionHandler = { [weak self] textfield in - // The email can only be changed via a password manager. - // In this case, don't update username for social accounts. - // This prevents inadvertent account linking. - if self?.loginFields.meta.socialService != nil { - cell.updateEmailAddress(self?.loginFields.username) - } else { - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.loginFields.emailAddress = textfield.nonNilTrimmedText() - } - - self?.configureSubmitButton(animating: false) - } - } - - /// Configure the instruction cell for social accounts or simplified login. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - let displayStrings = WordPressAuthenticator.shared.displayStrings - let instructions: String? = { - if let service = loginFields.meta.socialService { - return (service == .google) ? displayStrings.googlePasswordInstructions : - displayStrings.applePasswordInstructions - } - return configuration.wpcomPasswordInstructions - }() - - guard let instructions else { - return - } - - cell.configureLabel(text: instructions) - } - - /// Configure the password textfield cell. - /// - func configurePasswordTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .password, - placeholder: WordPressAuthenticator.shared.displayStrings.passwordPlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - passwordField = cell.textField - cell.textField.delegate = self - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.password = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - SigninEditingState.signinEditingStateActive = true - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive VoiceOver elements. - passwordField?.placeholder = nil - } - } - - /// Configure the forgot password link cell. - /// - func configureForgotPasswordButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.resetPasswordButtonTitle, - accessibilityTrait: .link, - showBorder: true) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .forgottenPassword) - - // If information is currently processing, ignore button tap. - guard self.enableSubmit(animating: false) else { - return - } - - WordPressAuthenticator.openForgotPasswordURL(self.loginFields) - } - } - - /// Configure the "send magic link" cell. - /// - func configureSendMagicLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.getLoginLinkButtonTitle, - accessibilityTrait: .link, - showBorder: true) - cell.accessibilityIdentifier = AccessibilityIdentifier.loginWithMagicLink - - // Save reference to the login link cell so it can be enabled/disabled. - loginLinkCell = cell - - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - cell.enableButton(false) - - Task { @MainActor [weak self] in - await self?.loginWithMagicLink() - } - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - cell.accessibilityIdentifier = "Password Error" - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - passwordField?.becomeFirstResponder() - } - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - func configureForAccessibility() { - view.accessibilityElements = [ - passwordField as Any, - tableView as Any, - submitButton as Any - ] - - if isMagicLinkShownAsSecondaryAction { - view.accessibilityElements?.append(secondaryButton as Any) - } - - UIAccessibility.post(notification: .screenChanged, argument: passwordField) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController") - return - } - - vc.loginFields = self.loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Build the alert message when the email address is invalid. - /// - func buildInvalidEmailAlert() -> UIAlertController { - let title = NSLocalizedString("Can Not Request Link", - comment: "Title of an alert letting the user know") - let message = NSLocalizedString("A valid email address is needed to mail an authentication link. Please return to the previous screen and provide a valid email address.", - comment: "An error message.") - let helpActionTitle = NSLocalizedString("Need help?", - comment: "Takes the user to get help") - let okActionTitle = NSLocalizedString("OK", - comment: "Dismisses the alert") - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - alert.addActionWithTitle(helpActionTitle, - style: .cancel, - handler: { _ in - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: self, sourceTag: .loginEmail) - }) - - alert.addActionWithTitle(okActionTitle, style: .default, handler: nil) - - return alert - } - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case password - case forgotPassword - case sendMagicLink - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .password: - return TextFieldTableViewCell.reuseIdentifier - case .sendMagicLink: - return TextLinkButtonTableViewCell.reuseIdentifier - case .forgotPassword: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -private extension PasswordViewController { - /// Localization constants - /// - enum Localization { - static let missingInfoError = NSLocalizedString("Please fill out all the fields", - comment: "A short prompt asking the user to properly fill out all login fields.") - static let loginWithMagicLink = NSLocalizedString("Or log in with magic link", - comment: "The button title for a secondary call-to-action button on the password screen. When the user wants to try sending a magic link instead of entering a password.") - } - - enum AccessibilityIdentifier { - static let loginWithMagicLink = "Get Login Link Button" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift deleted file mode 100644 index ff323fecaf0a..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.swift +++ /dev/null @@ -1,106 +0,0 @@ -import UIKit - -/// GravatarEmailTableViewCell: Gravatar image + Email address in a UITableViewCell. -/// -class GravatarEmailTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var gravatarImageView: UIImageView? - @IBOutlet private weak var emailLabel: UITextField? - @IBOutlet private var containerView: UIView! - - @IBOutlet private var containerViewMargins: [NSLayoutConstraint]! - @IBOutlet private var gravatarImageViewSizeConstraints: [NSLayoutConstraint]! - - private let gridiconSize = CGSize(width: 48, height: 48) - private let girdiconSmallSize = CGSize(width: 32, height: 32) - - /// Public properties - /// - public static let reuseIdentifier = "GravatarEmailTableViewCell" - public var onChangeSelectionHandler: ((_ sender: UITextField) -> Void)? - private var gravatarPlaceholderImage: UIImage? = nil - private var gravatarPreferredSize: CGSize = .zero - private var email: String? - - required init?(coder: NSCoder) { - super.init(coder: coder) - NotificationCenter.default.addObserver(self, selector: #selector(refreshAvatar), name: .GravatarQEAvatarUpdateNotification, object: nil) - } - - /// Public Methods - /// - public func configure(withEmail email: String?, andPlaceholder placeholderImage: UIImage? = nil, hasBorders: Bool = false) { - self.email = email - gravatarImageView?.tintColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - emailLabel?.textColor = WordPressAuthenticator.shared.unifiedStyle?.gravatarEmailTextColor ?? WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor ?? WordPressAuthenticator.shared.style.subheadlineColor - emailLabel?.font = UIFont.preferredFont(forTextStyle: .body) - emailLabel?.text = email - - let gridicon: UIImage = .gridicon(.userCircle, size: hasBorders ? girdiconSmallSize : gridiconSize) - - guard let email, - email.isValidEmail() else { - gravatarImageView?.image = gridicon - return - } - self.gravatarPlaceholderImage = placeholderImage ?? gridicon - self.gravatarPreferredSize = gridicon.size - Task { - try await downloadAvatar() - } - - gravatarImageViewSizeConstraints.forEach { constraint in - constraint.constant = gridicon.size.width - } - - let margin: CGFloat = hasBorders ? 16 : 0 - containerViewMargins.forEach { constraint in - constraint.constant = margin - } - - containerView.layer.borderWidth = hasBorders ? 1 : 0 - containerView.layer.cornerRadius = hasBorders ? 8 : 0 - containerView.layer.borderColor = hasBorders ? UIColor.systemGray3.cgColor : UIColor.clear.cgColor - } - - @objc private func refreshAvatar(_ notification: Foundation.Notification) { - guard let email, notification.userInfoHasEmail(email) else { return } - Task { - try await downloadAvatar(forceRefresh: true) - } - } - - private func downloadAvatar(forceRefresh: Bool = false) async throws { - guard let email, let gravatarPlaceholderImage else { return } - try await gravatarImageView?.setGravatarImage(with: email, placeholder: gravatarPlaceholderImage, preferredSize: gravatarPreferredSize, forceRefresh: forceRefresh) - } - - func updateEmailAddress(_ email: String?) { - emailLabel?.text = email - } -} - -// MARK: - Password Manager Handling - -private extension GravatarEmailTableViewCell { - - // MARK: - All Password Managers - - /// Call the handler when the text field changes. - /// - /// - Note: we have to manually add an action to the textfield - /// because the delegate method `textFieldDidChangeSelection(_ textField: UITextField)` - /// is only available to iOS 13+. When we no longer support iOS 12, - /// `textFieldDidChangeSelection`, and `onChangeSelectionHandler` can - /// be deleted in favor of adding the delegate method to view controllers. - /// - @IBAction func textFieldDidChangeSelection() { - guard let emailTextField = emailLabel else { - return - } - - onChangeSelectionHandler?(emailTextField) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib deleted file mode 100644 index 49db946227c5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/GravatarEmailTableViewCell.xib +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift deleted file mode 100644 index 509631504805..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.swift +++ /dev/null @@ -1,237 +0,0 @@ -import UIKit -import WordPressShared - -/// TextFieldTableViewCell: a textfield with a custom border line in a UITableViewCell. -/// -final class TextFieldTableViewCell: UITableViewCell { - - /// Private properties. - /// - @IBOutlet private weak var borderView: UIView! - @IBOutlet private weak var borderWidth: NSLayoutConstraint! - private var secureTextEntryToggle: UIButton? - private var secureTextEntryImageVisible: UIImage? - private var secureTextEntryImageHidden: UIImage? - private var textfieldStyle: TextFieldStyle = .url - - /// Register an action for the SiteAddress URL textfield. - /// - Note: we have to manually add an action to the textfield - /// because the delegate method `textFieldDidChangeSelection(_ textField: UITextField)` - /// is only available to iOS 13+. When we no longer support iOS 12, - /// `registerTextFieldAction`, `textFieldDidChangeSelection`, and `onChangeSelectionHandler` can - /// be deleted in favor of adding the delegate method to SiteAddressViewController. - @IBAction func registerTextFieldAction() { - onChangeSelectionHandler?(textField) - } - - /// Public properties. - /// - @IBOutlet public weak var textField: UITextField! // public so it can be the first responder - @IBInspectable public var showSecureTextEntryToggle: Bool = false { - didSet { - configureSecureTextEntryToggle() - } - } - - public var onChangeSelectionHandler: ((_ sender: UITextField) -> Void)? - public static let reuseIdentifier = "TextFieldTableViewCell" - - override func awakeFromNib() { - super.awakeFromNib() - styleBorder() - setCommonTextFieldStyles() - } - - /// Configures the textfield for URL, username, or entering a password. - /// - Parameter style: changes the textfield behavior and appearance. - /// - Parameter placeholder: the placeholder text, if any. - /// - Parameter text: the field text, if any. - /// - public func configure(withStyle style: TextFieldStyle = .url, placeholder: String? = nil, text: String? = nil) { - textfieldStyle = style - applyTextFieldStyle(style) - textField.placeholder = placeholder - textField.text = text - } - - override func prepareForReuse() { - super.prepareForReuse() - - textField.keyboardType = .default - textField.returnKeyType = .default - setSecureTextEntry(false) - showSecureTextEntryToggle = false - textField.rightView = nil - textField.accessibilityLabel = nil - textField.accessibilityIdentifier = nil - } -} - -// MARK: - Private methods -private extension TextFieldTableViewCell { - - /// Style the bottom cell border, called borderView. - /// - func styleBorder() { - let borderColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - borderView.backgroundColor = borderColor - borderWidth.constant = WPStyleGuide.hairlineBorderWidth - } - - /// Apply common keyboard traits and font styles. - /// - func setCommonTextFieldStyles() { - textField.font = UIFont.preferredFont(forTextStyle: .body) - textField.autocorrectionType = .no - } - - /// Sets the textfield keyboard type and applies common traits. - /// - note: Don't assign first responder here. It's too early in the view lifecycle. - /// - func applyTextFieldStyle(_ style: TextFieldStyle) { - switch style { - case .url: - textField.keyboardType = .URL - textField.returnKeyType = .continue - registerTextFieldAction() - textField.accessibilityLabel = Constants.siteAddress - textField.accessibilityIdentifier = Constants.siteAddressID - case .username: - textField.keyboardType = .default - textField.returnKeyType = .next - textField.accessibilityLabel = Constants.username - textField.accessibilityIdentifier = Constants.usernameID - case .password: - textField.keyboardType = .default - textField.returnKeyType = .continue - setSecureTextEntry(true) - showSecureTextEntryToggle = true - configureSecureTextEntryToggle() - textField.accessibilityLabel = Constants.password - textField.accessibilityIdentifier = Constants.passwordID - case .numericCode: - textField.keyboardType = .numberPad - textField.returnKeyType = .continue - textField.accessibilityLabel = Constants.otp - textField.accessibilityIdentifier = Constants.otpID - case .email: - textField.keyboardType = .emailAddress - textField.returnKeyType = .continue - textField.textContentType = .username // So the password autofill appears on the keyboard - textField.accessibilityLabel = Constants.email - textField.accessibilityIdentifier = Constants.emailID - } - if WordPressAuthenticator.shared.configuration.disableAutofill { - textField.textContentType = nil - } - } - - /// Call the handler when the textfield changes. - /// - @objc func textFieldDidChangeSelection() { - onChangeSelectionHandler?(textField) - } -} - -// MARK: - Secure Text Entry -/// Methods ported from WPWalkthroughTextField.h/.m -/// -private extension TextFieldTableViewCell { - - /// Build the show / hide icon in the textfield. - /// - func configureSecureTextEntryToggle() { - guard showSecureTextEntryToggle else { - return - } - - secureTextEntryImageVisible = UIImage.gridicon(.visible) - secureTextEntryImageHidden = UIImage.gridicon(.notVisible) - - secureTextEntryToggle = UIButton(type: .custom) - secureTextEntryToggle?.clipsToBounds = true - // The icon should match the border color. - let tintColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - secureTextEntryToggle?.tintColor = tintColor - - secureTextEntryToggle?.addTarget(self, - action: #selector(secureTextEntryToggleAction), - for: .touchUpInside) - - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - textField.rightView = secureTextEntryToggle - textField.rightViewMode = .always - } - - func setSecureTextEntry(_ secureTextEntry: Bool) { - textField.font = UIFont.preferredFont(forTextStyle: .body) - - textField.isSecureTextEntry = secureTextEntry - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - } - - @objc func secureTextEntryToggleAction(_ sender: Any) { - textField.isSecureTextEntry.toggle() - - // Save and re-apply the current selection range to save the cursor position - let currentTextRange = textField.selectedTextRange - textField.becomeFirstResponder() - textField.selectedTextRange = currentTextRange - updateSecureTextEntryToggleImage() - updateSecureTextEntryForAccessibility() - } - - func updateSecureTextEntryToggleImage() { - let image = textField.isSecureTextEntry ? secureTextEntryImageHidden : secureTextEntryImageVisible - secureTextEntryToggle?.setImage(image, for: .normal) - secureTextEntryToggle?.sizeToFit() - } - - func updateSecureTextEntryForAccessibility() { - secureTextEntryToggle?.accessibilityLabel = Constants.showPassword - secureTextEntryToggle?.accessibilityIdentifier = Constants.showPassword - secureTextEntryToggle?.accessibilityValue = textField.isSecureTextEntry ? Constants.passwordHidden : Constants.passwordShown - } -} - -// MARK: - Constants -extension TextFieldTableViewCell { - - /// TextField configuration options. - /// - enum TextFieldStyle { - case url - case username - case password - case numericCode - case email - } - - struct Constants { - /// Accessibility Hints - /// - static let passwordHidden = NSLocalizedString("Hidden", - comment: "Accessibility value if login page's password field is hiding the password (i.e. with asterisks).") - static let passwordShown = NSLocalizedString("Shown", - comment: "Accessibility value if login page's password field is displaying the password.") - static let showPassword = NSLocalizedString("Show password", - comment: "Accessibility label for the 'Show password' button in the login page's password field.") - static let siteAddress = NSLocalizedString("Site address", - comment: "Accessibility label of the site address field shown when adding a self-hosted site.") - static let username = NSLocalizedString("Username", - comment: "Accessibility label for the username text field in the self-hosted login page.") - static let password = NSLocalizedString("Password", - comment: "Accessibility label for the password text field in the self-hosted login page.") - static let otp = NSLocalizedString("Authentication code", - comment: "Accessibility label for the 2FA text field.") - static let email = NSLocalizedString("Email address", - comment: "Accessibility label for the email address text field.") - static let siteAddressID = "Site address" - static let usernameID = "Username" - static let passwordID = "Password" - static let otpID = "Authentication code" - static let emailID = "Email address" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib deleted file mode 100644 index c0ada44fb922..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextFieldTableViewCell.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift deleted file mode 100644 index 59ec4ba96bd7..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -import UIKit - -/// TextLabelTableViewCell: a text label in a UITableViewCell. -/// -public final class TextLabelTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var label: UILabel! - - /// Public properties - /// - public static let reuseIdentifier = "TextLabelTableViewCell" - - public func configureLabel(text: String?, style: TextLabelStyle = .body) { - label.text = text - - switch style { - case .body: - label.textColor = WordPressAuthenticator.shared.unifiedStyle?.textColor ?? WordPressAuthenticator.shared.style.instructionColor - label.font = UIFont.preferredFont(forTextStyle: .body) - case .error: - label.textColor = WordPressAuthenticator.shared.unifiedStyle?.errorColor ?? UIColor.red - label.font = UIFont.preferredFont(forTextStyle: .body) - } - } - - /// Override methods - /// - public override func prepareForReuse() { - super.prepareForReuse() - label.text = nil - } -} - -public extension TextLabelTableViewCell { - /// The label style to display - /// - enum TextLabelStyle { - case body - case error - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib deleted file mode 100644 index 734e76ccc2ff..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLabelTableViewCell.xib +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift deleted file mode 100644 index 96bdf661e504..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.swift +++ /dev/null @@ -1,76 +0,0 @@ -import UIKit -import WordPressShared - -/// TextLinkButtonTableViewCell: a plain button made to look like a text link. -/// -class TextLinkButtonTableViewCell: UITableViewCell { - - /// Private properties - /// - @IBOutlet private weak var iconView: UIImageView! - @IBOutlet private weak var button: UIButton! - @IBOutlet private weak var borderView: UIView! - @IBOutlet private weak var borderWidth: NSLayoutConstraint! - @IBAction private func textLinkButtonTapped(_ sender: UIButton) { - actionHandler?() - } - - /// Public properties - /// - public static let reuseIdentifier = "TextLinkButtonTableViewCell" - - public var actionHandler: (() -> Void)? - - override func awakeFromNib() { - super.awakeFromNib() - - button.titleLabel?.adjustsFontForContentSizeCategory = true - styleBorder() - } - - public func configureButton(text: String?, - icon: UIImage? = nil, - accessibilityTrait: UIAccessibilityTraits = .button, - showBorder: Bool = false, - accessibilityIdentifier: String? = nil) { - button.setTitle(text, for: .normal) - - let buttonTitleColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let buttonHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - button.setTitleColor(buttonTitleColor, for: .normal) - button.setTitleColor(buttonHighlightColor, for: .highlighted) - button.accessibilityTraits = accessibilityTrait - button.accessibilityIdentifier = accessibilityIdentifier - - borderView.isHidden = !showBorder - - iconView.image = icon - iconView.isHidden = icon == nil - iconView.tintColor = buttonTitleColor - } - - /// Toggle button enabled / disabled - /// - public func enableButton(_ isEnabled: Bool) { - button.isEnabled = isEnabled - } -} - -// MARK: - Private methods -private extension TextLinkButtonTableViewCell { - - /// Style the bottom cell border, called borderView. - /// - func styleBorder() { - let borderColor = WordPressAuthenticator.shared.unifiedStyle?.borderColor ?? WordPressAuthenticator.shared.style.primaryNormalBorderColor - borderView.backgroundColor = borderColor - borderWidth.constant = WPStyleGuide.hairlineBorderWidth - } -} - -// MARK: - Constants -extension TextLinkButtonTableViewCell { - struct Constants { - static let passkeysID = "Passkeys" - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib deleted file mode 100644 index 37d749d71ce0..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextLinkButtonTableViewCell.xib +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift deleted file mode 100644 index 9028f54fa91d..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -import UIKit - -/// TextWithLinkTableViewCell: a button with the title regular text and an underlined link. -/// -class TextWithLinkTableViewCell: UITableViewCell { - - /// Public properties - /// - static let reuseIdentifier = "TextWithLinkTableViewCell" - var actionHandler: (() -> Void)? - - /// Private properties - /// - @IBOutlet private weak var button: UIButton! - @IBAction private func buttonTapped(_ sender: UIButton) { - actionHandler?() - } - - override func awakeFromNib() { - super.awakeFromNib() - button.titleLabel?.adjustsFontForContentSizeCategory = true - } - - /// Creates an attributed string from the provided marked text and assigns it to the button title. - /// - /// - Parameters: - /// - markedText: string with the text to be formatted as a link marked with "_". - /// Example: "this _is_ a link" will format "is" as an underlined link. - /// - accessibilityTrait: accessibilityTrait of button (optional) - /// - func configureButton(markedText text: String, accessibilityTrait: UIAccessibilityTraits = .link) { - let textColor = WordPressAuthenticator.shared.unifiedStyle?.textSubtleColor ?? WordPressAuthenticator.shared.style.subheadlineColor - let linkColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonColor ?? WordPressAuthenticator.shared.style.textButtonColor - let linkHighlightColor = WordPressAuthenticator.shared.unifiedStyle?.textButtonHighlightColor ?? WordPressAuthenticator.shared.style.textButtonHighlightColor - - let attributedString = text.underlined(color: textColor, underlineColor: linkColor) - let highlightAttributedString = text.underlined(color: textColor, underlineColor: linkHighlightColor) - - button.setAttributedTitle(attributedString, for: .normal) - button.setAttributedTitle(highlightAttributedString, for: .highlighted) - button.accessibilityTraits = accessibilityTrait - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib deleted file mode 100644 index db080cb388b9..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/ReusableViews/TextWithLinkTableViewCell.xib +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift deleted file mode 100644 index a571b8c300ac..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/SignupMagicLinkViewController.swift +++ /dev/null @@ -1,195 +0,0 @@ -import UIKit -import WordPressShared - -/// SignupMagicLinkViewController: step two in the signup flow. -/// This VC prompts the user to open their email app to look for the magic link we sent. -/// -final class SignupMagicLinkViewController: LoginViewController { - - // MARK: Properties - - @IBOutlet private weak var tableView: UITableView! - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - override var sourceTag: WordPressSupportSourceTag { - get { - return .wpComSignupMagicLink - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .openEmailClient) - tracker.track(step: .emailOpened) - - let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) - let appSelector = AppSelector(sourceView: sender) - linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) - } - - // MARK: - View lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - validationCheck() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .magicLinkRequested) - } else { - tracker.set(step: .magicLinkRequested) - } - } - - /// Validation check while we are bypassing screens. - /// - func validationCheck() { - let email = loginFields.username - if !email.isValidEmail() { - WPLogError("The value of loginFields.username was not a valid email address.") - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.openMailButtonTitle, for: .normal) - } -} - -// MARK: - UITableViewDataSource -extension SignupMagicLinkViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension SignupMagicLinkViewController { - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.persona, .instructions, .checkSpam, .oops] - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .checkSpam: - configureCheckSpamLabel(cell) - case let cell as TextLabelTableViewCell where row == .oops: - configureoopsLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.openMailSignupInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureCheckSpamLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.checkSpamInstructions, style: .body) - } - - /// Configure the "Check spam" cell. - /// - func configureoopsLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.oopsInstructions, style: .body) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case persona - case instructions - case checkSpam - case oops - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .checkSpam, .oops: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard deleted file mode 100644 index 2b0ec0487483..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignup.storyboard +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift deleted file mode 100644 index 694cd9644805..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SignUp/UnifiedSignupViewController.swift +++ /dev/null @@ -1,249 +0,0 @@ -import UIKit -import WordPressShared - -/// UnifiedSignupViewController: sign up to .com with an email address. -/// -class UnifiedSignupViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .requestMagicLink) - requestAuthenticationLink() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.signUpTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tracker.set(flow: .signup) - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Override the title on 'submit' button - /// - override func localizePrimaryButton() { - submitButton?.setTitle(WordPressAuthenticator.shared.displayStrings.magicLinkButtonTitle, for: .normal) - } - - /// Reload the tableview and show errors, if any. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource -extension UnifiedSignupViewController: UITableViewDataSource { - - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - UITableViewDelegate conformance -extension UnifiedSignupViewController: UITableViewDelegate { } - -// MARK: - Private methods -private extension UnifiedSignupViewController { - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.gravatarEmail, .instructions] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.magicLinkSignupInstructions, style: .body) - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case gravatarEmail - case instructions - case errorMessage - - var reuseIdentifier: String { - switch self { - case .gravatarEmail: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } - - enum ErrorMessage: String { - case availabilityCheckFail = "availability_check_fail" - case magicLinkRequestFail = "magic_link_request_fail" - - func description() -> String { - switch self { - case .availabilityCheckFail: - return NSLocalizedString("Unable to verify the email address. Please try again later.", comment: "Error message displayed when an error occurred checking for email availability.") - case .magicLinkRequestFail: - return NSLocalizedString("We were unable to send you an email at this time. Please try again later.", comment: "Error message displayed when an error occurred sending the magic link email.") - } - } - } -} - -// MARK: - Instance Methods -/// Implementation methods imported from SignupEmailViewController. -/// -extension UnifiedSignupViewController { - // MARK: - Send email - - /// Makes the call to request a magic signup link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .signup - - configureSubmitButton(animating: true) - - let service = WordPressComAccountService() - service.requestSignupLink(for: loginFields.username, - success: { [weak self] in - self?.didRequestSignupLink() - self?.configureSubmitButton(animating: false) - }, failure: { [weak self] (error: Error) in - WPLogError("Request for signup link email failed.") - - guard let self else { - return - } - - self.tracker.track(failure: error.localizedDescription) - self.displayError(message: ErrorMessage.magicLinkRequestFail.description()) - self.configureSubmitButton(animating: false) - }) - } - - func didRequestSignupLink() { - guard let vc = SignupMagicLinkViewController.instantiate(from: .unifiedSignup) else { - WPLogError("Failed to navigate from UnifiedSignupViewController to SignupMagicLinkViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - - navigationController?.pushViewController(vc, animated: true) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard deleted file mode 100644 index daf2db63f942..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddress.storyboard +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift deleted file mode 100644 index 4fa95ab7c4af..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController+Extensions.swift +++ /dev/null @@ -1,60 +0,0 @@ -import UIKit -import SwiftUI -import WordPressUI - -extension SiteAddressViewController { - static func showSiteAddressHelpAlert( - from presentingViewController: UIViewController, - sourceTag: WordPressSupportSourceTag, - moreHelpTapped: (() -> Void)? = nil, - onDismiss: (() -> Void)? = nil - ) { - let alert = AlertView { - AlertHeaderView( - title: Strings.title, - description: Strings.description - ) - } content: { - Image("site-address-illustration", bundle: WordPressAuthenticator.bundle) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(.horizontal, 32) - .padding(.bottom, 32) - } actions: { - AlertDismissButton(onDismiss: onDismiss) - - Button(Strings.moreHelp) { - presentingViewController.presentedViewController?.dismiss(animated: true) { - guard WordPressAuthenticator.shared.delegate?.supportEnabled == true, - let viewController = UIApplication.shared.delegate?.window??.topmostPresentedViewController - else { - return - } - - moreHelpTapped?() - WordPressAuthenticator.shared.delegate?.presentSupportRequest(from: viewController, sourceTag: sourceTag) - } - } - } - - alert.present(in: presentingViewController) - } -} - -private enum Strings { - static let title = NSLocalizedString( - "login.siteAddressHelp.title", - value: "What's my site address?", - comment: "Title of alert helping users understand their site address during login" - ) - static let description = NSLocalizedString( - "login.siteAddressHelp.description", - value: "Your site address appears in the bar at the top of the screen when you visit your site in Safari.", - comment: "Description text explaining where to find site address during login" - ) - static let moreHelp = NSLocalizedString( - "login.siteAddressHelp.moreHelpButton", - value: "Need more help?", - comment: "Button title to get additional help finding site address during login" - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift deleted file mode 100644 index 3e53f9d3f3f1..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewController.swift +++ /dev/null @@ -1,681 +0,0 @@ -import UIKit -import WordPressUI -import WordPressKit - -/// SiteAddressViewController: log in by Site Address. -/// -final class SiteAddressViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - private var rows = [Row]() - private weak var siteURLField: UITextField? - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - /// A state variable that is `true` if network calls are currently happening and so the - /// view should be showing a loading indicator. - /// - /// This should only be modified within `configureViewLoading(_ loading:)`. - /// - /// This state is mainly used in `configureSubmitButton()` to determine whether the button - /// 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 - private let configuration = WordPressAuthenticator.shared.configuration - private lazy var viewModel: SiteAddressViewModel = { - return SiteAddressViewModel( - isSiteDiscovery: isSiteDiscovery, - xmlrpcFacade: WordPressXMLRPCAPIFacade(), - authenticationDelegate: authenticationDelegate, - blogService: WordPressComBlogService(), - loginFields: loginFields - ) - }() - - 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) - - validateForm() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - removeGoogleWaitingView() - configureNavBar() - setupTable() - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureSubmitButton() - configureForAccessibility() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - siteURLField?.text = loginFields.siteAddress - configureSubmitButton() - - // Nav bar could be hidden from the host app, so reshow it. - navigationController?.setNavigationBarHidden(false, animated: animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isSiteDiscovery { - tracker.set(flow: .siteDiscovery) - } else { - tracker.set(flow: .loginWithSiteAddress) - } - - if isMovingToParent { - tracker.track(step: .start) - } else { - tracker.set(step: .start) - } - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - configureViewForEditingIfNeeded() - } - - // MARK: - Overrides - - override func styleBackground() { - view.backgroundColor = .systemBackground - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - /// Use this instead of the overridden `configureSubmitButton(animating:)` since this uses the - /// _current_ `viewIsLoading` state. - private func configureSubmitButton() { - configureSubmitButton(animating: viewIsLoading) - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - // This matches the string in WPiOS UI tests. - submitButton?.accessibilityIdentifier = "Site Address Next Button" - - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && canSubmit() - ) - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and quiets repetitive elements. - /// - private func configureForAccessibility() { - view.accessibilityElements = [ - siteURLField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: siteURLField) - - if UIAccessibility.isVoiceOverRunning { - // Remove the placeholder if VoiceOver is running, because it speaks the label - // and the placeholder together. Since the placeholder matches the label, it's - // like VoiceOver is reading the same thing twice. - siteURLField?.placeholder = nil - } - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - viewIsLoading = loading - - siteURLField?.isEnabled = !loading - - configureSubmitButton() - navigationItem.hidesBackButton = loading - } - - /// Configure the view for an editing state. Should only be called from viewWillAppear - /// as this method skips animating any change in height. - /// - @objc func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - siteURLField?.becomeFirstResponder() - } - } - - override func displayRemoteError(_ error: Error) { - guard authenticationDelegate.shouldHandleError(error) else { - super.displayRemoteError(error) - return - } - - authenticationDelegate.handleError(error) { customUI in - self.navigationController?.pushViewController(customUI, animated: true) - } - } - - /// Reload the tableview and show errors, if any. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } -} - -// MARK: - UITableViewDataSource -extension SiteAddressViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Keyboard Notifications -extension SiteAddressViewController: NUXKeyboardResponder { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - TextField Delegate conformance -extension SiteAddressViewController: UITextFieldDelegate { - - /// Handle the keyboard `return` button action. - /// - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if canSubmit() { - validateForm() - return true - } - - return false - } -} - -// MARK: - Private methods -private extension SiteAddressViewController { - - // MARK: - Configuration - - func configureNavBar() { - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - } - - func setupTable() { - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - } - - // MARK: - Table Management - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .siteAddress] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - if WordPressAuthenticator.shared.configuration.displayHintButtons { - rows.append(.findSiteAddress) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell: - configureTextField(cell) - case let cell as TextLinkButtonTableViewCell: - configureTextLinkButton(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.siteLoginInstructions, style: .body) - } - - /// Configure the textfield cell. - /// - func configureTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .url, - placeholder: WordPressAuthenticator.shared.displayStrings.siteAddressPlaceholder) - - // Save a reference to the first textField so it can becomeFirstResponder. - siteURLField = cell.textField - cell.textField.delegate = self - cell.textField.text = loginFields.siteAddress - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.siteAddress = textfield.nonNilTrimmedText() - self?.configureSubmitButton() - } - - SigninEditingState.signinEditingStateActive = true - } - - /// Configure the "Find your site address" cell. - /// - func configureTextLinkButton(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.findSiteButtonTitle) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .showHelp) - - SiteAddressViewController.showSiteAddressHelpAlert( - from: self, - sourceTag: self.sourceTag, - moreHelpTapped: { - self.tracker.track(click: .helpFindingSiteAddress) - }, - onDismiss: { - self.tracker.track(click: .dismiss) - - // Since we're showing an alert on top of this VC, `viewDidAppear` will not be called - // once the alert is dismissed (which is where the step would be reset automagically), - // so we need to manually reset the step here. - self.tracker.set(step: .start) - }) - - self.tracker.track(step: .help) - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Push a custom view controller, provided by a host app, to the navigation stack - func pushCustomUI(_ customUI: UIViewController) { - /// Assign the help button of the newly injected UI to the same help button we are currently displaying - /// We are making a somewhat big assumption here: the chrome of the new UI we insert would look like the UI - /// WPAuthenticator is already displaying. Which is risky, but also kind of makes sense, considering - /// we are also pushing that injected UI to the current navigation controller. - if WordPressAuthenticator.shared.delegate?.supportActionEnabled == true { - customUI.navigationItem.rightBarButtonItems = self.navigationItem.rightBarButtonItems - } - - self.navigationController?.pushViewController(customUI, animated: true) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case siteAddress - case findSiteAddress - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .siteAddress: - return TextFieldTableViewCell.reuseIdentifier - case .findSiteAddress: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Instance Methods - -private extension SiteAddressViewController { - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - func validateForm() { - view.endEditing(true) - displayError(message: "") - - // We need to to this here because before this point we need the URL to be pre-validated - // exactly as the user inputs it, and after this point we need the URL to be the base site URL. - // This isn't really great, but it's the only sane solution I could come up with given the current - // architecture of this pod. - loginFields.siteAddress = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - - configureViewLoading(true) - - guard let url = URL(string: loginFields.siteAddress) else { - configureViewLoading(false) - return displayError(message: Localization.invalidURL, moveVoiceOverFocus: true) - } - - // Checks that the site exists - checkSiteExistence(url: url) { [weak self] in - guard let self else { return } - // skips XMLRPC check for site discovery or site address login if needed - if (self.isSiteDiscovery && self.configuration.skipXMLRPCCheckForSiteDiscovery) || - self.configuration.skipXMLRPCCheckForSiteAddressLogin { - self.fetchSiteInfo() - return - } - // Proceeds to check for the site's WordPress - self.guessXMLRPCURL(for: url.absoluteString) - } - } - - func checkSiteExistence(url: URL, onCompletion: @escaping () -> Void) { - var request = URLRequest(url: url) - request.httpMethod = "GET" - 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 else { return } - - if let error, (error as NSError).code != NSURLErrorAppTransportSecurityRequiresSecureConnection { - self.configureViewLoading(false) - - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { customUI in - self.pushCustomUI(customUI) - } - return - } - - var message: String? - - // Use `URLError`'s error message (which usually contains more accurate description), if the - // error is SSL error. - if let urlError = error as? URLError, urlError.failureURLPeerTrust != nil { - message = urlError.localizedDescription - } - - return self.displayError(message: message ?? Localization.nonExistentSiteError, moveVoiceOverFocus: true) - } - - onCompletion() - } - } - task.resume() - } - - func guessXMLRPCURL(for siteAddress: String) { - viewModel.guessXMLRPCURL( - for: siteAddress, - loading: { [weak self] isLoading in - self?.configureViewLoading(isLoading) - }, - completion: { [weak self] result -> Void in - guard let self else { return } - switch result { - case .success: - // Let's try to grab site info in preparation for the next screen. - self.fetchSiteInfo() - case .error(let error, let errorMessage): - if let message = errorMessage { - self.displayError(message: message, moveVoiceOverFocus: true) - } else { - self.displayError(error, sourceTag: self.sourceTag) - } - case .troubleshootSite: - WordPressAuthenticator.shared.delegate?.troubleshootSite(nil, in: self.navigationController) - case .customUI(let viewController): - self.pushCustomUI(viewController) - } - }) - } - - func fetchSiteInfo() { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - let service = WordPressComBlogService() - - let successBlock: (WordPressComSiteInfo) -> Void = { [weak self] siteInfo in - guard let self else { - return - } - self.configureViewLoading(false) - if siteInfo.isWPCom && WordPressAuthenticator.shared.delegate?.allowWPComLogin == false { - // Hey, you have to log out of your existing WP.com account before logging into another one. - self.promptUserToLogoutBeforeConnectingWPComSite() - return - } - self.presentNextControllerIfPossible(siteInfo: siteInfo) - } - - service.fetchUnauthenticatedSiteInfoForAddress(for: baseSiteUrl, success: successBlock, failure: { [weak self] error in - self?.configureViewLoading(false) - guard let self else { - return - } - - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { [weak self] customUI in - self?.navigationController?.pushViewController(customUI, animated: true) - } - } else { - self.displayError(message: Localization.invalidURL) - } - }) - } - - func presentNextControllerIfPossible(siteInfo: WordPressComSiteInfo?) { - - // Ensure that we're using the verified URL before passing the `loginFields` to the next - // view controller. - // - // In some scenarios, the text field change callback in `configureTextField()` gets executed - // right after we validated and modified `loginFields.siteAddress` in `validateForm()`. And - // this causes the value of `loginFields.siteAddress` to be reset to what the user entered. - // - // Using the user-entered `loginFields.siteAddress` causes problems when we try to log - // the user in especially if they just use a domain. For example, validating their - // self-hosted site credentials fails because the - // `WordPressOrgXMLRPCValidator.guessXMLRPCURLForSite` expects a complete site URL. - // - // This routine fixes that problem. We'll use what we already validated from - // `fetchSiteInfo()`. - // - if let verifiedSiteAddress = siteInfo?.url { - loginFields.siteAddress = verifiedSiteAddress - } - - guard isSiteDiscovery == false else { - WordPressAuthenticator.shared.delegate?.troubleshootSite(siteInfo, in: navigationController) - return - } - - guard siteInfo?.isWPCom == false else { - showGetStarted() - return - } - - WordPressAuthenticator.shared.delegate?.shouldPresentUsernamePasswordController(for: siteInfo, onCompletion: { result in - switch result { - case let .error(error): - self.displayError(message: error.localizedDescription) - case let .presentPasswordController(isSelfHosted): - if isSelfHosted { - self.showSelfHostedUsernamePassword() - return - } - - self.showWPUsernamePassword() - case .presentEmailController: - self.showGetStarted() - case let .injectViewController(customUI): - self.pushCustomUI(customUI) - } - }) - } - - func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - - return err - } - - /// Here we will continue with the self-hosted flow. - /// - func showSelfHostedUsernamePassword() { - configureViewLoading(false) - guard let vc = SiteCredentialsViewController.instantiate(from: .siteAddress) else { - WPLogError("Failed to navigate from SiteAddressViewController to SiteCredentialsViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Break away from the self-hosted flow. - /// Display a username / password login screen for WP.com sites. - /// - func showWPUsernamePassword() { - configureViewLoading(false) - - guard let vc = LoginUsernamePasswordViewController.instantiate(from: .login) else { - WPLogError("Failed to navigate from SiteAddressViewController to LoginUsernamePasswordViewController") - return - } - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// If the site is WordPressDotCom, redirect to WP login. - /// - func showGetStarted() { - guard let vc = GetStartedViewController.instantiate(from: .getStarted) else { - WPLogError("Failed to navigate from SiteAddressViewController to GetStartedViewController") - return - } - vc.source = .wpComSiteAddress - - vc.loginFields = loginFields - vc.dismissBlock = dismissBlock - vc.errorToPresent = errorToPresent - - navigationController?.pushViewController(vc, animated: true) - } - - /// Whether the form can be submitted. - /// - func canSubmit() -> Bool { - return loginFields.validateSiteForSignin() - } - - @objc private func promptUserToLogoutBeforeConnectingWPComSite() { - let acceptActionTitle = NSLocalizedString("OK", comment: "Alert dismissal title") - let message = NSLocalizedString("Please log out before connecting to a different wordpress.com site", comment: "Message for alert to prompt user to logout before connecting to a different wordpress.com site.") - let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(acceptActionTitle) - 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.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift deleted file mode 100644 index aa81e20a74c2..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteAddressViewModel.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Foundation -import WordPressKit -import WordPressShared - -struct SiteAddressViewModel { - private let isSiteDiscovery: Bool - private let xmlrpcFacade: WordPressXMLRPCAPIFacade - private unowned let authenticationDelegate: WordPressAuthenticatorDelegate - private let blogService: WordPressComBlogService - private var loginFields: LoginFields - - private let tracker = AuthenticatorAnalyticsTracker.shared - - init(isSiteDiscovery: Bool, - xmlrpcFacade: WordPressXMLRPCAPIFacade, - authenticationDelegate: WordPressAuthenticatorDelegate, - blogService: WordPressComBlogService, - loginFields: LoginFields - ) { - self.isSiteDiscovery = isSiteDiscovery - self.xmlrpcFacade = xmlrpcFacade - self.authenticationDelegate = authenticationDelegate - self.blogService = blogService - self.loginFields = loginFields - } - - enum GuessXMLRPCURLResult: Equatable { - case success - case error(NSError, String?) - case troubleshootSite - case customUI(UIViewController) - } - - func guessXMLRPCURL( - for siteAddress: String, - loading: @escaping ((Bool) -> ()), - completion: @escaping (GuessXMLRPCURLResult) -> () - ) { - xmlrpcFacade.guessXMLRPCURL(forSite: siteAddress, success: { 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 { - self.loginFields.meta.xmlrpcURL = url as NSURL - } - - completion(.success) - }, failure: { error in - guard let error else { - return - } - // Intentionally log the attempted address on failures. - // It's not guaranteed to be included in the error object depending on the error. - WPLogInfo("Error attempting to connect to site address: \(self.loginFields.siteAddress)") - WPLogError(error.localizedDescription) - - self.tracker.track(failure: .loginFailedToGuessXMLRPC) - - loading(false) - - guard self.isSiteDiscovery == false else { - completion(.troubleshootSite) - return - } - - let err = self.originalErrorOrError(error: error as NSError) - self.handleGuessXMLRPCURLError(error: err, loading: loading, completion: completion) - }) - } - - private func handleGuessXMLRPCURLError( - error: NSError, - loading: @escaping ((Bool) -> ()), - completion: @escaping (GuessXMLRPCURLResult) -> () - ) { - let completion: (NSError, String?) -> Void = { error, errorMessage in - if self.authenticationDelegate.shouldHandleError(error) { - self.authenticationDelegate.handleError(error) { customUI in - completion(.customUI(customUI)) - } - if let message = errorMessage { - self.tracker.track(failure: message) - } - return - } - - completion(.error(error, errorMessage)) - } - - /// Confirm the site is not a WordPress site before describing it as an invalid WP site - if let xmlrpcValidatorError = error as? WordPressOrgXMLRPCValidatorError, xmlrpcValidatorError == .invalid { - loading(true) - isWPSite { isWP in - loading(false) - if isWP { - let error = WordPressOrgXMLRPCValidatorError.xmlrpc_missing - completion(error as NSError, error.localizedDescription) - } else { - completion(error, Strings.notWPSiteErrorMessage) - } - } - } else if (error.domain == NSURLErrorDomain && error.code == NSURLErrorCannotFindHost) || - (error.domain == NSURLErrorDomain && error.code == NSURLErrorNetworkConnectionLost) { - completion(error, Strings.notWPSiteErrorMessage) - } else { - completion(error, (error as? WordPressOrgXMLRPCValidatorError)?.localizedDescription) - } - } - - private func originalErrorOrError(error: NSError) -> NSError { - guard let err = error.userInfo[XMLRPCOriginalErrorKey] as? NSError else { - return error - } - - return err - } -} - -extension SiteAddressViewModel { - private func isWPSite(_ completion: @escaping (Bool) -> ()) { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: loginFields.siteAddress) - blogService.fetchUnauthenticatedSiteInfoForAddress( - for: baseSiteUrl, - success: { siteInfo in - completion(siteInfo.isWP) - }, - failure: { _ in - completion(false) - }) - } -} - -private extension SiteAddressViewModel { - struct Strings { - static let notWPSiteErrorMessage = NSLocalizedString("The site at this address is not a WordPress site. For us to connect to it, the site must use WordPress.", comment: "Error message shown when a URL does not point to an existing site.") - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift deleted file mode 100644 index 89b4d15d8eaa..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/SiteAddress/SiteCredentialsViewController.swift +++ /dev/null @@ -1,589 +0,0 @@ -import UIKit -import WordPressShared - -/// Part two of the self-hosted sign in flow: username + password. Used by WPiOS and NiOS. -/// A valid site address should be acquired before presenting this view controller. -/// -final class SiteCredentialsViewController: LoginViewController { - - /// Private properties. - /// - @IBOutlet private weak var tableView: UITableView! - @IBOutlet var bottomContentConstraint: NSLayoutConstraint? - - private weak var usernameField: UITextField? - private weak var passwordField: UITextField? - private var rows = [Row]() - private var errorMessage: String? - private var shouldChangeVoiceOverFocus: Bool = false - - private let isDismissible: Bool - private let completionHandler: ((WordPressOrgCredentials) -> Void)? - private let configuration = WordPressAuthenticator.shared.configuration - - init?(coder: NSCoder, isDismissible: Bool, onCompletion: @escaping (WordPressOrgCredentials) -> Void) { - self.isDismissible = isDismissible - self.completionHandler = onCompletion - super.init(coder: coder) - } - - required init?(coder: NSCoder) { - self.isDismissible = false - self.completionHandler = nil - super.init(coder: coder) - } - - // Required for `NUXKeyboardResponder` but unused here. - var verticalCenterConstraint: NSLayoutConstraint? - - override var sourceTag: WordPressSupportSourceTag { - get { - return .loginUsernamePassword - } - } - - override var loginFields: LoginFields { - didSet { - // Clear the password (if any) from LoginFields - loginFields.password = "" - } - } - - // MARK: - Actions - @IBAction func handleContinueButtonTapped(_ sender: NUXButton) { - tracker.track(click: .submit) - - validateForm() - } - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - loginFields.meta.userIsDotCom = false - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - if isDismissible { - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismissView)) - } - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - localizePrimaryButton() - registerTableViewCells() - loadRows() - configureForAccessibility() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isMovingToParent { - tracker.track(step: .usernamePassword) - } else { - tracker.set(step: .usernamePassword) - } - - configureSubmitButton(animating: false) - configureViewLoading(false) - - registerForKeyboardEvents(keyboardWillShowAction: #selector(handleKeyboardWillShow(_:)), - keyboardWillHideAction: #selector(handleKeyboardWillHide(_:))) - configureViewForEditingIfNeeded() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - unregisterForKeyboardEvents() - } - - // MARK: - Overrides - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - super.styleBackground() - return - } - - view.backgroundColor = unifiedBackgroundColor - } - - override var preferredStatusBarStyle: UIStatusBarStyle { - return WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Configures the appearance and state of the submit button. - /// - override func configureSubmitButton(animating: Bool) { - submitButton?.showActivityIndicator(animating) - - submitButton?.isEnabled = ( - !animating && - !loginFields.username.isEmpty && - !loginFields.password.isEmpty - ) - } - - /// Sets up accessibility elements in the order which they should be read aloud - /// and chooses which element to focus on at the beginning. - /// - private func configureForAccessibility() { - view.accessibilityElements = [ - usernameField as Any, - tableView as Any, - submitButton as Any - ] - - UIAccessibility.post(notification: .screenChanged, argument: usernameField) - } - - /// Sets the view's state to loading or not loading. - /// - /// - Parameter loading: True if the form should be configured to a "loading" state. - /// - override func configureViewLoading(_ loading: Bool) { - usernameField?.isEnabled = !loading - passwordField?.isEnabled = !loading - - configureSubmitButton(animating: loading) - navigationItem.hidesBackButton = loading - } - - /// Set error messages and reload the table to display them. - /// - override func displayError(message: String, moveVoiceOverFocus: Bool = false) { - if errorMessage != message { - if !message.isEmpty { - tracker.track(failure: message) - } - - errorMessage = message - shouldChangeVoiceOverFocus = moveVoiceOverFocus - loadRows() - tableView.reloadData() - } - } - - /// No-op. Required by LoginFacade. - func displayLoginMessage(_ message: String) {} -} - -// MARK: - UITableViewDataSource -extension SiteCredentialsViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - UITableViewDelegate conformance -extension SiteCredentialsViewController: UITableViewDelegate { - /// After a textfield cell is done displaying, remove the textfield reference. - /// - func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { - guard let row = rows[safe: indexPath.row] else { - return - } - - if row == .username { - usernameField = nil - } else if row == .password { - passwordField = nil - } - } -} - -// MARK: - Keyboard Notifications -extension SiteCredentialsViewController: NUXKeyboardResponder { - @objc func handleKeyboardWillShow(_ notification: Foundation.Notification) { - keyboardWillShow(notification) - } - - @objc func handleKeyboardWillHide(_ notification: Foundation.Notification) { - keyboardWillHide(notification) - } -} - -// MARK: - TextField Delegate conformance -extension SiteCredentialsViewController: UITextFieldDelegate { - - /// Handle the keyboard `return` button action. - /// - func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if textField == usernameField { - if UIAccessibility.isVoiceOverRunning { - passwordField?.placeholder = nil - } - passwordField?.becomeFirstResponder() - } else if textField == passwordField { - validateForm() - } - return true - } -} - -// MARK: - Private Methods -private extension SiteCredentialsViewController { - - @objc func dismissView() { - dismissBlock?(true) - } - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib(), - TextFieldTableViewCell.reuseIdentifier: TextFieldTableViewCell.loadNib(), - TextLinkButtonTableViewCell.reuseIdentifier: TextLinkButtonTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Describes how the tableView rows should be rendered. - /// - func loadRows() { - rows = [.instructions, .username, .password] - - if let errorText = errorMessage, !errorText.isEmpty { - rows.append(.errorMessage) - } - - if configuration.displayHintButtons { - rows.append(.forgotPassword) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextFieldTableViewCell where row == .username: - storeUsernameTextField(cell) - case let cell as TextFieldTableViewCell where row == .password: - configurePasswordTextField(cell) - case let cell as TextLinkButtonTableViewCell: - configureForgotPassword(cell) - case let cell as TextLabelTableViewCell where row == .errorMessage: - configureErrorLabel(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the instruction cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - let displayURL = sanitizedSiteAddress(siteAddress: loginFields.siteAddress) - let text = String.localizedStringWithFormat(WordPressAuthenticator.shared.displayStrings.siteCredentialInstructions, displayURL) - cell.configureLabel(text: text, style: .body) - } - - /// Configure the username textfield cell. - /// - func storeUsernameTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .username, - placeholder: WordPressAuthenticator.shared.displayStrings.usernamePlaceholder, - text: loginFields.username) - - // Save a reference to the textField so it can becomeFirstResponder. - usernameField = cell.textField - cell.textField.delegate = self - - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.username = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - SigninEditingState.signinEditingStateActive = true - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - usernameField?.placeholder = nil - } - } - - /// Configure the password textfield cell. - /// - func configurePasswordTextField(_ cell: TextFieldTableViewCell) { - cell.configure(withStyle: .password, - placeholder: WordPressAuthenticator.shared.displayStrings.passwordPlaceholder, - text: loginFields.password) - passwordField = cell.textField - cell.textField.delegate = self - cell.onChangeSelectionHandler = { [weak self] textfield in - self?.loginFields.password = textfield.nonNilTrimmedText() - self?.configureSubmitButton(animating: false) - } - - if UIAccessibility.isVoiceOverRunning { - // Quiet repetitive elements in VoiceOver. - passwordField?.placeholder = nil - } - } - - /// Configure the forgot password cell. - /// - func configureForgotPassword(_ cell: TextLinkButtonTableViewCell) { - cell.configureButton(text: WordPressAuthenticator.shared.displayStrings.resetPasswordButtonTitle, accessibilityTrait: .link) - cell.actionHandler = { [weak self] in - guard let self else { - return - } - - self.tracker.track(click: .forgottenPassword) - - // If information is currently processing, ignore button tap. - guard self.enableSubmit(animating: false) else { - return - } - - WordPressAuthenticator.openForgotPasswordURL(self.loginFields) - } - } - - /// Configure the error message cell. - /// - func configureErrorLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: errorMessage, style: .error) - if shouldChangeVoiceOverFocus { - UIAccessibility.post(notification: .layoutChanged, argument: cell) - } - } - - /// Configure the view for an editing state. - /// - func configureViewForEditingIfNeeded() { - // Check the helper to determine whether an editing state should be assumed. - adjustViewForKeyboard(SigninEditingState.signinEditingStateActive) - if SigninEditingState.signinEditingStateActive { - usernameField?.becomeFirstResponder() - } - } - - /// Presents verify email instructions screen - /// - /// - Parameters: - /// - loginFields: `LoginFields` instance created using `makeLoginFieldsUsing` helper method - /// - func presentVerifyEmail(loginFields: LoginFields) { - guard let vc = VerifyEmailViewController.instantiate(from: .verifyEmail) else { - WPLogError("Failed to navigate from SiteCredentialsViewController to VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - /// Used for creating `LoginFields` - /// - /// - Parameters: - /// - xmlrpc: XML-RPC URL as a String - /// - options: Dictionary received from .org site credential authentication response. (Containing `jetpack_user_email` and `home_url` values) - /// - /// - Returns: A valid `LoginFields` instance or `nil` - /// - func makeLoginFieldsUsing(xmlrpc: String, options: [AnyHashable: Any]) -> LoginFields? { - guard let xmlrpcURL = URL(string: xmlrpc) else { - WPLogError("Failed to initiate XML-RPC URL from \(xmlrpc)") - return nil - } - - // `jetpack_user_email` to be used for WPCOM login - guard let email = options["jetpack_user_email"] as? [String: Any], - let userName = email["value"] as? String else { - WPLogError("Failed to find jetpack_user_email value.") - return nil - } - - // Site address - guard let home_url = options["home_url"] as? [String: Any], - let siteAddress = home_url["value"] as? String else { - WPLogError("Failed to find home_url value.") - return nil - } - - let loginFields = LoginFields() - loginFields.meta.xmlrpcURL = xmlrpcURL as NSURL - loginFields.username = userName - loginFields.siteAddress = siteAddress - return loginFields - } - - func validateFormAndTriggerDelegate() { - view.endEditing(true) - displayError(message: "") - - // Is everything filled out? - if !loginFields.validateFieldsPopulatedForSignin() { - let errorMsg = NSLocalizedString("Please fill out all the fields", - comment: "A short prompt asking the user to properly fill out all login fields.") - displayError(message: errorMsg) - - return - } - - configureViewLoading(true) - - guard let delegate = WordPressAuthenticator.shared.delegate else { - fatalError("Error: Where did the delegate go?") - } - // manually construct the XMLRPC since this is needed to get the site address later - let xmlrpc = loginFields.siteAddress + "/xmlrpc.php" - let wporg = WordPressOrgCredentials(username: loginFields.username, - password: loginFields.password, - xmlrpc: xmlrpc, - options: [:]) - delegate.handleSiteCredentialLogin(credentials: wporg, onLoading: { [weak self] shouldShowLoading in - self?.configureViewLoading(shouldShowLoading) - }, onSuccess: { [weak self] in - self?.finishedLogin(withUsername: wporg.username, - password: wporg.password, - xmlrpc: wporg.xmlrpc, - options: wporg.options) - }, onFailure: { [weak self] error, incorrectCredentials in - self?.handleLoginFailure(error: error, incorrectCredentials: incorrectCredentials) - }) - } - - func handleLoginFailure(error: Error, incorrectCredentials: Bool) { - configureViewLoading(false) - guard configuration.enableManualErrorHandlingForSiteCredentialLogin == false else { - WordPressAuthenticator.shared.delegate?.handleSiteCredentialLoginFailure(error: error, for: loginFields.siteAddress, in: self) - return - } - if incorrectCredentials { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } - - func syncDataOrPresentWPComLogin(with wporgCredentials: WordPressOrgCredentials) { - if configuration.isWPComLoginRequiredForSiteCredentialsLogin { - presentWPComLogin(wporgCredentials: wporgCredentials) - return - } - // Client didn't explicitly ask for WPCOM credentials. (`isWPComLoginRequiredForSiteCredentialsLogin` is false) - // So, sync the available credentials and finish sign in. - // - let credentials = AuthenticatorCredentials(wporg: wporgCredentials) - WordPressAuthenticator.shared.delegate?.sync(credentials: credentials) { [weak self] in - NotificationCenter.default.post(name: Foundation.Notification.Name(rawValue: WordPressAuthenticator.WPSigninDidFinishNotification), object: nil) - self?.showLoginEpilogue(for: credentials) - } - } - - func presentWPComLogin(wporgCredentials: WordPressOrgCredentials) { - // Try to get the jetpack email from XML-RPC response dictionary. - // - guard let loginFields = makeLoginFieldsUsing(xmlrpc: wporgCredentials.xmlrpc, - options: wporgCredentials.options) else { - WPLogError("Unexpected response from .org site credentials sign in using XMLRPC.") - let credentials = AuthenticatorCredentials(wporg: wporgCredentials) - showLoginEpilogue(for: credentials) - return - } - - // Present verify email instructions screen. Passing loginFields will prefill the jetpack email in `VerifyEmailViewController` - // - presentVerifyEmail(loginFields: loginFields) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row { - case instructions - case username - case password - case forgotPassword - case errorMessage - - var reuseIdentifier: String { - switch self { - case .instructions: - return TextLabelTableViewCell.reuseIdentifier - case .username: - return TextFieldTableViewCell.reuseIdentifier - case .password: - return TextFieldTableViewCell.reuseIdentifier - case .forgotPassword: - return TextLinkButtonTableViewCell.reuseIdentifier - case .errorMessage: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Instance Methods -/// Implementation methods copied from LoginSelfHostedViewController. -/// -extension SiteCredentialsViewController { - /// Sanitize and format the site address we show to users. - /// - @objc func sanitizedSiteAddress(siteAddress: String) -> String { - let baseSiteUrl = WordPressAuthenticator.baseSiteURL(string: siteAddress) as NSString - if let str = baseSiteUrl.components(separatedBy: "://").last { - return str - } - return siteAddress - } - - /// Validates what is entered in the various form fields and, if valid, - /// proceeds with the submit action. - /// - @objc func validateForm() { - guard configuration.enableManualSiteCredentialLogin else { - return validateFormAndLogin() // handles login with XMLRPC normally - } - - // asks the delegate to handle the login - validateFormAndTriggerDelegate() - } - - func finishedLogin(withUsername username: String, password: String, xmlrpc: String, options: [AnyHashable: Any]) { - let wporg = WordPressOrgCredentials(username: username, password: password, xmlrpc: xmlrpc, options: options) - /// If `completionHandler` is available, return early with the credentials. - if let completionHandler { - completionHandler(wporg) - } else { - syncDataOrPresentWPComLogin(with: wporg) - } - } - - override func displayRemoteError(_ error: Error) { - configureViewLoading(false) - let err = error as NSError - if err.code == 403 { - let message = NSLocalizedString("It looks like this username/password isn't associated with this site.", - comment: "An error message shown during log in when the username or password is incorrect.") - displayError(message: message, moveVoiceOverFocus: true) - } else { - displayError(error, sourceTag: sourceTag) - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard deleted file mode 100644 index d844a3f5afe5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmail.storyboard +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift b/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift deleted file mode 100644 index 3c61ccc7cfa6..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/UnifiedAuth/ViewRelated/VerifyEmail/VerifyEmailViewController.swift +++ /dev/null @@ -1,258 +0,0 @@ -import UIKit -import WordPressShared - -final class VerifyEmailViewController: LoginViewController { - - // MARK: - Properties - - @IBOutlet private weak var tableView: UITableView! - private var buttonViewController: NUXButtonViewController? - private let rows = Row.allCases - - // MARK: - View lifecycle - override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.title = WordPressAuthenticator.shared.displayStrings.logInTitle - - // Store default margin, and size table for the view. - defaultTableViewMargin = tableViewLeadingConstraint?.constant ?? 0 - setTableViewMargins(forWidth: view.frame.width) - - registerTableViewCells() - configureButtonViewController() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - if isBeingPresentedInAnyWay { - tracker.track(step: .verifyEmailInstructions) - } else { - tracker.set(step: .verifyEmailInstructions) - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - - if let vc = segue.destination as? NUXButtonViewController { - buttonViewController = vc - } - } - - // MARK: - Overrides - - override var sourceTag: WordPressSupportSourceTag { - .verifyEmailInstructions - } - - /// Style individual ViewController backgrounds, for now. - /// - override func styleBackground() { - guard let unifiedBackgroundColor = WordPressAuthenticator.shared.unifiedStyle?.viewControllerBackgroundColor else { - return super.styleBackground() - } - - view.backgroundColor = unifiedBackgroundColor - } - - /// Style individual ViewController status bars. - /// - override var preferredStatusBarStyle: UIStatusBarStyle { - WordPressAuthenticator.shared.unifiedStyle?.statusBarStyle ?? WordPressAuthenticator.shared.style.statusBarStyle - } - - /// Customise loading state of view. - /// - override func configureViewLoading(_ loading: Bool) { - buttonViewController?.setTopButtonState(isLoading: loading, - isEnabled: !loading) - buttonViewController?.setBottomButtonState(isLoading: false, - isEnabled: !loading) - navigationItem.hidesBackButton = loading - } -} - -// MARK: - UITableViewDataSource -extension VerifyEmailViewController: UITableViewDataSource { - /// Returns the number of rows in a section. - /// - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - rows.count - } - - /// Configure cells delegate method. - /// - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let row = rows[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier, for: indexPath) - configure(cell, for: row, at: indexPath) - - return cell - } -} - -// MARK: - Private Methods -private extension VerifyEmailViewController { - /// Configure bottom buttons. - /// - func configureButtonViewController() { - guard let buttonViewController else { - return - } - - buttonViewController.hideShadowView() - - // Setup `Send email verification link` button - buttonViewController.setupTopButton(title: ButtonConfiguration.SendEmailVerificationLink.title, - isPrimary: true) { [weak self] in - self?.handleSendEmailVerificationLinkButtonTapped() - } - - // Setup `Login with account password` button - buttonViewController.setupBottomButton(title: ButtonConfiguration.LoginWithAccountPassword.title, - isPrimary: false) { [weak self] in - self?.handleLoginWithAccountPasswordButtonTapped() - } - } - - // MARK: - Actions - @objc func handleSendEmailVerificationLinkButtonTapped() { - tracker.track(click: .requestMagicLink) - requestAuthenticationLink() - } - - @objc func handleLoginWithAccountPasswordButtonTapped() { - tracker.track(click: .loginWithAccountPassword) - presentUnifiedPassword() - } - - /// Registers all of the available TableViewCells. - /// - func registerTableViewCells() { - let cells = [ - GravatarEmailTableViewCell.reuseIdentifier: GravatarEmailTableViewCell.loadNib(), - TextLabelTableViewCell.reuseIdentifier: TextLabelTableViewCell.loadNib() - ] - - for (reuseIdentifier, nib) in cells { - tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) - } - } - - /// Configure cells. - /// - func configure(_ cell: UITableViewCell, for row: Row, at indexPath: IndexPath) { - switch cell { - case let cell as GravatarEmailTableViewCell where row == .persona: - configureGravatarEmail(cell) - case let cell as TextLabelTableViewCell where row == .instructions: - configureInstructionLabel(cell) - case let cell as TextLabelTableViewCell where row == .typePassword: - configureTypePasswordButton(cell) - default: - WPLogError("Error: Unidentified tableViewCell type found.") - } - } - - /// Configure the gravatar + email cell. - /// - func configureGravatarEmail(_ cell: GravatarEmailTableViewCell) { - cell.configure(withEmail: loginFields.username) - } - - /// Configure the instructions cell. - /// - func configureInstructionLabel(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.verifyMailLoginInstructions, - style: .body) - } - - /// Configure the enter password instructions cell. - /// - func configureTypePasswordButton(_ cell: TextLabelTableViewCell) { - cell.configureLabel(text: WordPressAuthenticator.shared.displayStrings.alternativelyEnterPasswordInstructions, - style: .body) - } - - /// Makes the call to request a magic authentication link be emailed to the user. - /// - func requestAuthenticationLink() { - loginFields.meta.emailMagicLinkSource = .login - - let email = loginFields.username - - configureViewLoading(true) - let service = WordPressComAccountService() - service.requestAuthenticationLink(for: email, - jetpackLogin: loginFields.meta.jetpackLogin, - success: { [weak self] in - self?.didRequestAuthenticationLink() - self?.configureViewLoading(false) - }, failure: { [weak self] (error: Error) in - guard let self else { return } - - self.tracker.track(failure: error.localizedDescription) - - self.displayError(error, sourceTag: self.sourceTag) - self.configureViewLoading(false) - }) - } - - /// When a magic link successfully sends, navigate the user to the next step. - /// - func didRequestAuthenticationLink() { - guard let vc = LoginMagicLinkViewController.instantiate(from: .unifiedLoginMagicLink) else { - WPLogError("Failed to navigate to LoginMagicLinkViewController from VerifyEmailViewController") - return - } - - vc.loginFields = loginFields - vc.loginFields.restrictToWPCom = true - navigationController?.pushViewController(vc, animated: true) - } - - /// Presents unified password screen - /// - func presentUnifiedPassword() { - guard let vc = PasswordViewController.instantiate(from: .password) else { - WPLogError("Failed to navigate to PasswordViewController from VerifyEmailViewController") - return - } - vc.loginFields = loginFields - navigationController?.pushViewController(vc, animated: true) - } - - // MARK: - Private Constants - - /// Rows listed in the order they were created. - /// - enum Row: CaseIterable { - case persona - case instructions - case typePassword - - var reuseIdentifier: String { - switch self { - case .persona: - return GravatarEmailTableViewCell.reuseIdentifier - case .instructions, .typePassword: - return TextLabelTableViewCell.reuseIdentifier - } - } - } -} - -// MARK: - Button configuration -private extension VerifyEmailViewController { - enum ButtonConfiguration { - enum SendEmailVerificationLink { - static let title = WordPressAuthenticator.shared.displayStrings.sendEmailVerificationLinkButtonTitle - } - - enum LoginWithAccountPassword { - static let title = WordPressAuthenticator.shared.displayStrings.loginWithAccountPasswordButtonTitle - } - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift deleted file mode 100644 index c1c62127079c..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComAccountService.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import WordPressKit - -// MARK: - WordPressComAccountService -// -public class WordPressComAccountService { - - /// Makes the initializer public for external access. - public init() {} - - /// Indicates if a WordPress.com account is "PasswordLess": This kind of account must be authenticated via a Magic Link. - /// - public func isPasswordlessAccount(username: String, success: @escaping (Bool) -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.isPasswordlessAccount(username, success: { isPasswordless in - success(isPasswordless) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Connects a WordPress.com account with the specified Social Service. - /// - func connect(wpcomAuthToken: String, - serviceName: SocialServiceName, - serviceToken: String, - connectParameters: [String: AnyObject]? = nil, - success: @escaping () -> Void, - failure: @escaping (Error) -> Void) { - let loggedAPI = WordPressComRestApi(oAuthToken: wpcomAuthToken, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - let remote = AccountServiceRemoteREST(wordPressComRestApi: loggedAPI) - - remote.connectToSocialService(serviceName, - serviceIDToken: serviceToken, - connectParameters: connectParameters, - oAuthClientID: configuration.wpcomClientId, - oAuthClientSecret: configuration.wpcomSecret, - success: success, - failure: { error in - failure(error) - }) - } - - /// Requests a WordPress.com Authentication Link to be sent to the specified email address. - /// - public func requestAuthenticationLink(for email: String, jetpackLogin: Bool, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.requestWPComAuthLink(forEmail: email, - clientID: configuration.wpcomClientId, - clientSecret: configuration.wpcomSecret, - source: jetpackLogin ? .jetpackConnect : .default, - wpcomScheme: configuration.wpcomScheme, - success: success, - failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Requests a WordPress.com SignUp Link to be sent to the specified email address. - /// - func requestSignupLink(for email: String, success: @escaping () -> Void, failure: @escaping (Error) -> Void) { - let remote = AccountServiceRemoteREST(wordPressComRestApi: anonymousAPI) - - remote.requestWPComSignupLink(forEmail: email, - clientID: configuration.wpcomClientId, - clientSecret: configuration.wpcomSecret, - wpcomScheme: configuration.wpcomScheme, - success: success, - failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - /// Returns an anonymous WordPressComRestApi Instance. - /// - private var anonymousAPI: WordPressComRestApi { - return WordPressComRestApi(oAuthToken: nil, - userAgent: configuration.userAgent, - baseURL: configuration.wpcomAPIBaseURL) - } - - /// Returns the current WordPressAuthenticatorConfiguration Instance. - /// - private var configuration: WordPressAuthenticatorConfiguration { - return WordPressAuthenticator.shared.configuration - } -} - -// MARK: - Nested Types -// -extension WordPressComAccountService { - - enum ServiceError: Error { - case unknown - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift deleted file mode 100644 index 4c5e51842471..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComBlogService.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation -import WordPressKit - -// MARK: - WordPress.com BlogService -// -class WordPressComBlogService { - - /// Returns a new anonymous instance of WordPressComRestApi. - /// - private var anonymousAPI: WordPressComRestApi { - let userAgent = WordPressAuthenticator.shared.configuration.userAgent - let baseUrl = WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL - return WordPressComRestApi(oAuthToken: nil, userAgent: userAgent, baseURL: baseUrl, useEphemeralSession: true) - } - - /// Retrieves the WordPressComSiteInfo instance associated to a WordPress.com Site Address. - /// - func fetchSiteInfo(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let remote = BlogServiceRemoteREST(wordPressComRestApi: anonymousAPI, siteID: 0) - - remote.fetchSiteInfo(forAddress: address, success: { response in - guard let response else { - failure(ServiceError.unknown) - return - } - - let site = WordPressComSiteInfo(remote: response) - success(site) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } - - func fetchUnauthenticatedSiteInfoForAddress(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let remote = BlogServiceRemoteREST(wordPressComRestApi: anonymousAPI, siteID: 0) - remote.fetchUnauthenticatedSiteInfo(forAddress: address, success: { response in - guard let response else { - failure(ServiceError.unknown) - return - } - - let site = WordPressComSiteInfo(remote: response) - guard site.url != Constants.wordPressBlogURL else { - failure(ServiceError.invalidWordPressAddress) - return - } - success(site) - }, failure: { error in - let result = error ?? ServiceError.unknown - failure(result) - }) - } -} - -// MARK: - Nested Types -// -extension WordPressComBlogService { - enum Constants { - static let wordPressBlogURL = "https://wordpress.com/blog" - } - - enum ServiceError: Error { - case unknown - case invalidWordPressAddress - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift deleted file mode 100644 index 579ec5abc957..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacade.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import WordPressKit - -@objc public class WordPressComOAuthClientFacade: NSObject, WordPressComOAuthClientFacadeProtocol { - - private let client: WordPressComOAuthClient - - @objc required public init(client: String, secret: String) { - self.client = WordPressComOAuthClient( - clientID: client, - secret: secret, - wordPressComBaseURL: WordPressAuthenticator.shared.configuration.wpcomBaseURL, - wordPressComApiBaseURL: WordPressAuthenticator.shared.configuration.wpcomAPIBaseURL - ) - } - - public func authenticate( - username: String, - password: String, - multifactorCode: String?, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping ((_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo?) -> Void), - failure: ((_ error: Error) -> Void)? - ) { - self.client.authenticate(username: username, password: password, multifactorCode: multifactorCode, needsMultifactor: needsMultifactor, success: success, failure: { error in - if case let .endpointError(authenticationFailure) = error, authenticationFailure.kind == .needsMultifactorCode { - needsMultifactor(0, nil) - } else { - failure?(error) - } - }) - } - - public func requestOneTimeCode( - username: String, - password: String, - success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.requestOneTimeCode(username: username, password: password, success: success, failure: failure) - } - - public func requestSocial2FACode( - userID: Int, - nonce: String, - success: @escaping (_ newNonce: String) -> Void, - failure: @escaping (_ error: Error, _ newNonce: String?) -> Void - ) { - self.client.requestSocial2FACode(userID: userID, nonce: nonce, success: success, failure: failure) - } - - public func authenticate( - socialIDToken: String, - service: String, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping (_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo) -> Void, - existingUserNeedsConnection: @escaping (_ email: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticate( - socialIDToken: socialIDToken, - service: service, - success: success, - needsMultifactor: needsMultifactor, - existingUserNeedsConnection: existingUserNeedsConnection, - failure: failure - ) - } - - public func authenticate( - socialLoginUser userID: Int, - authType: String, - twoStepCode: String, - twoStepNonce: String, - success: @escaping (_ authToken: String?) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticate( - socialLoginUserID: userID, - authType: authType, - twoStepCode: twoStepCode, - twoStepNonce: twoStepNonce, - success: success, - failure: failure - ) - } - - public func requestWebauthnChallenge( - userID: Int64, - twoStepNonce: String, - success: @escaping (_ challengeData: WebauthnChallengeInfo) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.requestWebauthnChallenge(userID: userID, twoStepNonce: twoStepNonce, success: success, failure: failure) - } - - public func authenticateWebauthnSignature( - userID: Int64, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data, - success: @escaping (_ authToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) { - self.client.authenticateWebauthnSignature( - userID: userID, - twoStepNonce: twoStepNonce, - credentialID: credentialID, - clientDataJson: clientDataJson, - authenticatorData: authenticatorData, - signature: signature, - userHandle: userHandle, - success: success, - failure: failure - ) - } -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift b/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift deleted file mode 100644 index 7abd37d69fc5..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressComOAuthClientFacadeProtocol.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation - -import WordPressKit - -@objc public protocol WordPressComOAuthClientFacadeProtocol { - - init(client: String, secret: String) - - func authenticate( - username: String, - password: String, - multifactorCode: String?, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping ((_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo?) -> Void), - failure: ((_ error: Error) -> Void)? - ) - - func requestOneTimeCode( - username: String, - password: String, - success: @escaping () -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func requestSocial2FACode( - userID: Int, - nonce: String, - success: @escaping (_ newNonce: String) -> Void, - failure: @escaping (_ error: Error, _ newNonce: String?) -> Void - ) - - func authenticate( - socialIDToken: String, - service: String, - success: @escaping (_ authToken: String?) -> Void, - needsMultifactor: @escaping (_ userID: Int, _ nonceInfo: SocialLogin2FANonceInfo) -> Void, - existingUserNeedsConnection: @escaping (_ email: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func authenticate( - socialLoginUser userID: Int, - authType: String, - twoStepCode: String, - twoStepNonce: String, - success: @escaping (_ authToken: String?) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func requestWebauthnChallenge( - userID: Int64, - twoStepNonce: String, - success: @escaping (_ challengeData: WebauthnChallengeInfo) -> Void, - failure: @escaping (_ error: Error) -> Void - ) - - func authenticateWebauthnSignature( - userID: Int64, - twoStepNonce: String, - credentialID: Data, - clientDataJson: Data, - authenticatorData: Data, - signature: Data, - userHandle: Data, - success: @escaping (_ authToken: String) -> Void, - failure: @escaping (_ error: Error) -> Void - ) -} diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h b/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h deleted file mode 100644 index f1689a3c2280..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.h +++ /dev/null @@ -1,23 +0,0 @@ -#import - -@protocol WordPressXMLRPCAPIFacade - -extern NSString *const XMLRPCOriginalErrorKey; - -- (instancetype)initWithUserAgent:(NSString *)userAgent; - -- (void)guessXMLRPCURLForSite:(NSString *)url - success:(void (^)(NSURL *xmlrpcURL))success - failure:(void (^)(NSError *error))failure; - -- (void)getBlogOptionsWithEndpoint:(NSURL *)xmlrpc - username:(NSString *)username - password:(NSString *)password - success:(void (^)(NSDictionary *options))success - failure:(void (^)(NSError *error))failure; - -@end - -@interface WordPressXMLRPCAPIFacade : NSObject - -@end diff --git a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m b/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m deleted file mode 100644 index 0fcce7f6602b..000000000000 --- a/Sources/WordPressAuthenticator/Helpers/WordPressXMLRPCAPIFacade.m +++ /dev/null @@ -1,96 +0,0 @@ -#import "WordPressXMLRPCAPIFacade.h" -#import - -@import WordPressKit; - -@interface WordPressXMLRPCAPIFacade () - -@property (nonatomic, strong) NSString *userAgent; - -@end - -NSString *const XMLRPCOriginalErrorKey = @"XMLRPCOriginalErrorKey"; - -@implementation WordPressXMLRPCAPIFacade - -- (instancetype)initWithUserAgent:(NSString *)userAgent -{ - self = [super init]; - if (self) { - _userAgent = userAgent; - } - - return self; -} - -- (void)guessXMLRPCURLForSite:(NSString *)url - success:(void (^)(NSURL *xmlrpcURL))success - failure:(void (^)(NSError *error))failure -{ - WordPressOrgXMLRPCValidator *validator = [[WordPressOrgXMLRPCValidator alloc] init]; - [validator guessXMLRPCURLForSite:url - userAgent:self.userAgent - success:success - failure:^(NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - failure([self errorForGuessXMLRPCApiFailure:error]); - }); - }]; -} - -- (NSError *)errorForGuessXMLRPCApiFailure:(NSError *)error -{ - WPLogError(@"Error on trying to guess XMLRPC site: %@", error); - NSArray *errorCodes = @[ - @(NSURLErrorUserCancelledAuthentication), - @(NSURLErrorNotConnectedToInternet), - @(NSURLErrorNetworkConnectionLost), - ]; - if ([error.domain isEqual:NSURLErrorDomain] && [errorCodes containsObject:@(error.code)]) { - return error; - } else { - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: NSLocalizedString(@"Unable to read the WordPress site at that URL. Tap 'Need more help?' to view the FAQ.", nil), - NSLocalizedFailureReasonErrorKey: error.localizedDescription, - XMLRPCOriginalErrorKey: error - }; - - NSError *err = [NSError errorWithDomain:WordPressAuthenticator.errorDomain code:NSURLErrorBadURL userInfo:userInfo]; - return err; - } -} - -- (void)getBlogOptionsWithEndpoint:(NSURL *)xmlrpc - username:(NSString *)username - password:(NSString *)password - success:(void (^)(NSDictionary *options))success - failure:(void (^)(NSError *error))failure; -{ - - WordPressOrgXMLRPCApi *api = [[WordPressOrgXMLRPCApi alloc] initWithEndpoint:xmlrpc userAgent:self.userAgent]; - [api checkCredentials:username password:password success:^(id responseObject, NSHTTPURLResponse *httpResponse __unused) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (![responseObject isKindOfClass:[NSDictionary class]]) { - if (failure) { - NSDictionary *userInfo = @{NSLocalizedDescriptionKey: NSLocalizedString(@"Unable to read the WordPress site at that URL. Tap 'Need more help?' to view the FAQ.", nil)}; - NSError *error = [NSError errorWithDomain:WordPressOrgXMLRPCApiErrorDomain code:WordPressOrgXMLRPCApiErrorResponseSerializationFailed userInfo:userInfo]; - failure(error); - } - return; - } - if (success) { - success((NSDictionary *)responseObject); - } - }); - - } failure:^(NSError *error, NSHTTPURLResponse *httpResponse __unused) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (failure) { - failure(error); - } - }); - }]; -} - - -@end diff --git a/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json b/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json deleted file mode 100644 index 4509ea16b8d9..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/jetpack.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":95,"w":310,"h":464,"nm":"Jetpack","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Jetpack Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.344],"y":[1]},"o":{"x":[0.655],"y":[0]},"n":["0p344_1_0p655_0"],"t":20,"s":[0],"e":[1800]},{"t":50}],"ix":10},"p":{"a":0,"k":[155,291,0],"ix":2},"a":{"a":0,"k":[68,68,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-14.126,-27.274],[-14.126,37.664],[19.293,-27.274]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[85.523,84.065],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[14.124,27.282],[14.124,-37.658],[-19.296,27.282]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[50.476,51.935],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[37.556,0],[0,-37.556],[-37.556,0],[0,37.556]],"o":[[-37.556,0],[0,37.556],[37.556,0],[0,-37.556]],"v":[[0,-68],[-68,0],[0,68],[68,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8,0.807843137255,0.81568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[68,68],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,303],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/notifications.json b/Sources/WordPressAuthenticator/Resources/Animations/notifications.json deleted file mode 100644 index bafe03becc1d..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/notifications.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":47,"w":310,"h":464,"nm":"Notifications","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"notification-01 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":7,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":14,"s":[100],"e":[100]},{"t":17}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,222.086,0],"ix":2},"a":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":7,"s":[115,73,0],"e":[115,83,0],"to":[0,1.66666662693024,0],"ti":[0,-1.66666662693024,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":14,"s":[115,83,0],"e":[115,83,0],"to":[0,0,0],"ti":[0,0,0]},{"t":17}],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":7,"s":[75,75,100],"e":[103,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[103,103,100],"e":[100,100,100]},{"t":17}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.81,0.213],[3.849,-0.037],[3.849,-0.838],[-0.276,-1.284],[-0.879,-0.193],[-3.849,-0.064],[-3.849,1.004],[0.332,1.285]],"o":[[-3.849,-1.005],[-3.849,0.064],[-1.278,0.278],[0.203,0.942],[3.849,0.836],[3.849,0.038],[1.277,-0.334],[-0.225,-0.872]],"v":[[-54.525,-12.412],[-66.074,-13.557],[-77.622,-12.412],[-79.436,-9.584],[-77.622,-7.761],[-66.074,-6.617],[-54.525,-7.761],[-52.812,-10.691]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[1.162,0.067],[9.639,0.219],[9.64,-0.071],[9.64,-0.253],[9.639,-0.434],[-0.056,-1.284],[-1.181,-0.057],[-9.639,-0.168],[-9.639,-0.106],[-9.64,0.075],[-9.639,0.528],[0.07,1.285]],"o":[[-9.639,-0.53],[-9.64,-0.075],[-9.639,0.107],[-9.639,0.17],[-1.278,0.056],[0.055,1.211],[9.639,0.434],[9.64,0.256],[9.64,0.071],[9.639,-0.219],[1.279,-0.07],[-0.064,-1.194]],"v":[[77.454,-12.377],[48.536,-13.335],[19.616,-13.505],[-9.303,-13.218],[-38.223,-12.377],[-40.434,-9.949],[-38.223,-7.727],[-9.303,-6.889],[19.616,-6.6],[48.536,-6.77],[77.454,-7.727],[79.643,-10.18]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.81,0.212],[3.85,-0.037],[3.851,-0.838],[-0.276,-1.284],[-0.879,-0.193],[-3.849,-0.063],[-3.85,1.005],[0.333,1.284]],"o":[[-3.85,-1.005],[-3.849,0.063],[-1.277,0.278],[0.203,0.941],[3.851,0.837],[3.85,0.038],[1.278,-0.333],[-0.224,-0.872]],"v":[[53.143,7.742],[41.594,6.597],[30.045,7.742],[28.232,10.569],[30.045,12.392],[41.594,13.536],[53.143,12.392],[54.855,9.463]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[-0.097,-1.15],[1.277,-0.109],[6.212,-0.219],[6.212,0.074],[6.212,0.254],[6.212,0.434],[0.081,1.174],[-1.278,0.088],[-6.211,0.169],[-6.212,0.107],[-6.211,-0.151],[-6.212,-0.529]],"o":[[0.108,1.284],[-6.212,0.529],[-6.211,0.15],[-6.212,-0.107],[-6.211,-0.17],[-1.137,-0.078],[-0.089,-1.286],[6.212,-0.434],[6.212,-0.254],[6.212,-0.075],[6.212,0.219],[1.108,0.093]],"v":[[11.233,9.87],[9.116,12.392],[-9.521,13.349],[-28.156,13.52],[-46.791,13.232],[-65.426,12.392],[-67.579,10.229],[-65.426,7.742],[-46.791,6.902],[-28.156,6.614],[-9.521,6.785],[9.116,7.742]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.882352941176,0.886274509804,0.886274509804,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[113.584,60.612],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,5.545],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[5.525,0]],"v":[[39.855,3.918],[39.855,-13.956],[-39.855,-13.956],[-39.855,13.956],[29.851,13.956]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[113.857,108.956],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-95.5,54.501],[95.5,54.501],[95.5,-54.501],[-95.5,-54.501]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[114.5,80.501],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.964705882353,0.964705882353,0.964705882353,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,303],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/post.json b/Sources/WordPressAuthenticator/Resources/Animations/post.json deleted file mode 100644 index 974f82d2818b..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/post.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":135,"w":310,"h":464,"nm":"post-on-the-go","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 4 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":34,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":36,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":82,"s":[100],"e":[0]},{"t":84}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,0.228],[-4.59,0.075],[-4.589,-1.191],[0.396,-1.524],[0.966,-0.253],[4.589,0.044],[4.59,0.992],[-0.329,1.524]],"o":[[4.59,-0.993],[4.589,-0.045],[1.524,0.395],[-0.267,1.034],[-4.589,1.191],[-4.59,-0.075],[-1.522,-0.329],[0.241,-1.116]],"v":[[-13.742,-2.757],[0.027,-4.115],[13.794,-2.757],[15.836,0.717],[13.794,2.758],[0.027,4.116],[-13.742,2.758],[-15.903,-0.596]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[169.994,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":22,"op":84,"st":22,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 5 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":28,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":30,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":84,"s":[100],"e":[0]},{"t":86}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,0.228],[-4.589,0.075],[-4.589,-1.191],[0.395,-1.524],[0.965,-0.253],[4.59,0.044],[4.589,0.992],[-0.329,1.524]],"o":[[4.589,-0.993],[4.59,-0.045],[1.523,0.395],[-0.268,1.034],[-4.589,1.191],[-4.589,-0.075],[-1.523,-0.329],[0.242,-1.116]],"v":[[-13.741,-2.757],[0.027,-4.115],[13.795,-2.757],[15.837,0.717],[13.795,2.758],[0.027,4.116],[-13.741,2.758],[-15.903,-0.596]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[126.146,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":86,"st":20,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Layer 6 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":27,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":87,"s":[100],"e":[0]},{"t":89}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.357,0.093],[-7.415,0.2],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.628],[0.13,-1.524],[1.323,-0.111],[7.415,-0.26],[7.416,0.088],[7.415,0.301],[7.415,0.514],[-0.106,1.525]],"o":[[7.415,-0.515],[7.415,-0.303],[7.416,-0.089],[7.415,0.259],[1.525,0.129],[-0.115,1.367],[-7.415,0.628],[-7.415,0.179],[-7.415,-0.127],[-7.415,-0.202],[-1.525,-0.106],[0.097,-1.396]],"v":[[-44.611,-2.761],[-22.365,-3.757],[-0.12,-4.1],[22.126,-3.897],[44.372,-2.761],[46.899,0.234],[44.372,2.762],[22.126,3.898],[-0.12,4.101],[-22.365,3.759],[-44.611,2.762],[-47.181,-0.191]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[47.029,185.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17,"op":89,"st":17,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Layer 3 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":16,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":18,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":93,"s":[100],"e":[0]},{"t":95}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.302,0.067],[-10.625,0.201],[-10.625,0.127],[-10.625,-0.089],[-10.625,-0.629],[0.077,-1.525],[1.279,-0.081],[10.624,-0.259],[10.624,0.084],[10.624,0.302],[10.624,0.515],[-0.063,1.525]],"o":[[10.624,-0.515],[10.624,-0.302],[10.624,-0.084],[10.624,0.26],[1.408,0.083],[-0.071,1.419],[-10.625,0.628],[-10.625,0.09],[-10.625,-0.127],[-10.625,-0.201],[-1.409,-0.068],[0.06,-1.439]],"v":[[-63.704,-2.761],[-31.83,-3.758],[0.043,-4.1],[31.917,-3.898],[63.79,-2.761],[66.2,0.151],[63.79,2.761],[31.917,3.897],[0.043,4.1],[-31.83,3.758],[-63.704,2.761],[-66.14,-0.123]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[163.797,162.199],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":11,"op":95,"st":11,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Layer 7 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":9,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":11,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":95,"s":[100],"e":[0]},{"t":97}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155.028,232.392,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.33,0.107],[-6.293,0.2],[-6.293,0.127],[-6.294,-0.179],[-6.293,-0.628],[0.153,-1.525],[1.288,-0.126],[6.292,-0.26],[6.293,0.089],[6.293,0.301],[6.292,0.515],[-0.125,1.525]],"o":[[6.292,-0.515],[6.293,-0.303],[6.293,-0.089],[6.292,0.259],[1.525,0.152],[-0.133,1.338],[-6.293,0.629],[-6.294,0.179],[-6.293,-0.127],[-6.293,-0.202],[-1.525,-0.124],[0.112,-1.373]],"v":[[-37.884,-2.761],[-19.005,-3.757],[-0.126,-4.1],[18.754,-3.897],[37.633,-2.761],[40.118,0.276],[37.633,2.761],[18.754,3.898],[-0.126,4.1],[-19.005,3.759],[-37.884,2.761],[-40.42,-0.225]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.271,162.189],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9,"op":97,"st":9,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"text-bodymovin Outlines 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,232.087,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.242,-1.118],[-1.525,-0.33],[-4.596,-0.076],[-4.594,1.193],[-0.268,1.037],[1.526,0.396],[4.595,-0.044],[4.595,-0.993]],"o":[[-0.33,1.524],[4.595,0.995],[4.595,0.044],[0.967,-0.254],[0.396,-1.525],[-4.594,-1.192],[-4.596,0.076],[-1.049,0.228]],"v":[[-114.981,28.062],[-112.816,31.42],[-99.03,32.78],[-85.245,31.42],[-83.2,29.375],[-85.245,25.897],[-99.03,24.538],[-112.816,25.897]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.069],[-10.625,-0.201],[-10.625,-0.126],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.084],[10.624,0.259],[10.624,-0.085],[10.624,-0.302],[10.624,-0.514]],"o":[[-0.063,1.526],[10.624,0.514],[10.624,0.302],[10.624,0.085],[10.624,-0.259],[1.279,-0.081],[0.077,-1.526],[-10.625,-0.628],[-10.625,-0.09],[-10.625,0.127],[-10.625,0.201],[-1.302,0.068]],"v":[[-17.344,-28.749],[-14.908,-25.863],[16.966,-24.867],[48.839,-24.525],[80.713,-24.728],[112.586,-25.863],[114.996,-28.474],[112.586,-31.387],[80.713,-32.522],[48.839,-32.725],[16.966,-32.383],[-14.908,-31.387]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.293,-0.127],[-6.294,0.179],[-6.293,0.628],[-0.134,1.338],[1.525,0.152],[6.292,0.26],[6.293,-0.088],[6.294,-0.302],[6.293,-0.515]],"o":[[-0.125,1.525],[6.293,0.515],[6.294,0.302],[6.293,0.088],[6.292,-0.26],[1.288,-0.128],[0.152,-1.525],[-6.293,-0.628],[-6.294,-0.179],[-6.293,0.127],[-6.293,0.201],[-1.329,0.107]],"v":[[-115.149,-28.861],[-112.613,-25.874],[-93.734,-24.878],[-74.855,-24.535],[-55.975,-24.738],[-37.096,-25.874],[-34.61,-28.36],[-37.096,-31.397],[-55.975,-32.533],[-74.855,-32.736],[-93.734,-32.393],[-112.613,-31.397]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.065,-1.438],[-1.526,-0.067],[-11.508,-0.201],[-11.508,-0.127],[-11.508,0.089],[-11.508,0.629],[-0.077,1.419],[1.525,0.083],[11.507,0.259],[11.507,-0.084],[11.507,-0.303],[11.507,-0.515]],"o":[[-0.068,1.525],[11.507,0.515],[11.507,0.302],[11.507,0.084],[11.507,-0.26],[1.385,-0.08],[0.084,-1.525],[-11.508,-0.628],[-11.508,-0.09],[-11.508,0.127],[-11.508,0.2],[-1.41,0.067]],"v":[[-68.421,28.495],[-65.783,31.379],[-31.26,32.376],[3.262,32.718],[37.785,32.516],[72.308,31.379],[74.918,28.769],[72.308,25.857],[37.785,24.721],[3.262,24.518],[-31.26,24.861],[-65.783,25.857]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.242,-1.117],[-1.525,-0.33],[-4.596,-0.075],[-4.596,1.192],[-0.268,1.036],[1.525,0.396],[4.596,-0.044],[4.595,-0.994]],"o":[[-0.329,1.526],[4.595,0.994],[4.596,0.044],[0.967,-0.253],[0.396,-1.525],[-4.596,-1.193],[-4.596,0.075],[-1.049,0.229]],"v":[[-0.778,-0.621],[1.387,2.738],[15.173,4.097],[28.959,2.738],[31.003,0.693],[28.959,-2.785],[15.173,-4.144],[1.387,-2.785]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[-1.357,0.093],[-7.415,0.201],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.629],[0.129,-1.525],[1.323,-0.111],[7.415,-0.259],[7.415,0.089],[7.416,0.303],[7.415,0.514],[-0.106,1.525]],"o":[[7.415,-0.514],[7.416,-0.302],[7.415,-0.088],[7.415,0.259],[1.526,0.129],[-0.116,1.367],[-7.415,0.628],[-7.415,0.179],[-7.415,-0.127],[-7.415,-0.2],[-1.525,-0.106],[0.096,-1.396]],"v":[[-112.582,-2.785],[-90.336,-3.782],[-68.09,-4.124],[-45.844,-3.921],[-23.599,-2.785],[-21.071,0.21],[-23.599,2.738],[-45.844,3.874],[-68.09,4.077],[-90.336,3.734],[-112.582,2.738],[-115.151,-0.215]],"c":true},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847058823529,0.870588235294,0.894117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,104.848],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":7,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"text-bodymovin Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,232.087,0],"ix":2},"a":{"a":0,"k":[115,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[12.516,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,-12.516],[0,0],[0,0],[0,0]],"v":[[-96.144,18.343],[115.035,18.343],[115.035,4.32],[92.373,-18.343],[-115,-18.343],[-115,18.343]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.435294117647,0.576470588235,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,18.343],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Image Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10,"s":[155,374,0],"e":[155,402,0],"to":[0,4.66666650772095,0],"ti":[0,-4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":12,"s":[155,402,0],"e":[155,402,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":25,"s":[155,402,0],"e":[155,430,0],"to":[0,4.66666650772095,0],"ti":[0,-4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":27,"s":[155,430,0],"e":[155,430,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":87,"s":[155,430,0],"e":[155,402,0],"to":[0,-4.66666650772095,0],"ti":[0,4.66666650772095,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":89,"s":[155,402,0],"e":[155,402,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":94,"s":[155,402,0],"e":[155,374,0],"to":[0,-4.66666650772095,0],"ti":[0,4.66666650772095,0]},{"t":96}],"ix":2},"a":{"a":0,"k":[115,70,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-12.15],[12.15,0],[0,12.15],[-12.15,0]],"o":[[0,12.15],[-12.15,0],[0,-12.15],[12.15,0]],"v":[[22,0],[0,22],[-22,0],[0,-22]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[191.039,37.805],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,1.83],[0,0],[26.664,3.881],[44.378,51.841],[0,0],[0,0]],"o":[[0,0],[-11.598,-14.112],[-48.045,-6.995],[0,0],[0,0],[1.83,0]],"v":[[115,46.772],[115,19.79],[56.449,-6.075],[-115,-50.085],[-115,50.085],[111.687,50.085]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.435294117647,0.576470588235,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,89.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.83,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,1.83]],"v":[[111.687,69.855],[-115,69.855],[-115,-69.855],[115,-69.855],[115,66.542]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.733333333333,0.788235294118,0.835294117647,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,70.001],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":4,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153.328,303.21],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/reader.json b/Sources/WordPressAuthenticator/Resources/Animations/reader.json deleted file mode 100644 index 0d9cd7a9b3b7..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/reader.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"4.7.0","fr":30,"ip":0,"op":150,"w":306,"h":462,"nm":"Reader","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Mask Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[152.724,28.147,0]},"a":{"a":0,"k":[135,217.5,0]},"s":{"a":0,"k":[84.074,13.103,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[185.879,-169.017]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":2},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-134.937,27.126],[135.188,27.06],[135.25,-27.016],[-135,-27]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[135.174,643.654],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[118.777,759.676],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-141.244,223.056],[146.374,223.056],[136.711,-211.481],[-136.783,-210.554],[-140.45,-160.256]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941177,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[135,217.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100.332,103.011],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Cards Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.967],"y":[1]},"o":{"x":[0.158],"y":[0]},"n":["0p967_1_0p158_0"],"t":10,"s":[153],"e":[153]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.205],"y":[0]},"n":["0p833_1_0p205_0"],"t":46,"s":[153],"e":[153]},{"i":{"x":[0.985],"y":[1]},"o":{"x":[0.015],"y":[0]},"n":["0p985_1_0p015_0"],"t":90,"s":[153],"e":[153]},{"t":110}]},"y":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.167],"y":[0]},"n":["1_1_0p167_0"],"t":0,"s":[431],"e":[432]},{"i":{"x":[0.069],"y":[1]},"o":{"x":[0.216],"y":[0]},"n":["0p069_1_0p216_0"],"t":10,"s":[432],"e":[98]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.205],"y":[0.185]},"n":["0p833_1_0p205_0p185"],"t":44,"s":[98],"e":[98]},{"i":{"x":[0],"y":[1]},"o":{"x":[0.301],"y":[0]},"n":["0_1_0p301_0"],"t":90,"s":[98],"e":[432]},{"t":118}]}},"a":{"a":0,"k":[115,300,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.047,-0.229],[-4.59,-0.075],[-4.589,1.191],[0.396,1.524],[0.966,0.253],[4.589,-0.044],[4.59,-0.992],[-0.329,-1.524]],"o":[[4.59,0.992],[4.589,0.044],[1.524,-0.396],[-0.267,-1.035],[-4.589,-1.191],[-4.59,0.075],[-1.522,0.329],[0.241,1.115]],"v":[[41.252,-80.641],[55.021,-79.284],[68.788,-80.641],[70.83,-84.115],[68.788,-86.157],[55.021,-87.514],[41.252,-86.157],[39.091,-82.802]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-1.048,-0.229],[-4.589,-0.075],[-4.588,1.191],[0.396,1.524],[0.966,0.253],[4.59,-0.044],[4.589,-0.992],[-0.329,-1.524]],"o":[[4.589,0.992],[4.59,0.044],[1.524,-0.396],[-0.267,-1.035],[-4.588,-1.191],[-4.589,0.075],[-1.523,0.329],[0.241,1.115]],"v":[[-2.595,-80.641],[11.173,-79.284],[24.94,-80.641],[26.982,-84.115],[24.94,-86.157],[11.173,-87.514],[-2.595,-86.157],[-4.757,-82.802]],"c":true}},"nm":"Path 2","mn":"ADBE Vector Shape - Group"},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[-1.358,-0.093],[-7.415,-0.201],[-7.415,-0.127],[-7.415,0.178],[-7.415,0.628],[0.129,1.525],[1.323,0.112],[7.415,0.259],[7.415,-0.088],[7.416,-0.301],[7.415,-0.514],[-0.106,-1.526]],"o":[[7.415,0.515],[7.416,0.302],[7.415,0.088],[7.415,-0.26],[1.526,-0.129],[-0.116,-1.366],[-7.415,-0.627],[-7.415,-0.179],[-7.415,0.127],[-7.415,0.202],[-1.526,0.107],[0.096,1.395]],"v":[[-112.581,-80.637],[-90.336,-79.641],[-68.09,-79.298],[-45.844,-79.501],[-23.599,-80.637],[-21.071,-83.633],[-23.599,-86.161],[-45.844,-87.296],[-68.09,-87.499],[-90.336,-87.157],[-112.581,-86.161],[-115.151,-83.207]],"c":true}},"nm":"Path 3","mn":"ADBE Vector Shape - Group"},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.241,-1.116],[-1.522,-0.329],[-4.59,-0.076],[-4.589,1.191],[-0.267,1.035],[1.524,0.396],[4.589,-0.044],[4.59,-0.992]],"o":[[-0.329,1.523],[4.59,0.993],[4.589,0.044],[0.966,-0.253],[0.396,-1.523],[-4.589,-1.191],[-4.59,0.075],[-1.047,0.229]],"v":[[39.091,177.251],[41.252,180.605],[55.021,181.963],[68.788,180.605],[70.83,178.563],[68.788,175.089],[55.021,173.732],[41.252,175.089]],"c":true}},"nm":"Path 4","mn":"ADBE Vector Shape - Group"},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.241,-1.116],[-1.523,-0.329],[-4.589,-0.076],[-4.588,1.191],[-0.267,1.035],[1.524,0.396],[4.59,-0.044],[4.589,-0.992]],"o":[[-0.329,1.523],[4.589,0.993],[4.59,0.044],[0.966,-0.253],[0.396,-1.523],[-4.588,-1.191],[-4.589,0.075],[-1.048,0.229]],"v":[[-4.757,177.251],[-2.595,180.605],[11.173,181.963],[24.94,180.605],[26.982,178.563],[24.94,175.089],[11.173,173.732],[-2.595,175.089]],"c":true}},"nm":"Path 5","mn":"ADBE Vector Shape - Group"},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0.096,-1.396],[-1.526,-0.105],[-7.415,-0.2],[-7.415,-0.127],[-7.415,0.178],[-7.415,0.628],[-0.116,1.366],[1.526,0.129],[7.415,0.26],[7.415,-0.088],[7.416,-0.301],[7.415,-0.515]],"o":[[-0.106,1.525],[7.415,0.515],[7.416,0.303],[7.415,0.089],[7.415,-0.259],[1.323,-0.111],[0.129,-1.526],[-7.415,-0.628],[-7.415,-0.179],[-7.415,0.127],[-7.415,0.202],[-1.358,0.092]],"v":[[-115.151,177.656],[-112.581,180.609],[-90.336,181.605],[-68.09,181.948],[-45.844,181.745],[-23.599,180.609],[-21.071,178.082],[-23.599,175.086],[-45.844,173.95],[-68.09,173.747],[-90.336,174.089],[-112.581,175.086]],"c":true}},"nm":"Path 6","mn":"ADBE Vector Shape - Group"},{"ind":6,"ty":"sh","ix":7,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.069],[-10.625,-0.201],[-10.625,-0.126],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.083],[10.624,0.259],[10.624,-0.084],[10.624,-0.303],[10.624,-0.514]],"o":[[-0.063,1.525],[10.624,0.513],[10.624,0.301],[10.624,0.084],[10.624,-0.259],[1.279,-0.081],[0.077,-1.525],[-10.625,-0.628],[-10.625,-0.089],[-10.625,0.127],[-10.625,0.2],[-1.302,0.068]],"v":[[-17.344,154.239],[-14.908,157.125],[16.966,158.121],[48.839,158.463],[80.713,158.26],[112.586,157.125],[114.996,154.513],[112.586,151.601],[80.713,150.465],[48.839,150.262],[16.966,150.605],[-14.908,151.601]],"c":true}},"nm":"Path 7","mn":"ADBE Vector Shape - Group"},{"ind":7,"ty":"sh","ix":8,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.292,-0.127],[-6.294,0.178],[-6.293,0.628],[-0.134,1.337],[1.525,0.153],[6.292,0.259],[6.294,-0.088],[6.294,-0.301],[6.293,-0.514]],"o":[[-0.125,1.525],[6.293,0.515],[6.294,0.303],[6.294,0.088],[6.292,-0.26],[1.288,-0.128],[0.153,-1.526],[-6.293,-0.627],[-6.294,-0.179],[-6.292,0.127],[-6.293,0.202],[-1.329,0.108]],"v":[[-115.149,154.127],[-112.613,157.114],[-93.734,158.11],[-74.855,158.453],[-55.975,158.25],[-37.096,157.114],[-34.61,154.628],[-37.096,151.59],[-55.975,150.455],[-74.855,150.252],[-93.734,150.594],[-112.613,151.59]],"c":true}},"nm":"Path 8","mn":"ADBE Vector Shape - Group"},{"ind":8,"ty":"sh","ix":9,"ks":{"a":0,"k":{"i":[[0.242,-1.117],[-1.525,-0.329],[-4.595,-0.075],[-4.594,1.193],[-0.268,1.036],[1.526,0.396],[4.595,-0.045],[4.595,-0.994]],"o":[[-0.33,1.526],[4.595,0.994],[4.595,0.045],[0.967,-0.253],[0.396,-1.525],[-4.594,-1.193],[-4.595,0.075],[-1.049,0.229]],"v":[[-114.981,-121.151],[-112.816,-117.792],[-99.03,-116.433],[-85.245,-117.792],[-83.2,-119.837],[-85.245,-123.315],[-99.03,-124.674],[-112.816,-123.315]],"c":true}},"nm":"Path 9","mn":"ADBE Vector Shape - Group"},{"ind":9,"ty":"sh","ix":10,"ks":{"a":0,"k":{"i":[[0.06,-1.438],[-1.408,-0.068],[-10.625,-0.201],[-10.625,-0.127],[-10.625,0.09],[-10.625,0.628],[-0.071,1.419],[1.408,0.083],[10.624,0.26],[10.624,-0.085],[10.624,-0.302],[10.624,-0.514]],"o":[[-0.063,1.526],[10.624,0.515],[10.624,0.302],[10.624,0.084],[10.624,-0.259],[1.279,-0.081],[0.077,-1.526],[-10.625,-0.628],[-10.625,-0.09],[-10.625,0.127],[-10.625,0.201],[-1.302,0.068]],"v":[[-17.344,-177.961],[-14.908,-175.076],[16.966,-174.079],[48.839,-173.737],[80.713,-173.94],[112.586,-175.076],[114.996,-177.686],[112.586,-180.599],[80.713,-181.735],[48.839,-181.937],[16.966,-181.595],[-14.908,-180.599]],"c":true}},"nm":"Path 10","mn":"ADBE Vector Shape - Group"},{"ind":10,"ty":"sh","ix":11,"ks":{"a":0,"k":{"i":[[0.112,-1.372],[-1.525,-0.125],[-6.293,-0.201],[-6.292,-0.127],[-6.294,0.179],[-6.293,0.628],[-0.134,1.338],[1.525,0.152],[6.292,0.259],[6.294,-0.089],[6.294,-0.303],[6.293,-0.515]],"o":[[-0.125,1.526],[6.293,0.514],[6.294,0.302],[6.294,0.088],[6.292,-0.26],[1.288,-0.128],[0.153,-1.526],[-6.293,-0.628],[-6.294,-0.179],[-6.292,0.127],[-6.293,0.2],[-1.329,0.107]],"v":[[-115.149,-178.074],[-112.613,-175.086],[-93.734,-174.09],[-74.855,-173.747],[-55.975,-173.95],[-37.096,-175.086],[-34.61,-177.572],[-37.096,-180.609],[-55.975,-181.745],[-74.855,-181.948],[-93.734,-181.605],[-112.613,-180.609]],"c":true}},"nm":"Path 11","mn":"ADBE Vector Shape - Group"},{"ind":11,"ty":"sh","ix":12,"ks":{"a":0,"k":{"i":[[0.065,-1.439],[-1.526,-0.069],[-11.508,-0.201],[-11.508,-0.126],[-11.508,0.09],[-11.508,0.628],[-0.077,1.418],[1.525,0.084],[11.507,0.26],[11.508,-0.084],[11.507,-0.302],[11.507,-0.514]],"o":[[-0.068,1.525],[11.507,0.514],[11.507,0.302],[11.508,0.084],[11.507,-0.258],[1.385,-0.081],[0.084,-1.525],[-11.508,-0.628],[-11.508,-0.089],[-11.508,0.127],[-11.508,0.201],[-1.41,0.068]],"v":[[-68.421,-120.717],[-65.783,-117.832],[-31.26,-116.836],[3.262,-116.494],[37.785,-116.697],[72.308,-117.832],[74.918,-120.443],[72.308,-123.356],[37.785,-124.492],[3.262,-124.695],[-31.26,-124.352],[-65.783,-123.356]],"c":true}},"nm":"Path 12","mn":"ADBE Vector Shape - Group"},{"ind":12,"ty":"sh","ix":13,"ks":{"a":0,"k":{"i":[[0.242,-1.118],[-1.525,-0.33],[-4.596,-0.075],[-4.596,1.192],[-0.268,1.036],[1.525,0.395],[4.596,-0.044],[4.595,-0.995]],"o":[[-0.329,1.525],[4.595,0.993],[4.596,0.045],[0.967,-0.254],[0.396,-1.525],[-4.596,-1.193],[-4.596,0.076],[-1.049,0.228]],"v":[[-0.778,-149.833],[1.387,-146.474],[15.173,-145.116],[28.959,-146.474],[31.003,-148.519],[28.959,-151.997],[15.173,-153.357],[1.387,-151.997]],"c":true}},"nm":"Path 13","mn":"ADBE Vector Shape - Group"},{"ind":13,"ty":"sh","ix":14,"ks":{"a":0,"k":{"i":[[-1.358,0.092],[-7.415,0.201],[-7.415,0.127],[-7.415,-0.179],[-7.415,-0.628],[0.129,-1.525],[1.323,-0.111],[7.415,-0.26],[7.415,0.088],[7.416,0.302],[7.415,0.515],[-0.106,1.524]],"o":[[7.415,-0.515],[7.416,-0.302],[7.415,-0.089],[7.415,0.259],[1.526,0.128],[-0.116,1.367],[-7.415,0.628],[-7.415,0.178],[-7.415,-0.127],[-7.415,-0.201],[-1.526,-0.106],[0.096,-1.396]],"v":[[-112.581,-151.997],[-90.336,-152.994],[-68.09,-153.336],[-45.844,-153.133],[-23.599,-151.997],[-21.071,-149.002],[-23.599,-146.474],[-45.844,-145.338],[-68.09,-145.135],[-90.336,-145.478],[-112.581,-146.474],[-115.151,-149.427]],"c":true}},"nm":"Path 14","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8470588,0.8705882,0.8941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,354.135],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":15,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.464,-1.615],[-2.528,5.622]],"o":[[-3.106,3.428],[1.811,-4.028]],"v":[[-1.597,-3.845],[2.892,-0.161]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[126.659,443.744],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.818,2.02],[3.963,-4.719]],"o":[[1.736,-4.288],[-2.84,3.382]],"v":[[-4.207,-0.767],[1.062,1.673]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[201.302,439.586],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.817,2.02],[2.556,-3.602]],"o":[[1.737,-4.288],[-2.555,3.602]],"v":[[-2.993,-1.016],[1.254,1.702]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[177.04,420.634],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[2.178,-0.091],[-4.251,1.194]],"o":[[-4.622,0.192],[4.252,-1.194]],"v":[[-0.38,-3.009],[0.751,1.905]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[198.404,379.927],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.456,-1.241],[-4.251,1.194]],"o":[[-4.456,1.241],[4.252,-1.194]],"v":[[-0.463,-2.434],[0.668,2.48]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529412,0.9607843,0.9647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[154.751,386.841],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.397,-1.477],[-0.458,-0.982],[-0.423,-0.219],[-0.108,-0.053],[-0.159,-0.008],[-0.338,0.02],[-1.422,0.465],[-2.715,1.657],[-2.597,1.948],[-4.737,4.567],[-4.492,4.831],[-4.051,5.145],[-2.962,5.657],[-0.774,2.887],[-0.142,0.714],[-0.036,0.346],[0,0],[0,0],[0.596,0.653],[1.319,0.095],[0.711,-0.06],[0.74,-0.225],[2.825,-1.639],[0,0],[0.339,0.585],[-0.445,0.375],[-3.349,1.406],[-2.038,0.076],[-0.261,-0.012],[0,0],[-0.268,-0.043],[0,0],[-0.267,-0.12],[0,0],[-0.462,-0.401],[-0.379,-0.473],[-0.265,-0.522],[-0.075,-1.012],[0.365,-1.807],[1.474,-3.168],[4.036,-5.49],[4.542,-5.015],[4.979,-4.615],[5.628,-3.936],[3.109,-1.6],[1.665,-0.631],[0.844,-0.289],[0.913,-0.167],[2.069,0.423],[0.509,0.181],[0.25,0.137],[0.223,0.216],[0.332,1.105],[-0.788,1.626],[-1.188,1.25],[-0.246,-0.233],[0.132,-0.239]],"o":[[-0.795,1.435],[-0.403,1.458],[0.264,0.452],[0.081,0.076],[0.129,0.035],[0.297,0.03],[1.236,0.071],[2.852,-0.899],[2.757,-1.605],[5.206,-3.894],[4.799,-4.498],[4.516,-4.81],[4.032,-5.149],[1.473,-2.818],[0.229,-0.729],[0.063,-0.355],[0,0],[0,0],[-0.028,-1.302],[-0.571,-0.67],[-0.644,-0.077],[-0.722,0.14],[-2.974,0.781],[0,0],[-0.584,0.338],[-0.302,-0.52],[2.62,-2.218],[1.671,-0.705],[0.255,-0.007],[0,0],[0.267,0.03],[0,0],[0.272,0.088],[0,0],[0.544,0.251],[0.496,0.378],[0.367,0.485],[0.404,1.042],[0.202,2.043],[-0.705,3.637],[-3.008,6.314],[-4.055,5.482],[-4.58,4.981],[-5.027,4.557],[-2.822,1.957],[-1.561,0.785],[-0.811,0.344],[-0.874,0.25],[-1.813,0.335],[-0.504,-0.07],[-0.256,-0.078],[-0.247,-0.163],[-0.96,-0.749],[-0.497,-2.26],[0.819,-1.639],[0.233,-0.245],[0.207,0.197],[0,0]],"v":[[-38.758,32.822],[-40.631,37.209],[-40.629,41.168],[-39.655,42.181],[-39.309,42.323],[-38.941,42.447],[-38.075,42.566],[-33.98,41.953],[-25.566,37.936],[-17.577,32.487],[-2.702,19.679],[11.171,5.569],[24.045,-9.373],[34.8,-25.553],[38.298,-34.177],[38.743,-36.321],[38.834,-37.353],[38.876,-37.867],[38.848,-38.345],[37.918,-41.382],[35.012,-42.575],[32.942,-42.502],[30.75,-42.031],[22.038,-38.12],[22.031,-38.116],[20.359,-38.561],[20.627,-40.108],[29.515,-45.652],[35.053,-46.937],[35.818,-46.957],[36.617,-46.881],[37.418,-46.784],[38.238,-46.552],[39.051,-46.281],[39.845,-45.866],[41.305,-44.798],[42.474,-43.421],[43.288,-41.882],[44.039,-38.76],[43.681,-32.991],[40.156,-22.894],[29.27,-5.393],[16.249,10.255],[1.924,24.636],[-13.991,37.469],[-22.864,42.84],[-27.696,44.975],[-30.238,45.846],[-32.911,46.478],[-38.747,46.546],[-40.302,46.046],[-41.065,45.639],[-41.794,45.148],[-43.744,42.192],[-42.832,36.36],[-39.729,32.085],[-38.862,32.063],[-38.748,32.804]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[76.621,402.73],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[4.953,-21.002],[21.895,7.258],[-2.144,21.472],[-21.188,-4.084]],"o":[[-4.953,21.002],[-20.482,-6.789],[2.407,-24.099],[22.139,4.268]],"v":[[36.814,8.758],[-9.791,35.815],[-39.622,-7.907],[6.122,-38.989]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[77.475,404.954],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":8,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[115,69.859],[-115.538,69.859],[-115.538,-69.859],[115,-69.859]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.7333333,0.7882353,0.8352941,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,403.141],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":9,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-12.15],[12.15,0],[0,12.15],[-12.15,0]],"o":[[0,12.15],[-12.15,0],[0,-12.15],[12.15,0]],"v":[[22,0],[0,22],[-22,0],[0,-22]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[191.039,37.805],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":10,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,1.83],[0,0],[26.664,3.882],[44.378,51.842],[0,0],[0,0]],"o":[[0,0],[-11.598,-14.111],[-48.045,-6.994],[0,0],[0,0],[1.83,0]],"v":[[115,46.771],[115,19.788],[56.449,-6.074],[-115,-50.084],[-115,50.084],[111.687,50.084]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.4352941,0.5764706,0.6784314,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,89.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 11","np":2,"cix":2,"ix":11,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.83,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,1.83]],"v":[[111.687,69.855],[-115,69.855],[-115,-69.855],[115,-69.855],[115,66.542]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.7333333,0.7882353,0.8352941,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[115,70],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":12,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"iPhone Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[153,303,0]},"a":{"a":0,"k":[153,303,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.0039216,0.3764706,0.5294118,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.1372549,0.2078431,0.2941176,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[153.134,303.104],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"}],"ip":0,"op":419,"st":0,"bm":0,"sr":1}]} diff --git a/Sources/WordPressAuthenticator/Resources/Animations/stats.json b/Sources/WordPressAuthenticator/Resources/Animations/stats.json deleted file mode 100644 index 35557355eebc..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Animations/stats.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.3.4","fr":30,"ip":0,"op":67,"w":310,"h":464,"nm":"Stats","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"stats-01 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,274.891,0],"ix":2},"a":{"a":0,"k":[115,117,0],"ix":1},"s":{"a":0,"k":[144.195,144.195,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.161,0.067],[9.64,0.219],[9.64,-0.071],[9.64,-0.254],[9.639,-0.434],[-0.057,-1.284],[-1.181,-0.056],[-9.64,-0.169],[-9.64,-0.107],[-9.64,0.076],[-9.64,0.529],[0.07,1.284]],"o":[[-9.64,-0.53],[-9.64,-0.075],[-9.64,0.106],[-9.64,0.17],[-1.279,0.056],[0.055,1.211],[9.639,0.434],[9.64,0.255],[9.64,0.071],[9.64,-0.218],[1.278,-0.07],[-0.065,-1.195]],"v":[[57.921,-14.887],[29.002,-15.845],[0.082,-16.014],[-28.837,-15.727],[-57.756,-14.887],[-59.967,-12.458],[-57.756,-10.237],[-28.837,-9.398],[0.082,-9.109],[29.002,-9.28],[57.921,-10.237],[60.109,-12.689]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-0.071,-1.15],[0.94,-0.109],[4.57,-0.218],[4.571,0.075],[4.571,0.254],[4.571,0.433],[0.06,1.175],[-0.94,0.089],[-4.57,0.169],[-4.571,0.107],[-4.571,-0.152],[-4.57,-0.529]],"o":[[0.08,1.284],[-4.57,0.529],[-4.571,0.15],[-4.571,-0.107],[-4.57,-0.17],[-0.836,-0.079],[-0.065,-1.285],[4.571,-0.433],[4.571,-0.255],[4.571,-0.075],[4.57,0.218],[0.815,0.093]],"v":[[-2.124,12.36],[-3.682,14.883],[-17.394,15.839],[-31.106,16.01],[-44.818,15.722],[-58.53,14.883],[-60.114,12.719],[-58.53,10.232],[-44.818,9.393],[-31.106,9.105],[-17.394,9.276],[-3.682,10.232]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8,0.807843137255,0.81568627451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[112.118,59.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,22.126],[15.628,22.126],[15.628,-22.126],[-15.628,-22.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[162.101,209.788],"ix":2},"a":{"a":0,"k":[0,22],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":21,"s":[100,0],"e":[100,100]},{"t":35}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 3-2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,51.566],[15.628,51.566],[15.628,-51.567],[-15.628,-51.567]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[162.101,209.9],"ix":2},"a":{"a":0,"k":[0,51.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":16,"s":[100,0],"e":[100,100]},{"t":30}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 3-1","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,14.665],[15.628,14.665],[15.628,-14.665],[-15.628,-14.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115.079,209.776],"ix":2},"a":{"a":0,"k":[0,14.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[100,0],"e":[100,100]},{"t":29}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 2-2","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,34.178],[15.628,34.178],[15.628,-34.178],[-15.628,-34.178]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[115,209.736],"ix":2},"a":{"a":0,"k":[0,34],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":9,"s":[100,0],"e":[100,100]},{"t":20}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 2-1","np":2,"cix":2,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,9.493],[15.628,9.493],[15.628,-9.493],[-15.628,-9.493]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[67.823,210.07],"ix":2},"a":{"a":0,"k":[0,9.5],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":8,"s":[100,0],"e":[100,100]},{"t":19}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 1-2","np":2,"cix":2,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-15.628,22.126],[15.628,22.126],[15.628,-22.126],[-15.628,-22.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.274509803922,0.474509803922,0.603921568627,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[67.899,209.788],"ix":2},"a":{"a":0,"k":[0,22],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":4,"s":[100,0],"e":[100,100]},{"t":13}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"bar 1-1","np":2,"cix":2,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"iPhone Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[155,305,0],"ix":2},"a":{"a":0,"k":[153,303,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,27],[135,27],[135,-27],[-135,-27]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.003921568627,0.376470588235,0.529411764706,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,84],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-135,217.5],[135,217.5],[135,-217.5],[-135,-217.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153,328.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-23.1],[0,0],[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0]],"o":[[-23.1,0],[0,0],[0,23.1],[0,0],[23.1,0],[0,0],[0,-23.1],[0,0]],"v":[[-111,-303],[-153,-261],[-153,261],[-111,303],[111,303],[153,261],[153,-261],[111,-303]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.137254901961,0.207843137255,0.294117647059,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[153.242,303.477],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419,"st":0,"bm":0}],"markers":[]} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7fc..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json deleted file mode 100644 index 92b10d87b4f7..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "darkgrey-shadow.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "darkgrey-shadow@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "darkgrey-shadow@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png deleted file mode 100644 index e3d1026dcccf..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@2x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@2x.png deleted file mode 100644 index f2711587adef..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@2x.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png deleted file mode 100644 index f324520d705e..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/darkgrey-shadow.imageset/darkgrey-shadow@3x.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json deleted file mode 100644 index 177c91924aa0..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "email.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf deleted file mode 100644 index 11c730458006..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/email.imageset/email.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/Contents.json deleted file mode 100644 index 3eb79b3a6b1f..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "google.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "google@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "google@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google.png deleted file mode 100644 index a13d4dc9ffb7..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png deleted file mode 100644 index 88a86b1f22e3..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@2x.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png deleted file mode 100644 index b4d46452567d..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/google.imageset/google@3x.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json deleted file mode 100644 index ff89e46ad289..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-password-field.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf deleted file mode 100644 index d4a27e75af09..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-password-field.imageset/icon-password-field.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json deleted file mode 100644 index f8148aff0f0e..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-post-search-highlight.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf deleted file mode 100644 index be4a1eab88cf..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-post-search-highlight.imageset/icon-post-search-highlight.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/Contents.json deleted file mode 100644 index 50860130d1c4..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-url-field.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template", - "preserves-vector-representation" : true - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/icon-url-field.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/icon-url-field.pdf deleted file mode 100644 index 50900158b1c8..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-url-field.imageset/icon-url-field.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/Contents.json deleted file mode 100644 index 567949098b5a..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "icon-username-field.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/icon-username-field.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/icon-username-field.pdf deleted file mode 100644 index 7c5de844c4fb..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/icon-username-field.imageset/icon-username-field.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json deleted file mode 100644 index 7ee24619adaf..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "key.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf deleted file mode 100644 index 08fcc72ce3c5..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/key-icon.imageset/key.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json deleted file mode 100644 index bc416ad2f8a0..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "login-magic-link.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf deleted file mode 100644 index 81c37e8ea531..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/login-magic-link.imageset/login-magic-link.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/Contents.json deleted file mode 100644 index 7a89bd061fb8..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "phone.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/phone.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/phone.pdf deleted file mode 100644 index 3fa0f016c461..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/phone-icon.imageset/phone.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/Contents.json deleted file mode 100644 index 67bed98c588f..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "example-domain.png", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/example-domain.png b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/example-domain.png deleted file mode 100644 index 96899f0591a1..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/site-address-illustration.imageset/example-domain.png and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json deleted file mode 100644 index 8dc235dba4ad..000000000000 --- a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "social-signup-waiting.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf b/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf deleted file mode 100644 index 22d1f69bf595..000000000000 Binary files a/Sources/WordPressAuthenticator/Resources/Assets.xcassets/social-signup-waiting.imageset/social-signup-waiting.pdf and /dev/null differ diff --git a/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist b/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist deleted file mode 100644 index e085bae01f0e..000000000000 --- a/Sources/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist +++ /dev/null @@ -1,18 +0,0 @@ - - - - - gmail - googlegmail:// - airmail - airmail:// - msOutlook - ms-outlook:// - spark - readdle-spark:// - yahooMail - ymail:// - fastmail - fastmail:// - - diff --git a/Sources/WordPressAuthenticator/Views/CircularImageView.swift b/Sources/WordPressAuthenticator/Views/CircularImageView.swift deleted file mode 100644 index 584c43f14567..000000000000 --- a/Sources/WordPressAuthenticator/Views/CircularImageView.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation - -/// UIImageView with a circular shape. -/// -class CircularImageView: UIImageView { - - override var frame: CGRect { - didSet { - refreshRadius() - } - } - - override func layoutSubviews() { - super.layoutSubviews() - refreshRadius() - } - - private func refreshRadius() { - layer.cornerRadius = frame.width * 0.5 - layer.masksToBounds = true - } -} diff --git a/Sources/WordPressAuthenticator/Views/LoginTextField.swift b/Sources/WordPressAuthenticator/Views/LoginTextField.swift deleted file mode 100644 index 0f05abfcf4ef..000000000000 --- a/Sources/WordPressAuthenticator/Views/LoginTextField.swift +++ /dev/null @@ -1,77 +0,0 @@ -import UIKit -import WordPressShared - -open class LoginTextField: WPWalkthroughTextField { - - /// Make a Swift-only property communicate a color to the - /// Objective-C only class, WPWalkthroughTextField. - /// - open override var secureTextEntryImageColor: UIColor! { - set { - // no-op. Usually set in Interface Builder. - } - get { - return WordPressAuthenticator.shared.style.secondaryNormalBorderColor - } - } - - open override func awakeFromNib() { - super.awakeFromNib() - backgroundColor = WordPressAuthenticator.shared.style.textFieldBackgroundColor - } - - override open func draw(_ rect: CGRect) { - if showTopLineSeparator { - guard let context = UIGraphicsGetCurrentContext() else { - return - } - - drawTopLine(rect: rect, context: context) - drawBottomLine(rect: rect, context: context) - } - } - - override open var placeholder: String? { - didSet { - guard let placeholder, - let font else { - return - } - - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: WordPressAuthenticator.shared.style.placeholderColor, - .font: font - ] - attributedPlaceholder = NSAttributedString(string: placeholder, attributes: attributes) - } - } - - override open var leftViewImage: UIImage! { - set { - let newImage = newValue.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) - super.leftViewImage = newImage - } - get { - return super.leftViewImage - } - } - - private func drawTopLine(rect: CGRect, context: CGContext) { - drawBorderLine(from: CGPoint(x: rect.minX, y: rect.minY), to: CGPoint(x: rect.maxX, y: rect.minY), context: context) - } - - private func drawBottomLine(rect: CGRect, context: CGContext) { - drawBorderLine(from: CGPoint(x: rect.minX, y: rect.maxY), to: CGPoint(x: rect.maxX, y: rect.maxY), context: context) - } - - private func drawBorderLine(from startPoint: CGPoint, to endPoint: CGPoint, context: CGContext) { - let path = UIBezierPath() - - path.move(to: startPoint) - path.addLine(to: endPoint) - path.lineWidth = UIScreen.main.scale / 2.0 - context.addPath(path.cgPath) - context.setStrokeColor(WordPressAuthenticator.shared.style.secondaryNormalBorderColor.cgColor) - context.strokePath() - } -} diff --git a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift b/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift deleted file mode 100644 index ba765d95ebdc..000000000000 --- a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.swift +++ /dev/null @@ -1,160 +0,0 @@ -import UIKit -import WordPressShared - -// MARK: - SearchTableViewCellDelegate -// -public protocol SearchTableViewCellDelegate: AnyObject { - func startSearch(for: String) -} - -// MARK: - SearchTableViewCell -// -open class SearchTableViewCell: UITableViewCell { - - /// UITableView's Reuse Identifier - /// - public static let reuseIdentifier = "SearchTableViewCell" - - /// Search 'UITextField's reference! - /// - @IBOutlet public var textField: LoginTextField! - - /// UITextField's listener - /// - open weak var delegate: SearchTableViewCellDelegate? - - /// If `true` the search delegate callback is called as the text field is edited. - /// This class does not implement any Debouncer or assume a minimum character count because - /// each search is different. - /// - open var liveSearch: Bool = false - - /// If `true` then the user can type in spaces regularly. If `false` the whitespaces will be - /// stripped before they're entered into the field. - /// - open var allowSpaces: Bool = true - - /// Search UITextField's placeholder - /// - open var placeholder: String? { - get { - return textField.placeholder - } - set { - textField.placeholder = newValue - } - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override open func awakeFromNib() { - super.awakeFromNib() - textField.delegate = self - textField.returnKeyType = .search - textField.contentInsets = Constants.textInsetsWithIcon - textField.accessibilityIdentifier = "Search field" - textField.leftViewImage = textField?.leftViewImage?.imageWithTintColor(WordPressAuthenticator.shared.style.placeholderColor) - - contentView.backgroundColor = WordPressAuthenticator.shared.style.viewControllerBackgroundColor - } -} - -// MARK: - Settings -// -private extension SearchTableViewCell { - enum Constants { - static let textInsetsWithIcon = WPStyleGuide.edgeInsetForLoginTextFields() - } -} - -// MARK: - UITextFieldDelegate -// -extension SearchTableViewCell: UITextFieldDelegate { - open func textFieldShouldClear(_ textField: UITextField) -> Bool { - if !liveSearch { - delegate?.startSearch(for: "") - } - - return true - } - - open func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if !liveSearch, - let searchText = textField.text { - delegate?.startSearch(for: searchText) - } - - return false - } - - open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - let sanitizedString: String - - if allowSpaces { - sanitizedString = string - } else { - sanitizedString = string.trimmingCharacters(in: .whitespacesAndNewlines) - } - - let hasValidEdits = !sanitizedString.isEmpty || range.length > 0 - - if hasValidEdits { - guard let start = textField.position(from: textField.beginningOfDocument, offset: range.location), - let end = textField.position(from: start, offset: range.length), - let textRange = textField.textRange(from: start, to: end) else { - - // This shouldn't really happen but if it does, let's at least let the edit go through - return true - } - - textField.replace(textRange, withText: sanitizedString) - - if liveSearch { - startLiveSearch() - } - } - - return false - } - - /// Convenience method to abstract the logic that tells the delegate to start a live search. - /// - /// - Precondition: make sure you check if `liveSearch` is enabled before calling this method. - /// - private func startLiveSearch() { - guard let delegate, - let text = textField.text else { - return - } - - if text.isEmpty { - delegate.startSearch(for: "") - } else { - delegate.startSearch(for: text) - } - } -} - -// MARK: - Loader -// -public extension SearchTableViewCell { - func showLoader() { - guard let leftView = textField.leftView else { return } - let spinner = UIActivityIndicatorView(frame: leftView.frame) - addSubview(spinner) - spinner.startAnimating() - - textField.leftView?.alpha = 0 - } - - func hideLoader() { - for subview in subviews where subview is UIActivityIndicatorView { - subview.removeFromSuperview() - break - } - - textField.leftView?.alpha = 1 - } -} diff --git a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib b/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib deleted file mode 100644 index a52ee3c47f84..000000000000 --- a/Sources/WordPressAuthenticator/Views/SearchTableViewCell.xib +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift b/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift deleted file mode 100644 index ee704ec72d61..000000000000 --- a/Sources/WordPressAuthenticator/Views/SiteInfoHeaderView.swift +++ /dev/null @@ -1,116 +0,0 @@ -import UIKit -import WordPressShared - -// MARK: - SiteInfoHeaderView -// -class SiteInfoHeaderView: UIView { - - // MARK: - Outlets - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var subtitleLabel: UILabel! - @IBOutlet private var blavatarImageView: UIImageView! - - // MARK: - Properties - - /// Site Title - /// - var title: String? { - get { - return titleLabel.text - } - set { - titleLabel.text = newValue - } - } - - /// Site Subtitle - /// - var subtitle: String? { - get { - return subtitleLabel.text - } - set { - subtitleLabel.text = newValue - } - } - - /// When enabled, the Subtitle won't be rendered. - /// - var subtitleIsHidden: Bool = true { - didSet { - refreshLabelStyles() - } - } - - /// When enabled, renders a border around the Blavatar. - /// - var blavatarBorderIsHidden: Bool = false { - didSet { - refreshBlavatarStyle() - } - } - - /// Returns (or sets) the Site's Blavatar Image. - /// - var blavatarImage: UIImage? { - get { - return blavatarImageView.image - } - set { - blavatarImageView.image = newValue - } - } - - /// Downloads the Blavatar Image at the specified URL. - /// - func downloadBlavatar(at path: String) { - blavatarImageView.image = .siteIconPlaceholderImage - - if let url = URL(string: path) { - blavatarImageView.downloadImage(from: url) - } - } - - // MARK: - Overridden Methods - - override func awakeFromNib() { - super.awakeFromNib() - refreshLabelStyles() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - guard previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory else { - return - } - - refreshLabelStyles() - } -} - -// MARK: - Private -// -private extension SiteInfoHeaderView { - - func refreshLabelStyles() { - let titleWeight: UIFont.Weight = subtitleIsHidden ? .regular : .semibold - titleLabel.font = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: titleWeight) - titleLabel.textColor = WPStyleGuide.darkGrey() - - subtitleLabel.isHidden = subtitleIsHidden - subtitleLabel.font = WPStyleGuide.fontForTextStyle(.footnote) - subtitleLabel.textColor = WPStyleGuide.darkGrey() - } - - func refreshBlavatarStyle() { - if blavatarBorderIsHidden { - blavatarImageView.layer.borderWidth = 0 - blavatarImageView.tintColor = WordPressAuthenticator.shared.style.placeholderColor - } else { - blavatarImageView.layer.borderColor = WordPressAuthenticator.shared.style.instructionColor.cgColor - blavatarImageView.layer.borderWidth = 1 - blavatarImageView.tintColor = WordPressAuthenticator.shared.style.placeholderColor - } - } -} diff --git a/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift b/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift deleted file mode 100644 index 6569ed913ccd..000000000000 --- a/Sources/WordPressAuthenticator/Views/WebAuthenticationPresentationContext.swift +++ /dev/null @@ -1,15 +0,0 @@ -import AuthenticationServices -import Foundation - -class WebAuthenticationPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding { - let viewController: UIViewController - - init(viewController: UIViewController) { - self.viewController = viewController - super.init() - } - - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - return viewController.view.window! - } -} diff --git a/Sources/WordPressAuthenticator/WordPressAuthenticator.h b/Sources/WordPressAuthenticator/WordPressAuthenticator.h deleted file mode 100644 index 1fc583463b77..000000000000 --- a/Sources/WordPressAuthenticator/WordPressAuthenticator.h +++ /dev/null @@ -1,15 +0,0 @@ -#import - -//! Project version number for WordPressAuthenticator. -FOUNDATION_EXPORT double WordPressAuthenticatorVersionNumber; - -//! Project version string for WordPressAuthenticator. -FOUNDATION_EXPORT const unsigned char WordPressAuthenticatorVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - -#import -#import -#import -#import diff --git a/Tests/KeystoneTests/WordPressUnitTests.xctestplan b/Tests/KeystoneTests/WordPressUnitTests.xctestplan index 301b4c9017a8..7e1fda82d20c 100644 --- a/Tests/KeystoneTests/WordPressUnitTests.xctestplan +++ b/Tests/KeystoneTests/WordPressUnitTests.xctestplan @@ -31,13 +31,6 @@ "testRepetitionMode" : "retryOnFailure" }, "testTargets" : [ - { - "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4AD953BA2C21451700D0EEFA", - "name" : "WordPressAuthenticatorTests" - } - }, { "target" : { "containerPath" : "container:..\/Modules", diff --git a/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift b/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift deleted file mode 100644 index 6dc5b8eee022..000000000000 --- a/Tests/WordPressAuthenticatorTests/Analytics/AnalyticsTrackerTests.swift +++ /dev/null @@ -1,296 +0,0 @@ -import XCTest -import WordPressShared -@testable import WordPressAuthenticator - -class AnalyticsTrackerTests: XCTestCase { - - // MARK: - Expectations: Building the properties dictionary - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step) -> [String: String] { - - return [ - AuthenticatorAnalyticsTracker.Property.source.rawValue: source.rawValue, - AuthenticatorAnalyticsTracker.Property.flow.rawValue: flow.rawValue, - AuthenticatorAnalyticsTracker.Property.step.rawValue: step.rawValue - ] - } - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step, failure: String) -> [String: String] { - - var properties = expectedProperties(source: source, flow: flow, step: step) - properties[AuthenticatorAnalyticsTracker.Property.failure.rawValue] = failure - - return properties - } - - private func expectedProperties(source: AuthenticatorAnalyticsTracker.Source, flow: AuthenticatorAnalyticsTracker.Flow, step: AuthenticatorAnalyticsTracker.Step, click: AuthenticatorAnalyticsTracker.ClickTarget) -> [String: String] { - - var properties = expectedProperties(source: source, flow: flow, step: step) - properties[AuthenticatorAnalyticsTracker.Property.click.rawValue] = click.rawValue - - return properties - } - - /// Test that when tracking an event through the AnalyticsTracker, the backing analytics tracker - /// receives a matching event. - /// - func testBackingTracker() { - let source = AuthenticatorAnalyticsTracker.Source.reauthentication - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.step.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - - waitForExpectations(timeout: 0.1) - } - - /// Test that tracking a failure maintains the source, flow and step from the previously recorded step. - /// - /// Ref: pbArwn-I6-p2 - /// - func testFailure() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - let failure = "some error" - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.failure.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step, failure: failure) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - // We'll ignore the first event and only check the properties from the failure. - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - tracker.track(failure: failure) - - waitForExpectations(timeout: 0.1) - } - - /// Test that tracking a click maintains the source, flow and step from the previously recorded step. - /// - /// Ref: pbArwn-I6-p2 - /// - func testClick() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flow = AuthenticatorAnalyticsTracker.Flow.loginWithGoogle - let step = AuthenticatorAnalyticsTracker.Step.start - let click = AuthenticatorAnalyticsTracker.ClickTarget.dismiss - - let expectedEventName = AuthenticatorAnalyticsTracker.EventType.interaction.rawValue - let expectedEventProperties = self.expectedProperties(source: source, flow: flow, step: step, click: click) - let trackingIsOk = expectation(description: "The parameters of the tracking call are as expected") - - let track = { (event: AnalyticsEvent) in - // We'll ignore the first event and only check the properties from the failure. - if event.name == expectedEventName - && event.properties == expectedEventProperties { - - trackingIsOk.fulfill() - } - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - tracker.set(flow: flow) - tracker.track(step: step) - tracker.track(click: click) - - waitForExpectations(timeout: 0.1) - } - - // MARK: - Legacy Tracking Support Tests - - /// Tests legacy tracking for a step - /// - func testStepLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let step = AuthenticatorAnalyticsTracker.Step.start - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(step: step, ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a step - /// - func testStepNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let step = AuthenticatorAnalyticsTracker.Step.start - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(step: step, ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests legacy tracking for a click interaction - /// - func testClickLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let click = AuthenticatorAnalyticsTracker.ClickTarget.connectSite - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(click: click, ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a click interaction - /// - func testClickNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - let click = AuthenticatorAnalyticsTracker.ClickTarget.connectSite - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(click: click, ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests legacy tracking for a failure - /// - func testFailureLegacyTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - XCTFail() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: false, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(failure: "error", ifTrackingNotEnabled: { - legacyTrackingExecuted.fulfill() - }) - } - - waitForExpectations(timeout: 0.1) - } - - /// Tests the new tracking for a failure - /// - func testFailureNewTracking() { - let source = AuthenticatorAnalyticsTracker.Source.default - let flows: [AuthenticatorAnalyticsTracker.Flow] = [.loginWithApple, .signupWithApple, .loginWithGoogle, .signupWithGoogle, .loginWithSiteAddress] - - let legacyTrackingExecuted = expectation(description: "The legacy tracking block was executed.") - legacyTrackingExecuted.expectedFulfillmentCount = flows.count - - let track = { (_: AnalyticsEvent) in - legacyTrackingExecuted.fulfill() - } - - let tracker = AuthenticatorAnalyticsTracker(enabled: true, track: track) - - tracker.set(source: source) - - for flow in flows { - tracker.set(flow: flow) - tracker.track(failure: "error", ifTrackingNotEnabled: { - XCTFail() - }) - } - - waitForExpectations(timeout: 0.1) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift deleted file mode 100644 index 7bab55898707..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticator+TestsUtils.swift +++ /dev/null @@ -1,50 +0,0 @@ -@testable import WordPressAuthenticator - -extension WordPressAuthenticator { - - static func initializeForTesting() { - WordPressAuthenticator.initialize( - configuration: WordPressAuthenticatorConfiguration( - wpcomClientId: "a", - wpcomSecret: "b", - wpcomScheme: "c", - wpcomTermsOfServiceURL: URL(string: "https://w.org")!, - googleLoginClientId: "e", - googleLoginServerClientId: "f", - googleLoginScheme: "g", - userAgent: "h" - ), - style: WordPressAuthenticatorStyle( - primaryNormalBackgroundColor: .red, - primaryNormalBorderColor: .none, - primaryHighlightBackgroundColor: .orange, - primaryHighlightBorderColor: .none, - secondaryNormalBackgroundColor: .yellow, - secondaryNormalBorderColor: .green, - secondaryHighlightBackgroundColor: .blue, - secondaryHighlightBorderColor: .systemIndigo, - disabledBackgroundColor: .purple, - disabledBorderColor: .red, - primaryTitleColor: .orange, - secondaryTitleColor: .yellow, - disabledTitleColor: .green, - disabledButtonActivityIndicatorColor: .blue, - textButtonColor: .systemIndigo, - textButtonHighlightColor: .purple, - instructionColor: .red, - subheadlineColor: .orange, - placeholderColor: .yellow, - viewControllerBackgroundColor: .green, - textFieldBackgroundColor: .blue, - navBarImage: UIImage(), - navBarBadgeColor: .systemIndigo, - navBarBackgroundColor: .purple - ), - unifiedStyle: .none, - displayImages: WordPressAuthenticatorDisplayImages( - magicLink: UIImage() - ), - displayStrings: WordPressAuthenticatorDisplayStrings() - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift deleted file mode 100644 index 5773a045a4d8..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorDisplayTextTests.swift +++ /dev/null @@ -1,24 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - WordPressAuthenticator Display Text Unit Tests -// -class WordPressAuthenticatorDisplayTextTests: XCTestCase { - /// Default display text instance - /// - let displayTextDefaults = WordPressAuthenticatorDisplayStrings.defaultStrings - - /// Verifies that values in defaultText are not nil - /// - func testThatDefaultTextValuesAreNotNil() { - XCTAssertNotNil(displayTextDefaults.emailLoginInstructions) - XCTAssertNotNil(displayTextDefaults.siteLoginInstructions) - } - - /// Verifies that values in defaultText are not empty strings - /// - func testThatDefaultTextValuesAreNotEmpty() { - XCTAssertFalse(displayTextDefaults.emailLoginInstructions.isEmpty) - XCTAssertFalse(displayTextDefaults.siteLoginInstructions.isEmpty) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift deleted file mode 100644 index 7cd454a619ff..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressAuthenticatorTests.swift +++ /dev/null @@ -1,176 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - WordPressAuthenticator Unit Tests -// -class WordPressAuthenticatorTests: XCTestCase { - let timeout = TimeInterval(3) - - override class func setUp() { - super.setUp() - - WordPressAuthenticator.initialize( - configuration: WordpressAuthenticatorProvider.wordPressAuthenticatorConfiguration(), - style: WordpressAuthenticatorProvider.wordPressAuthenticatorStyle(.random), - unifiedStyle: WordpressAuthenticatorProvider.wordPressAuthenticatorUnifiedStyle(.random) - ) - } - - func testBaseSiteURL() { - var baseURL = "testsite.wordpress.com" - var url = WordPressAuthenticator.baseSiteURL(string: "http://\(baseURL)") - XCTAssert(url == "https://\(baseURL)", "Should force https for a wpcom site having http.") - - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert(url == "https://\(baseURL)", "Should force https for a wpcom site without a scheme.") - - baseURL = "www.selfhostedsite.com" - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert((url == "https://\(baseURL)"), "Should add https:\\ for a non wpcom site missing a scheme.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-login.php") - XCTAssert((url == "https://\(baseURL)"), "Should remove wp-login.php from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-admin") - XCTAssert((url == "https://\(baseURL)"), "Should remove /wp-admin from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/wp-admin/") - XCTAssert((url == "https://\(baseURL)"), "Should remove /wp-admin/ from the path.") - - url = WordPressAuthenticator.baseSiteURL(string: "\(baseURL)/") - XCTAssert((url == "https://\(baseURL)"), "Should remove a trailing slash from the url.") - - // Check non-latin characters and puny code - baseURL = "http://例.例" - let punycode = "http://xn--fsq.xn--fsq" - url = WordPressAuthenticator.baseSiteURL(string: baseURL) - XCTAssert(url == punycode) - url = WordPressAuthenticator.baseSiteURL(string: punycode) - XCTAssert(url == punycode) - } - - func testBaseSiteURLKeepsHTTPSchemeForNonWPSites() { - let url = "http://selfhostedsite.com" - let correctedURL = WordPressAuthenticator.baseSiteURL(string: url) - XCTAssertEqual(correctedURL, url) - } - - // MARK: View Tests - func testWordpressAuthIsAuthenticationViewController() { - let loginViewcontroller = LoginViewController() - let nuxViewController = NUXViewController() - let nuxTableViewController = NUXTableViewController() - let basicViewController = UIViewController() - - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(loginViewcontroller)) - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(nuxViewController)) - XCTAssertTrue(WordPressAuthenticator.isAuthenticationViewController(nuxTableViewController)) - XCTAssertFalse(WordPressAuthenticator.isAuthenticationViewController(basicViewController)) - } - - func testShowLoginFromPresenterReturnsLoginInitialVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginFromPresenter(presenterSpy, animated: true) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testShowLoginForJustWPComPresentsCorrectVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForJustWPCom(from: presenterSpy) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testSignInForWPOrgReturnsVC() { - let vc = WordPressAuthenticator.signinForWPOrg() - - XCTAssertTrue(vc is LoginSiteAddressViewController) - } - - func testShowLoginForJustWPComSetsMetaProperties() throws { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForJustWPCom(from: presenterSpy, - jetpackLogin: false, - connectedEmail: "email-address@example.com") - - let navController = try XCTUnwrap(presenterSpy.presentedVC as? LoginNavigationController) - let controller = try XCTUnwrap(navController.viewControllers.first as? LoginEmailViewController) - - wait(for: [expectation], timeout: timeout) - - XCTAssertEqual(controller.loginFields.restrictToWPCom, true) - XCTAssertEqual(controller.loginFields.username, "email-address@example.com") - } - - func testShowLoginForSelfHostedSitePresentsCorrectVC() { - let presenterSpy = ModalViewControllerPresentingSpy() - let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(block: { _, _ -> Bool in - return presenterSpy.presentedVC != nil - }), object: .none) - - WordPressAuthenticator.showLoginForSelfHostedSite(presenterSpy) - wait(for: [expectation], timeout: timeout) - - XCTAssertTrue(presenterSpy.presentedVC is LoginNavigationController) - } - - func testSignInForWPComWithLoginFieldsReturnsVC() throws { - let navController = try XCTUnwrap(WordPressAuthenticator.signinForWPCom(dotcomEmailAddress: "example@email.com", dotcomUsername: "username") as? UINavigationController) - let vc = navController.topViewController - - XCTAssertTrue(vc is LoginWPComViewController) - } - - func testSignInForWPComSetsEmptyLoginFields() throws { - let navController = try XCTUnwrap(WordPressAuthenticator.signinForWPCom(dotcomEmailAddress: nil, dotcomUsername: nil) as? UINavigationController) - let vc = try XCTUnwrap(navController.topViewController as? LoginWPComViewController) - - XCTAssertEqual(vc.loginFields.emailAddress, "") - XCTAssertEqual(vc.loginFields.username, "") - } - - // MARK: WordPressAuthenticator URL verification Tests - func testIsGoogleAuthURL() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let googleURL = URL(string: "com.googleuserconsent.apps/82ekn2932nub23h23hn3")! - let magicLinkURL = URL(string: "https://magic-login")! - let wordpressComURL = URL(string: "https://WordPress.com")! - - XCTAssertTrue(authenticator.isGoogleAuthUrl(googleURL)) - XCTAssertFalse(authenticator.isGoogleAuthUrl(magicLinkURL)) - XCTAssertFalse(authenticator.isGoogleAuthUrl(wordpressComURL)) - } - - func testIsWordPressAuthURL() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let magicLinkURL = URL(string: "https://magic-login")! - let googleURL = URL(string: "https://google.com")! - let wordpressComURL = URL(string: "https://WordPress.com")! - - XCTAssertTrue(authenticator.isWordPressAuthUrl(magicLinkURL)) - XCTAssertFalse(authenticator.isWordPressAuthUrl(googleURL)) - XCTAssertFalse(authenticator.isWordPressAuthUrl(wordpressComURL)) - } - - func testHandleWordPressAuthURLReturnsTrueOnSuccess() { - let authenticator = WordpressAuthenticatorProvider.getWordpressAuthenticator() - let url = URL(string: "https://wordpress.com/wp-login.php?token=1234567890%26action&magic-login&sr=1&signature=1234567890oienhdtsra&flow=signup") - - XCTAssertTrue(authenticator.handleWordPressAuthUrl(url!, rootViewController: UIViewController(), automatedTesting: true)) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift b/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift deleted file mode 100644 index d2c7d3a38834..000000000000 --- a/Tests/WordPressAuthenticatorTests/Authenticator/WordPressSourceTagTests.swift +++ /dev/null @@ -1,131 +0,0 @@ -import XCTest -import WordPressAuthenticator - -class WordPressSourceTagTests: XCTestCase { - - func testGeneralLoginSourceTag() { - let tag = WordPressSupportSourceTag.generalLogin - - XCTAssertEqual(tag.name, "generalLogin") - XCTAssertEqual(tag.origin, "origin:login-screen") - } - - func testJetpackLoginSourceTag() { - let tag = WordPressSupportSourceTag.jetpackLogin - - XCTAssertEqual(tag.name, "jetpackLogin") - XCTAssertEqual(tag.origin, "origin:jetpack-login-screen") - } - - func testLoginEmailSourceTag() { - let tag = WordPressSupportSourceTag.loginEmail - - XCTAssertEqual(tag.name, "loginEmail") - XCTAssertEqual(tag.origin, "origin:login-email") - } - - func testLoginAppleSourceTag() { - let tag = WordPressSupportSourceTag.loginApple - - XCTAssertEqual(tag.name, "loginApple") - XCTAssertEqual(tag.origin, "origin:login-apple") - } - - func testlogin2FASourceTag() { - let tag = WordPressSupportSourceTag.login2FA - - XCTAssertEqual(tag.name, "login2FA") - XCTAssertEqual(tag.origin, "origin:login-2fa") - } - - func testLoginMagicLinkSourceTag() { - let tag = WordPressSupportSourceTag.loginMagicLink - - XCTAssertEqual(tag.name, "loginMagicLink") - XCTAssertEqual(tag.origin, "origin:login-magic-link") - } - - func testSiteAddressSourceTag() { - let tag = WordPressSupportSourceTag.loginSiteAddress - - XCTAssertEqual(tag.name, "loginSiteAddress") - XCTAssertEqual(tag.origin, "origin:login-site-address") - } - - func testVerifyEmailInstructionsSourceTag() { - let tag = WordPressSupportSourceTag.verifyEmailInstructions - - XCTAssertEqual(tag.name, "verifyEmailInstructions") - XCTAssertEqual(tag.origin, "origin:login-site-address") - } - - func testLoginUsernameSourceTag() { - let tag = WordPressSupportSourceTag.loginUsernamePassword - - XCTAssertEqual(tag.name, "loginUsernamePassword") - XCTAssertEqual(tag.origin, "origin:login-username-password") - } - - func testLoginUsernamePasswordSourceTag() { - let tag = WordPressSupportSourceTag.loginWPComUsernamePassword - - XCTAssertEqual(tag.name, "loginWPComUsernamePassword") - XCTAssertEqual(tag.origin, "origin:wpcom-login-username-password") - } - - func testLoginWPComPasswordSourceTag() { - let tag = WordPressSupportSourceTag.loginWPComPassword - - XCTAssertEqual(tag.name, "loginWPComPassword") - XCTAssertEqual(tag.origin, "origin:login-wpcom-password") - } - - func testWPComSignupEmailSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupEmail - - XCTAssertEqual(tag.name, "wpComSignupEmail") - XCTAssertEqual(tag.origin, "origin:wpcom-signup-email-entry") - } - - func testWPComSignupSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignup - - XCTAssertEqual(tag.name, "wpComSignup") - XCTAssertEqual(tag.origin, "origin:signup-screen") - } - - func testWPComSignupWaitingForGoogleSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupWaitingForGoogle - - XCTAssertEqual(tag.name, "wpComSignupWaitingForGoogle") - XCTAssertEqual(tag.origin, "origin:signup-waiting-for-google") - } - - func testWPComAuthGoogleSignupWaitingForGoogleSourceTag() { - let tag = WordPressSupportSourceTag.wpComAuthWaitingForGoogle - - XCTAssertEqual(tag.name, "wpComAuthWaitingForGoogle") - XCTAssertEqual(tag.origin, "origin:auth-waiting-for-google") - } - - func testWPComAuthGoogleSignupConfirmationSourceTag() { - let tag = WordPressSupportSourceTag.wpComAuthGoogleSignupConfirmation - - XCTAssertEqual(tag.name, "wpComAuthGoogleSignupConfirmation") - XCTAssertEqual(tag.origin, "origin:auth-google-signup-confirmation") - } - - func testWPComSignupMagicLinkSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupMagicLink - - XCTAssertEqual(tag.name, "wpComSignupMagicLink") - XCTAssertEqual(tag.origin, "origin:signup-magic-link") - } - - func testWPComSignupAppleSourceTag() { - let tag = WordPressSupportSourceTag.wpComSignupApple - - XCTAssertEqual(tag.name, "wpComSignupApple") - XCTAssertEqual(tag.origin, "origin:signup-apple") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift b/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift deleted file mode 100644 index 8ed2e00f6073..000000000000 --- a/Tests/WordPressAuthenticatorTests/Credentials/CredentialsTests.swift +++ /dev/null @@ -1,127 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -class CredentialsTests: XCTestCase { - - let token = "arstdhneio123456789qwfpgjluy" - let siteURL = "https://example.com" - let username = "user123" - let password = "arstdhneio" - let xmlrpc = "https://example.com/xmlrpc.php" - - func testWordpressComCredentialsInit() { - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - - XCTAssertEqual(wpcomCredentials.authToken, token) - XCTAssertEqual(wpcomCredentials.isJetpackLogin, false) - XCTAssertEqual(wpcomCredentials.multifactor, false) - XCTAssertEqual(wpcomCredentials.siteURL, siteURL) - } - - func testWordPressComCredentialsSiteURLReturnsDefaultValue() { - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: "") - - let expected = "https://wordpress.com" - - XCTAssertEqual(wpcomCredentials.siteURL, expected) - } - - func testWordPressComCredentialsEquatableReturnsCorrectValue() { - let credential = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let match = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let differentJetpack = WordPressComCredentials(authToken: token, - isJetpackLogin: true, - multifactor: false, - siteURL: siteURL) - let differentMultifactor = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: true, - siteURL: siteURL) - let differentSiteURL = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: "") - let differentAuthToken = WordPressComCredentials(authToken: "ARSTDBVCXZ(*&^%$", - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - - XCTAssertEqual(credential, match) - XCTAssertEqual(credential, differentJetpack) - XCTAssertEqual(credential, differentMultifactor) - XCTAssertNotEqual(credential, differentSiteURL) - XCTAssertNotEqual(credential, differentAuthToken) - } - - func testWordpressOrgCredentialsInit() { - let wporgcredentials = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertEqual(wporgcredentials.username, username) - XCTAssertEqual(wporgcredentials.password, password) - XCTAssertEqual(wporgcredentials.xmlrpc, xmlrpc) - } - - func testWordPressOrgCredentialsEquatable() { - let lhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - let rhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertTrue(lhs == rhs) - } - - func testWordPressOrgCredentialsNotEquatable() { - let lhs = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - - let rhs = WordPressOrgCredentials(username: "username5678", - password: password, - xmlrpc: xmlrpc, - options: [:]) - - XCTAssertFalse(lhs == rhs) - } - - func testAuthenticatorCredentialsInit() { - let wporgCredentials = WordPressOrgCredentials(username: username, - password: password, - xmlrpc: xmlrpc, - options: [:]) - let wpcomCredentials = WordPressComCredentials(authToken: token, - isJetpackLogin: false, - multifactor: false, - siteURL: siteURL) - let authenticatorCredentials = AuthenticatorCredentials(wpcom: wpcomCredentials, - wporg: wporgCredentials) - - XCTAssertEqual(authenticatorCredentials.wpcom?.authToken, token) - XCTAssertEqual(authenticatorCredentials.wpcom?.isJetpackLogin, false) - XCTAssertEqual(authenticatorCredentials.wpcom?.multifactor, false) - XCTAssertEqual(authenticatorCredentials.wpcom?.siteURL, siteURL) - XCTAssertEqual(authenticatorCredentials.wporg?.username, username) - XCTAssertEqual(authenticatorCredentials.wporg?.password, password) - XCTAssertEqual(authenticatorCredentials.wporg?.xmlrpc, xmlrpc) - } -} diff --git a/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift b/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift deleted file mode 100644 index 350ae072e552..000000000000 --- a/Tests/WordPressAuthenticatorTests/EmailClientPicker/AppSelectorTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -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() - } - - func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: (@MainActor @Sendable (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, sourceView: UIView(), urlHandler: urlHandler) - // Then - XCTAssertNotNil(appSelector) - XCTAssertNotNil(appSelector?.alertController) - XCTAssertEqual(appSelector!.alertController.actions.count, 3) - waitForExpectations(timeout: 4) { error in - if let error { - XCTFail("waitForExpectationsWithTimeout errored: \(error)") - } - } - } - - func testSelectorInitializationFailsWithNoApps() { - // Given - let urlHandler = MockUrlHandler() - // When - let appSelector = AppSelector(with: [:], sourceView: UIView(), 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, sourceView: UIView(), urlHandler: urlHandler) - // Then - XCTAssertNil(appSelector) - waitForExpectations(timeout: 4) { error in - if let error { - XCTFail("waitForExpectationsWithTimeout errored: \(error)") - } - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift deleted file mode 100644 index 99d85c71d98e..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Character+URLSafeTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -@testable import WordPressAuthenticator -import Foundation -import XCTest - -class Character_URLSafeTests: XCTestCase { - - func testURLSafeCharacters() throws { - let urlSafe = CharacterSet(Character.urlSafeCharacters.map { "\($0)" }.joined().unicodeScalars) - - // Ensure `Character.urlSafeCharacters` is a subset of `CharacterSet.urlQueryAllowed` - XCTAssertTrue(urlSafe.isStrictSubset(of: CharacterSet.urlQueryAllowed)) - - // Notice that `CharacterSet.urlQueryAllowed` is not a subset of - // `Character.urlSafeCharacters`, though, because URL queries allow characters such as &. - XCTAssertFalse(CharacterSet.urlQueryAllowed.isStrictSubset(of: urlSafe)) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift deleted file mode 100644 index 9d8a42a831cf..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/CodeVerifier+Fixture.swift +++ /dev/null @@ -1,12 +0,0 @@ -@testable import WordPressAuthenticator - -extension ProofKeyForCodeExchange.CodeVerifier { - - /// A code verifier for testing purposes that is guaranteed to be valid and deterministic. - /// - /// The reason we care about it being deterministic is because we don't want implicit randomness test. - /// The only place were we want to use random values in the `CodeVerifier` tests which explicitly check the random generation. - static func fixture() -> Self { - .init(value: (0.. String { - (0.. - - init(data: Data) { - self.init(result: .success(data)) - } - - init(error: Error) { - self.init(result: .failure(error)) - } - - init(result: Result) { - self.result = result - } - - func data(for request: URLRequest) async throws -> Data { - switch result { - case .success(let data): return data - case .failure(let error): throw error - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift deleted file mode 100644 index f4f73bc44ff2..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleClientIdTests.swift +++ /dev/null @@ -1,23 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class GoogleClientIdTests: XCTestCase { - - func testFailsInitIfNotAValidFormat() { - XCTAssertNil(GoogleClientId(string: "invalid")) - } - - func testDoesNotFailInitIfValidFormat() { - XCTAssertNotNil(GoogleClientId(string: "com.something.something")) - XCTAssertNotNil(GoogleClientId(string: "a.b.c")) - } - - func testRedirectURIGeneration() { - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.redirectURI(path: .none), "c.b.a") - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.redirectURI(path: "a_path"), "c.b.a:/a_path") - } - - func testDefaultRedirectURI() { - XCTAssertEqual(GoogleClientId(string: "a.b.c")?.defaultRedirectURI, "c.b.a:/oauth2callback") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift deleted file mode 100644 index b17e2653e2af..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGetterTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class GoogleOAuthTokenGetterTests: XCTestCase { - - func testThrowsWhenReceivingAnError() async throws { - let dataGettingStub = DataGettingStub(error: TestError(id: 1)) - - let getter = GoogleOAuthTokenGetter(dataGetter: dataGettingStub) - - do { - _ = try await getter.getToken( - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - authCode: "abc", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected error to be thrown") - } catch { - let error = try XCTUnwrap(error as? TestError) - XCTAssertEqual(error.id, 1) - } - } - - func testReturnsTokenWhenReceivingOne() async throws { - let expectedResponse = OAuthTokenResponseBody( - accessToken: "a", - expiresIn: 1, - rawIDToken: .none, - refreshToken: .none, - scope: "s", - tokenType: "t" - ) - let dataGettingStub = DataGettingStub(data: try JSONEncoder().encode(expectedResponse)) - let getter = GoogleOAuthTokenGetter(dataGetter: dataGettingStub) - - let response = try await getter.getToken( - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - authCode: "abc", - pkce: ProofKeyForCodeExchange() - ) - - XCTAssertEqual(response, expectedResponse) - } -} - -struct TestError: Equatable, Error { - let id: Int -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift deleted file mode 100644 index 9e92ea826584..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/GoogleOAuthTokenGettingStub.swift +++ /dev/null @@ -1,30 +0,0 @@ -@testable import WordPressAuthenticator - -struct GoogleOAuthTokenGettingStub: GoogleOAuthTokenGetting { - - let result: Result - - init(response: OAuthTokenResponseBody) { - self.init(result: .success(response)) - } - - init(error: Error) { - self.init(result: .failure(error)) - } - - init(result: Result) { - self.result = result - } - - func getToken( - clientId: GoogleClientId, - audience: String, - authCode: String, - pkce: ProofKeyForCodeExchange - ) async throws -> OAuthTokenResponseBody { - switch result { - case .success(let response): return response - case .failure(let error): throw error - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift deleted file mode 100644 index f52e881fd9b6..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/IDTokenTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class IDTokenTests: XCTestCase { - - func testInitWithJWTWithoutNameNorEmailFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTString)))) - } - - func testInitWithJWTWithoutEmailFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithNameOnly)))) - } - - func testInitWithJWTWithoutNameFails() throws { - XCTAssertNil(IDToken(jwt: try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithEmailOnly)))) - } - - func testInitWithJWTWithNameAndEmailSucceeds() throws { - let jwt = try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTStringWithNameAndEmail)) - let token = try XCTUnwrap(IDToken(jwt: jwt)) - - XCTAssertEqual(token.name, JSONWebToken.nameFromValidJWTStringWithEmail) - XCTAssertEqual(token.email, JSONWebToken.emailFromValidJWTStringWithEmail) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift deleted file mode 100644 index 6f5a01b6642a..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JSONWebToken+Fixtures.swift +++ /dev/null @@ -1,58 +0,0 @@ -@testable import WordPressAuthenticator - -extension JSONWebToken { - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "key": "value", - // "other_key": "other_value" - // } - private(set) static var validJWTString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsIm90aGVyX2tleSI6Im90aGVyX3ZhbHVlIn0.Koc07zTGuATtQK7EvfAuwgZ-Nsr6P6J3HV4h3QLlXpM" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "key": "value", - // "email": "test@email.com" - // } - private(set) static var validJWTStringWithEmailOnly = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20ifQ.b-2oTvjpc_qHM5dU6akk_ESe3eWUZwL21pvTsCmW2gE" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "name": "John Doe", - // "key": "value" - // } - private(set) static var validJWTStringWithNameOnly = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJrZXkiOiJ2YWx1ZSJ9.P7Se5_EMlFBg5q8PV4C2IQ1YojTTSgitCBX7FgmXZzs" - - // Created with https://jwt.io/ with input: - // - // header: { - // "alg": "HS256", - // "typ": "JWT" - // } - // payload: { - // "name": "John Doe", - // "key": "value", - // "email": "test@email.com" - // } - private(set) static var validJWTStringWithNameAndEmail = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJrZXkiOiJ2YWx1ZSIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20ifQ.-xzg0r5mMnSZ8hE3hk7S93iCZHhOez1QFYdheSmDlx4" - - // For convenience, this exposes the email and name value used in the fixtures. - // This allows us to use raw strings in tests, rather than having to implement encoding the JWT from an arbitrary string. - private(set) static var emailFromValidJWTStringWithEmail = "test@email.com" - private(set) static var nameFromValidJWTStringWithEmail = "John Doe" -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift deleted file mode 100644 index c3ea0435cf74..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/JWTokenTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class JWTokenTests: XCTestCase { - - func testJWTokenDecodingFromInvalidStringFails() { - XCTAssertNil(JSONWebToken(encodedString: "invalid")) - } - - func testJWTokenDecodingWithoutHeaderFails() { - let inputWithoutHeader = JSONWebToken.validJWTString.split(separator: ".").dropFirst().joined(separator: ".") - XCTAssertNil(JSONWebToken(encodedString: inputWithoutHeader)) - } - - func testJWTokenDecodingFromValidString() throws { - let token = try XCTUnwrap(JSONWebToken(encodedString: JSONWebToken.validJWTString)) - - XCTAssertEqual( - token.header as? [String: String], - ["alg": "HS256", "typ": "JWT"] - ) - - XCTAssertEqual( - token.payload as? [String: String], - ["key": "value", "other_key": "other_value"] - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift deleted file mode 100644 index eebeb0d8b9b8..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/NewGoogleAuthenticatorTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class NewGoogleAuthenticatorTests: XCTestCase { - - let fakeClientId = GoogleClientId(string: "a.b.c")! - - func testRequestingOAuthTokenThrowsIfCodeCannotBeExtractedFromURL() async throws { - // Notice the use of a stub that returns a successful value. - // This way, if we get an error, we can be more confident it's legit. - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture()) - ) - let url = URL(string: "https://test.com?without=code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? OAuthError) - guard case .urlDidNotContainCodeParameter(let urlFromError) = error else { - return XCTFail("Received unexpected error \(error)") - } - XCTAssertEqual(urlFromError, url) - } - } - - func testRequestingOAuthTokenRethrowsTheErrorItRecives() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(error: TestError(id: 1)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? TestError) - XCTAssertEqual(error.id, 1) - } - } - - func testRequestingOAuthTokenThrowsIfIdTokenMissingFromResponse() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture(rawIDToken: .none)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - _ = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTFail("Expected an error to be thrown") - } catch { - let error = try XCTUnwrap(error as? OAuthError) - guard case .tokenResponseDidNotIncludeIdToken = error else { - return XCTFail("Received unexpected error \(error)") - } - } - } - - func testRequestingOAuthTokenReturnsTokenIfSuccessful() async throws { - let authenticator = NewGoogleAuthenticator( - clientId: fakeClientId, - scheme: "scheme", - audience: "audience", - oautTokenGetter: GoogleOAuthTokenGettingStub(response: .fixture(rawIDToken: JSONWebToken.validJWTStringWithNameAndEmail)) - ) - let url = URL(string: "https://test.com?code=a_code")! - - do { - let response = try await authenticator.requestOAuthToken( - url: url, - clientId: GoogleClientId(string: "a.b.c")!, - audience: "audience", - pkce: ProofKeyForCodeExchange() - ) - XCTAssertEqual(response.email, JSONWebToken.emailFromValidJWTStringWithEmail) - } catch { - XCTFail("Expected value, got error '\(error)'") - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift deleted file mode 100644 index ded44ae4bb31..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthRequestBody+GoogleSignInTests.swift +++ /dev/null @@ -1,22 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class OAuthRequestBodyGoogleSignInTests: XCTestCase { - - func testGoogleSignInTokenRequestBody() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - let pkce = ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .plain) - let body = OAuthTokenRequestBody.googleSignInRequestBody( - clientId: GoogleClientId(string: "com.app.123-abc")!, - audience: "audience", - authCode: "codeValue", - pkce: pkce - ) - - XCTAssertEqual(body.clientId, "com.app.123-abc") - XCTAssertEqual(body.clientSecret, "") - XCTAssertEqual(body.codeVerifier, codeVerifier) - XCTAssertEqual(body.grantType, "authorization_code") - XCTAssertEqual(body.redirectURI, "123-abc.app.com:/oauth2callback") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift deleted file mode 100644 index 917f5e75aa28..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenRequestBodyTests.swift +++ /dev/null @@ -1,28 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class OAuthTokenRequestBodyTests: XCTestCase { - - func testURLEncodedDataConversion() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - let body = OAuthTokenRequestBody( - clientId: "clientId", - clientSecret: "clientSecret", - audience: "audience", - code: "codeValue", - codeVerifier: codeVerifier, - grantType: "grantType", - redirectURI: "redirectUri" - ) - - let data = try body.asURLEncodedData() - - let decodedData = try XCTUnwrap(String(data: data, encoding: .utf8)) - - XCTAssertTrue(decodedData.contains("client_id=clientId")) - XCTAssertTrue(decodedData.contains("client_secret=clientSecret")) - XCTAssertTrue(decodedData.contains("code_verifier=\(codeVerifier.rawValue)")) - XCTAssertTrue(decodedData.contains("grant_type=grantType")) - XCTAssertTrue(decodedData.contains("redirect_uri=redirectUri")) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift deleted file mode 100644 index 22bda7321e41..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/OAuthTokenResponseBody+Fixture.swift +++ /dev/null @@ -1,15 +0,0 @@ -@testable import WordPressAuthenticator - -extension OAuthTokenResponseBody { - - static func fixture(rawIDToken: String? = JSONWebToken.validJWTString) -> Self { - OAuthTokenResponseBody( - accessToken: "access_token", - expiresIn: 1, - rawIDToken: rawIDToken, - refreshToken: .none, - scope: "s", - tokenType: "t" - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift deleted file mode 100644 index 708ea7a203ca..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/ProofKeyForCodeExchangeTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class ProofKeyForCodeExchangeTests: XCTestCase { - - func testCodeChallengeInPlainModeIsTheSameAsCodeVerifier() throws { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier.fixture() - - XCTAssertEqual( - ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .plain).codeChallenge, - codeVerifier.rawValue - ) - } - - func testCodeChallengeInS256ModeIsEncodedAsPerSpec() { - let codeVerifier = ProofKeyForCodeExchange.CodeVerifier(value: (0..<9).map { _ in "test-" }.joined())! - - XCTAssertEqual( - ProofKeyForCodeExchange(codeVerifier: codeVerifier, method: .s256).codeChallenge, - "lWvomVEGuL8FR3DY2DP_9E2q_imlqUHi-s1SPqRhO2c" - ) - } - - func testMethodURLQueryParameterValuePlain() { - XCTAssertEqual(ProofKeyForCodeExchange.Method.plain.urlQueryParameterValue, "plain") - } - - func testMethodURLQueryParameterValueS256() { - XCTAssertEqual(ProofKeyForCodeExchange.Method.s256.urlQueryParameterValue, "S256") - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift deleted file mode 100644 index 7b9a86064533..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/Result+ConvenienceInitTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class ResultConvenienceInitTests: XCTestCase { - - func testResultWithOptionalInputs() throws { - // Syntax sugar to keep line length shorter. SUT = System Under Test - typealias SUT = Result - - let testError = NSError(domain: "test", code: 1, userInfo: .none) - - // When value is some and error is nil, returns the value - XCTAssertEqual( - try XCTUnwrap(SUT(value: 1, error: .none, inconsistentStateError: testError).get()), - 1 - ) - - // When value is some and error is some, returns the error - let someError = NSError(domain: "test", code: 2) - XCTAssertThrowsError( - try SUT(value: 1, error: someError, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, someError) - } - - // When value is none and error is some, returns the error - XCTAssertThrowsError( - try SUT(value: .none, error: someError, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, someError) - } - - // When both value and error are none, returns the given error for this inconsistent state - XCTAssertThrowsError( - try SUT(value: .none, error: .none, inconsistentStateError: testError).get() - ) { error in - XCTAssertEqual(error as NSError, testError) - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift deleted file mode 100644 index a112065b7f95..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URL+GoogleSignInTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class URLGoogleSignInTests: XCTestCase { - - func testGoogleSignInAuthURL() throws { - let pkce = try ProofKeyForCodeExchange() - let url = try URL.googleSignInAuthURL( - clientId: GoogleClientId(string: "123-abc245def.apps.googleusercontent.com")!, - pkce: pkce - ) - - assert(url, matchesBaseURL: "https://accounts.google.com/o/oauth2/v2/auth") - assertQueryItems( - for: url, - includeItemNamed: "client_id", - withValue: "123-abc245def.apps.googleusercontent.com" - ) - assertQueryItems( - for: url, - includeItemNamed: "code_challenge", - withValue: pkce.codeChallenge - ) - assertQueryItems( - for: url, - includeItemNamed: "code_challenge_method", - withValue: pkce.method.urlQueryParameterValue - ) - assertQueryItems( - for: url, - includeItemNamed: "redirect_uri", - withValue: "com.googleusercontent.apps.123-abc245def:/oauth2callback" - ) - assertQueryItems( - for: url, - includeItemNamed: "scope", - withValue: "profile email" - ) - assertQueryItems(for: url, includeItemNamed: "response_type", withValue: "code") - } -} - -func assert( - _ actual: URL, - matchesBaseURL baseURLString: String, - file: StaticString = #file, - line: UInt = #line -) { - guard var components = URLComponents(url: actual, resolvingAgainstBaseURL: false) else { - return XCTFail( - "Could not created `URLComponents` from given `URL` \(actual).", - file: file, - line: line - ) - } - - components.query = .none - - guard let baseURL = components.url else { - return XCTFail( - "Could not extract `URL` from `URLComponents` created from \(actual).", - file: file, - line: line - ) - } - - XCTAssertEqual(baseURL.absoluteString, baseURLString, file: file, line: line) -} - -func assertQueryItems( - for url: URL, - includeItemNamed name: String, - withValue value: String?, - file: StaticString = #file, - line: UInt = #line -) { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return XCTFail( - "Could not created `URLComponents` from given `URL` \(url).", - file: file, - line: line - ) - } - - guard let queryItems = components.queryItems else { - XCTFail("URL \(url) has no query items", file: file, line: line) - return - } - - XCTAssertTrue( - queryItems.contains(where: { $0.name == name && $0.value == value }), - "Could not find query item with name '\(name)' and value '\(value ?? "nil")'. Query items found: \(queryItems.map { "'name: \($0.name), value: \($0.value ?? "nil")'" }.joined(separator: ", "))", - file: file, - line: line - ) -} diff --git a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift b/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift deleted file mode 100644 index 384bb5556cf7..000000000000 --- a/Tests/WordPressAuthenticatorTests/GoogleSignIn/URLRequest+GoogleSignInTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class URLRequestOAuthTokenRequestTests: XCTestCase { - - let testBody = OAuthTokenRequestBody( - clientId: "a", - clientSecret: "b", - audience: "audience", - code: "c", - codeVerifier: ProofKeyForCodeExchange.CodeVerifier.fixture(), - grantType: "e", - redirectURI: "f" - ) - - func testURL() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual(request.url, URL(string: "https://oauth2.googleapis.com/token")!) - } - - func testMethodPost() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual(request.httpMethod, "POST") - } - - func testContentTypeFormURLEncoded() throws { - let request = try URLRequest.googleSignInTokenRequest(body: testBody) - XCTAssertEqual( - request.value(forHTTPHeaderField: "Content-Type"), - "application/x-www-form-urlencoded; charset=UTF-8" - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m b/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m deleted file mode 100644 index a0a7a5c6755f..000000000000 --- a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.m +++ /dev/null @@ -1,79 +0,0 @@ -#import - -@import WordPressAuthenticator; -@import WordPressSharedObjC; - -@interface CaptureLogs : NSObject - -@property (nonatomic, strong) NSMutableArray *infoLogs; -@property (nonatomic, strong) NSMutableArray *errorLogs; - -@end - -// We are leaving some protocol methods intentionally unimplemented to then test that calling them -// will not cause a crash. -// -// See https://github.com/wordpress-mobile/WordPressAuthenticator-iOS/pull/720#issuecomment-1374952619 -#pragma clang diagnostic ignored "-Wprotocol" -@implementation CaptureLogs - -- (instancetype)init -{ - if ((self = [super init])) { - self.infoLogs = [NSMutableArray new]; - self.errorLogs = [NSMutableArray new]; - } - return self; -} - -- (void)logInfo:(NSString *)str -{ - [self.infoLogs addObject:str]; -} - -- (void)logError:(NSString *)str -{ - [self.errorLogs addObject:str]; -} - -@end -#pragma clang diagnostic pop - -@interface ObjCLoggingTest : XCTestCase - -@property (nonatomic, strong) CaptureLogs *logger; - -@end - -@implementation ObjCLoggingTest - -- (void)setUp -{ - self.logger = [CaptureLogs new]; - WPSetLoggingDelegate(self.logger); -} - -- (void)testLogging -{ - WPLogInfo(@"This is an info log"); - WPLogInfo(@"This is an info log %@", @"with an argument"); - XCTAssertEqualObjects(self.logger.infoLogs, (@[@"This is an info log", @"This is an info log with an argument"])); - - WPLogError(@"This is an error log"); - WPLogError(@"This is an error log %@", @"with an argument"); - XCTAssertEqualObjects(self.logger.errorLogs, (@[@"This is an error log", @"This is an error log with an argument"])); -} - -- (void)testUnimplementedLoggingMethod -{ - XCTAssertNoThrow(WPLogVerbose(@"verbose logging is not implemented")); -} - -- (void)testNoLogging -{ - WPSetLoggingDelegate(nil); - XCTAssertNoThrow(WPLogInfo(@"this log should not be printed")); - XCTAssertEqual(self.logger.infoLogs.count, 0); -} - -@end diff --git a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift b/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift deleted file mode 100644 index c750bf2f7c47..000000000000 --- a/Tests/WordPressAuthenticatorTests/Logging/LoggingTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -import XCTest -import WordPressShared - -@testable import WordPressAuthenticator - -private class CaptureLogs: NSObject, WordPressLoggingDelegate { - var verboseLogs = [String]() - var debugLogs = [String]() - var infoLogs = [String]() - var warningLogs = [String]() - var errorLogs = [String]() - - func logError(_ str: String) { - errorLogs.append(str) - } - - func logWarning(_ str: String) { - warningLogs.append(str) - } - - func logInfo(_ str: String) { - infoLogs.append(str) - } - - func logDebug(_ str: String) { - debugLogs.append(str) - } - - func logVerbose(_ str: String) { - verboseLogs.append(str) - } -} - -class LoggingTest: XCTestCase { - - private let logger = CaptureLogs() - - override func setUp() { - WPSetLoggingDelegate(logger) - } - - func testLogging() { - WPLogVerbose("This is a verbose log") - WPLogVerbose("This is a verbose log %@", "with an argument") - XCTAssertEqual(self.logger.verboseLogs, ["This is a verbose log", "This is a verbose log with an argument"]) - - WPLogDebug("This is a debug log") - WPLogDebug("This is a debug log %@", "with an argument") - XCTAssertEqual(self.logger.debugLogs, ["This is a debug log", "This is a debug log with an argument"]) - - WPLogInfo("This is an info log") - WPLogInfo("This is an info log %@", "with an argument") - XCTAssertEqual(self.logger.infoLogs, ["This is an info log", "This is an info log with an argument"]) - - WPLogWarning("This is a warning log") - WPLogWarning("This is a warning log %@", "with an argument") - XCTAssertEqual(self.logger.warningLogs, ["This is a warning log", "This is a warning log with an argument"]) - - WPLogError("This is an error log") - WPLogError("This is an error log %@", "with an argument") - XCTAssertEqual(self.logger.errorLogs, ["This is an error log", "This is an error log with an argument"]) - } - - func testNoLogging() { - WPSetLoggingDelegate(nil) - XCTAssertNoThrow(WPLogInfo("this log should not be printed")) - XCTAssertEqual(self.logger.infoLogs.count, 0) - } -} diff --git a/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift b/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift deleted file mode 100644 index 316df137c78b..000000000000 --- a/Tests/WordPressAuthenticatorTests/MemoryManagementTests.swift +++ /dev/null @@ -1,60 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -final class MemoryManagementTests: XCTestCase { - override func setUp() { - super.setUp() - - WordPressAuthenticator.initialize( - configuration: WordpressAuthenticatorProvider.wordPressAuthenticatorConfiguration(), - style: WordpressAuthenticatorProvider.wordPressAuthenticatorStyle(.random), - unifiedStyle: WordpressAuthenticatorProvider.wordPressAuthenticatorUnifiedStyle(.random) - ) - } - - func testViewControllersDeallocatedAfterDismissing() { - let viewControllers: [UIViewController] = [ - Storyboard.login.instance.instantiateInitialViewController()!, - LoginPrologueLoginMethodViewController.instantiate(from: .login)!, - LoginPrologueSignupMethodViewController.instantiate(from: .login)!, - Login2FAViewController.instantiate(from: .login)!, - LoginEmailViewController.instantiate(from: .login)!, - LoginSelfHostedViewController.instantiate(from: .login)!, - LoginSiteAddressViewController.instantiate(from: .login)!, - LoginUsernamePasswordViewController.instantiate(from: .login)!, - LoginWPComViewController.instantiate(from: .login)!, - SignupEmailViewController.instantiate(from: .signup)!, - SignupGoogleViewController.instantiate(from: .signup)!, - GetStartedViewController.instantiate(from: .getStarted)!, - VerifyEmailViewController.instantiate(from: .verifyEmail)!, - PasswordViewController.instantiate(from: .password)!, - TwoFAViewController.instantiate(from: .twoFA)!, - GoogleAuthViewController.instantiate(from: .googleAuth)!, - SiteAddressViewController.instantiate(from: .siteAddress)!, - SiteCredentialsViewController.instantiate(from: .siteAddress)! - ] - - for viewController in viewControllers { - viewController.loadViewIfNeeded() - } - - verifyObjectsDeallocatedAfterTeardown(viewControllers) - } - - // MARK: - Helpers - - private func verifyObjectsDeallocatedAfterTeardown(_ objects: [AnyObject]) { - /// Create the array of weak objects so we could assert them in the teardown block - let weakObjects: [() -> AnyObject?] = objects.map { object in { [weak object] in - return object - } - } - - /// All the weak items should be deallocated in the teardown block unless there's a retain cycle holding them - addTeardownBlock { - for object in weakObjects { - XCTAssertNil(object(), "\(object()!.self) is not deallocated after teardown") - } - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift b/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift deleted file mode 100644 index a0e5cadfd329..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/MockNavigationController.swift +++ /dev/null @@ -1,10 +0,0 @@ -import UIKit - -final class MockNavigationController: UINavigationController { - var pushedViewController: UIViewController? - - override func pushViewController(_ viewController: UIViewController, animated: Bool) { - pushedViewController = viewController - super.pushViewController(viewController, animated: true) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift b/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift deleted file mode 100644 index 01caa7da932b..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/ModalViewControllerPresentingSpy.swift +++ /dev/null @@ -1,8 +0,0 @@ -@testable import WordPressAuthenticator - -class ModalViewControllerPresentingSpy: UIViewController { - internal var presentedVC: UIViewController? = .none - override func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) { - presentedVC = viewControllerToPresent - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift b/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift deleted file mode 100644 index eb5d21b39807..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/WordPressAuthenticatorDelegateSpy.swift +++ /dev/null @@ -1,82 +0,0 @@ -@testable import WordPressAuthenticator -import WordPressKit -import WordPressShared - -class WordPressAuthenticatorDelegateSpy: WordPressAuthenticatorDelegate { - var dismissActionEnabled: Bool = true - var supportActionEnabled: Bool = true - var wpcomTermsOfServiceEnabled: Bool = true - var supportEnabled: Bool = true - var allowWPComLogin: Bool = true - var shouldHandleError: Bool = false - - private(set) var presentSignupEpilogueCalled = false - private(set) var socialUser: SocialUser? - - func createdWordPressComAccount(username: String, authToken: String) { - // no-op - } - - func userAuthenticatedWithAppleUserID(_ appleUserID: String) { - // no-op - } - - func presentSupportRequest(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag) { - // no-op - } - - func shouldPresentUsernamePasswordController(for siteInfo: WordPressComSiteInfo?, onCompletion: @escaping (WordPressAuthenticatorResult) -> Void) { - // no-op - } - - func presentLoginEpilogue(in navigationController: UINavigationController, for credentials: AuthenticatorCredentials, source: SignInSource?, onDismiss: @escaping () -> Void) { - // no-op - } - - func presentSignupEpilogue( - in navigationController: UINavigationController, - for credentials: AuthenticatorCredentials, - socialUser: SocialUser? - ) { - presentSignupEpilogueCalled = true - self.socialUser = socialUser - } - - func presentSupport(from sourceViewController: UIViewController, sourceTag: WordPressSupportSourceTag, lastStep: AuthenticatorAnalyticsTracker.Step, lastFlow: AuthenticatorAnalyticsTracker.Flow) { - // no-op - } - - func shouldPresentLoginEpilogue(isJetpackLogin: Bool) -> Bool { - true - } - - func shouldHandleError(_ error: Error) -> Bool { - shouldHandleError - } - - func handleError(_ error: Error, onCompletion: @escaping (UIViewController) -> Void) { - if shouldHandleError { - onCompletion(UIViewController()) - } - } - - func shouldPresentSignupEpilogue() -> Bool { - true - } - - func sync(credentials: AuthenticatorCredentials, onCompletion: @escaping () -> Void) { - // no-op - } - - func track(event: WPAnalyticsStat) { - // no-op - } - - func track(event: WPAnalyticsStat, properties: [AnyHashable: Any]) { - // no-op - } - - func track(event: WPAnalyticsStat, error: Error) { - // no-op - } -} diff --git a/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift b/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift deleted file mode 100644 index 578027a3d80d..000000000000 --- a/Tests/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift +++ /dev/null @@ -1,111 +0,0 @@ -@testable import WordPressAuthenticator - -@objc -public class WordpressAuthenticatorProvider: NSObject { - static func wordPressAuthenticatorConfiguration() -> WordPressAuthenticatorConfiguration { - return WordPressAuthenticatorConfiguration(wpcomClientId: "23456", - wpcomSecret: "arfv35dj57l3g2323", - wpcomScheme: "https", - wpcomTermsOfServiceURL: URL(string: "https://wordpress.com/tos/")!, - googleLoginClientId: "", - googleLoginServerClientId: "", - googleLoginScheme: "com.googleuserconsent.apps", - userAgent: "") - } - - static func wordPressAuthenticatorStyle(_ style: AuthenticatorStyleType) -> WordPressAuthenticatorStyle { - var wpAuthStyle: WordPressAuthenticatorStyle! - - switch style { - case .random: - wpAuthStyle = WordPressAuthenticatorStyle( - primaryNormalBackgroundColor: UIColor.random(), - primaryNormalBorderColor: UIColor.random(), - primaryHighlightBackgroundColor: UIColor.random(), - primaryHighlightBorderColor: UIColor.random(), - secondaryNormalBackgroundColor: UIColor.random(), - secondaryNormalBorderColor: UIColor.random(), - secondaryHighlightBackgroundColor: UIColor.random(), - secondaryHighlightBorderColor: UIColor.random(), - disabledBackgroundColor: UIColor.random(), - disabledBorderColor: UIColor.random(), - primaryTitleColor: UIColor.random(), - secondaryTitleColor: UIColor.random(), - disabledTitleColor: UIColor.random(), - disabledButtonActivityIndicatorColor: UIColor.random(), - textButtonColor: UIColor.random(), - textButtonHighlightColor: UIColor.random(), - instructionColor: UIColor.random(), - subheadlineColor: UIColor.random(), - placeholderColor: UIColor.random(), - viewControllerBackgroundColor: UIColor.random(), - textFieldBackgroundColor: UIColor.random(), - navBarImage: UIImage(color: UIColor.random()), - navBarBadgeColor: UIColor.random(), - navBarBackgroundColor: UIColor.random() - ) - return wpAuthStyle - } - } - - static func wordPressAuthenticatorUnifiedStyle(_ style: AuthenticatorStyleType) -> WordPressAuthenticatorUnifiedStyle { - var wpUnifiedAuthStyle: WordPressAuthenticatorUnifiedStyle! - - switch style { - case .random: - wpUnifiedAuthStyle = WordPressAuthenticatorUnifiedStyle( - borderColor: UIColor.random(), - errorColor: UIColor.random(), - textColor: UIColor.random(), - textSubtleColor: UIColor.random(), - textButtonColor: UIColor.random(), - textButtonHighlightColor: UIColor.random(), - viewControllerBackgroundColor: UIColor.random(), - navBarBackgroundColor: UIColor.random(), - navButtonTextColor: UIColor.random(), - navTitleTextColor: UIColor.random() - ) - return wpUnifiedAuthStyle - } - } - - static func getWordpressAuthenticator() -> WordPressAuthenticator { - return WordPressAuthenticator( - configuration: wordPressAuthenticatorConfiguration(), - style: wordPressAuthenticatorStyle(.random), - unifiedStyle: wordPressAuthenticatorUnifiedStyle(.random), - displayImages: WordPressAuthenticatorDisplayImages.defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings.defaultStrings) - } - - @objc(initializeWordPressAuthenticator) - public static func initializeWordPressAuthenticator() { - WordPressAuthenticator.initialize( - configuration: wordPressAuthenticatorConfiguration(), - style: wordPressAuthenticatorStyle(.random), - unifiedStyle: wordPressAuthenticatorUnifiedStyle(.random), - displayImages: WordPressAuthenticatorDisplayImages.defaultImages, - displayStrings: WordPressAuthenticatorDisplayStrings.defaultStrings) - } -} - -enum AuthenticatorStyleType { - case random -} - -extension CGFloat { - static func random() -> CGFloat { - return CGFloat(arc4random()) / CGFloat(UInt32.max) - } -} - -extension UIColor { - static func random() -> UIColor { - return UIColor( - red: .random(), - green: .random(), - blue: .random(), - alpha: 1.0 - ) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift b/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift deleted file mode 100644 index 395fed091acc..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class LoginFieldsTests: XCTestCase { - - func testSignInWithAppleParametersNilWhenNoSocialUser() { - XCTAssertNil(LoginFields().parametersForSignInWithApple) - } - - func testSignInWithAppleParametersNilWhenSocialUserNotApple() { - let fields = LoginFields() - fields.meta = LoginFieldsMeta( - socialUser: SocialUser(email: "email", fullName: "name", service: .google) - ) - - XCTAssertNil(fields.parametersForSignInWithApple) - } - - func testSignInWithAppleParametersHasEmailAndNameWhenSocialUserIsApple() throws { - let fields = LoginFields() - fields.meta = LoginFieldsMeta( - socialUser: SocialUser(email: "email", fullName: "name", service: .apple) - ) - - let parameters = try XCTUnwrap(fields.parametersForSignInWithApple) - XCTAssertEqual(parameters["user_email"] as? String, "email") - XCTAssertEqual(parameters["user_name"] as? String, "name") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift b/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift deleted file mode 100644 index 35ef3972972c..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/LoginFieldsValidationTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -// MARK: - LoginFields Validation Tests -// -class LoginFieldsValidationTests: XCTestCase { - - func testValidateFieldsPopulatedForSignin() { - let loginFields = LoginFields() - loginFields.meta.userIsDotCom = true - - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "Empty fields should not validate.") - - loginFields.username = "user" - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "Should not validate with just a username") - - loginFields.password = "password" - XCTAssert(loginFields.validateFieldsPopulatedForSignin(), "should validate wpcom with username and password.") - - loginFields.meta.userIsDotCom = false - XCTAssertFalse(loginFields.validateFieldsPopulatedForSignin(), "should not validate self-hosted with just username and password.") - - loginFields.siteAddress = "example.com" - XCTAssert(loginFields.validateFieldsPopulatedForSignin(), "should validate self-hosted with username, password, and site.") - } - - func testValidateSiteForSignin() { - let loginFields = LoginFields() - - loginFields.siteAddress = "" - XCTAssertFalse(loginFields.validateSiteForSignin(), "Empty site should not validate.") - - loginFields.siteAddress = "hostname" - XCTAssertTrue(loginFields.validateSiteForSignin(), "Hostnames should validate.") - - loginFields.siteAddress = "http://hostname" - XCTAssert(loginFields.validateSiteForSignin(), "Since we want to validate simple mistakes, to use a hostname you'll need an http:// or https:// prefix.") - - loginFields.siteAddress = "https://hostname" - XCTAssert(loginFields.validateSiteForSignin(), "Since we want to validate simple mistakes, to use a hostname you'll need an http:// or https:// prefix.") - } -} diff --git a/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift b/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift deleted file mode 100644 index 72f955f689ca..000000000000 --- a/Tests/WordPressAuthenticatorTests/Model/WordPressComSiteInfoTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class WordPressComSiteInfoTests: XCTestCase { - private var subject: WordPressComSiteInfo! - - override func setUp() { - subject = WordPressComSiteInfo(remote: mock()) - super.setUp() - } - - override func tearDown() { - super.tearDown() - subject = nil - } - - func testJetpackActiveMatchesExpectation() { - XCTAssertTrue(subject.isJetpackActive) - } - - func testHasJetpackMatchesExpectation() { - XCTAssertTrue(subject.hasJetpack) - } - - func testJetpackConnectedMatchesExpectation() { - XCTAssertTrue(subject.isJetpackConnected) - } - - func testWPComMatchesExpectation() { - XCTAssertFalse(subject.isWPCom) - } - - func testWPMatchesExpectation() { - XCTAssertTrue(subject.isWP) - } -} - -private extension WordPressComSiteInfoTests { - func mock() -> [AnyHashable: Any] { - return [ - "isJetpackActive": true, - "jetpackVersion": false, - "isWordPressDotCom": false, - "urlAfterRedirects": "https://somewhere.com", - "hasJetpack": true, - "isWordPress": true, - "isJetpackConnected": true - ] as [AnyHashable: Any] - } -} diff --git a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift b/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift deleted file mode 100644 index eac10b685094..000000000000 --- a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterAccountTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class NavigationToAccountTests: XCTestCase { - func testNavigationCommandNavigatesToExpectedDestination() { - let origin = UIViewController() - let navigationController = MockNavigationController(rootViewController: origin) - - let command = NavigateToEnterAccount(signInSource: .wpCom) - command.execute(from: origin) - - let pushedViewController = navigationController.pushedViewController - - XCTAssertNotNil(pushedViewController) - XCTAssertTrue(pushedViewController is GetStartedViewController) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift b/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift deleted file mode 100644 index 8c8452548545..000000000000 --- a/Tests/WordPressAuthenticatorTests/Navigation/NavigationToEnterSiteTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import WordPressAuthenticator - -final class NavigationToEnterSiteTests: XCTestCase { - func testNavigationCommandNavigatesToExpectedDestination() { - let origin = UIViewController() - let navigationController = MockNavigationController(rootViewController: origin) - - let command = NavigateToEnterSite() - command.execute(from: origin) - - let pushedViewController = navigationController.pushedViewController - - XCTAssertNotNil(pushedViewController) - XCTAssertTrue(pushedViewController is SiteAddressViewController) - } -} diff --git a/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m b/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m deleted file mode 100644 index 2cd3ff932a83..000000000000 --- a/Tests/WordPressAuthenticatorTests/Services/LoginFacadeTests.m +++ /dev/null @@ -1,269 +0,0 @@ -#import "WordPressAuthenticatorTests-Swift.h" -#import "LoginFacade.h" -#import "WordPressXMLRPCAPIFacade.h" - -@import OCMock; -@import XCTest; -@import WordPressShared; -@import WordPressAuthenticator; -@import WordPressKit; - -@interface LoginFacadeTests: XCTestCase - -@property (nonatomic) LoginFacade *loginFacade; -@property (nonatomic) id mockOAuthFacade; -@property (nonatomic) id mockXMLRPCAPIFacade; -@property (nonatomic) id mockLoginFacade; -@property (nonatomic) id mockLoginFacadeDelegate; -@property (nonatomic) LoginFields *loginFields; -@property (nonatomic) NSURL *xmlrpc; -@property (nonatomic) NSMutableDictionary *xmlrpcOptions; - -@end - -@implementation LoginFacadeTests - -- (void)setUp { - [super setUp]; - - [WordpressAuthenticatorProvider initializeWordPressAuthenticator]; - - self.mockOAuthFacade = [OCMockObject niceMockForProtocol:@protocol(WordPressComOAuthClientFacadeProtocol)]; - self.mockXMLRPCAPIFacade = [OCMockObject niceMockForProtocol:@protocol(WordPressXMLRPCAPIFacade)]; - self.mockLoginFacadeDelegate = [OCMockObject niceMockForProtocol:@protocol(LoginFacadeDelegate)]; - - self.loginFacade = [LoginFacade new]; - self.loginFacade.wordpressComOAuthClientFacade = self.mockOAuthFacade; - self.loginFacade.wordpressXMLRPCAPIFacade = self.mockXMLRPCAPIFacade; - self.loginFacade.delegate = self.mockLoginFacadeDelegate; - - self.mockLoginFacade = OCMPartialMock(self.loginFacade); - OCMStub([[self.mockLoginFacade ignoringNonObjectArgs] track:0]); - OCMStub([[self.mockLoginFacade ignoringNonObjectArgs] track:0 error:[OCMArg any]]); - - self.loginFields = [LoginFields new]; - self.loginFields.username = @"username"; - self.loginFields.password = @"password"; - self.loginFields.siteAddress = @"www.mysite.com"; - self.loginFields.multifactorCode = @"123456"; -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. -} - -// MARK: - WordPress.com - -- (void)testDotComExampleShouldDisplayMessageAboutConnectinToWordPressCom { - self.loginFields.userIsDotCom = YES; - - [[self.mockLoginFacadeDelegate expect] displayLoginMessage:NSLocalizedString(@"Connecting to WordPress.com", nil)]; - [self.loginFacade signInWithLoginFields:self.loginFields]; - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldAuthenticateUserCredentials { - self.loginFields.userIsDotCom = YES; - - [[self.mockOAuthFacade expect] authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockOAuthFacade verify]; -} - -- (void)testDotComShouldCallLoginFacadeDelegateFinishedLoginWithUsername { - self.loginFields.userIsDotCom = YES; - - NSString *authToken = @"auth-token"; - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSString *); - [invocation getArgument:&successStub atIndex:5]; - - successStub(authToken); - }]; - [[self.mockLoginFacadeDelegate expect] finishedLoginWithAuthToken:authToken requiredMultifactorCode:self.loginFields.requiredMultifactor]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeNeedsMultifactorCodeWhenAuthentificationRequired { - self.loginFields.userIsDotCom = YES; - - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained needsMultifactorStub)(NSInteger, SocialLogin2FANonceInfo *); - [invocation getArgument:&needsMultifactorStub atIndex:6]; - - needsMultifactorStub(0, nil); - }]; - [[self.mockLoginFacadeDelegate expect] needsMultifactorCode]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeNeedsMultifactorCode { - self.loginFields.userIsDotCom = YES; - - // Expected parameters - NSInteger userID = 1234; - SocialLogin2FANonceInfo * info = [SocialLogin2FANonceInfo new]; - - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained needsMultifactorStub)(NSInteger, SocialLogin2FANonceInfo *); - [invocation getArgument:&needsMultifactorStub atIndex:6]; - - needsMultifactorStub(userID, info); - }]; - [[self.mockLoginFacadeDelegate expect] needsMultifactorCodeForUserID:userID andNonceInfo:info]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testDotComShouldCallLoginFacadeDisplayRemoteError { - self.loginFields.userIsDotCom = YES; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockOAuthFacade authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:7]; - - failureStub(error); - }]; - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -// MARK: - Self-Hosted - -- (void)testSelfHostedShoulDisplayAuthentificatingMessage { - self.loginFields.userIsDotCom = NO; - - [[self.mockLoginFacadeDelegate expect] displayLoginMessage:NSLocalizedString(@"Authenticating", nil)]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldGuessingXMLRPCForSite { - self.loginFields.userIsDotCom = NO; - - [[self.mockXMLRPCAPIFacade expect] guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockXMLRPCAPIFacade verify]; -} - -- (void)testSelfHostedShouldRetrieveBlogOptions { - [self mockXMLRPCFacade]; - - [[self.mockXMLRPCAPIFacade expect] getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockXMLRPCAPIFacade verify]; -} - -- (void)testSelfHostedShouldIndicateLoginFacadeDelegateAfterRetrievingBlogOptions { - [self mockXMLRPCSuccessfulBlogOptions]; - - [[self.mockLoginFacadeDelegate expect] finishedLoginWithUsername:self.loginFields.username password:self.loginFields.password xmlrpc:[self.xmlrpc absoluteString] options:self.xmlrpcOptions]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldAttemptAuthentificateDotComAfterRetrievingBlogOptions { - [self mockXMLRPCSuccessfulBlogOptions]; - - self.xmlrpcOptions[@"wordpress.com"] = @YES; - [[self.mockOAuthFacade expect] authenticateWithUsername:self.loginFields.username password:self.loginFields.password multifactorCode:self.loginFields.multifactorCode success:OCMOCK_ANY needsMultifactor:OCMOCK_ANY failure:OCMOCK_ANY]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockOAuthFacade verify]; -} - -- (void)testSelfHostedShouldDisplayErrorOnFailureRetrievingBlogOptions { - [self mockXMLRPCFacade]; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - - // Intercept failure callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:6]; - - failureStub(error); - }]; - - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -- (void)testSelfHostedShouldDisplayErrorOnGuessXMLRPC { - self.loginFields.userIsDotCom = NO; - - NSError *error = [NSError errorWithDomain:@"org.wordpress" code:-1 userInfo:@{ NSLocalizedDescriptionKey : @"Error" }]; - - // Intercept failure callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained failureStub)(NSError *); - [invocation getArgument:&failureStub atIndex:4]; - - failureStub(error); - }]; - - [[self.mockLoginFacadeDelegate expect] displayRemoteError:error]; - - [self.loginFacade signInWithLoginFields:self.loginFields]; - - [self.mockLoginFacadeDelegate verify]; -} - -// MARK: - Mocks - -- (void)mockXMLRPCFacade { - self.loginFields.userIsDotCom = NO; - - self.xmlrpc = [NSURL URLWithString:@"http://www.selfhosted.com/xmlrpc.php"]; - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade guessXMLRPCURLForSite:self.loginFields.siteAddress success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSURL *); - [invocation getArgument:&successStub atIndex:3]; - - successStub(self.xmlrpc); - }]; -} - -- (void)mockXMLRPCSuccessfulBlogOptions { - [self mockXMLRPCFacade]; - - self.xmlrpcOptions = [NSMutableDictionary dictionaryWithDictionary:@{@"software_version":@{@"value":@"4.2"}}]; - - // Intercept success callback and execute it when appropriate - [OCMStub([self.mockXMLRPCAPIFacade getBlogOptionsWithEndpoint:self.xmlrpc username:self.loginFields.username password:self.loginFields.password success:OCMOCK_ANY failure:OCMOCK_ANY]) andDo:^(NSInvocation *invocation) { - void (^ __unsafe_unretained successStub)(NSDictionary *); - [invocation getArgument:&successStub atIndex:5]; - - successStub(self.xmlrpcOptions); - }]; -} - -@end diff --git a/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift deleted file mode 100644 index 88ea942e0081..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/AppleAuthenticatorTests.swift +++ /dev/null @@ -1,107 +0,0 @@ -import AuthenticationServices -@testable import WordPressAuthenticator -import XCTest - -class AppleAuthenticatorTests: XCTestCase { - - // showSignupEpilogue with loginFields.meta.appleUser set will pass SocialService.apple to the delegate - func testShowingSignupEpilogueWithApple() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let socialUserCreatingStub = SocialUserCreatingStub(appleResult: .success((true, true, true, "a", "b"))) - let sut = AppleAuthenticator(signupService: socialUserCreatingStub) - - // Before acting on the SUT, we need to ensure the login fields are set as we expect - let presenterViewController = UIViewController() - // We need to create this because it's accessed by showFrom(viewController:) - _ = UINavigationController(rootViewController: presenterViewController) - sut.showFrom(viewController: presenterViewController) - sut.createWordPressComUser( - appleUserId: "apple-user-id", - email: "test@email.com", - name: "Full Name", - token: "abcd" - ) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - let service = try XCTUnwrap(delegateSpy.socialUser?.service) - guard case .apple = service else { - return XCTFail("Expected Apple social service, got \(service) instead") - } - } - - // showSignupEpilogue with loginFields.meta.appleUser set will not pass SocialService.apple to the delegate - func testShowingSignupEpilogueWithoutAppleUser() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let sut = AppleAuthenticator(signupService: SocialUserCreatingStub()) - - // Before acting on the SUT, we need to ensure the login fields are set as we expect - let presenterViewController = UIViewController() - // We need to create this because it's accessed by showFrom(viewController:) - _ = UINavigationController(rootViewController: presenterViewController) - sut.showFrom(viewController: presenterViewController) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - // The delegate is called, but without social service. - // - // By the way, the type system and runtime allow this to happen, but does it actually - // make sense? Not so sure. How can we callback from Sign In with Apple without the - // matching social service? - XCTAssertTrue(delegateSpy.presentSignupEpilogueCalled) - XCTAssertNil(delegateSpy.socialUser) - } -} - -// This doesn't live in a dedicated file because we currently only need it for this test. -class SocialUserCreatingStub: SocialUserCreating { - - // is new account, user name, WPCom token - private let googleResult: Result<(Bool, String, String), Error> - // is new account, existing non-social account, existing MFA account, user name, WPCom token - private let appleResult: Result<(Bool, Bool, Bool, String, String), Error> - - init( - appleResult: Result<(Bool, Bool, Bool, String, String), Error> = .failure(TestError(id: 1)), - googleResult: Result<(Bool, String, String), Error> = .failure(TestError(id: 2)) - ) { - self.appleResult = appleResult - self.googleResult = googleResult - } - - func createWPComUserWithGoogle(token: String, success: @escaping (Bool, String, String) -> Void, failure: @escaping (Error) -> Void) { - switch googleResult { - case .success((let isNewAccount, let userName, let wpComToken)): - success(isNewAccount, userName, wpComToken) - case .failure(let error): - failure(error) - } - } - - func createWPComUserWithApple(token: String, email: String, fullName: String?, success: @escaping (Bool, Bool, Bool, String, String) -> Void, failure: @escaping (Error) -> Void) { - switch appleResult { - case .success((let isNewAccount, let existingNonSocialAccount, let existing2FAAccount, let username, let wpComToken)): - success(isNewAccount, existingNonSocialAccount, existing2FAAccount, username, wpComToken) - case .failure(let error): - failure(error) - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift deleted file mode 100644 index c71c8012d834..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/LoginViewControllerTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -@testable import WordPressAuthenticator -import XCTest - -class LoginViewControllerTests: XCTestCase { - - // showSignupEpilogue with loginFields.meta.appleUser set will pass SocialService.apple to - // the delegate - func testShowingSignupEpilogueWithGoogleUser() throws { - WordPressAuthenticator.initializeForTesting() - let delegateSpy = WordPressAuthenticatorDelegateSpy() - WordPressAuthenticator.shared.delegate = delegateSpy - - // This might be unnecessary because delegateSpy should be deallocated once the test method finished. - // Leaving it here, just in case. - addTeardownBlock { - WordPressAuthenticator.shared.delegate = nil - } - - let sut = LoginViewController() - // We need to embed the SUT in a navigation controller because it expects its - // navigationController property to not be nil. - _ = UINavigationController(rootViewController: sut) - - sut.loginFields.meta.socialUser = SocialUser(email: "test@email.com", fullName: "Full Name", service: .google) - - sut.showSignupEpilogue(for: AuthenticatorCredentials()) - - let service = try XCTUnwrap(delegateSpy.socialUser?.service) - guard case .google = service else { - return XCTFail("Expected Google social service, got \(service) instead") - } - } -} diff --git a/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift b/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift deleted file mode 100644 index d74ba5017f9d..000000000000 --- a/Tests/WordPressAuthenticatorTests/SingIn/SiteAddressViewModelTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -import XCTest -import WordPressKit -@testable import WordPressAuthenticator - -final class SiteAddressViewModelTests: XCTestCase { - private var isSiteDiscovery: Bool! - private var xmlrpcFacade: MockWordPressXMLRPCAPIFacade! - private var authenticationDelegateSpy: WordPressAuthenticatorDelegateSpy! - private var blogService: MockWordPressComBlogService! - private var loginFields: LoginFields! - private var viewModel: SiteAddressViewModel! - - override func setUp() { - super.setUp() - isSiteDiscovery = false - xmlrpcFacade = MockWordPressXMLRPCAPIFacade() - authenticationDelegateSpy = WordPressAuthenticatorDelegateSpy() - blogService = MockWordPressComBlogService() - loginFields = LoginFields() - - WordPressAuthenticator.initializeForTesting() - - viewModel = SiteAddressViewModel(isSiteDiscovery: isSiteDiscovery, xmlrpcFacade: xmlrpcFacade, authenticationDelegate: authenticationDelegateSpy, blogService: blogService, loginFields: loginFields) - } - - func testGuessXMLRPCURLSuccess() { - xmlrpcFacade.success = true - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://wordpress.com", loading: { _ in }) { res in - result = res - } - - XCTAssertEqual(result, .success) - } - - func testGuessXMLRPCURLError() { - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://error.com", loading: { _ in }) { res in - result = res - } - if case .error(let error, _) = result { - XCTAssertEqual(error.code, 1) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCURLErrorInvalidNotWP() { - xmlrpcFacade.error = WordPressOrgXMLRPCValidatorError.invalid as NSError - blogService.isWP = false - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://invalid.com", loading: { _ in }) { res in - result = res - } - - if case .error(let error, _) = result { - XCTAssertEqual(error.code, WordPressOrgXMLRPCValidatorError.invalid.rawValue) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCURLErrorInvalidIsWP() { - xmlrpcFacade.error = WordPressOrgXMLRPCValidatorError.invalid as NSError - blogService.isWP = true - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://invalidwp.com", loading: { _ in }) { res in - result = res - } - if case .error(let error, _) = result { - XCTAssertEqual(error.code, WordPressOrgXMLRPCValidatorError.xmlrpc_missing.rawValue) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } - - func testGuessXMLRPCTroubleshootSite() { - viewModel = SiteAddressViewModel(isSiteDiscovery: true, xmlrpcFacade: xmlrpcFacade, authenticationDelegate: authenticationDelegateSpy, blogService: blogService, loginFields: loginFields) - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://troubleshoot.com", loading: { _ in }) { res in - result = res - } - XCTAssertEqual(result, .troubleshootSite) - } - - func testGuessXMLRPCURLErrorHandledByDelegate() { - xmlrpcFacade.error = NSError(domain: "SomeDomain", code: 1, userInfo: nil) - authenticationDelegateSpy.shouldHandleError = true - - var result: SiteAddressViewModel.GuessXMLRPCURLResult? - viewModel.guessXMLRPCURL(for: "https://delegatehandles.com", loading: { _ in }) { res in - result = res - } - - if case .customUI = result { - XCTAssertTrue(true) - } else { - XCTFail("Unexpected result: \(String(describing: result))") - } - } -} - -private class MockWordPressXMLRPCAPIFacade: WordPressXMLRPCAPIFacade { - var success: Bool = false - var error: NSError? - - override func guessXMLRPCURL(forSite siteAddress: String, success: @escaping (URL?) -> (), failure: @escaping (Error?) -> ()) { - if self.success { - success(URL(string: "https://successful.site")) - } else { - failure(self.error) - } - } -} - -private class MockWordPressComBlogService: WordPressComBlogService { - var isWP = false - - override func fetchUnauthenticatedSiteInfoForAddress(for address: String, success: @escaping (WordPressComSiteInfo) -> Void, failure: @escaping (Error) -> Void) { - let siteInfo = WordPressComSiteInfo(remote: ["isWordPress": isWP]) - success(siteInfo) - } -} diff --git a/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan b/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan deleted file mode 100644 index ad7dd91d5a76..000000000000 --- a/Tests/WordPressAuthenticatorTests/WordPressAuthenticator.xctestplan +++ /dev/null @@ -1,24 +0,0 @@ -{ - "configurations" : [ - { - "id" : "EBC0D38A-5F0A-4C69-BBED-1B55B7711736", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:WordPress.xcodeproj", - "identifier" : "4AD953BA2C21451700D0EEFA", - "name" : "WordPressAuthenticatorTests" - } - } - ], - "version" : 1 -} diff --git a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift index 7f91da481216..d05383d4f0cb 100644 --- a/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift +++ b/WordPress/Classes/Login/SelfHostedSiteAuthenticator.swift @@ -338,12 +338,7 @@ struct SelfHostedSiteAuthenticator { if let options = responseObject as? [AnyHashable: Any] { continuation.resume(returning: options) } else { - let error = NSError( - domain: "WordPressOrgXMLRPCApiErrorDomain", - code: 7, // responseSerializationFailed - userInfo: [NSLocalizedDescriptionKey: "Unable to read the WordPress site at that URL."] - ) - continuation.resume(throwing: error) + continuation.resume(throwing: WordPressOrgXMLRPCApiError.responseSerializationFailed) } } failure: { error, _ in continuation.resume(throwing: error) diff --git a/WordPress/Classes/Login/WordPressDotComAuthenticator.swift b/WordPress/Classes/Login/WordPressDotComAuthenticator.swift index 571a37714f23..703a3299e362 100644 --- a/WordPress/Classes/Login/WordPressDotComAuthenticator.swift +++ b/WordPress/Classes/Login/WordPressDotComAuthenticator.swift @@ -52,7 +52,9 @@ struct WordPressDotComAuthenticator { case loadingSites(Error) } - private static let callbackNotification = Foundation.Notification.Name(rawValue: "WordPressDotComAuthenticatorCallbackURL") + private static let callbackNotification = Foundation.Notification.Name( + rawValue: "WordPressDotComAuthenticatorCallbackURL" + ) static func redirectURI(for scheme: String) -> String { "\(scheme)://oauth2-callback" @@ -96,7 +98,10 @@ struct WordPressDotComAuthenticator { /// - Parameters: /// - email: When provided, the signed-in account must be the account with the given email address. @MainActor - func signIn(from viewController: UIViewController, context: SignInContext) async -> TaggedManagedObjectID? { + func signIn( + from viewController: UIViewController, + context: SignInContext + ) async -> TaggedManagedObjectID? { WPAnalytics.track(.wpcomWebSignIn, properties: ["stage": "start"]) do { let account = try await attemptSignIn(from: viewController, context: context) @@ -116,7 +121,10 @@ struct WordPressDotComAuthenticator { /// - Parameters: /// - email: When provided, the signed-in account must be the account with the given email address. @MainActor - func attemptSignIn(from viewController: UIViewController, context: SignInContext) async throws(SignInError) -> TaggedManagedObjectID { + func attemptSignIn( + from viewController: UIViewController, + context: SignInContext + ) async throws(SignInError) -> TaggedManagedObjectID { let defaultAccount = try? WPAccount.lookupDefaultWordPressComAccount(in: coreDataStack.mainContext) let hasAlreadySignedIn = defaultAccount != nil @@ -125,7 +133,11 @@ struct WordPressDotComAuthenticator { if let tokenLaunchArgument = UserDefaults.standard.string(forKey: "ui-test-wpcom-token") { token = tokenLaunchArgument } else { - token = try await authenticate(from: viewController, prefersEphemeralWebBrowserSession: hasAlreadySignedIn, accountEmail: context.accountEmail(in: coreDataStack.mainContext)) + token = try await authenticate( + from: viewController, + prefersEphemeralWebBrowserSession: hasAlreadySignedIn, + accountEmail: context.accountEmail(in: coreDataStack.mainContext) + ) } } catch { throw .authentication(error) @@ -143,9 +155,17 @@ struct WordPressDotComAuthenticator { // Fetch WP.com account details let user: RemoteUser do { - let service = AccountServiceRemoteREST(wordPressComRestApi: WordPressComRestApi.defaultApi(oAuthToken: token, userAgent: WPUserAgent.wordPress())) + let service = AccountServiceRemoteREST( + wordPressComRestApi: WordPressComRestApi.defaultApi( + oAuthToken: token, + userAgent: WPUserAgent.wordPress() + ) + ) user = try await withCheckedThrowingContinuation { continuation in - service.getAccountDetails(success: { continuation.resume(returning: $0!) }, failure: { continuation.resume(throwing: $0!) }) + service.getAccountDetails( + success: { continuation.resume(returning: $0!) }, + failure: { continuation.resume(throwing: $0!) } + ) } } catch { throw .fetchUser(error) @@ -178,10 +198,12 @@ struct WordPressDotComAuthenticator { } // Post a notification if the current signed-in account is set as the default account. - // This sending notification code exists because that's what the existing login system does. We can consider - // removing this notification once WordPressAuthenticator is removed. + // Several parts of the app (My Sites, widgets, shortcuts, sidebar) observe this + // notification to react to a completed sign-in. if case .default = context { - let notification = Foundation.Notification.Name(rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification) + let notification = Foundation.Notification.Name( + rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification + ) let newAccount = try? coreDataStack.mainContext.existingObject(with: accountID) NotificationCenter.default.post(name: notification, object: newAccount) } @@ -218,7 +240,7 @@ struct WordPressDotComAuthenticator { "client_id": clientId, "redirect_uri": redirectURI, "response_type": "code", - "scope": "global", + "scope": "global" ] if let accountEmail { queries["user_email"] = accountEmail @@ -227,16 +249,31 @@ struct WordPressDotComAuthenticator { // Using Alamofire instead of URL to encode query string because URL do not encoded "+" (which may present // in user's email) in query. WP.com treat "+" in URL query as a whitespace, which cause the login page to // prepopulate the email address incorrectly, i.e. "foo+bar@baz.com" shows as "foo bar@baz.com" - let authorizeURL = try? URLEncoding.queryString.encode(URLRequest(url: URL(string: "https://public-api.wordpress.com/oauth2/authorize")!), with: queries).url + let authorizeURL = try? URLEncoding.queryString + .encode(URLRequest(url: URL(string: "https://public-api.wordpress.com/oauth2/authorize")!), with: queries) + .url guard let authorizeURL else { throw .urlError(URLError(.badURL)) } - let callbackURL = try await authorize(from: viewController, url: authorizeURL, prefersEphemeralWebBrowserSession: prefersEphemeralWebBrowserSession, redirectURI: redirectURI) + let callbackURL = try await authorize( + from: viewController, + url: authorizeURL, + prefersEphemeralWebBrowserSession: prefersEphemeralWebBrowserSession, + redirectURI: redirectURI + ) do { - return try await handleAuthorizeCallbackURL(callbackURL, clientId: clientId, clientSecret: clientSecret, redirectURI: redirectURI) + return try await handleAuthorizeCallbackURL( + callbackURL, + clientId: clientId, + clientSecret: clientSecret, + redirectURI: redirectURI + ) } catch { if case .loginDenied = error, recoverDenyAccess { - return try await self.recoverLoginDeniedError(viewController: viewController, accountEmail: accountEmail) + return try await self.recoverLoginDeniedError( + viewController: viewController, + accountEmail: accountEmail + ) } else { throw error } @@ -244,7 +281,12 @@ struct WordPressDotComAuthenticator { } @MainActor - private func authorize(from viewController: UIViewController, url authorizeURL: URL, prefersEphemeralWebBrowserSession: Bool, redirectURI: String) async throws(AuthenticationError) -> URL { + private func authorize( + from viewController: UIViewController, + url authorizeURL: URL, + prefersEphemeralWebBrowserSession: Bool, + redirectURI: String + ) async throws(AuthenticationError) -> URL { if let authenticator { return try authenticator(authorizeURL) } @@ -279,7 +321,9 @@ struct WordPressDotComAuthenticator { let callbackURLViaWebAuthenticationSession = PassthroughSubject() let provider = WebAuthenticationPresentationAnchorProvider(anchor: viewController.view.window ?? UIWindow()) - let session = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: redirectURIScheme) { url, error in + let session = ASWebAuthenticationSession(url: authorizeURL, callbackURLScheme: redirectURIScheme) { + url, + error in if let url { callbackURLViaWebAuthenticationSession.send(url) callbackURLViaWebAuthenticationSession.send(completion: .finished) @@ -295,17 +339,21 @@ struct WordPressDotComAuthenticator { do { return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in - cancellable.cancellable = Publishers.Merge(callbackURLViaOpenAppURL, callbackURLViaWebAuthenticationSession) + cancellable.cancellable = + Publishers.Merge(callbackURLViaOpenAppURL, callbackURLViaWebAuthenticationSession) .first() - .sink(receiveCompletion: { [session] in - session.cancel() - - if case let .failure(error) = $0 { - continuation.resume(throwing: error) + .sink( + receiveCompletion: { [session] in + session.cancel() + + if case let .failure(error) = $0 { + continuation.resume(throwing: error) + } + }, + receiveValue: { url in + continuation.resume(returning: url) } - }, receiveValue: { url in - continuation.resume(returning: url) - }) + ) } } onCancel: { [session] in session.cancel() @@ -347,7 +395,7 @@ struct WordPressDotComAuthenticator { "client_id": clientId, "client_secret": clientSecret, "redirect_uri": redirectURI, - "code": code, + "code": code ] do { @@ -381,7 +429,10 @@ struct WordPressDotComAuthenticator { } // Present an alert to ask the user to re-authenticate after they tap the "Deny" button. - private func recoverLoginDeniedError(viewController: UIViewController, accountEmail: String?) async throws(AuthenticationError) -> String { + private func recoverLoginDeniedError( + viewController: UIViewController, + accountEmail: String? + ) async throws(AuthenticationError) -> String { let reLogin = await withCheckedContinuation { continuation in DispatchQueue.main.async { let alert = UIAlertController( @@ -389,12 +440,16 @@ struct WordPressDotComAuthenticator { message: Strings.loginDeniedAlertMessage(), preferredStyle: .alert ) - alert.addAction(UIAlertAction(title: SharedStrings.Button.close, style: .cancel) { _ in - continuation.resume(returning: false) - }) - alert.addAction(UIAlertAction(title: Strings.useDifferentAccount, style: .default) { _ in - continuation.resume(returning: true) - }) + alert.addAction( + UIAlertAction(title: SharedStrings.Button.close, style: .cancel) { _ in + continuation.resume(returning: false) + } + ) + alert.addAction( + UIAlertAction(title: Strings.useDifferentAccount, style: .default) { _ in + continuation.resume(returning: true) + } + ) viewController.present(alert, animated: true) } } @@ -404,7 +459,12 @@ struct WordPressDotComAuthenticator { } // Use an ephemeral session here to ignore the existing account in Safari and allow user to sign in with whatever account they'd like to use. - return try await self.authenticate(from: viewController, prefersEphemeralWebBrowserSession: true, accountEmail: accountEmail, recoverDenyAccess: false) + return try await self.authenticate( + from: viewController, + prefersEphemeralWebBrowserSession: true, + accountEmail: accountEmail, + recoverDenyAccess: false + ) } } @@ -448,14 +508,48 @@ private extension WordPressDotComAuthenticator.AuthenticationError { } private enum Strings { - static let accessDenied = NSLocalizedString("wpComLogin.error.accessDenied", value: "Access denied. You need to approve to log in to WordPress.com", comment: "Error message when user denies access to WordPress.com") - static let loginDeniedTitle = NSLocalizedString("wpComLogin.loginDenied.title", value: "Login Cancelled", comment: "Title of alert shown when user cancels WordPress.com login") - static let loginDeniedMessage = NSLocalizedString("wpComLogin.loginDenied.message", value: "You can sign in with a different account if you need a different one. Tap \"%@\" to start.", comment: "Message shown when user denies WordPress.com login, offering option to try with different account") - static let useDifferentAccount = NSLocalizedString("wpComLogin.loginDenied.useDifferentAccount", value: "Use Different Account", comment: "Button title for signing in with a different WordPress.com account") - static let fetchUserError = NSLocalizedString("wpComLogin.error.fetchUser", value: "Failed to load user details", comment: "Error message when failing to load user details during WordPress.com login") - static let mismatchedEmail = NSLocalizedString("wpComLogin.error.mismatchedEmail", value: "Please sign in with email address %@", comment: "Error message when user signs in with an unexpected email address. The first argument is the expected email address") - static let alreadySignedIn = NSLocalizedString("wpComLogin.error.alreadySignedIn", value: "You have already signed in with email address %@. Please sign out try again.", comment: "Error message when user signs in with an different account than the account that's alredy signed in. The first argument is the current signed-in account email address") - static let loadingSitesError = NSLocalizedString("wpComLogin.error.loadingSites", value: "Your account's sites cannot be loaded. Please try again later.", comment: "Error message when failing to load account's site after signing in") + static let accessDenied = NSLocalizedString( + "wpComLogin.error.accessDenied", + value: "Access denied. You need to approve to log in to WordPress.com", + comment: "Error message when user denies access to WordPress.com" + ) + static let loginDeniedTitle = NSLocalizedString( + "wpComLogin.loginDenied.title", + value: "Login Cancelled", + comment: "Title of alert shown when user cancels WordPress.com login" + ) + static let loginDeniedMessage = NSLocalizedString( + "wpComLogin.loginDenied.message", + value: "You can sign in with a different account if you need a different one. Tap \"%@\" to start.", + comment: "Message shown when user denies WordPress.com login, offering option to try with different account" + ) + static let useDifferentAccount = NSLocalizedString( + "wpComLogin.loginDenied.useDifferentAccount", + value: "Use Different Account", + comment: "Button title for signing in with a different WordPress.com account" + ) + static let fetchUserError = NSLocalizedString( + "wpComLogin.error.fetchUser", + value: "Failed to load user details", + comment: "Error message when failing to load user details during WordPress.com login" + ) + static let mismatchedEmail = NSLocalizedString( + "wpComLogin.error.mismatchedEmail", + value: "Please sign in with email address %@", + comment: + "Error message when user signs in with an unexpected email address. The first argument is the expected email address" + ) + static let alreadySignedIn = NSLocalizedString( + "wpComLogin.error.alreadySignedIn", + value: "You have already signed in with email address %@. Please sign out try again.", + comment: + "Error message when user signs in with an different account than the account that's alredy signed in. The first argument is the current signed-in account email address" + ) + static let loadingSitesError = NSLocalizedString( + "wpComLogin.error.loadingSites", + value: "Your account's sites cannot be loaded. Please try again later.", + comment: "Error message when failing to load account's site after signing in" + ) static func loginDeniedAlertMessage() -> String { String(format: loginDeniedMessage, useDifferentAccount) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index ccc9e0f7a4a2..bec4b3a95dca 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -48,8 +48,6 @@ 0C5A8A7D2D9B22F100C25301 /* React.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D38F2D2C4BA3008ACD86 /* React.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5A8A7E2D9B22F100C25301 /* RNTAztecView.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; }; 0C5A8A7F2D9B22F100C25301 /* RNTAztecView.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 0C5A8A802D9B22F100C25301 /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 0C5A8A812D9B22F100C25301 /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5A8A842D9B22F100C25301 /* yoga.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; }; 0C5A8A852D9B22F100C25301 /* yoga.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C5F80182E8E98A200D3F8EC /* XcodeTarget_UITests in Frameworks */ = {isa = PBXBuildFile; productRef = 0C5F80172E8E98A200D3F8EC /* XcodeTarget_UITests */; }; @@ -64,7 +62,6 @@ 0C6AC6222C364A7B00BF7600 /* XcodeTarget_NotificationServiceExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6212C364A7B00BF7600 /* XcodeTarget_NotificationServiceExtension */; }; 0C6AC6242C364A8000BF7600 /* XcodeTarget_StatsWidget in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6232C364A8000BF7600 /* XcodeTarget_StatsWidget */; }; 0C6AC6262C364A8500BF7600 /* XcodeTarget_Intents in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6252C364A8500BF7600 /* XcodeTarget_Intents */; }; - 0C6AC6282C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator in Frameworks */ = {isa = PBXBuildFile; productRef = 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */; }; 0CCA99512DAD76AD0048F0A9 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 0CCA99592DAD76BA0048F0A9 /* Noticons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F5A34D0C25DF2F7700C9654B /* Noticons.ttf */; }; 0CCA995A2DAD76C20048F0A9 /* n.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5D69DBC3165428CA00A2D1F7 /* n.caf */; }; @@ -75,15 +72,11 @@ 0CECA92D2E043D0200F4EE83 /* XcodeTarget_App in Frameworks */ = {isa = PBXBuildFile; productRef = 0CECA92C2E043D0200F4EE83 /* XcodeTarget_App */; }; 0CECA92E2E043D2800F4EE83 /* WordPressData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */; }; 0CECA92F2E043D2800F4EE83 /* WordPressData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 0CECA9332E043D3700F4EE83 /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 0CECA9342E043D3700F4EE83 /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0CED2AD92D95BB46003015CF /* Gutenberg.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3902D2C4BA3008ACD86 /* Gutenberg.xcframework */; }; 0CED2ADB2D95BB46003015CF /* hermes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3922D2C4BA3008ACD86 /* hermes.xcframework */; }; 0CED2ADD2D95BB46003015CF /* React.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D38F2D2C4BA3008ACD86 /* React.xcframework */; }; 0CED2ADF2D95BB46003015CF /* RNTAztecView.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3912D2C4BA3008ACD86 /* RNTAztecView.xcframework */; }; - 0CED2AE12D95BB46003015CF /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; 0CED2AE52D95BB46003015CF /* yoga.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3F60D3932D2C4BA4008ACD86 /* yoga.xcframework */; }; - 0CFFFECB2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests in Frameworks */ = {isa = PBXBuildFile; productRef = 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */; }; 1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; }; 24351254264DCA08009BB2B6 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24351253264DCA08009BB2B6 /* Secrets.swift */; }; 24351255264DCA08009BB2B6 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24351253264DCA08009BB2B6 /* Secrets.swift */; }; @@ -125,8 +118,6 @@ 433840C722C2BA5B00CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 433840C822C2BA6300CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; 433840C922C2BA6400CB13F8 /* AppImages.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 433840C622C2BA5B00CB13F8 /* AppImages.xcassets */; }; - 4A0274862C224FB000290D8B /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4A0274872C224FB000290D8B /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4A690C152BA791B100A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C142BA790BC00A8E0C5 /* PrivacyInfo.xcprivacy */; }; 4A690C162BA791B200A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C142BA790BC00A8E0C5 /* PrivacyInfo.xcprivacy */; }; 4A690C182BA794C800A8E0C5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4A690C172BA794C300A8E0C5 /* PrivacyInfo.xcprivacy */; }; @@ -145,10 +136,6 @@ 4ABCAB3A2DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ABCAB392DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift */; }; 4AC9545A2DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC954592DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift */; }; 4AC9F8182DE528E40095EA51 /* Secrets-WordPressShareExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC9F8172DE528E40095EA51 /* Secrets-WordPressShareExtension.swift */; }; - 4AD953C72C21451700D0EEFA /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4AD953C82C21451700D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 4AD9555A2C21716A00D0EEFA /* WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; }; - 4AD9555B2C21716A00D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5D69DBC4165428CA00A2D1F7 /* n.caf in Resources */ = {isa = PBXBuildFile; fileRef = 5D69DBC3165428CA00A2D1F7 /* n.caf */; }; 7335AC6021220D550012EF2D /* RemoteNotificationActionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7335AC5F21220D550012EF2D /* RemoteNotificationActionParser.swift */; }; 7358E6BF210BD318002323EB /* WordPressNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7358E6B8210BD318002323EB /* WordPressNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -294,13 +281,6 @@ remoteGlobalIDString = 0CED016F2D95B897003015CF; remoteInfo = Keystone; }; - 0C5A8A822D9B22F100C25301 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 0CECA9302E043D2800F4EE83 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -308,20 +288,6 @@ remoteGlobalIDString = 3F7AE0B42D9B30A100AB4892; remoteInfo = WordPressData; }; - 0CECA9352E043D3700F4EE83 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 0CED2AE32D95BB46003015CF /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 3F0FD9FE2D9B92F700CD05D6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -364,27 +330,6 @@ remoteGlobalIDString = 1D6058900D05DD3D006BFB54; remoteInfo = WordPress; }; - 4AD953BD2C21451700D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 4AD953C52C21451700D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; - 4AD9555C2C21716A00D0EEFA /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 4AD953B32C21451700D0EEFA; - remoteInfo = WordPressAuthenticator; - }; 7358E6BD210BD318002323EB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; @@ -479,7 +424,6 @@ files = ( 0C5A8A752D9B22F100C25301 /* Gutenberg.xcframework in Embed Frameworks */, 0C5A8A792D9B22F100C25301 /* WordPress.framework in Embed Frameworks */, - 0C5A8A812D9B22F100C25301 /* WordPressAuthenticator.framework in Embed Frameworks */, 0C5A8A772D9B22F100C25301 /* hermes.xcframework in Embed Frameworks */, 0C5A8A852D9B22F100C25301 /* yoga.xcframework in Embed Frameworks */, 0C5A8A7D2D9B22F100C25301 /* React.xcframework in Embed Frameworks */, @@ -559,39 +503,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 0C68E7832C35F9320023DB42 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 0CECA9322E043D2800F4EE83 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 0CECA9342E043D3700F4EE83 /* WordPressAuthenticator.framework in Embed Frameworks */, 0CECA92F2E043D2800F4EE83 /* WordPressData.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 4A0274882C224FB000290D8B /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 4A0274872C224FB000290D8B /* WordPressAuthenticator.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; 4AD953C92C21451800D0EEFA /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -600,7 +522,6 @@ files = ( 3F60D3972D2C4BA4008ACD86 /* Gutenberg.xcframework in Embed Frameworks */, 3F60D3992D2C4BA4008ACD86 /* RNTAztecView.xcframework in Embed Frameworks */, - 4AD953C82C21451700D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */, 3F60D3952D2C4BA4008ACD86 /* React.xcframework in Embed Frameworks */, 3F0FD9FD2D9B92F700CD05D6 /* WordPressData.framework in Embed Frameworks */, 3F60D39B2D2C4BA4008ACD86 /* hermes.xcframework in Embed Frameworks */, @@ -615,7 +536,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 4AD9555B2C21716A00D0EEFA /* WordPressAuthenticator.framework in Embed Frameworks */, 3F608F822D2D1A9E008ACD86 /* hermes.xcframework in Embed Frameworks */, 3F608F802D2D1A9E008ACD86 /* Gutenberg.xcframework in Embed Frameworks */, 3F608F842D2D1A9E008ACD86 /* React.xcframework in Embed Frameworks */, @@ -790,8 +710,6 @@ 4ABCAB392DE533A5005A6B84 /* Secrets-WordPressNotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-WordPressNotificationServiceExtension.swift"; path = "../Secrets/Secrets-WordPressNotificationServiceExtension.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 4AC954592DE51FE40095EA51 /* Secrets-JetpackStatsWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-JetpackStatsWidgets.swift"; path = "../Secrets/Secrets-JetpackStatsWidgets.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 4AC9F8172DE528E40095EA51 /* Secrets-WordPressShareExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Secrets-WordPressShareExtension.swift"; path = "../Secrets/Secrets-WordPressShareExtension.swift"; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WordPressAuthenticator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WordPressAuthenticatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5D69DBC3165428CA00A2D1F7 /* n.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = n.caf; path = Resources/Sounds/n.caf; sourceTree = ""; }; 6EDC0E8E105881A800F68A1D /* iTunesArtwork */ = {isa = PBXFileReference; lastKnownFileType = file; path = iTunesArtwork; sourceTree = ""; }; 7335AC5F21220D550012EF2D /* RemoteNotificationActionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteNotificationActionParser.swift; sourceTree = ""; }; @@ -949,17 +867,6 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 0C2390242D9ADFAA00981631 /* Exceptions for "WordPressAuthenticator" folder in "WordPressAuthenticator" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - publicHeaders = ( - Features/NUX/WPNUXMainButton.h, - Features/NUX/WPWalkthroughTextField.h, - Helpers/LoginFacade.h, - Helpers/WordPressXMLRPCAPIFacade.h, - WordPressAuthenticator.h, - ); - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - }; 0C3CC0152DA058E6009F3BFB /* Exceptions for "WordPress" folder in "WordPress" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -1134,14 +1041,6 @@ path = Sources; sourceTree = ""; }; - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 0C2390242D9ADFAA00981631 /* Exceptions for "WordPressAuthenticator" folder in "WordPressAuthenticator" target */, - ); - path = WordPressAuthenticator; - sourceTree = ""; - }; 0C3313B82E0439A8000C3760 /* Miniature */ = { isa = PBXFileSystemSynchronizedRootGroup; path = Miniature; @@ -1176,11 +1075,6 @@ path = JetpackStatsWidgets; sourceTree = ""; }; - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = WordPressAuthenticatorTests; - sourceTree = ""; - }; 0C5A3FAB2D9B1EF400C25301 /* Reader */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -1277,7 +1171,6 @@ files = ( 0CECA92E2E043D2800F4EE83 /* WordPressData.framework in Frameworks */, 0CECA92B2E043CEC00F4EE83 /* XcodeTarget_App in Frameworks */, - 0CECA9332E043D3700F4EE83 /* WordPressAuthenticator.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1296,7 +1189,6 @@ 0C5A8A742D9B22F100C25301 /* Gutenberg.xcframework in Frameworks */, 0C28A01C2DAD7CFD00F81F20 /* XcodeTarget_App in Frameworks */, 0C5A8A782D9B22F100C25301 /* WordPress.framework in Frameworks */, - 0C5A8A802D9B22F100C25301 /* WordPressAuthenticator.framework in Frameworks */, 0C5A8A762D9B22F100C25301 /* hermes.xcframework in Frameworks */, 0C5A8A842D9B22F100C25301 /* yoga.xcframework in Frameworks */, 0C5A8A7C2D9B22F100C25301 /* React.xcframework in Frameworks */, @@ -1310,7 +1202,6 @@ buildActionMask = 2147483647; files = ( 0CED2ADB2D95BB46003015CF /* hermes.xcframework in Frameworks */, - 0CED2AE12D95BB46003015CF /* WordPressAuthenticator.framework in Frameworks */, 0CED2AD92D95BB46003015CF /* Gutenberg.xcframework in Frameworks */, 0CED2AE52D95BB46003015CF /* yoga.xcframework in Frameworks */, 3F1AFCC22DA3AAEA00786B92 /* WebKit.framework in Frameworks */, @@ -1339,7 +1230,6 @@ 93E5285619A77BAC003A1A9C /* NotificationCenter.framework in Frameworks */, 93A3F7DE1843F6F00082FEEA /* CoreTelephony.framework in Frameworks */, A01C542E0E24E88400D411F2 /* SystemConfiguration.framework in Frameworks */, - 4AD953C72C21451700D0EEFA /* WordPressAuthenticator.framework in Frameworks */, 374CB16215B93C0800DD0EBC /* AudioToolbox.framework in Frameworks */, E10B3655158F2D7800419A93 /* CoreGraphics.framework in Frameworks */, 24E55D4F2CC9A5CD008D071D /* ImagePlayground.framework in Frameworks */, @@ -1391,23 +1281,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B12C21451700D0EEFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0C6AC6282C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B82C21451700D0EEFA /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0CFFFECB2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests in Frameworks */, - 4A0274862C224FB000290D8B /* WordPressAuthenticator.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B5210BD318002323EB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1499,7 +1372,6 @@ 3F608F852D2D1A9E008ACD86 /* RNTAztecView.xcframework in Frameworks */, FABB26302602FC2C00C8785C /* AVFoundation.framework in Frameworks */, FABB26312602FC2C00C8785C /* Foundation.framework in Frameworks */, - 4AD9555A2C21716A00D0EEFA /* WordPressAuthenticator.framework in Frameworks */, 3F608F872D2D1A9E008ACD86 /* yoga.xcframework in Frameworks */, FABB26322602FC2C00C8785C /* Security.framework in Frameworks */, FABB26332602FC2C00C8785C /* MapKit.framework in Frameworks */, @@ -1543,7 +1415,6 @@ children = ( 0C73A9BD2DAEDFDE00CC0F3A /* KeystoneTests */, 0C3313C62E0439A9000C3760 /* MiniatureTests */, - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */, 3F7AE0C22D9B30A200AB4892 /* WordPressDataTests */, 4A8280FE2E5FE9B60037E180 /* WordPressKitTests */, ); @@ -1559,7 +1430,6 @@ 0C5A3FAB2D9B1EF400C25301 /* Reader */, 0C5C46FE2D98397A00F2CD55 /* Keystone */, 0C3313B82E0439A8000C3760 /* Miniature */, - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */, 3F7AE0B62D9B30A100AB4892 /* WordPressData */, 0C3E79892DB164B3000C7072 /* JetpackStatsWidgets */, ); @@ -1583,8 +1453,6 @@ 80F6D05428EE866A00953C1A /* JetpackNotificationServiceExtension.appex */, 0107E0EA28F97D5000DE87DB /* JetpackStatsWidgets.appex */, 0107E15428FE9DB200DE87DB /* JetpackIntents.appex */, - 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */, - 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */, 0CED01702D95B897003015CF /* WordPress.framework */, 0C5A3F8C2D9B1E3700C25301 /* Reader.app */, 3F7AE0B52D9B30A100AB4892 /* WordPressData.framework */, @@ -1962,13 +1830,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953AF2C21451700D0EEFA /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -2032,7 +1893,6 @@ ); dependencies = ( 0CECA9312E043D2800F4EE83 /* PBXTargetDependency */, - 0CECA9362E043D3700F4EE83 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 0C3313B82E0439A8000C3760 /* Miniature */, @@ -2083,7 +1943,6 @@ ); dependencies = ( 0C5A8A7B2D9B22F100C25301 /* PBXTargetDependency */, - 0C5A8A832D9B22F100C25301 /* PBXTargetDependency */, 0C28C63C2DAD830F00F81F20 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2111,7 +1970,6 @@ buildRules = ( ); dependencies = ( - 0CED2AE42D95BB46003015CF /* PBXTargetDependency */, 0C128FED2D9ECE6200C69EBA /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2147,7 +2005,6 @@ 932225B01C7CE50300443B02 /* PBXTargetDependency */, 7457667B202B558C00F42E40 /* PBXTargetDependency */, 7358E6BE210BD318002323EB /* PBXTargetDependency */, - 4AD953C62C21451700D0EEFA /* PBXTargetDependency */, 3F0FD9FF2D9B92F700CD05D6 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2236,56 +2093,6 @@ productReference = 4A8280FD2E5FE9B60037E180 /* WordPressKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4AD953D22C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticator" */; - buildPhases = ( - 4AD953AF2C21451700D0EEFA /* Headers */, - 4AD953B02C21451700D0EEFA /* Sources */, - 4AD953B12C21451700D0EEFA /* Frameworks */, - 4AD953B22C21451700D0EEFA /* Resources */, - 0C68E7832C35F9320023DB42 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - 0C238F782D9ADF0200981631 /* WordPressAuthenticator */, - ); - name = WordPressAuthenticator; - packageProductDependencies = ( - 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */, - ); - productName = WordPressAuthenticator; - productReference = 4AD953B42C21451700D0EEFA /* WordPressAuthenticator.framework */; - productType = "com.apple.product-type.framework"; - }; - 4AD953BA2C21451700D0EEFA /* WordPressAuthenticatorTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 4AD953D32C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticatorTests" */; - buildPhases = ( - 4AD953B72C21451700D0EEFA /* Sources */, - 4AD953B82C21451700D0EEFA /* Frameworks */, - 4AD953B92C21451700D0EEFA /* Resources */, - 4A0274882C224FB000290D8B /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 4AD953BE2C21451700D0EEFA /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 0C5A1A042D9B080900C25301 /* WordPressAuthenticatorTests */, - ); - name = WordPressAuthenticatorTests; - packageProductDependencies = ( - 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */, - ); - productName = WordPressAuthenticatorTests; - productReference = 4AD953BB2C21451700D0EEFA /* WordPressAuthenticatorTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 7358E6B7210BD318002323EB /* WordPressNotificationServiceExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 7358E6C4210BD318002323EB /* Build configuration list for PBXNativeTarget "WordPressNotificationServiceExtension" */; @@ -2496,7 +2303,6 @@ 8096212728E5411400940A5D /* PBXTargetDependency */, 8096219028E55F8600940A5D /* PBXTargetDependency */, 80F6D05F28EE88FC00953C1A /* PBXTargetDependency */, - 4AD9555D2C21716A00D0EEFA /* PBXTargetDependency */, 3F0FDA032D9B930100CD05D6 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( @@ -2586,12 +2392,6 @@ CreatedOnToolsVersion = 16.4; TestTargetID = 1D6058900D05DD3D006BFB54; }; - 4AD953B32C21451700D0EEFA = { - CreatedOnToolsVersion = 15.4; - }; - 4AD953BA2C21451700D0EEFA = { - CreatedOnToolsVersion = 15.4; - }; 7358E6B7210BD318002323EB = { CreatedOnToolsVersion = 9.4.1; LastSwiftMigration = 1000; @@ -2713,8 +2513,6 @@ 0107E0B128F97D5000DE87DB /* JetpackStatsWidgets */, 0107E13828FE9DB200DE87DB /* JetpackIntents */, 0C5A3F8B2D9B1E3700C25301 /* Reader */, - 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */, - 4AD953BA2C21451700D0EEFA /* WordPressAuthenticatorTests */, 0CED016F2D95B897003015CF /* Keystone */, 3F7AE0B42D9B30A100AB4892 /* WordPressData */, 3F7AE0BB2D9B30A200AB4892 /* WordPressDataTests */, @@ -2811,20 +2609,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B22C21451700D0EEFA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B92C21451700D0EEFA /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B6210BD318002323EB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3362,20 +3146,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 4AD953B02C21451700D0EEFA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 4AD953B72C21451700D0EEFA /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 7358E6B4210BD318002323EB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3507,26 +3277,11 @@ target = 0CED016F2D95B897003015CF /* Keystone */; targetProxy = 0C5A8A7A2D9B22F100C25301 /* PBXContainerItemProxy */; }; - 0C5A8A832D9B22F100C25301 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0C5A8A822D9B22F100C25301 /* PBXContainerItemProxy */; - }; 0CECA9312E043D2800F4EE83 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3F7AE0B42D9B30A100AB4892 /* WordPressData */; targetProxy = 0CECA9302E043D2800F4EE83 /* PBXContainerItemProxy */; }; - 0CECA9362E043D3700F4EE83 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0CECA9352E043D3700F4EE83 /* PBXContainerItemProxy */; - }; - 0CED2AE42D95BB46003015CF /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 0CED2AE32D95BB46003015CF /* PBXContainerItemProxy */; - }; 3F0FD9FF2D9B92F700CD05D6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3F7AE0B42D9B30A100AB4892 /* WordPressData */; @@ -3557,21 +3312,6 @@ target = 1D6058900D05DD3D006BFB54 /* WordPress */; targetProxy = 4A8281012E5FE9B60037E180 /* PBXContainerItemProxy */; }; - 4AD953BE2C21451700D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD953BD2C21451700D0EEFA /* PBXContainerItemProxy */; - }; - 4AD953C62C21451700D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD953C52C21451700D0EEFA /* PBXContainerItemProxy */; - }; - 4AD9555D2C21716A00D0EEFA /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 4AD953B32C21451700D0EEFA /* WordPressAuthenticator */; - targetProxy = 4AD9555C2C21716A00D0EEFA /* PBXContainerItemProxy */; - }; 7358E6BE210BD318002323EB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7358E6B7210BD318002323EB /* WordPressNotificationServiceExtension */; @@ -5183,271 +4923,6 @@ }; name = "Release-Alpha"; }; - 4AD953CA2C21451800D0EEFA /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F62CC214BF00DF1760 /* Common.debug.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 4AD953CB2C21451800D0EEFA /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C96AC202D92FC17000779B8 /* Common.release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 4AD953CD2C21451800D0EEFA /* Release-Alpha */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F72CC2188400DF1760 /* Common.alpha.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = ""; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 WordPress. All rights reserved."; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticator; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release-Alpha"; - }; - 4AD953CE2C21451800D0EEFA /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F62CC214BF00DF1760 /* Common.debug.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 4AD953CF2C21451800D0EEFA /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 0C96AC202D92FC17000779B8 /* Common.release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Manual; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 4AD953D12C21451800D0EEFA /* Release-Alpha */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F9DD3F72CC2188400DF1760 /* Common.alpha.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = org.wordpress.library.WordPressAuthenticatorTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_TREAT_WARNINGS_AS_ERRORS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = "Release-Alpha"; - }; 7358E6C0210BD318002323EB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = F14B5F70208E648200439554 /* WordPress.debug.xcconfig */; @@ -7157,26 +6632,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 4AD953D22C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticator" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4AD953CA2C21451800D0EEFA /* Debug */, - 4AD953CB2C21451800D0EEFA /* Release */, - 4AD953CD2C21451800D0EEFA /* Release-Alpha */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 4AD953D32C21451800D0EEFA /* Build configuration list for PBXNativeTarget "WordPressAuthenticatorTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 4AD953CE2C21451800D0EEFA /* Debug */, - 4AD953CF2C21451800D0EEFA /* Release */, - 4AD953D12C21451800D0EEFA /* Release-Alpha */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 7358E6C4210BD318002323EB /* Build configuration list for PBXNativeTarget "WordPressNotificationServiceExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -7360,10 +6815,6 @@ isa = XCSwiftPackageProductDependency; productName = XcodeTarget_Intents; }; - 0C6AC6272C364A9000BF7600 /* XcodeTarget_WordPressAuthentificator */ = { - isa = XCSwiftPackageProductDependency; - productName = XcodeTarget_WordPressAuthentificator; - }; 0CECA92A2E043CEC00F4EE83 /* XcodeTarget_App */ = { isa = XCSwiftPackageProductDependency; productName = XcodeTarget_App; @@ -7372,10 +6823,6 @@ isa = XCSwiftPackageProductDependency; productName = XcodeTarget_App; }; - 0CFFFECA2C36F5760044709B /* XcodeTarget_WordPressAuthentificatorTests */ = { - isa = XCSwiftPackageProductDependency; - productName = XcodeTarget_WordPressAuthentificatorTests; - }; 3F0F25A52D9BDB5800CD05D6 /* XcodeTarget_WordPressData */ = { isa = XCSwiftPackageProductDependency; productName = XcodeTarget_WordPressData; diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme index a6b1da6f3212..f98f3e373927 100644 --- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme +++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPress.xcscheme @@ -54,17 +54,6 @@ ReferencedContainer = "container:WordPress.xcodeproj"> - - - - diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme deleted file mode 100644 index dcb72daccf6b..000000000000 --- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/WordPressAuthenticator.xcscheme +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index ae5c693c6b71..e29a0f9c8876 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -194,7 +194,6 @@ def generate_strings_file(gutenberg_path:, derived_data_path:) paths: [ 'WordPress/', 'Modules/Sources/', - 'Sources/WordPressAuthenticator', gutenberg_path, *REMOTE_SWIFT_PACKAGES_TO_LOCALIZE.map { |name| File.join(derived_data_path, 'SourcePackages', 'checkouts', name, 'Sources') } ],