diff --git a/Example/ReCaptcha/ViewController.swift b/Example/ReCaptcha/ViewController.swift index 8fbe858..f9aa1be 100644 --- a/Example/ReCaptcha/ViewController.swift +++ b/Example/ReCaptcha/ViewController.swift @@ -58,9 +58,11 @@ class ViewController: UIViewController { } @IBAction private func didPressButton(button: UIButton) { + label.text = "" + disposeBag = DisposeBag() - let validate = recaptcha.rx.validate(on: view) + let validate = recaptcha.rx.validate(on: view, resetOnError: false) .debug("validate") .share() diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift index 2f1e75d..f4d07e5 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift @@ -164,4 +164,25 @@ class ReCaptchaDecoder__Tests: XCTestCase { // Check XCTAssertEqual(result, .didLoad) } + + + func test__Decode__Verify() { + let exp = expectation(description: "send verify") + let expectedResult = true + var result: Result? + + assertResult = { res in + result = res + exp.fulfill() + } + + // Send + let message = MockMessage(message: ["verify": expectedResult]) + decoder.send(message: message) + + waitForExpectations(timeout: 1) + + // Check + XCTAssertEqual(result, .verify(expectedResult)) + } } diff --git a/Example/ReCaptcha_Tests/Helpers/ReCaptchaDecoder+Helper.swift b/Example/ReCaptcha_Tests/Helpers/ReCaptchaDecoder+Helper.swift index 63b2a8b..1978c0c 100644 --- a/Example/ReCaptcha_Tests/Helpers/ReCaptchaDecoder+Helper.swift +++ b/Example/ReCaptcha_Tests/Helpers/ReCaptchaDecoder+Helper.swift @@ -48,6 +48,9 @@ extension ReCaptchaDecoder.Result: Equatable { case (.error(let lhe), .error(let rhe)): return lhe == rhe + case (.verify(let lhv), .verify(let rhv)): + return lhv == rhv + default: return false } diff --git a/Example/ReCaptcha_Tests/mock.html b/Example/ReCaptcha_Tests/mock.html index 37df0ea..d5c701d 100644 --- a/Example/ReCaptcha_Tests/mock.html +++ b/Example/ReCaptcha_Tests/mock.html @@ -2,8 +2,8 @@ diff --git a/ReCaptcha/Assets/recaptcha.html b/ReCaptcha/Assets/recaptcha.html index 6a43791..2aa553a 100644 --- a/ReCaptcha/Assets/recaptcha.html +++ b/ReCaptcha/Assets/recaptcha.html @@ -7,12 +7,12 @@ }; console.log = function(message) { - post({log: message}); + post({ log: message }); }; var showReCaptcha = function() { - console.log("showReCaptcha"); - post({action: "showReCaptcha"}); + console.log('showReCaptcha'); + post({ action: 'showReCaptcha' }); }; var observeDOM = function(element, completion) { @@ -21,42 +21,49 @@ completion(); }); }) - .observe(element, {attributes: true, attributeFilter: ['style']}) + .observe(element, { + attributes: true, + attributeFilter: ['style'], + }); }; var execute = function() { - console.log("executing"); + console.log('executing'); // Removes ReCaptcha dismissal when clicking outside div area try { - document.getElementsByTagName("div")[4].outerHTML = "" - } - catch(e) { + document.getElementsByTagName('div')[4].outerHTML = ''; } + catch(e) {} // Listens to changes on the div element that presents the ReCaptcha challenge - observeDOM(document.getElementsByTagName("div")[3], showReCaptcha); + observeDOM(document.getElementsByTagName('div')[3], showReCaptcha); grecaptcha.execute(); }; var onSubmit = function(token) { console.log(token); - post({token: token}); + post({ token }); }; var onloadCallback = function() { - console.log("did load"); + console.log('did load'); grecaptcha.render('submit', { - 'sitekey' : '${apiKey}', - 'callback' : onSubmit, - 'size': 'invisible' + sitekey : '${apiKey}', + callback : onSubmit, + size: 'invisible' }); }; var reset = function() { - console.log("resetting"); - grecaptcha.reset(); + console.log('resetting'); + grecaptcha.reset(); + }; + + var verify = function() { + const challengeDiv = document.getElementsByTagName('div')[3]; + post({ verify: challengeDiv !== undefined }); }; diff --git a/ReCaptcha/Classes/ReCaptchaDecoder.swift b/ReCaptcha/Classes/ReCaptchaDecoder.swift index bb205a9..a16f5b0 100644 --- a/ReCaptcha/Classes/ReCaptchaDecoder.swift +++ b/ReCaptcha/Classes/ReCaptchaDecoder.swift @@ -30,6 +30,9 @@ internal class ReCaptchaDecoder: NSObject { /// Logs a string onto the console case log(String) + + /// Verifies the configuration + case verify(Bool) } /// The closure that receives messages @@ -89,6 +92,12 @@ fileprivate extension ReCaptchaDecoder.Result { if let token = response["token"] as? String { return .token(token) } + else if let success = response["verify"] as? Bool { + return .verify(success) + } + else if let message = response["log"] as? String { + return .log(message) + } if let action = response["action"] as? String { switch action { @@ -103,10 +112,6 @@ fileprivate extension ReCaptchaDecoder.Result { } } - if let message = response["log"] as? String { - return .log(message) - } - return .error(.wrongMessageFormat) } } diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 599dd68..712a67c 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -13,10 +13,13 @@ import WebKit /** Handles comunications with the webview containing the ReCaptcha challenge. */ internal class ReCaptchaWebViewManager { + enum JSCommand: String { + case execute = "execute();" + case reset = "reset();" + case verify = "verify();" + } fileprivate struct Constants { - static let ExecuteJSCommand = "execute();" - static let ResetCommand = "reset();" static let BotUserAgent = "Googlebot/2.1" } @@ -55,11 +58,15 @@ internal class ReCaptchaWebViewManager { /// 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 { + if didFinishLoading { // 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() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.executeJS(command: .verify) + + if self?.completion != nil { + self?.executeJS(command: .execute) + } } } } @@ -135,7 +142,7 @@ internal class ReCaptchaWebViewManager { webView.isHidden = false view.addSubview(webView) - execute() + executeJS(command: .execute) } @@ -150,14 +157,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 +168,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` @@ -208,6 +194,10 @@ fileprivate extension ReCaptchaWebViewManager { completion?(.token(token)) case .error(let error): + #if DEBUG + print("[JS ERROR]:", error) + #endif + if shouldResetOnError, let view = webView.superview { reset() validate(on: view) @@ -228,8 +218,20 @@ fileprivate extension ReCaptchaWebViewManager { case .log(let message): #if DEBUG - print("[JS LOG]:", message) + print("[JS LOG]:", message) #endif + + case .verify(let success): + if !success { + #if DEBUG + // swiftlint:disable line_length + print(""" + ⚠️ 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 + #endif + } } } @@ -249,4 +251,24 @@ fileprivate extension ReCaptchaWebViewManager { NotificationCenter.default.removeObserver(observer) } } + + /** + - parameters: + - command: The JS command to be executed + + Executes the given JS command. If an error happens, it is thrown to the user. This method has no effect if the + webview hasn't finished loading. + */ + func executeJS(command: JSCommand) { + guard didFinishLoading else { + // Hasn't finished loading the HTML yet + return + } + + webView.evaluateJavaScript(command.rawValue) { [weak self] _, error in + if let error = error { + self?.decoder.send(error: .unexpected(error)) + } + } + } }