diff --git a/Example/ReCaptcha.xcodeproj/xcshareddata/xcschemes/ReCaptcha-Example.xcscheme b/Example/ReCaptcha.xcodeproj/xcshareddata/xcschemes/ReCaptcha-Example.xcscheme index 81d7408..eb2d44a 100644 --- a/Example/ReCaptcha.xcodeproj/xcshareddata/xcschemes/ReCaptcha-Example.xcscheme +++ b/Example/ReCaptcha.xcodeproj/xcshareddata/xcschemes/ReCaptcha-Example.xcscheme @@ -78,7 +78,7 @@ Void) { + configureWebView = configure + } + + func validate(on view: UIView, resetOnError: Bool = true, completion: @escaping (ReCaptchaResult) -> Void) { + self.shouldResetOnError = resetOnError + self.completion = completion + + validate(on: view) + } } diff --git a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift index 61f4c7d..8f96b63 100644 --- a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift +++ b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift @@ -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.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() diff --git a/ReCaptcha/Classes/ReCaptcha.swift b/ReCaptcha/Classes/ReCaptcha.swift index 7489b67..bc722b8 100644 --- a/ReCaptcha/Classes/ReCaptcha.swift +++ b/ReCaptcha/Classes/ReCaptcha.swift @@ -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 diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 3ddec5b..cd0a37a 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -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)) diff --git a/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift b/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift index 72bce5d..824367c 100644 --- a/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift +++ b/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift @@ -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 { return Single.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 { return AnyObserver { [weak base] event in