diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift index 2f1e75d..4fa3d39 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift @@ -164,4 +164,23 @@ class ReCaptchaDecoder__Tests: XCTestCase { // Check XCTAssertEqual(result, .didLoad) } + + func test__Decode__Error_Setup_Failed() { + let exp = expectation(description: "send error") + var result: Result? + + assertResult = { res in + result = res + exp.fulfill() + } + + // Send + let message = MockMessage(message: ["error": 27]) + decoder.send(message: message) + + waitForExpectations(timeout: 1) + + // Check + XCTAssertEqual(result, .error(.failedSetup)) + } } diff --git a/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift b/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift index 7e768fe..fcf3dc6 100644 --- a/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift +++ b/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift @@ -15,7 +15,8 @@ extension ReCaptchaError: Equatable { case (.htmlLoadError, .htmlLoadError), (.apiKeyNotFound, .apiKeyNotFound), (.baseURLNotFound, .baseURLNotFound), - (.wrongMessageFormat, .wrongMessageFormat): + (.wrongMessageFormat, .wrongMessageFormat), + (.failedSetup, .failedSetup): return true case (.unexpected(let lhe as NSError), .unexpected(let rhe as NSError)): return lhe == rhe @@ -25,11 +26,12 @@ extension ReCaptchaError: Equatable { } static func random() -> ReCaptchaError { - switch arc4random_uniform(4) { + switch arc4random_uniform(5) { case 0: return .htmlLoadError case 1: return .apiKeyNotFound case 2: return .baseURLNotFound case 3: return .wrongMessageFormat + case 4: return .failedSetup default: return .unexpected(NSError()) } } diff --git a/Example/ReCaptcha_Tests/mock.html b/Example/ReCaptcha_Tests/mock.html index 37df0ea..5a70867 100644 --- a/Example/ReCaptcha_Tests/mock.html +++ b/Example/ReCaptcha_Tests/mock.html @@ -6,11 +6,11 @@ var endpoint = "${endpoint}"; var shouldFail = ${shouldFail}; - var post = function(value) { + var post = (value) => { window.webkit.messageHandlers.recaptcha.postMessage(value); }; - var execute = function() { + var execute = () => { if (shouldFail) { post("error"); } @@ -19,10 +19,12 @@ } }; - var reset = function() { + var reset = () => { shouldFail = false; - post({action: "didLoad"}); + post({ action: "didLoad" }); }; + + post({ action: "didLoad" });
diff --git a/ReCaptcha/Assets/recaptcha.html b/ReCaptcha/Assets/recaptcha.html index 6a43791..2de0e25 100644 --- a/ReCaptcha/Assets/recaptcha.html +++ b/ReCaptcha/Assets/recaptcha.html @@ -2,61 +2,81 @@ diff --git a/ReCaptcha/Classes/ReCaptchaDecoder.swift b/ReCaptcha/Classes/ReCaptchaDecoder.swift index bb205a9..83242d7 100644 --- a/ReCaptcha/Classes/ReCaptchaDecoder.swift +++ b/ReCaptcha/Classes/ReCaptchaDecoder.swift @@ -89,6 +89,12 @@ fileprivate extension ReCaptchaDecoder.Result { if let token = response["token"] as? String { return .token(token) } + else if let message = response["log"] as? String { + return .log(message) + } + else if let message = response["error"] as? Int { + return .error(.failedSetup) + } if let action = response["action"] as? String { switch action { diff --git a/ReCaptcha/Classes/ReCaptchaError.swift b/ReCaptcha/Classes/ReCaptchaError.swift index d311d06..8db5b00 100644 --- a/ReCaptcha/Classes/ReCaptchaError.swift +++ b/ReCaptcha/Classes/ReCaptchaError.swift @@ -25,6 +25,8 @@ public enum ReCaptchaError: Error, CustomStringConvertible { /// Received an unexpected message from javascript case wrongMessageFormat + /// ReCaptcha setup failed + case failedSetup /// A human-readable description for each error public var description: String { @@ -43,6 +45,14 @@ public enum ReCaptchaError: Error, CustomStringConvertible { case .wrongMessageFormat: return "Unexpected message from javascript" + + case .failedSetup: + // swiftlint:disable line_length + return """ + ⚠️ WARNING! ReCaptcha wasn't successfully configured. Please double check your ReCaptchaKey and ReCaptchaDomain. + Also check that you're using ReCaptcha's **SITE KEY** for client side integration. + """ + // swiftlint:enable line_length } } } diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 599dd68..f184df9 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -13,6 +13,10 @@ import WebKit /** Handles comunications with the webview containing the ReCaptcha challenge. */ internal class ReCaptchaWebViewManager { + enum JSCommand: String { + case execute = "execute();" + case reset = "reset();" + } fileprivate struct Constants { static let ExecuteJSCommand = "execute();" @@ -53,24 +57,11 @@ internal class ReCaptchaWebViewManager { fileprivate var decoder: ReCaptchaDecoder! /// Indicates if the script has already been loaded by the `webView` - fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString - didSet { - if didFinishLoading && completion != nil { - // User has requested for validation - // A small delay is necessary to allow JS to wrap its operations and avoid errors. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in - self?.execute() - } - } - } - } + fileprivate var didFinishLoading = false /// The observer for `.UIWindowDidBecomeVisible` fileprivate var observer: NSObjectProtocol? - /// The observer for `\WKWebView.estimatedProgress` - fileprivate var loadingObservation: NSKeyValueObservation? - /// The endpoint url being used fileprivate var endpoint: String @@ -83,9 +74,6 @@ internal class ReCaptchaWebViewManager { webview.accessibilityIdentifier = "webview" webview.accessibilityTraits = UIAccessibilityTraits.link webview.isHidden = true - self.loadingObservation = webview.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in - self?.didFinishLoading = change.newValue == 1 - } return webview }() @@ -135,7 +123,7 @@ internal class ReCaptchaWebViewManager { webView.isHidden = false view.addSubview(webView) - execute() + executeJS(command: .execute) } @@ -150,14 +138,9 @@ internal class ReCaptchaWebViewManager { The reset is achieved by calling `grecaptcha.reset()` on the JS API. */ func reset() { - didFinishLoading = false configureWebViewDispatchToken = UUID() - - webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in - if let error = error { - self?.decoder.send(error: .unexpected(error)) - } - } + executeJS(command: .reset) + didFinishLoading = false } } @@ -166,22 +149,6 @@ internal class ReCaptchaWebViewManager { /** Private methods for ReCaptchaWebViewManager */ fileprivate extension ReCaptchaWebViewManager { - /** Executes the JS command that loads the ReCaptcha challenge. - This method has no effect if the webview hasn't finished loading. - */ - func execute() { - guard didFinishLoading else { - // Hasn't finished loading the HTML yet - return - } - - webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in - if let error = error { - self?.decoder.send(error: .unexpected(error)) - } - } - } - /** - returns: An instance of `WKWebViewConfiguration` @@ -223,12 +190,14 @@ fileprivate extension ReCaptchaWebViewManager { } case .didLoad: - // For testing purposes didFinishLoading = true + if completion != nil { + executeJS(command: .execute) + } case .log(let message): #if DEBUG - print("[JS LOG]:", message) + print("[JS LOG]:", message) #endif } } @@ -249,4 +218,24 @@ fileprivate extension ReCaptchaWebViewManager { NotificationCenter.default.removeObserver(observer) } } + + /** + - parameters: + - command: The JavaScript command to be executed + + Executes the JS command that loads the ReCaptcha challenge. This method has no effect if the webview hasn't + finished loading. + */ + func executeJS(command: JSCommand) { + guard didFinishLoading else { + // Hasn't finished loading all the resources + return + } + + webView.evaluateJavaScript(command.rawValue) { [weak self] _, error in + if let error = error { + self?.decoder.send(error: .unexpected(error)) + } + } + } }