Fix: Better detection of resources loading end

Fix #16
This commit is contained in:
Flávio Caetano 2017-12-21 18:35:53 -02:00
parent d46cc50a33
commit 429b6a699c
11 changed files with 211 additions and 41 deletions

View File

@ -1,8 +1,8 @@
PODS:
- AppSwizzle (1.2)
- ReCaptcha/Core (1.0.0):
- ReCaptcha/Core (1.0.1):
- Result (~> 3.0)
- ReCaptcha/RxSwift (1.0.0):
- ReCaptcha/RxSwift (1.0.1):
- ReCaptcha/Core
- RxSwift (~> 4.0)
- Result (3.2.4)
@ -31,7 +31,7 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
AppSwizzle: bbd3782652fc426ce59c045a92ec61d36f261984
ReCaptcha: ea35d3bdbb2098eb3c89d1cf3b5cdbdd4d77f4b8
ReCaptcha: f281cd074be9b282f528c6dda9337476f13bd776
Result: d2d07204ce72856f1fd9130bbe42c35a7b0fea10
RxCocoa: d62846ca96495d862fa4c59ea7d87e5031d7340e
RxSwift: fd680d75283beb5e2559486f3c0ff852f0d35334

View File

@ -15,6 +15,7 @@
BD850CB2DF4C9C94FC51226C /* Pods_ReCaptcha_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62BEEA62161F672468CCFD64 /* Pods_ReCaptcha_Example.framework */; };
D091B6E053FD250B4757E34C /* Pods_ReCaptcha_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9417A28DC340FF0BC1627B3F /* Pods_ReCaptcha_Tests.framework */; };
F206BAD51F8D3FEB00A25807 /* Cartfile in Resources */ = {isa = PBXBuildFile; fileRef = F206BAD41F8D3FEB00A25807 /* Cartfile */; };
F231B3971FEC325A00F82943 /* DispatchQueue__Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F231B3961FEC325A00F82943 /* DispatchQueue__Tests.swift */; };
F288E9451F9537760018688D /* ReCaptchaError+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F288E9441F9537760018688D /* ReCaptchaError+Equatable.swift */; };
F2E2685E1F7AEE3400CD876D /* ReCaptcha__Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */; };
F2ECCF8A1E9FCEFE0097B199 /* ReCaptchaDecoder__Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ECCF891E9FCEFE0097B199 /* ReCaptchaDecoder__Tests.swift */; };
@ -54,6 +55,7 @@
C8537003ECC47117AF54DCA9 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
F206BAD41F8D3FEB00A25807 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = "<group>"; };
F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; };
F231B3961FEC325A00F82943 /* DispatchQueue__Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueue__Tests.swift; sourceTree = "<group>"; };
F288E9441F9537760018688D /* ReCaptchaError+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReCaptchaError+Equatable.swift"; sourceTree = "<group>"; };
F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReCaptcha__Tests.swift; sourceTree = "<group>"; };
F2ECCF761E9FC47B0097B199 /* ReCaptcha_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReCaptcha_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -170,6 +172,7 @@
F2ECCF891E9FCEFE0097B199 /* ReCaptchaDecoder__Tests.swift */,
F2ECCF8D1E9FE68C0097B199 /* ReCaptchaWebViewManager__Tests.swift */,
F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */,
F231B3961FEC325A00F82943 /* DispatchQueue__Tests.swift */,
);
path = Core;
sourceTree = "<group>";
@ -214,6 +217,7 @@
607FACCE1AFB9204008FA782 /* Resources */,
8F03FFB3F5C55E873C23C682 /* [CP] Embed Pods Frameworks */,
ED1C0E07490C9C4B4A401061 /* [CP] Copy Pods Resources */,
F231B3981FEC3B7F00F82943 /* SwiftLint */,
);
buildRules = (
);
@ -418,6 +422,20 @@
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReCaptcha_Example/Pods-ReCaptcha_Example-resources.sh\"\n";
showEnvVarsInLog = 0;
};
F231B3981FEC3B7F00F82943 /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = SwiftLint;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\" --path \"${PROJECT_DIR}/..\"";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -437,6 +455,7 @@
F2ECCF961EA00A5B0097B199 /* ReCaptchaWebViewManager+Helpers.swift in Sources */,
F2ECCF8E1E9FE68C0097B199 /* ReCaptchaWebViewManager__Tests.swift in Sources */,
F2ECCF981EA011370097B199 /* Result+Helpers.swift in Sources */,
F231B3971FEC325A00F82943 /* DispatchQueue__Tests.swift in Sources */,
F2E2685E1F7AEE3400CD876D /* ReCaptcha__Tests.swift in Sources */,
F2ECCF931EA009360097B199 /* ReCaptcha+Rx__Tests.swift in Sources */,
F288E9451F9537760018688D /* ReCaptchaError+Equatable.swift in Sources */,
@ -620,7 +639,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.flaviocaetano.ReCaptcha-Tests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG UNIT_TESTS";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 4.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ReCaptcha_Example.app/ReCaptcha_Example";

View File

@ -0,0 +1,45 @@
//
// DispatchQueue__Tests.swift
// ReCaptcha
//
// Created by Flávio Caetano on 21/12/17.
// Copyright © 2017 ReCaptcha. All rights reserved.
//
@testable import ReCaptcha
import XCTest
class DispatchQueue__Tests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func test__Throttle() {
// Execute closure called once
let exp0 = expectation(description: "did call single closure")
DispatchQueue.main.throttle(deadline: .now() + 0.1) {
exp0.fulfill()
}
waitForExpectations(timeout: 1)
// Does not execute first closure
let exp1 = expectation(description: "")
DispatchQueue.main.throttle(deadline: .now() + 0.1) {
XCTFail("Shouldn't be called")
}
DispatchQueue.main.throttle(deadline: .now() + 0.1) {
exp1.fulfill()
}
waitForExpectations(timeout: 1)
}
}

View File

@ -1,16 +1,20 @@
<html>
<head>
<meta name="viewport" content="width=device-width" />
<script type="text/javascript">
var key = "${apiKey}";
var execute = function() {
window.webkit.messageHandlers.recaptcha.postMessage(${message});
}
var endpoint = "${endpoint}";
</script>
</head>
<body>
<span id="submit" style="visibility: hidden;"></span>
</body>
<head>
<meta name="viewport" content="width=device-width" />
<script type="text/javascript">
var key = "${apiKey}";
var endpoint = "${endpoint}";
var execute = function() {
window.webkit.messageHandlers.recaptcha.postMessage(${message});
}
window.onload = function() {
window.webkit.messageHandlers.recaptcha.postMessage({action: "didLoad"});
}
</script>
</head>
<body>
<span id="submit" style="visibility: hidden;"></span>
</body>
</html>
cartha

View File

@ -1,4 +1,4 @@
source 'https://rubygems.org'
gem 'fastlane', '~> 2.62.1'
gem 'fastlane', '~> 2.69.2'
gem 'cocoapods', '~> 1.3.1'

View File

@ -64,7 +64,7 @@ GEM
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.0)
fastlane (2.62.1)
fastlane (2.69.2)
CFPropertyList (>= 2.3, < 3.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@ -92,9 +92,9 @@ GEM
slack-notifier (>= 1.3, < 2.0.0)
terminal-notifier (>= 1.6.2, < 2.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (~> 0.5.0)
tty-screen (~> 0.6.3)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.5.0, < 2.0.0)
xcodeproj (>= 1.5.2, < 2.0.0)
xcpretty (>= 0.2.4, < 1.0.0)
xcpretty-travis-formatter (>= 0.0.3)
fourflusher (2.0.1)
@ -107,7 +107,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
googleauth (0.6.1)
googleauth (0.6.2)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
@ -115,7 +115,7 @@ GEM
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
highline (1.7.8)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
@ -141,7 +141,7 @@ GEM
nap (1.1.0)
netrc (0.11.0)
os (0.9.6)
plist (3.3.0)
plist (3.4.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
@ -162,7 +162,7 @@ GEM
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
tty-screen (0.5.1)
tty-screen (0.6.3)
tzinfo (1.2.4)
thread_safe (~> 0.1)
uber (0.1.0)
@ -186,7 +186,7 @@ PLATFORMS
DEPENDENCIES
cocoapods (~> 1.3.1)
fastlane (~> 2.62.1)
fastlane (~> 2.69.2)
BUNDLED WITH
1.15.4
1.16.0

View File

@ -11,6 +11,7 @@
F206BAD31F8D3E7600A25807 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F206BAD21F8D3E7600A25807 /* Result.framework */; };
F206BB1D1F8D4DBC00A25807 /* ReCaptcha_RxSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = F206BB1B1F8D4DBC00A25807 /* ReCaptcha_RxSwift.h */; settings = {ATTRIBUTES = (Public, ); }; };
F206BB221F8D4DDF00A25807 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F206BB121F8D4D1400A25807 /* RxSwift.framework */; };
F231B39A1FEC51C800F82943 /* DispatchQueue+Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F231B3991FEC51C800F82943 /* DispatchQueue+Throttle.swift */; };
F24EA1E11F9683FB001DEC17 /* ReCaptcha+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24EA1DD1F9683F5001DEC17 /* ReCaptcha+Rx.swift */; };
F24EA1E21F968403001DEC17 /* ReCaptchaDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24EA1D81F9683F5001DEC17 /* ReCaptchaDecoder.swift */; };
F24EA1E31F968403001DEC17 /* ReCaptchaWebViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24EA1D91F9683F5001DEC17 /* ReCaptchaWebViewManager.swift */; };
@ -41,6 +42,7 @@
F206BB191F8D4DBC00A25807 /* ReCaptcha_RxSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReCaptcha_RxSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F206BB1B1F8D4DBC00A25807 /* ReCaptcha_RxSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReCaptcha_RxSwift.h; sourceTree = "<group>"; };
F206BB1C1F8D4DBC00A25807 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F231B3991FEC51C800F82943 /* DispatchQueue+Throttle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Throttle.swift"; sourceTree = "<group>"; };
F24EA1D81F9683F5001DEC17 /* ReCaptchaDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReCaptchaDecoder.swift; sourceTree = "<group>"; };
F24EA1D91F9683F5001DEC17 /* ReCaptchaWebViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReCaptchaWebViewManager.swift; sourceTree = "<group>"; };
F24EA1DA1F9683F5001DEC17 /* String+Dict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Dict.swift"; sourceTree = "<group>"; };
@ -145,6 +147,7 @@
F24EA1D91F9683F5001DEC17 /* ReCaptchaWebViewManager.swift */,
F24EA1DA1F9683F5001DEC17 /* String+Dict.swift */,
F24EA1DB1F9683F5001DEC17 /* ReCaptchaError.swift */,
F231B3991FEC51C800F82943 /* DispatchQueue+Throttle.swift */,
F24EA1DC1F9683F5001DEC17 /* Rx */,
F24EA1DE1F9683F5001DEC17 /* ReCaptcha.swift */,
);
@ -291,6 +294,7 @@
F24EA1E61F968403001DEC17 /* ReCaptcha.swift in Sources */,
F24EA1E31F968403001DEC17 /* ReCaptchaWebViewManager.swift in Sources */,
F24EA1E41F968403001DEC17 /* String+Dict.swift in Sources */,
F231B39A1FEC51C800F82943 /* DispatchQueue+Throttle.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -13,7 +13,11 @@
var execute = function() {
// Removes ReCaptcha dismissal when clicking outside div area
document.getElementsByTagName("div")[4].outerHTML = ""
try {
document.getElementsByTagName("div")[4].outerHTML = ""
}
catch(e) {
}
// Listens to changes on the div element that presents the ReCaptcha challenge
observeDOM(document.getElementsByTagName("div")[3], function() {

View File

@ -0,0 +1,32 @@
//
// DispatchQueue+Throttle.swift
// ReCaptcha
//
// Created by Flávio Caetano on 21/12/17.
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
private var workItem: DispatchWorkItem?
extension DispatchQueue {
/**
- parameters:
- deadline: The timespan to delay a closure execution
- action: The closure to be executed
Delays a closure execution and ensures no other executions are made during deadline
*/
func throttle(deadline: DispatchTime, action: @escaping () -> Void) {
let worker = DispatchWorkItem {
defer { workItem = nil }
action()
}
asyncAfter(deadline: deadline, execute: worker)
workItem?.cancel()
workItem = worker
}
}

View File

@ -24,6 +24,9 @@ internal class ReCaptchaDecoder: NSObject {
/// Any errors
case error(ReCaptchaError)
/// Did finish loading resources
case didLoad
}
/// The closure that receives messages
@ -84,8 +87,17 @@ fileprivate extension ReCaptchaDecoder.Result {
return .token(token)
}
if let action = response["action"] as? String, action == "showReCaptcha" {
return .showReCaptcha
if let action = response["action"] as? String {
switch action {
case "showReCaptcha":
return .showReCaptcha
case "didLoad":
return .didLoad
default:
break
}
}
return .error(.wrongMessageFormat)

View File

@ -22,6 +22,9 @@ open class ReCaptchaWebViewManager {
/// The parent manager
private weak var manager: ReCaptchaWebViewManager?
/// The active requests' urls
private var activeRequests = Set<String>(minimumCapacity: 0)
/// - parameter manager: The parent manager
init(manager: ReCaptchaWebViewManager) {
self.manager = manager
@ -30,16 +33,55 @@ open class ReCaptchaWebViewManager {
/**
- parameters:
- webView: The web view invoking the delegate method.
- navigation: The navigation object that finished.
Called when the navigation is complete.
*/
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
manager?.didFinishLoading = true
- navigationAction: Descriptive information about the action triggering the navigation request.
- decisionHandler: The decision handler to call to allow or cancel the navigation. The argument is one of
the constants of the enumerated type WKNavigationActionPolicy.
if manager?.completion != nil {
// User has requested for validation
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
Decides whether to allow or cancel a navigation.
*/
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy
) -> Void) {
defer { decisionHandler(.allow) }
if let url = navigationAction.request.url, let host = url.host, let endpoint = manager?.endpoint,
endpoint.range(of: host) != nil {
activeRequests.insert(url.absoluteString)
}
}
/**
- parameters:
- webView: The web view invoking the delegate method.
- navigationResponse: Descriptive information about the navigation response.
- decisionHandler: A block to be called when your app has decided whether to allow or cancel the navigation
Decides whether to allow or cancel a navigation after its response is known.
*/
func webView(
_ webView: WKWebView,
decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
) {
defer { decisionHandler(.allow) }
guard let url = navigationResponse.response.url?.absoluteString,
activeRequests.remove(url) != nil, activeRequests.isEmpty else {
return
}
execute()
}
/// Flag the requests as finished and call ReCaptcha execution if necessary
func execute() {
DispatchQueue.main.throttle(deadline: .now() + 1) { [weak self] in
// Did finish loading the ReCaptcha JS source
self?.manager?.didFinishLoading = true
if self?.manager?.completion != nil {
// User has requested for validation
self?.manager?.execute()
}
}
@ -65,6 +107,9 @@ open class ReCaptchaWebViewManager {
/// The observer for `.UIWindowDidBecomeVisible`
fileprivate var observer: NSObjectProtocol?
/// The endpoint url being used
fileprivate var endpoint: String
/// The `webView` delegate implementation
fileprivate lazy var webviewDelegate: WebViewDelegate = {
WebViewDelegate(manager: self)
@ -90,7 +135,8 @@ open class ReCaptchaWebViewManager {
- endpoint: The JS API endpoint to be loaded onto the HTML file.
*/
init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
decoder = ReCaptchaDecoder { [weak self] result in
self.endpoint = endpoint
self.decoder = ReCaptchaDecoder { [weak self] result in
self?.handle(result: result)
}
@ -201,6 +247,10 @@ fileprivate extension ReCaptchaWebViewManager {
case .showReCaptcha:
configureWebView?(webView)
case .didLoad:
// For testing purposes
webviewDelegate.execute()
}
}