Skip to content

Commit

Permalink
Feature: Resettable ReCaptchas
Browse files Browse the repository at this point in the history
fix #23
  • Loading branch information
fjcaetano committed Mar 6, 2018
1 parent e2a38dd commit 9212a48
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "ReactiveX/RxSwift" "4.0.0"
github "ReactiveX/RxSwift" "4.1.2"
github "antitypical/Result" "3.2.4"
2 changes: 1 addition & 1 deletion Example/ReCaptcha.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
62BEEA62161F672468CCFD64 /* Pods_ReCaptcha_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReCaptcha_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
62C1DD0E80E9920845E5DA51 /* ReCaptcha.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = ReCaptcha.podspec; path = ../ReCaptcha.podspec; sourceTree = "<group>"; };
62C1DD0E80E9920845E5DA51 /* ReCaptcha.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = ReCaptcha.podspec; path = ../ReCaptcha.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
80FF4E03D71AACBD81A36301 /* Pods-ReCaptcha_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReCaptcha_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReCaptcha_Example/Pods-ReCaptcha_Example.debug.xcconfig"; sourceTree = "<group>"; };
930BD5ACA20B973070B89ACF /* Pods-ReCaptcha_UITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReCaptcha_UITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ReCaptcha_UITests/Pods-ReCaptcha_UITests.debug.xcconfig"; sourceTree = "<group>"; };
9417A28DC340FF0BC1627B3F /* Pods_ReCaptcha_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ReCaptcha_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down
1 change: 1 addition & 0 deletions Example/ReCaptcha_Tests/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ disabled_rules:
- nesting
- force_unwrapping
- explicit_top_level_acl
- function_body_length
60 changes: 58 additions & 2 deletions Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class ReCaptchaWebViewManager__Tests: XCTestCase {
XCTFail("should not ask to configure the webview")
}

manager.validate(on: presenterView) { response in
manager.validate(on: presenterView, resetOnError: false) { response in
result = response
exp.fulfill()
}
Expand All @@ -128,7 +128,7 @@ class ReCaptchaWebViewManager__Tests: XCTestCase {
XCTFail("should not ask to configure the webview")
}

manager.validate(on: presenterView) { response in
manager.validate(on: presenterView, resetOnError: false) { response in
result = response
exp.fulfill()
}
Expand Down Expand Up @@ -251,4 +251,60 @@ class ReCaptchaWebViewManager__Tests: XCTestCase {
XCTAssertNil(result?.error)
XCTAssertEqual(result?.value, endpoint)
}

// MARK: Reset

func test__Reset() {
let exp1 = expectation(description: "fail on first execution")
var result1: ReCaptchaWebViewManager.Response?

// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}

// Error
manager.validate(on: presenterView, resetOnError: false) { result in
result1 = result
exp1.fulfill()
}

waitForExpectations(timeout: 10)
XCTAssertEqual(result1?.error, .wrongMessageFormat)

// Resets and tries again
let exp2 = expectation(description: "validates after reset")
var result2: ReCaptchaWebViewManager.Response?

manager.reset()
manager.validate(on: presenterView, resetOnError: false) { result in
result2 = result
exp2.fulfill()
}

waitForExpectations(timeout: 10)

XCTAssertEqual(result2?.value, apiKey)
}

func test__Validate__Reset_On_Error() {
let exp = expectation(description: "fail on first execution")
var result: ReCaptchaWebViewManager.Response?

// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}

// Error
manager.validate(on: presenterView, resetOnError: true) { response in
result = response
exp.fulfill()
}

waitForExpectations(timeout: 10)
XCTAssertEqual(result?.value, apiKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@ import Foundation


extension ReCaptchaWebViewManager {
convenience init(messageBody: String, apiKey: String? = nil, endpoint: String? = nil) {
let localhost = URL(string: "http://localhost")!
let html = Bundle(for: ReCaptchaWebViewManager__Tests.self)
private static let unformattedHTML: String! = {
Bundle(for: ReCaptchaWebViewManager__Tests.self)
.path(forResource: "mock", ofType: "html")
.flatMap { try? String(contentsOfFile: $0) }
.map { String(format: $0, arguments: ["message": messageBody]) }
}()

convenience init(
messageBody: String,
apiKey: String? = nil,
endpoint: String? = nil,
shouldFail: Bool = false
) {
let localhost = URL(string: "http://localhost")!
let html = String(format: ReCaptchaWebViewManager.unformattedHTML, arguments: [
"message": messageBody,
"shouldFail": shouldFail.description
])

self.init(
html: html!,
html: html,
apiKey: apiKey ?? String(arc4random()),
baseURL: localhost,
endpoint: endpoint ?? localhost.absoluteString
Expand Down
104 changes: 103 additions & 1 deletion Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

@testable import ReCaptcha

import RxCocoa
import RxSwift
import XCTest

Expand Down Expand Up @@ -110,7 +111,7 @@ class ReCaptcha_Rx__Tests: XCTestCase {
let exp = expectation(description: "validate token")

// Validate
manager.rx.validate(on: presenterView)
manager.rx.validate(on: presenterView, resetOnError: false)
.subscribe { event in
switch event {
case .next(let value):
Expand Down Expand Up @@ -157,4 +158,105 @@ class ReCaptcha_Rx__Tests: XCTestCase {

waitForExpectations(timeout: 10)
}

// MARK: Reset

func test__Reset() {
let exp1 = expectation(description: "fail on first execution")
let exp2 = expectation(description: "resets after failure")
var result1: ReCaptchaWebViewManager.Response?

// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}

// Error
let validate = manager.rx.validate(on: presenterView, resetOnError: false)
.share(replay: 1)

validate
.subscribe { event in
switch event {
case .next(let value):
result1 = value

case .error(let error):
XCTFail(error.localizedDescription)

case .completed:
exp1.fulfill()
}
}
.disposed(by: disposeBag)

// Resets after failure
validate
.flatMap { result -> Observable<Void> in
switch result {
case .failure: return .just(())
default: return .empty()
}
}
.take(1)
.do(onCompleted: exp2.fulfill)
.bind(to: manager.rx.reset)
.disposed(by: disposeBag)

waitForExpectations(timeout: 10)
XCTAssertEqual(result1?.error, .wrongMessageFormat)

// Resets and tries again
let exp3 = expectation(description: "validates after reset")
var result2: ReCaptchaWebViewManager.Response?

manager.rx.validate(on: presenterView, resetOnError: false)
.subscribe { event in
switch event {
case .next(let value):
result2 = value

case .error(let error):
XCTFail(error.localizedDescription)

case .completed:
exp3.fulfill()
}
}
.disposed(by: disposeBag)

waitForExpectations(timeout: 10)
XCTAssertEqual(result2?.value, apiKey)
}

func test__Validate__Reset_On_Error() {
let exp = expectation(description: "executes after failure on first execution")
var result: ReCaptchaWebViewManager.Response?

// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}

// Error
manager.rx.validate(on: presenterView, resetOnError: true)
.subscribe { event in
switch event {
case .next(let value):
result = value

case .error(let error):
XCTFail(error.localizedDescription)

case .completed:
exp.fulfill()
}
}
.disposed(by: disposeBag)

waitForExpectations(timeout: 10)
XCTAssertEqual(result?.value, apiKey)
}
}
23 changes: 19 additions & 4 deletions Example/ReCaptcha_Tests/mock.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,29 @@
<script type="text/javascript">
var key = "${apiKey}";
var endpoint = "${endpoint}";
var shouldFail = ${shouldFail};

var post = function(value) {
window.webkit.messageHandlers.recaptcha.postMessage(value);
};

var execute = function() {
window.webkit.messageHandlers.recaptcha.postMessage(${message});
}
if (shouldFail) {
post("error");
}
else {
post(${message});
}
};

window.onload = function() {
window.webkit.messageHandlers.recaptcha.postMessage({action: "didLoad"});
}
post({action: "didLoad"});
};

var reset = function() {
shouldFail = false;
post({action: "didLoad"});
};
</script>
</head>
<body>
Expand Down
8 changes: 7 additions & 1 deletion ReCaptcha/Assets/recaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
};

var execute = function() {
console.log("executing");

// Removes ReCaptcha dismissal when clicking outside div area
try {
document.getElementsByTagName("div")[4].outerHTML = ""
Expand All @@ -35,7 +37,6 @@
// Listens to changes on the div element that presents the ReCaptcha challenge
observeDOM(document.getElementsByTagName("div")[3], showReCaptcha);

console.log("executing");
grecaptcha.execute();
};

Expand All @@ -52,6 +53,11 @@
'size': 'invisible'
});
};

var reset = function() {
console.log("resetting");
grecaptcha.reset();
};
</script>
</head>
<body>
Expand Down
36 changes: 31 additions & 5 deletions ReCaptcha/Classes/ReCaptchaWebViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ open class ReCaptchaWebViewManager {

fileprivate struct Constants {
static let ExecuteJSCommand = "execute();"
static let ResetCommand = "reset();"
}

/// Sends the result message
Expand All @@ -114,6 +115,9 @@ open class ReCaptchaWebViewManager {
/// The endpoint url being used
fileprivate var endpoint: String

/// If the ReCaptcha should be reset when it errors
fileprivate var shouldResetOnError = true

/// The `webView` delegate implementation
fileprivate lazy var webviewDelegate: WebViewDelegate = {
WebViewDelegate(manager: self)
Expand Down Expand Up @@ -166,16 +170,17 @@ open class ReCaptchaWebViewManager {

/**
- parameters:
- view: The view that should present the webview.
- completion: A closure that receives a Result<String, NSError> which may contain a valid result token.
- view: The view that should present the webview.
- resetOnError: If ReCaptcha should be reset if it errors. Defaults to `true`.
- completion: A closure that receives a Result<String, NSError> which may contain a valid result token.
Starts the challenge validation
*/
open func validate(on view: UIView, completion: @escaping (Response) -> Void) {
open func validate(on view: UIView, resetOnError: Bool = true, completion: @escaping (Response) -> Void) {
self.completion = completion
self.shouldResetOnError = resetOnError

webView.isHidden = false
webView.removeFromSuperview()
view.addSubview(webView)

execute()
Expand All @@ -199,6 +204,21 @@ open class ReCaptchaWebViewManager {
open func configureWebView(_ configure: @escaping (WKWebView) -> Void) {
self.configureWebView = configure
}

/**
Resets the ReCaptcha.
The reset is achieved by calling `grecaptcha.reset()` on the JS API.
*/
open func reset() {
didFinishLoading = false

webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
}

// MARK: - Private Methods
Expand Down Expand Up @@ -249,7 +269,13 @@ fileprivate extension ReCaptchaWebViewManager {
completion?(.success(token))

case .error(let error):
completion?(.failure(error))
if shouldResetOnError, let view = webView.superview, let completion = completion {
reset()
validate(on: view, completion: completion)
}
else {
completion?(.failure(error))
}

case .showReCaptcha:
configureWebView?(webView)
Expand Down
Loading

0 comments on commit 9212a48

Please sign in to comment.