Fix: Better encapsulation with new achitecture

This commit is contained in:
Flávio Caetano 2018-03-09 18:37:42 -03:00
parent 8a91279bd5
commit 8160d36ef9
8 changed files with 165 additions and 69 deletions

View File

@ -78,7 +78,7 @@
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""

View File

@ -309,4 +309,21 @@ class ReCaptchaWebViewManager__Tests: XCTestCase {
XCTAssertNil(result?.error)
XCTAssertEqual(result?.token, apiKey)
}
// MARK: Force Challenge Visible
func test__Force_Visible_Challenge() {
let manager = ReCaptchaWebViewManager()
// Initial value
XCTAssertFalse(manager.forceVisibleChallenge)
// Set True
manager.forceVisibleChallenge = true
XCTAssertEqual(manager.webView.customUserAgent, "Googlebot/2.1")
// Set False
manager.forceVisibleChallenge = false
XCTAssertNotEqual(manager.webView.customUserAgent?.isEmpty, false)
}
}

View File

@ -110,6 +110,17 @@ class ReCaptcha__Tests: XCTestCase {
)
XCTAssertEqual(config2?.apiKey, key)
}
func test__Force_Visible_Challenge() {
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager())
// Initial value
XCTAssertFalse(recaptcha.forceVisibleChallenge)
// Set true
recaptcha.forceVisibleChallenge = true
XCTAssertTrue(recaptcha.forceVisibleChallenge)
}
}

View File

@ -8,7 +8,7 @@
import Foundation
@testable import ReCaptcha
import WebKit
extension ReCaptchaWebViewManager {
private static let unformattedHTML: String! = {
@ -18,7 +18,7 @@ extension ReCaptchaWebViewManager {
}()
convenience init(
messageBody: String,
messageBody: String = "",
apiKey: String? = nil,
endpoint: String? = nil,
shouldFail: Bool = false
@ -36,4 +36,15 @@ extension ReCaptchaWebViewManager {
endpoint: endpoint ?? localhost.absoluteString
)
}
func configureWebView(_ configure: @escaping (WKWebView) -> Void) {
configureWebView = configure
}
func validate(on view: UIView, resetOnError: Bool = true, completion: @escaping (ReCaptchaResult) -> Void) {
self.shouldResetOnError = resetOnError
self.completion = completion
validate(on: view)
}
}

View File

@ -35,14 +35,14 @@ class ReCaptcha_Rx__Tests: XCTestCase {
func test__Validate__Token() {
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey)
manager.configureWebView { _ in
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey))
recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
do {
// Validate
let result = try manager.rx.validate(on: presenterView)
let result = try recaptcha.rx.validate(on: presenterView)
.toBlocking()
.single()
@ -56,16 +56,19 @@ class ReCaptcha_Rx__Tests: XCTestCase {
func test__Validate__Show_ReCaptcha() {
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}", apiKey: apiKey)
let recaptcha = ReCaptcha(
manager: ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}", apiKey: apiKey)
)
var didConfigureWebView = false
manager.configureWebView { _ in
recaptcha.configureWebView { _ in
didConfigureWebView = true
}
do {
// Validate
_ = try manager.rx.validate(on: presenterView)
_ = try recaptcha.rx.validate(on: presenterView)
.timeout(2, scheduler: MainScheduler.instance)
.toBlocking()
.single()
@ -80,14 +83,14 @@ class ReCaptcha_Rx__Tests: XCTestCase {
func test__Validate__Error() {
let manager = ReCaptchaWebViewManager(messageBody: "\"foobar\"", apiKey: apiKey)
manager.configureWebView { _ in
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "\"foobar\"", apiKey: apiKey))
recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
do {
// Validate
_ = try manager.rx.validate(on: presenterView, resetOnError: false)
_ = try recaptcha.rx.validate(on: presenterView, resetOnError: false)
.toBlocking()
.single()
@ -104,12 +107,12 @@ class ReCaptcha_Rx__Tests: XCTestCase {
let exp = expectation(description: "stop loading")
// Stop
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.configureWebView { _ in
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}"))
recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
let disposable = manager.rx.validate(on: presenterView)
let disposable = recaptcha.rx.validate(on: presenterView)
.do(onDispose: exp.fulfill)
.subscribe { _ in
XCTFail("should not validate")
@ -126,14 +129,17 @@ class ReCaptcha_Rx__Tests: XCTestCase {
func test__Reset() {
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
let recaptcha = ReCaptcha(
manager: ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
)
recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
do {
// Error
_ = try manager.rx.validate(on: presenterView, resetOnError: false)
_ = try recaptcha.rx.validate(on: presenterView, resetOnError: false)
.toBlocking()
.single()
}
@ -142,12 +148,12 @@ class ReCaptcha_Rx__Tests: XCTestCase {
// Resets after failure
_ = Observable<Void>.just(())
.bind(to: manager.rx.reset)
.bind(to: recaptcha.rx.reset)
}
do {
// Resets and tries again
let result = try manager.rx.validate(on: presenterView, resetOnError: false)
let result = try recaptcha.rx.validate(on: presenterView, resetOnError: false)
.toBlocking()
.single()
@ -160,14 +166,17 @@ class ReCaptcha_Rx__Tests: XCTestCase {
func test__Validate__Reset_On_Error() {
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
manager.configureWebView { _ in
let recaptcha = ReCaptcha(
manager: ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey, shouldFail: true)
)
recaptcha.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
do {
// Error
let result = try manager.rx.validate(on: presenterView, resetOnError: true)
let result = try recaptcha.rx.validate(on: presenterView, resetOnError: true)
.toBlocking()
.single()

View File

@ -10,9 +10,9 @@ import Foundation
import WebKit
/** The public facade of ReCaptcha
/**
*/
open class ReCaptcha: ReCaptchaWebViewManager {
public class ReCaptcha {
fileprivate struct Constants {
struct InfoDictKeys {
static let APIKey = "ReCaptchaKey"
@ -97,6 +97,9 @@ open class ReCaptcha: ReCaptchaWebViewManager {
}
}
/// The worker that handles webview events and communication
let manager: ReCaptchaWebViewManager
/**
- parameters:
- apiKey: The API key sent to the ReCaptcha init
@ -118,15 +121,81 @@ open class ReCaptcha: ReCaptchaWebViewManager {
Info.plist.
- Throws: Rethrows any exceptions thrown by `String(contentsOfFile:)`
*/
public init(apiKey: String? = nil, baseURL: URL? = nil, endpoint: Endpoint = .default) throws {
public convenience init(apiKey: String? = nil, baseURL: URL? = nil, endpoint: Endpoint = .default) throws {
let infoDict = Bundle.main.infoDictionary
let plistApiKey = infoDict?[Constants.InfoDictKeys.APIKey] as? String
let plistDomain = (infoDict?[Constants.InfoDictKeys.Domain] as? String).flatMap(URL.init(string:))
let config = try Config(apiKey: apiKey, infoPlistKey: plistApiKey, baseURL: baseURL, infoPlistURL: plistDomain)
super.init(html: config.html, apiKey: config.apiKey, baseURL: config.baseURL, endpoint: endpoint.url)
self.init(manager: ReCaptchaWebViewManager(
html: config.html,
apiKey: config.apiKey,
baseURL: config.baseURL,
endpoint: endpoint.url
))
}
/**
- parameter manager: A ReCaptchaWebViewManager instance.
Initializes ReCaptcha with the given manager
*/
init(manager: ReCaptchaWebViewManager) {
self.manager = manager
}
/**
- parameters:
- 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 ReCaptchaResult which may contain a valid result token.
Starts the challenge validation
*/
public func validate(on view: UIView, resetOnError: Bool = true, completion: @escaping (ReCaptchaResult) -> Void) {
manager.shouldResetOnError = resetOnError
manager.completion = completion
manager.validate(on: view)
}
/// Stops the execution of the webview
public func stop() {
manager.stop()
}
/**
- parameter configure: A closure that receives an instance of `WKWebView` for configuration.
Provides a closure to configure the webview for presentation if necessary.
If presentation is required, the webview will already be a subview of `presenterView` if one is provided. Otherwise
it might need to be added in a view currently visible.
*/
public func configureWebView(_ configure: @escaping (WKWebView) -> Void) {
manager.configureWebView = configure
}
/**
Resets the ReCaptcha.
The reset is achieved by calling `grecaptcha.reset()` on the JS API.
*/
public func reset() {
manager.reset()
}
#if DEBUG
/// Forces the challenge to be explicitly displayed.
public var forceVisibleChallenge: Bool {
get { return manager.forceVisibleChallenge }
set { manager.forceVisibleChallenge = newValue }
}
#endif
}
// MARK: - Private Methods

View File

@ -12,7 +12,7 @@ import WebKit
/** Handles comunications with the webview containing the ReCaptcha challenge.
*/
open class ReCaptchaWebViewManager {
internal class ReCaptchaWebViewManager {
/** The `webView` delegate object that performs execution uppon script loading
*/
fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate {
@ -99,7 +99,7 @@ open class ReCaptchaWebViewManager {
#if DEBUG
/// Forces the challenge to be explicitly displayed.
public var forceVisibleChallenge = false {
var forceVisibleChallenge = false {
didSet {
// Also works on iOS < 9
webView.performSelector(
@ -112,10 +112,13 @@ open class ReCaptchaWebViewManager {
#endif
/// Sends the result message
fileprivate var completion: ((ReCaptchaResult) -> Void)?
var completion: ((ReCaptchaResult) -> Void)?
/// Configures the webview for display when required
fileprivate var configureWebView: ((WKWebView) -> Void)?
var configureWebView: ((WKWebView) -> Void)?
/// If the ReCaptcha should be reset when it errors
var shouldResetOnError = true
/// The JS message recoder
fileprivate var decoder: ReCaptchaDecoder!
@ -129,16 +132,13 @@ 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)
}()
/// The webview that executes JS code
fileprivate lazy var webView: WKWebView = {
lazy var webView: WKWebView = {
let webview = WKWebView(
frame: CGRect(x: 0, y: 0, width: 1, height: 1),
configuration: self.buildConfiguration()
@ -181,19 +181,12 @@ open class ReCaptchaWebViewManager {
}
}
/**
- parameters:
- 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 ReCaptchaResult which may contain a valid result token.
- parameter view: The view that should present the webview.
Starts the challenge validation
*/
open func validate(on view: UIView, resetOnError: Bool = true, completion: @escaping (ReCaptchaResult) -> Void) {
self.completion = completion
self.shouldResetOnError = resetOnError
func validate(on view: UIView) {
webView.isHidden = false
view.addSubview(webView)
@ -202,29 +195,16 @@ open class ReCaptchaWebViewManager {
/// Stops the execution of the webview
open func stop() {
func stop() {
webView.stopLoading()
}
/**
- parameter configure: A closure that receives an instance of `WKWebView` for configuration.
Provides a closure to configure the webview for presentation if necessary.
If presentation is required, the webview will already be a subview of `presenterView` if one is provided. Otherwise
it might need to be added in a view currently visible.
*/
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() {
func reset() {
didFinishLoading = false
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
@ -240,7 +220,6 @@ open 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.
*/
@ -283,9 +262,9 @@ fileprivate extension ReCaptchaWebViewManager {
completion?(.token(token))
case .error(let error):
if shouldResetOnError, let view = webView.superview, let completion = completion {
if shouldResetOnError, let view = webView.superview {
reset()
validate(on: view, completion: completion)
validate(on: view)
}
else {
completion?(.error(error))

View File

@ -9,11 +9,11 @@
import RxSwift
import UIKit
/// Makes ReCaptchaWebViewManager compatible with RxSwift extensions
extension ReCaptchaWebViewManager: ReactiveCompatible {}
/// Makes ReCaptcha compatible with RxSwift extensions
extension ReCaptcha: ReactiveCompatible {}
/// Provides a public extension on ReCaptchaWebViewManager that makes it reactive.
public extension Reactive where Base: ReCaptchaWebViewManager {
/// Provides a public extension on ReCaptcha that makes it reactive.
public extension Reactive where Base: ReCaptcha {
/**
- parameters:
@ -26,8 +26,8 @@ public extension Reactive where Base: ReCaptchaWebViewManager {
Sends `stop()` uppon disposal.
- See: `ReCaptchaWebViewManager.validate(on:resetOnError:completion:)`
- See: `ReCaptchaWebViewManager.stop()`
- See: `ReCaptcha.validate(on:resetOnError:completion:)`
- See: `ReCaptcha.stop()`
*/
func validate(on view: UIView, resetOnError: Bool = true) -> Observable<String> {
return Single<String>.create { [weak base] single in
@ -53,7 +53,7 @@ public extension Reactive where Base: ReCaptchaWebViewManager {
The reset is achieved by calling `grecaptcha.reset()` on the JS API.
- See: `ReCaptchaWebViewManager.reset()`
- See: `ReCaptcha.reset()`
*/
var reset: AnyObserver<Void> {
return AnyObserver { [weak base] event in