diff --git a/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift b/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift index a79cf73..c5e8fce 100644 --- a/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift @@ -151,4 +151,47 @@ class DispatchQueue__Tests: XCTestCase { waitForExpectations(timeout: 5) } + + // MARK: Once + + func test__Once__Single_Dispatch() { + let token = 3 + var dispatchCount = 0 + + // Does dispatch the given action + DispatchQueue.once(token: token) { + dispatchCount = 1 + } + + XCTAssertEqual(dispatchCount, 1) + + // Does not dispatch again for the same token + DispatchQueue.once(token: token) { + dispatchCount = 2 + } + + XCTAssertEqual(dispatchCount, 1) + } + + func test__Once__Multiple_Dispatches() { + let token1 = 4 + var didDispatch1 = false + + // Does dispatch the given action + DispatchQueue.once(token: token1) { + didDispatch1 = true + } + + XCTAssertTrue(didDispatch1) + + // Dispatch for a different token + let token2 = 6 + var didDispatch2 = false + + DispatchQueue.once(token: token2) { + didDispatch2 = true + } + + XCTAssertTrue(didDispatch2) + } } diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift index 5f572a0..d690a0f 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift @@ -210,6 +210,34 @@ class ReCaptchaWebViewManager__Tests: XCTestCase { XCTAssertEqual(count, 1) } + func test__Configure_Web_View__Called_Again_With_Reset() { + let exp0 = expectation(description: "configure webview 0") + + let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") + manager.validate(on: presenterView) { _ in + XCTFail("should not call completion") + } + + // Configure Webview + manager.configureWebView { _ in + manager.webView.evaluateJavaScript("execute();") { XCTAssertNil($1) } + exp0.fulfill() + } + + waitForExpectations(timeout: 10) + + // Reset and ensure it calls again + let exp1 = expectation(description: "configure webview 1") + + manager.configureWebView { _ in + manager.webView.evaluateJavaScript("execute();") { XCTAssertNil($1) } + exp1.fulfill() + } + + manager.reset() + waitForExpectations(timeout: 10) + } + // MARK: Stop func test__Stop() { diff --git a/ReCaptcha/Classes/DispatchQueue+Throttle.swift b/ReCaptcha/Classes/DispatchQueue+Throttle.swift index c9cf725..64566e5 100644 --- a/ReCaptcha/Classes/DispatchQueue+Throttle.swift +++ b/ReCaptcha/Classes/DispatchQueue+Throttle.swift @@ -16,6 +16,9 @@ extension DispatchQueue { /// Stores the last call times for a given context private static var lastDebounceCallTimes = [AnyHashable: DispatchTime]() + /// Dispatched actions' token storage + private static var onceTokenStorage = Set() + /// An object representing a context if none is given private static let nilContext = UUID() @@ -61,4 +64,21 @@ extension DispatchQueue { DispatchQueue.lastDebounceCallTimes.removeValue(forKey: context) } } + + /** + - parameters: + - token: The control token for each dispatched action + - action: The closure to be executed + + Dispatch the action only once for each given token + */ + static func once(token: AnyHashable, action: () -> Void) { + guard !onceTokenStorage.contains(token) else { return } + + defer { objc_sync_exit(self) } + objc_sync_enter(self) + + onceTokenStorage.insert(token) + action() + } } diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 8b57f71..efb9c31 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -122,6 +122,9 @@ internal class ReCaptchaWebViewManager { /// Configures the webview for display when required var configureWebView: ((WKWebView) -> Void)? + /// The dispatch token used to ensure `configureWebView` is only called once. + var configureWebViewDispatchToken = UUID() + /// If the ReCaptcha should be reset when it errors var shouldResetOnError = true @@ -152,6 +155,7 @@ internal class ReCaptchaWebViewManager { webview.accessibilityIdentifier = "webview" webview.accessibilityTraits = UIAccessibilityTraitLink webview.isHidden = true + print("HIDDEN") return webview }() @@ -211,6 +215,7 @@ internal class ReCaptchaWebViewManager { */ func reset() { didFinishLoading = false + configureWebViewDispatchToken = UUID() webviewDelegate.reset() webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in @@ -277,8 +282,7 @@ fileprivate extension ReCaptchaWebViewManager { } case .showReCaptcha: - // Ensures `configureWebView` won't get called multiple times in a short period - DispatchQueue.main.debounce(interval: 1) { [weak self] in + DispatchQueue.once(token: configureWebViewDispatchToken) { [weak self] in guard let `self` = self else { return } self.configureWebView?(self.webView) }