diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index e5cd27d7f..2476df13b 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressAuthenticator" - s.version = "1.33.0" + s.version = "1.34.0" s.summary = "WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps." s.description = <<-DESC diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index 773db338e..63b61c7d2 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; }; 1A21EE9822832BC300C940C6 /* WordPressComOAuthClientFacade+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */; }; 1A4095182271AEFC009AA86D /* WPAuthenticator-Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3108613125AFA4830022F75E /* PasteboardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3108613025AFA4830022F75E /* PasteboardTests.swift */; }; 3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */; }; 3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */; }; 3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5223DA4AC6007E5897 /* URLHandler.swift */; }; @@ -213,6 +214,7 @@ 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WordPressComOAuthClientFacade+Swift.swift"; sourceTree = ""; }; 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "WPAuthenticator-Swift.h"; sourceTree = ""; }; 276354F054C34AD36CA32AB6 /* Pods-WordPressAuthenticator.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-alpha.xcconfig"; sourceTree = ""; }; + 3108613025AFA4830022F75E /* PasteboardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardTests.swift; sourceTree = ""; }; 33FEF45B466FF8EAAE5F3923 /* Pods-WordPressAuthenticator.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release.xcconfig"; sourceTree = ""; }; 37AFD4EF492B00CA7AEC11A3 /* Pods-WordPressAuthenticatorTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticatorTests/Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; sourceTree = ""; }; 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelectorTests.swift; sourceTree = ""; }; @@ -535,6 +537,7 @@ B501C03D208FC52500D1E58F /* Authenticator */ = { isa = PBXGroup; children = ( + 3108613025AFA4830022F75E /* PasteboardTests.swift */, B501C03E208FC52500D1E58F /* WordPressAuthenticatorTests.swift */, CE16177721B70C1A00B82A47 /* WordPressAuthenticatorDisplayTextTests.swift */, BA53D64724DFDF97001F1ABF /* WordPressSourceTagTests.swift */, @@ -1362,6 +1365,7 @@ BA53D64B24DFE07D001F1ABF /* WordpressAuthenticatorProvider.swift in Sources */, CE16177821B70C1A00B82A47 /* WordPressAuthenticatorDisplayTextTests.swift in Sources */, B501C048208FC79C00D1E58F /* LoginFacadeTests.m in Sources */, + 3108613125AFA4830022F75E /* PasteboardTests.swift in Sources */, D85C36F0256E118D00D56E34 /* NavigationToEnterAccountTests.swift in Sources */, D85C36E6256E0DDE00D56E34 /* NavigationToEnterSiteTests.swift in Sources */, D85C3882256E3FEC00D56E34 /* WordPressComSiteInfoTests.swift in Sources */, diff --git a/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift b/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift index ac170f81f..19adecb99 100644 --- a/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift +++ b/WordPressAuthenticator/Authenticator/WordPressAuthenticator.swift @@ -156,20 +156,34 @@ import WordPressKit /// - 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) { - defer { - trackOpenedLogin() + 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) -> UIViewController? { let storyboard = Storyboard.login.instance - if let controller = storyboard.instantiateInitialViewController() { - if let childController = controller.children.first as? LoginPrologueViewController { - childController.loginFields.restrictToWPCom = restrictToWPCom - childController.showCancel = showCancel - childController.onLoginButtonTapped = onLoginButtonTapped - } - controller.modalPresentationStyle = .fullScreen - presenter.present(controller, animated: animated, completion: onCompletion) + guard let controller = storyboard.instantiateInitialViewController() else { + assertionFailure("Cannot instantiate initial login controller from Login.storyboard") + return nil + } + if let childController = controller.children.first as? LoginPrologueViewController { + childController.loginFields.restrictToWPCom = restrictToWPCom + childController.showCancel = showCancel + childController.onLoginButtonTapped = onLoginButtonTapped } + controller.modalPresentationStyle = .fullScreen + return controller } /// Used to present the new wpcom-only login flow from the app delegate diff --git a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorStyles.swift b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorStyles.swift index 3deb540aa..ff85df1d1 100644 --- a/WordPressAuthenticator/Authenticator/WordPressAuthenticatorStyles.swift +++ b/WordPressAuthenticator/Authenticator/WordPressAuthenticatorStyles.swift @@ -39,6 +39,9 @@ public struct WordPressAuthenticatorStyle { 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 @@ -109,6 +112,7 @@ public struct WordPressAuthenticatorStyle { primaryTitleColor: UIColor, secondaryTitleColor: UIColor, disabledTitleColor: UIColor, + disabledButtonActivityIndicatorColor: UIColor, textButtonColor: UIColor, textButtonHighlightColor: UIColor, instructionColor: UIColor, @@ -139,6 +143,7 @@ public struct WordPressAuthenticatorStyle { self.primaryTitleColor = primaryTitleColor self.secondaryTitleColor = secondaryTitleColor self.disabledTitleColor = disabledTitleColor + self.disabledButtonActivityIndicatorColor = disabledButtonActivityIndicatorColor self.textButtonColor = textButtonColor self.textButtonHighlightColor = textButtonHighlightColor self.instructionColor = instructionColor diff --git a/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift b/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift index 7ad6b3acb..5665be7b1 100644 --- a/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift +++ b/WordPressAuthenticator/Extensions/UIPasteboard+Detect.swift @@ -25,4 +25,53 @@ extension UIPasteboard { } } } + + /// 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/WordPressAuthenticator/NUX/NUXButton.swift b/WordPressAuthenticator/NUX/NUXButton.swift index 929d5fb16..2f045e87f 100644 --- a/WordPressAuthenticator/NUX/NUXButton.swift +++ b/WordPressAuthenticator/NUX/NUXButton.swift @@ -12,7 +12,7 @@ import WordPressKit open override var isEnabled: Bool { didSet { if #available(iOS 13, *) { - activityIndicator.color = isEnabled ? style.primaryTitleColor : style.secondaryTitleColor + activityIndicator.color = isEnabled ? style.primaryTitleColor : style.disabledButtonActivityIndicatorColor } } } diff --git a/WordPressAuthenticator/Signin/Login2FAViewController.swift b/WordPressAuthenticator/Signin/Login2FAViewController.swift index 8c87b2e78..5aa3f8d4f 100644 --- a/WordPressAuthenticator/Signin/Login2FAViewController.swift +++ b/WordPressAuthenticator/Signin/Login2FAViewController.swift @@ -296,14 +296,12 @@ class Login2FAViewController: LoginViewController, NUXKeyboardResponder, UITextF } if #available(iOS 14.0, *) { - UIPasteboard.general.detect(patterns: [.number]) { [weak self] result in + UIPasteboard.general.detectAuthenticatorCode() { [weak self] result in switch result { - case .success(let detections): - if let pasteString = detections.first?.value as? String { - self?.handle(code: pasteString) - } - case .failure: - break + case .success(let authenticatorCode): + self?.handle(code: authenticatorCode) + case .failure: + break } } } else { diff --git a/WordPressAuthenticator/Unified Auth/View Related/2FA/TwoFAViewController.swift b/WordPressAuthenticator/Unified Auth/View Related/2FA/TwoFAViewController.swift index 47b1fa955..8242e6710 100644 --- a/WordPressAuthenticator/Unified Auth/View Related/2FA/TwoFAViewController.swift +++ b/WordPressAuthenticator/Unified Auth/View Related/2FA/TwoFAViewController.swift @@ -318,13 +318,10 @@ private extension TwoFAViewController { } if #available(iOS 14.0, *) { - UIPasteboard.general.detect(patterns: [.number]) { [weak self] result in + UIPasteboard.general.detectAuthenticatorCode() { [weak self] result in switch result { - case .success(let detections): - if let pasteCode = detections.first?.value as? Int { - let pasteString = String(pasteCode) - self?.handle(code: pasteString, textField: codeField) - } + case .success(let authenticatorCode): + self?.handle(code: authenticatorCode, textField: codeField) case .failure: break } diff --git a/WordPressAuthenticatorTests/Authenticator/PasteboardTests.swift b/WordPressAuthenticatorTests/Authenticator/PasteboardTests.swift new file mode 100644 index 000000000..9fbe98560 --- /dev/null +++ b/WordPressAuthenticatorTests/Authenticator/PasteboardTests.swift @@ -0,0 +1,54 @@ +import XCTest + +class PasteboardTests: XCTestCase { + let timeout = TimeInterval(3) + + override class func tearDown() { + super.tearDown() + let pasteboard = UIPasteboard.general + pasteboard.string = "" + } + + func testNominalAuthCode() throws { + guard #available(iOS 14.0, *) else { + throw XCTSkip("Unsupported iOS version") + } + + let expect = expectation(description: "Could read nominal auth code from pasteboard") + let pasteboard = UIPasteboard.general + pasteboard.string = "123456" + + UIPasteboard.general.detectAuthenticatorCode() { result in + switch result { + case .success(let authenticationCode): + XCTAssertEqual(authenticationCode, "123456") + case .failure(_): + XCTAssert(false) + } + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testLeadingZeroInAuthCodePreserved() throws { + guard #available(iOS 14.0, *) else { + throw XCTSkip("Unsupported iOS version") + } + + let expect = expectation(description: "Could read leading zero auth code from pasteboard") + let pasteboard = UIPasteboard.general + pasteboard.string = "012345" + + UIPasteboard.general.detectAuthenticatorCode() { result in + switch result { + case .success(let authenticationCode): + XCTAssertEqual(authenticationCode, "012345") + case .failure(_): + XCTAssert(false) + } + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + }} diff --git a/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift b/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift index 44af3dbe7..2ffb69177 100644 --- a/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift +++ b/WordPressAuthenticatorTests/Mocks/WordpressAuthenticatorProvider.swift @@ -32,6 +32,7 @@ public class WordpressAuthenticatorProvider: NSObject { primaryTitleColor: UIColor.random(), secondaryTitleColor: UIColor.random(), disabledTitleColor: UIColor.random(), + disabledButtonActivityIndicatorColor: UIColor.random(), textButtonColor: UIColor.random(), textButtonHighlightColor: UIColor.random(), instructionColor: UIColor.random(),