From 8ce8926e93e66496cf3cf26c3d024c43584c7e72 Mon Sep 17 00:00:00 2001 From: Alexande B Date: Tue, 16 Apr 2024 16:19:04 +0200 Subject: [PATCH] feat: don't emit sessionTimeout once result.dematerialize is called --- .../Core/HCaptchaResult__Tests.swift | 6 +- .../Core/HCaptchaWebViewManager__Tests.swift | 50 ++++++++++++- HCaptcha-Carthage.xcodeproj/project.pbxproj | 4 ++ HCaptcha/Classes/HCaptchaResult.swift | 8 ++- ...aWebViewManager+WKNavigationDelegate.swift | 45 ++++++++++++ HCaptcha/Classes/HCaptchaWebViewManager.swift | 72 +++++++------------ 6 files changed, 132 insertions(+), 53 deletions(-) create mode 100644 HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift diff --git a/Example/HCaptcha_Tests/Core/HCaptchaResult__Tests.swift b/Example/HCaptcha_Tests/Core/HCaptchaResult__Tests.swift index 0e3a647..a071918 100644 --- a/Example/HCaptcha_Tests/Core/HCaptchaResult__Tests.swift +++ b/Example/HCaptcha_Tests/Core/HCaptchaResult__Tests.swift @@ -13,7 +13,8 @@ import XCTest class HCaptchaResult__Tests: XCTestCase { func test__Get_Token() { let token = UUID().uuidString - let result = HCaptchaResult(token: token) + let manager = HCaptchaWebViewManager() + let result = HCaptchaResult(manager, token: token) do { let value = try result.dematerialize() @@ -26,7 +27,8 @@ class HCaptchaResult__Tests: XCTestCase { func test__Get_Token__Error() { let error = HCaptchaError.random() - let result = HCaptchaResult(error: error) + let manager = HCaptchaWebViewManager() + let result = HCaptchaResult(manager, error: error) do { _ = try result.dematerialize() diff --git a/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift b/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift index 0e2f21a..db9c07b 100644 --- a/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift +++ b/Example/HCaptcha_Tests/Core/HCaptchaWebViewManager__Tests.swift @@ -286,6 +286,41 @@ class HCaptchaWebViewManager__Tests: XCTestCase { waitForExpectations(timeout: 10) } + func test__Reset_After_Stop() { + let exp0 = expectation(description: "stop loading") + let exp1 = expectation(description: "configureWebView called") + let exp2 = expectation(description: "token recieved") + + // Stop + let manager = HCaptchaWebViewManager(messageBody: "{token: \"some_token\"}") + manager.stop() + manager.configureWebView { _ in + XCTFail("should not ask to configure the webview") + } + + manager.validate(on: presenterView) { _ in + XCTFail("should not validate") + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + exp0.fulfill() + } + + manager.reset() + + manager.configureWebView { _ in + exp1.fulfill() + } + + manager.validate(on: presenterView) { result in + let token = try? result.dematerialize() + XCTAssertEqual("some_token", token) + exp2.fulfill() + } + + waitForExpectations(timeout: 10) + } + // MARK: Setup func test__Key_Setup() { @@ -383,6 +418,7 @@ class HCaptchaWebViewManager__Tests: XCTestCase { var exp0Count = 0 let exp1 = expectation(description: "should call onEvent") let exp2 = expectation(description: "fail on first execution") + let exp3 = expectation(description: "hcaptcha opened") var result: HCaptchaResult? // Validate @@ -395,9 +431,17 @@ class HCaptchaWebViewManager__Tests: XCTestCase { } manager.onEvent = { (event, error) in - XCTAssertEqual(.error, event) - XCTAssertEqual(HCaptchaError.wrongMessageFormat, error as? HCaptchaError) - exp1.fulfill() + XCTAssertTrue([.error, .open].contains(event)) + switch event { + case .error: + XCTAssertEqual(.error, event) + XCTAssertEqual(HCaptchaError.wrongMessageFormat, error as? HCaptchaError) + exp1.fulfill() + case .open: + exp3.fulfill() + default: + XCTFail("Unexpected event \(event)") + } } // Error diff --git a/HCaptcha-Carthage.xcodeproj/project.pbxproj b/HCaptcha-Carthage.xcodeproj/project.pbxproj index b2215db..9feb3ba 100644 --- a/HCaptcha-Carthage.xcodeproj/project.pbxproj +++ b/HCaptcha-Carthage.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1F833B7F271DC69C00E4DAB2 /* RxSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F833B7E271DC69C00E4DAB2 /* RxSwift.xcframework */; }; 1F833B80271DC69C00E4DAB2 /* RxSwift.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1F833B7E271DC69C00E4DAB2 /* RxSwift.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + E614B37B2BCEBF3400CEA791 /* HCaptchaWebViewManager+WKNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E614B37A2BCEBF3400CEA791 /* HCaptchaWebViewManager+WKNavigationDelegate.swift */; }; E634944A2828856300130AC5 /* HCaptchaEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63494492828856300130AC5 /* HCaptchaEvent.swift */; }; E64C60082B7DFBE900D203F4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E64C60072B7DFBE900D203F4 /* PrivacyInfo.xcprivacy */; }; E65145E327786BDB0079668A /* HCaptchaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65145E227786BDB0079668A /* HCaptchaConfig.swift */; }; @@ -54,6 +55,7 @@ /* Begin PBXFileReference section */ 1F833B7E271DC69C00E4DAB2 /* RxSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = RxSwift.xcframework; path = Carthage/Build/RxSwift.xcframework; sourceTree = ""; }; + E614B37A2BCEBF3400CEA791 /* HCaptchaWebViewManager+WKNavigationDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HCaptchaWebViewManager+WKNavigationDelegate.swift"; sourceTree = ""; }; E63494492828856300130AC5 /* HCaptchaEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HCaptchaEvent.swift; sourceTree = ""; }; E64C60072B7DFBE900D203F4 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; E65145E227786BDB0079668A /* HCaptchaConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HCaptchaConfig.swift; sourceTree = ""; }; @@ -180,6 +182,7 @@ F24EA1D81F9683F5001DEC17 /* HCaptchaDecoder.swift */, E6DB9EA727B15954008F0327 /* HCaptchaDebugInfo.swift */, F24EA1D91F9683F5001DEC17 /* HCaptchaWebViewManager.swift */, + E614B37A2BCEBF3400CEA791 /* HCaptchaWebViewManager+WKNavigationDelegate.swift */, F24EA1DA1F9683F5001DEC17 /* String+Dict.swift */, F24EA1DB1F9683F5001DEC17 /* HCaptchaError.swift */, F2AE8613204F3B41002E28D7 /* HCaptchaResult.swift */, @@ -333,6 +336,7 @@ F24EA1E41F968403001DEC17 /* String+Dict.swift in Sources */, F231B39A1FEC51C800F82943 /* DispatchQueue+Throttle.swift in Sources */, E6DB9EA827B15954008F0327 /* HCaptchaDebugInfo.swift in Sources */, + E614B37B2BCEBF3400CEA791 /* HCaptchaWebViewManager+WKNavigationDelegate.swift in Sources */, E683772129053E560021BFD7 /* HCaptchaURLOpener.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/HCaptcha/Classes/HCaptchaResult.swift b/HCaptcha/Classes/HCaptchaResult.swift index 8ec0948..2046e62 100644 --- a/HCaptcha/Classes/HCaptchaResult.swift +++ b/HCaptcha/Classes/HCaptchaResult.swift @@ -21,7 +21,11 @@ public class HCaptchaResult: NSObject { /// Result error let error: HCaptchaError? - public init (token: String? = nil, error: HCaptchaError? = nil) { + /// Manager + let manager: HCaptchaWebViewManager + + internal init (_ manager: HCaptchaWebViewManager, token: String? = nil, error: HCaptchaError? = nil) { + self.manager = manager self.token = token self.error = error } @@ -35,6 +39,8 @@ public class HCaptchaResult: NSObject { */ @objc public func dematerialize() throws -> String { + manager.resultHandled = true + if let token = self.token { return token } diff --git a/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift b/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift new file mode 100644 index 0000000..98f4b07 --- /dev/null +++ b/HCaptcha/Classes/HCaptchaWebViewManager+WKNavigationDelegate.swift @@ -0,0 +1,45 @@ +// +// HCaptchaWebViewManager+WKNavigationDelegate.swift +// HCaptcha +// +// Copyright © 2024 HCaptcha. All rights reserved. +// + +import Foundation +import WebKit + +extension HCaptchaWebViewManager: WKNavigationDelegate { + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.targetFrame == nil, let url = navigationAction.request.url, urlOpener.canOpenURL(url) { + urlOpener.openURL(url) + } + decisionHandler(WKNavigationActionPolicy.allow) + } + + /// Tells the delegate that an error occurred during navigation. + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + Log.debug("WebViewManager.webViewDidFail with \(error)") + completion?(HCaptchaResult(self, error: .unexpected(error))) + } + + /// Tells the delegate that an error occurred during the early navigation process. + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + Log.debug("WebViewManager.webViewDidFailProvisionalNavigation with \(error)") + completion?(HCaptchaResult(self, error: .unexpected(error))) + } + + /// Tells the delegate that the web view’s content process was terminated. + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + Log.debug("WebViewManager.webViewWebContentProcessDidTerminate") + let kHCaptchaErrorWebViewProcessDidTerminate = -1 + let kHCaptchaErrorDomain = "com.hcaptcha.sdk-ios" + let error = NSError(domain: kHCaptchaErrorDomain, + code: kHCaptchaErrorWebViewProcessDidTerminate, + userInfo: [ + NSLocalizedDescriptionKey: "WebView web content process did terminate", + NSLocalizedRecoverySuggestionErrorKey: "Call HCaptcha.reset()"]) + completion?(HCaptchaResult(self, error: .unexpected(error))) + didFinishLoading = false + } +} diff --git a/HCaptcha/Classes/HCaptchaWebViewManager.swift b/HCaptcha/Classes/HCaptchaWebViewManager.swift index 00f7758..aa51c37 100644 --- a/HCaptcha/Classes/HCaptchaWebViewManager.swift +++ b/HCaptcha/Classes/HCaptchaWebViewManager.swift @@ -1,4 +1,3 @@ -// swiftlint:disable file_length // // HCaptchaWebViewManager.swift // HCaptcha @@ -39,6 +38,9 @@ internal class HCaptchaWebViewManager: NSObject { public var shouldSkipForTests = false #endif + /// True if validation token was dematerialized + internal var resultHandled: Bool = false + /// Sends the result message var completion: ((HCaptchaResult) -> Void)? @@ -67,7 +69,7 @@ internal class HCaptchaWebViewManager: NSObject { fileprivate var decoder: HCaptchaDecoder! /// Indicates if the script has already been loaded by the `webView` - fileprivate var didFinishLoading = false { + internal var didFinishLoading = false { didSet { if didFinishLoading { onDidFinishLoading?() @@ -114,7 +116,7 @@ internal class HCaptchaWebViewManager: NSObject { }() /// Responsible for external link handling - fileprivate let urlOpener: HCaptchaURLOpener + internal let urlOpener: HCaptchaURLOpener /** - parameters: @@ -163,9 +165,11 @@ internal class HCaptchaWebViewManager: NSObject { */ func validate(on view: UIView) { Log.debug("WebViewManager.validate on: \(view)") + resultHandled = false + #if DEBUG guard !shouldSkipForTests else { - completion?(HCaptchaResult(token: "")) + completion?(HCaptchaResult(self, token: "")) return } #endif @@ -182,6 +186,7 @@ internal class HCaptchaWebViewManager: NSObject { Log.debug("WebViewManager.stop") stopInitWebViewConfiguration = true webView.stopLoading() + resultHandled = true } /** @@ -193,6 +198,7 @@ internal class HCaptchaWebViewManager: NSObject { Log.debug("WebViewManager.reset") configureWebViewDispatchToken = UUID() stopInitWebViewConfiguration = false + resultHandled = false if didFinishLoading { executeJS(command: .reset) didFinishLoading = false @@ -229,8 +235,15 @@ fileprivate extension HCaptchaWebViewManager { */ func handle(result: HCaptchaDecoder.Result) { Log.debug("WebViewManager.handleResult: \(result)") + + guard !resultHandled else { + Log.debug("WebViewManager.handleResult skip as handled") + return + } + switch result { - case .token(let token): completion?(HCaptchaResult(token: token)) + case .token(let token): + completion?(HCaptchaResult(self, token: token)) case .error(let error): handle(error: error) onEvent?(.error, error) @@ -246,11 +259,10 @@ fileprivate extension HCaptchaWebViewManager { private func handle(error: HCaptchaError) { switch error { - case HCaptchaError.challengeClosed: - completion?(HCaptchaResult(error: error)) + case HCaptchaError.challengeClosed: completion?(HCaptchaResult(self, error: error)) case HCaptchaError.networkError: if let completion = completion { - completion(HCaptchaResult(error: error)) + completion(HCaptchaResult(self, error: error)) } else { lastError = error } @@ -259,7 +271,7 @@ fileprivate extension HCaptchaWebViewManager { reset() validate(on: view) } else { - completion?(HCaptchaResult(error: error)) + completion?(HCaptchaResult(self, error: error)) } } } @@ -342,9 +354,11 @@ fileprivate extension HCaptchaWebViewManager { guard didLoad else { if let error = lastError { DispatchQueue.main.async { [weak self] in + guard let self = self else { return } Log.debug("WebViewManager complete with pendingError: \(error)") - self?.completion?(HCaptchaResult(error: error)) - self?.lastError = nil + + self.completion?(HCaptchaResult(self, error: error)) + self.lastError = nil } if error == .networkError { Log.debug("WebViewManager reloads html after \(error) error") @@ -364,39 +378,3 @@ fileprivate extension HCaptchaWebViewManager { executeJS(command: command, didLoad: self.didFinishLoading) } } - -extension HCaptchaWebViewManager: WKNavigationDelegate { - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if navigationAction.targetFrame == nil, let url = navigationAction.request.url, urlOpener.canOpenURL(url) { - urlOpener.openURL(url) - } - decisionHandler(WKNavigationActionPolicy.allow) - } - - /// Tells the delegate that an error occurred during navigation. - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - Log.debug("WebViewManager.webViewDidFail with \(error)") - completion?(HCaptchaResult(error: .unexpected(error))) - } - - /// Tells the delegate that an error occurred during the early navigation process. - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - Log.debug("WebViewManager.webViewDidFailProvisionalNavigation with \(error)") - completion?(HCaptchaResult(error: .unexpected(error))) - } - - /// Tells the delegate that the web view’s content process was terminated. - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - Log.debug("WebViewManager.webViewWebContentProcessDidTerminate") - let kHCaptchaErrorWebViewProcessDidTerminate = -1 - let kHCaptchaErrorDomain = "com.hcaptcha.sdk-ios" - let error = NSError(domain: kHCaptchaErrorDomain, - code: kHCaptchaErrorWebViewProcessDidTerminate, - userInfo: [ - NSLocalizedDescriptionKey: "WebView web content process did terminate", - NSLocalizedRecoverySuggestionErrorKey: "Call HCaptcha.reset()"]) - completion?(HCaptchaResult(error: .unexpected(error))) - didFinishLoading = false - } -}