From 33fa4efc190a2e8a113a6d4250494e7a6655f2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Caetano?= Date: Tue, 24 Oct 2017 17:25:34 -0200 Subject: [PATCH] Feature: Swiftlint --- .swiftlint.yml | 47 ++++++ .travis.yml | 4 + Example/Podfile | 1 + Example/Podfile.lock | 11 +- Example/ReCaptcha.xcodeproj/project.pbxproj | 17 ++ Example/ReCaptcha/.swiftlint.yml | 2 + Example/ReCaptcha/AppDelegate.swift | 34 +--- Example/ReCaptcha/ViewController.swift | 30 ++-- Example/ReCaptcha_Tests/.swiftlint.yml | 5 + .../Core/ReCaptchaDecoder__Tests.swift | 98 ++++++------ .../Core/ReCaptchaWebViewManager__Tests.swift | 106 ++++++------- .../Core/ReCaptcha__Tests.swift | 19 ++- .../Helpers/ReCaptchaError+Equatable.swift | 13 +- .../ReCaptchaWebViewManager+Helpers.swift | 5 +- .../Helpers/Result+Helpers.swift | 2 +- .../RxSwift/ReCaptcha+Rx__Tests.swift | 76 ++++----- LICENSE | 2 +- ReCaptcha/Classes/ReCaptcha.swift | 71 +++++---- ReCaptcha/Classes/ReCaptchaDecoder.swift | 49 +++--- ReCaptcha/Classes/ReCaptchaError.swift | 2 +- .../Classes/ReCaptchaWebViewManager.swift | 149 +++++++++++------- ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift | 25 +-- ReCaptcha/Classes/String+Dict.swift | 16 +- 23 files changed, 456 insertions(+), 328 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Example/ReCaptcha/.swiftlint.yml create mode 100644 Example/ReCaptcha_Tests/.swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..a9946bd --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +excluded: + - Carthage + - Example/Pods + +disabled_rules: + - nesting + - opening_brace + - statement_position + - weak_delegate + +opt_in_rules: +# - array_init # is causing a false positive on `.map { !$0 }` + - closure_end_indentation + - closure_spacing + - contains_over_first_not_nil + - empty_count + - explicit_init + - explicit_top_level_acl + - extension_access_modifier + - fatal_error_message + - file_header + - first_where + - force_unwrapping + - implicit_return + - let_var_whitespace + - literal_expression_end_indentation + - multiline_arguments + - multiple_closures_with_trailing_closure + - operator_usage_whitespace + - redundant_nil_coalescing + - single_test_class + - sorted_imports +# - trailing_closure # causing a false positive on `.subscribe(onNext:)` + - unneeded_parentheses_in_closure_argument + +vertical_whitespace: + max_empty_lines: 2 + +file_header: + required_pattern: | + \/\/ + \/\/ .*?\.swift + \/\/ ReCaptcha + \/\/ + \/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2}\. + \/\/ Copyright © \d{4} ReCaptcha\. All rights reserved\. + \/\/ diff --git a/.travis.yml b/.travis.yml index df89ee4..0c98df6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ before_install: - pod --version - pod repo update --silent - brew outdated carthage || brew upgrade carthage + - brew outdated swiftlint || brew upgrade swiftlint install: - pushd Example; pod install; popd; @@ -17,6 +18,9 @@ env: - DESTINATION="platform=iOS Simulator,OS=9.0,name=iPhone 6" - DESTINATION="platform=iOS Simulator,OS=8.3,name=iPhone 4S" +before_script: + - swiftlint --strict --path "$TRAVIS_BUILD_DIR" + script: - xcodebuild -workspace Example/ReCaptcha.xcworkspace -scheme ReCaptcha-Example -destination "$DESTINATION" build | xcpretty - xcodebuild -workspace Example/ReCaptcha.xcworkspace -scheme ReCaptcha-Example -destination "$DESTINATION" test | xcpretty diff --git a/Example/Podfile b/Example/Podfile index 48c36ca..aaaa3af 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -8,6 +8,7 @@ inhibit_all_warnings! target 'ReCaptcha_Example' do pod 'ReCaptcha/RxSwift', :path => '../' pod 'RxCocoa', '~> 3.0' + pod 'SwiftLint', '~> 0.23' target 'ReCaptcha_Tests' do inherit! :search_paths diff --git a/Example/Podfile.lock b/Example/Podfile.lock index edcb965..68ed4ea 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,19 +1,21 @@ PODS: - AppSwizzle (1.1.2) - - ReCaptcha/Core (0.2.0): + - ReCaptcha/Core (0.3.0): - Result (~> 3.0) - - ReCaptcha/RxSwift (0.2.0): + - ReCaptcha/RxSwift (0.3.0): - ReCaptcha/Core - RxSwift (~> 3.0) - Result (3.2.4) - RxCocoa (3.6.1): - RxSwift (~> 3.6) - RxSwift (3.6.1) + - SwiftLint (0.23.1) DEPENDENCIES: - AppSwizzle (~> 1.1) - ReCaptcha/RxSwift (from `../`) - RxCocoa (~> 3.0) + - SwiftLint (~> 0.23) EXTERNAL SOURCES: ReCaptcha: @@ -21,11 +23,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppSwizzle: 45d1a9d71821e7e1bb3a2a503c2979ca6fe4e46d - ReCaptcha: d39a6bac7d877a2541d5e676e9f8c194a7d6c928 + ReCaptcha: 0ecf1105cae5040557d8b470909dda5196853c15 Result: d2d07204ce72856f1fd9130bbe42c35a7b0fea10 RxCocoa: 84a08739ab186248c7f31ce4ee92d6f8a947d690 RxSwift: f9de85ea20cd2f7716ee5409fc13523dc638e4e4 + SwiftLint: 1b670ce79284c76520f84060e87d645078fd32fa -PODFILE CHECKSUM: 8feb0da0c7db4d53374bb19369e025e179015e8b +PODFILE CHECKSUM: 02f550962dfe5b54072b9091c41d29819d67c422 COCOAPODS: 1.3.1 diff --git a/Example/ReCaptcha.xcodeproj/project.pbxproj b/Example/ReCaptcha.xcodeproj/project.pbxproj index e61aa5e..24fcef0 100644 --- a/Example/ReCaptcha.xcodeproj/project.pbxproj +++ b/Example/ReCaptcha.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ F206BAD41F8D3FEB00A25807 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = ""; }; F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; F288E9441F9537760018688D /* ReCaptchaError+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReCaptchaError+Equatable.swift"; sourceTree = ""; }; + F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; name = .swiftlint.yml; path = ../.swiftlint.yml; sourceTree = ""; }; F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReCaptcha__Tests.swift; sourceTree = ""; }; F2ECCF761E9FC47B0097B199 /* ReCaptcha_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReCaptcha_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F2ECCF7A1E9FC47B0097B199 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -143,6 +144,7 @@ F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */, F2ECCF871E9FCE930097B199 /* .travis.yml */, F2ECCF8F1EA008C20097B199 /* codecov.yml */, + F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */, ); name = "Podspec Metadata"; sourceTree = ""; @@ -220,6 +222,7 @@ 607FACCE1AFB9204008FA782 /* Resources */, 8F03FFB3F5C55E873C23C682 /* [CP] Embed Pods Frameworks */, ED1C0E07490C9C4B4A401061 /* [CP] Copy Pods Resources */, + F2C0FD7E1F9F8061009B7A6F /* SwiftLint */, ); buildRules = ( ); @@ -425,6 +428,20 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReCaptcha_Example/Pods-ReCaptcha_Example-resources.sh\"\n"; showEnvVarsInLog = 0; }; + F2C0FD7E1F9F8061009B7A6F /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n \"${PODS_ROOT}/SwiftLint/swiftlint\" --path \"${PROJECT_DIR}/..\"\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/Example/ReCaptcha/.swiftlint.yml b/Example/ReCaptcha/.swiftlint.yml new file mode 100644 index 0000000..84f8494 --- /dev/null +++ b/Example/ReCaptcha/.swiftlint.yml @@ -0,0 +1,2 @@ +disabled_rules: + - explicit_top_level_acl diff --git a/Example/ReCaptcha/AppDelegate.swift b/Example/ReCaptcha/AppDelegate.swift index c24a9f6..08e96cf 100644 --- a/Example/ReCaptcha/AppDelegate.swift +++ b/Example/ReCaptcha/AppDelegate.swift @@ -2,8 +2,8 @@ // AppDelegate.swift // ReCaptcha // -// Created by Flávio Caetano on 03/22/2017. -// Copyright (c) 2017 ReCaptcha. All rights reserved. +// Created by Flávio Caetano on 03/22/17. +// Copyright © 2017 ReCaptcha. All rights reserved. // import UIKit @@ -14,33 +14,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? + ) -> Bool { // Override point for customization after application launch. return true } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - } - diff --git a/Example/ReCaptcha/ViewController.swift b/Example/ReCaptcha/ViewController.swift index 68b5098..893a8b6 100644 --- a/Example/ReCaptcha/ViewController.swift +++ b/Example/ReCaptcha/ViewController.swift @@ -2,44 +2,45 @@ // ViewController.swift // ReCaptcha // -// Created by Flávio Caetano on 03/22/2017. -// Copyright (c) 2017 ReCaptcha. All rights reserved. +// Created by Flávio Caetano on 03/22/17. +// Copyright © 2017 ReCaptcha. All rights reserved. // -import UIKit import ReCaptcha import Result -import RxSwift import RxCocoa +import RxSwift +import UIKit class ViewController: UIViewController { fileprivate static let webViewTag = 123 - + + // swiftlint:disable force_try fileprivate let recaptcha = try! ReCaptcha() fileprivate var disposeBag = DisposeBag() - + @IBOutlet weak var label: UILabel! @IBOutlet weak var spinner: UIActivityIndicatorView! - - + + override func viewDidLoad() { super.viewDidLoad() - + recaptcha.configureWebView { [weak self] webview in webview.frame = self?.view.bounds ?? CGRect.zero webview.tag = ViewController.webViewTag } } - - + + @IBAction func didPressButton(button: UIButton) { disposeBag = DisposeBag() let validate = recaptcha.rx.validate(on: view) .debug("validate") .share() - + let isLoading = validate .map { _ in false } .startWith(true) @@ -53,7 +54,7 @@ class ViewController: UIViewController { .catchErrorJustReturn(false) .bind(to: button.rx.isEnabled) .disposed(by: disposeBag) - + validate .map { [weak self] _ in self?.view.viewWithTag(ViewController.webViewTag) @@ -62,11 +63,10 @@ class ViewController: UIViewController { subview?.removeFromSuperview() }) .disposed(by: disposeBag) - + validate .map { try $0.dematerialize() } .bind(to: label.rx.text) .disposed(by: disposeBag) } } - diff --git a/Example/ReCaptcha_Tests/.swiftlint.yml b/Example/ReCaptcha_Tests/.swiftlint.yml new file mode 100644 index 0000000..2d9f9c0 --- /dev/null +++ b/Example/ReCaptcha_Tests/.swiftlint.yml @@ -0,0 +1,5 @@ +disabled_rules: + - type_name + - nesting + - force_unwrapping + - explicit_top_level_acl diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift index 84ba366..8d62f28 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaDecoder__Tests.swift @@ -8,145 +8,145 @@ @testable import ReCaptcha -import XCTest import WebKit +import XCTest class ReCaptchaDecoder__Tests: XCTestCase { fileprivate typealias Result = ReCaptchaDecoder.Result - + fileprivate var assertResult: ((Result) -> Void)? fileprivate var decoder: ReCaptchaDecoder! - + override func setUp() { super.setUp() - + decoder = ReCaptchaDecoder { [weak self] result in self?.assertResult?(result) } } - + 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__Send_Error() { let exp = expectation(description: "send message") var result: Result? - + assertResult = { res in result = res exp.fulfill() } - - + + // Send let err = ReCaptchaError.random() decoder.send(error: err) - + waitForExpectations(timeout: 1) - - + + // Check XCTAssertNotNil(result) XCTAssertEqual(result?.error, err) XCTAssertNil(result?.token) XCTAssertFalse(result!.showReCaptcha) } - - + + func test__Decode__Wrong_Format() { let exp = expectation(description: "send unsupported message") var result: Result? - + assertResult = { res in result = res exp.fulfill() } - - + + // Send let message = MockMessage(message: "foobar") decoder.send(message: message) - + waitForExpectations(timeout: 1) - - + + // Check XCTAssertEqual(result?.error, .wrongMessageFormat) XCTAssertNil(result?.token) XCTAssertFalse(result!.showReCaptcha) } - - + + func test__Decode__Undefined() { let exp = expectation(description: "send message with undefined body") var result: Result? - + assertResult = { res in result = res exp.fulfill() } - - + + // Send let message = MockMessage(message: ["foo": "bar"]) decoder.send(message: message) - + waitForExpectations(timeout: 1) - - + + // Check XCTAssertEqual(result?.error, .wrongMessageFormat) XCTAssertNil(result?.token) XCTAssertFalse(result!.showReCaptcha) } - - + + func test__Decode__ShowReCaptcha() { let exp = expectation(description: "send message with undefined body") var result: Result? - + assertResult = { res in result = res exp.fulfill() } - - + + // Send let message = MockMessage(message: ["action": "showReCaptcha"]) decoder.send(message: message) - + waitForExpectations(timeout: 1) - - + + // Check XCTAssertNil(result?.error) XCTAssertNil(result?.token) XCTAssertTrue(result!.showReCaptcha) } - - + + func test__Decode__Token() { let exp = expectation(description: "send message with undefined body") var result: Result? - + assertResult = { res in result = res exp.fulfill() } - - + + // Send let token = String(arc4random()) let message = MockMessage(message: ["token": token]) decoder.send(message: message) - + waitForExpectations(timeout: 1) - - + + // Check XCTAssertNil(result?.error) XCTAssertEqual(result?.token, token) @@ -159,9 +159,9 @@ class MockMessage: WKScriptMessage { override var body: Any { return storedBody } - + fileprivate let storedBody: Any - + init(message: Any) { storedBody = message } @@ -182,12 +182,12 @@ extension ReCaptchaDecoder.Result { guard case .token(let token) = self else { return nil } return token } - + var showReCaptcha: Bool { guard case .showReCaptcha = self else { return false } return true } - + var error: ReCaptchaError? { guard case .error(let error) = self else { return nil } return error diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift index 53d5579..04892cb 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift @@ -8,133 +8,133 @@ @testable import ReCaptcha -import XCTest import Result import WebKit +import XCTest class ReCaptchaWebViewManager__Tests: XCTestCase { - + fileprivate var apiKey: String! fileprivate var presenterView: UIView! - + override func setUp() { super.setUp() - + presenterView = UIApplication.shared.keyWindow! apiKey = String(arc4random()) } - + override func tearDown() { presenterView = nil apiKey = nil - + super.tearDown() } - + // MARK: Validate - + func test__Validate__Token() { let exp1 = expectation(description: "load token") var result1: ReCaptchaWebViewManager.Response? - + // Validate let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey) manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - + manager.validate(on: presenterView) { response in result1 = response exp1.fulfill() } - + waitForExpectations(timeout: 3) - - + + // Verify XCTAssertNotNil(result1) XCTAssertNil(result1?.error) XCTAssertEqual(result1?.value, apiKey) - - + + // Validate again let exp2 = expectation(description: "reload token") var result2: ReCaptchaWebViewManager.Response? - + // Validate manager.validate(on: presenterView) { response in result2 = response exp2.fulfill() } - + waitForExpectations(timeout: 3) - - + + // Verify XCTAssertNotNil(result2) XCTAssertNil(result2?.error) XCTAssertEqual(result2?.value, apiKey) } - - + + func test__Validate__Show_ReCaptcha() { let exp = expectation(description: "show recaptcha") - + // Validate let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") manager.configureWebView { _ in exp.fulfill() } - - manager.validate(on: presenterView) { response in + + manager.validate(on: presenterView) { _ in XCTFail("should not call completion") } - + waitForExpectations(timeout: 3) } - - + + func test__Validate__Message_Error() { var result: ReCaptchaWebViewManager.Response? let exp = expectation(description: "message error") - + // Validate let manager = ReCaptchaWebViewManager(messageBody: "\"foobar\"") manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - + manager.validate(on: presenterView) { response in result = response exp.fulfill() } - + waitForExpectations(timeout: 3) - + // Verify XCTAssertNotNil(result) XCTAssertEqual(result?.error, .wrongMessageFormat) XCTAssertNil(result?.value) } - + func test__Validate__JS_Error() { var result: ReCaptchaWebViewManager.Response? let exp = expectation(description: "js error") - + // Validate let manager = ReCaptchaWebViewManager(messageBody: "foobar") manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - + manager.validate(on: presenterView) { response in result = response exp.fulfill() } - + waitForExpectations(timeout: 3) - + // Verify XCTAssertNotNil(result) XCTAssertNotNil(result?.error) @@ -147,62 +147,62 @@ class ReCaptchaWebViewManager__Tests: XCTestCase { XCTFail("Unexpected error received") } } - + // MARK: Configure WebView - + func test__Configure_Web_View__Empty() { let exp = expectation(description: "configure webview") - + // Configure WebView let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") - manager.validate(on: presenterView) { response in + manager.validate(on: presenterView) { _ in XCTFail("should not call completion") } - + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { exp.fulfill() } - + waitForExpectations(timeout: 3) } - + func test__Configure_Web_View() { let exp = expectation(description: "configure webview") - + // Configure WebView let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") manager.configureWebView { [unowned self] webView in XCTAssertEqual(webView.superview, self.presenterView) exp.fulfill() } - - manager.validate(on: presenterView) { response in + + manager.validate(on: presenterView) { _ in XCTFail("should not call completion") } - + waitForExpectations(timeout: 3) } - + // MARK: Stop - + func test__Stop() { let exp = expectation(description: "stop loading") - + // Stop let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") 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) { exp.fulfill() } - + waitForExpectations(timeout: 3) } diff --git a/Example/ReCaptcha_Tests/Core/ReCaptcha__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptcha__Tests.swift index e825b9e..dc5bcb3 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptcha__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptcha__Tests.swift @@ -6,11 +6,10 @@ // Copyright © 2017 ReCaptcha. All rights reserved. // -@testable import ReCaptcha - -import XCTest import AppSwizzle +@testable import ReCaptcha import RxSwift +import XCTest class ReCaptcha__Tests: XCTestCase { @@ -79,12 +78,22 @@ class ReCaptcha__Tests: XCTestCase { // Ensures plist key if nil key let plistKey = "bar" - let config1 = try? ReCaptcha.Config(apiKey: nil, infoPlistKey: plistKey, baseURL: URL(string: "foo"), infoPlistURL: nil) + let config1 = try? ReCaptcha.Config( + apiKey: nil, + infoPlistKey: plistKey, + baseURL: URL(string: "foo"), + infoPlistURL: nil + ) XCTAssertEqual(config1?.apiKey, plistKey) // Ensures preference of given key over plist entry let key = "foo" - let config2 = try? ReCaptcha.Config(apiKey: key, infoPlistKey: plistKey, baseURL: URL(string: "foo"), infoPlistURL: nil) + let config2 = try? ReCaptcha.Config( + apiKey: key, + infoPlistKey: plistKey, + baseURL: URL(string: "foo"), + infoPlistURL: nil + ) XCTAssertEqual(config2?.apiKey, key) } } diff --git a/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift b/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift index 86fe9d6..0fe9a5c 100644 --- a/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift +++ b/Example/ReCaptcha_Tests/Helpers/ReCaptchaError+Equatable.swift @@ -6,17 +6,16 @@ // Copyright © 2017 ReCaptcha. All rights reserved. // +import Foundation @testable import ReCaptcha -import Foundation - extension ReCaptchaError: Equatable { - public static func ==(lhs: ReCaptchaError, rhs: ReCaptchaError) -> Bool { + public static func == (lhs: ReCaptchaError, rhs: ReCaptchaError) -> Bool { switch (lhs, rhs) { - case (.htmlLoadError, .htmlLoadError), - (.apiKeyNotFound, .apiKeyNotFound), - (.baseURLNotFound, .baseURLNotFound), - (.wrongMessageFormat, .wrongMessageFormat): + case (.htmlLoadError, .htmlLoadError), + (.apiKeyNotFound, .apiKeyNotFound), + (.baseURLNotFound, .baseURLNotFound), + (.wrongMessageFormat, .wrongMessageFormat): return true case (.unexpected(let lhe as NSError), .unexpected(let rhe as NSError)): return lhe == rhe diff --git a/Example/ReCaptcha_Tests/Helpers/ReCaptchaWebViewManager+Helpers.swift b/Example/ReCaptcha_Tests/Helpers/ReCaptchaWebViewManager+Helpers.swift index 5cf8290..2761539 100644 --- a/Example/ReCaptcha_Tests/Helpers/ReCaptchaWebViewManager+Helpers.swift +++ b/Example/ReCaptcha_Tests/Helpers/ReCaptchaWebViewManager+Helpers.swift @@ -6,9 +6,8 @@ // Copyright © 2017 ReCaptcha. All rights reserved. // -@testable import ReCaptcha - import Foundation +@testable import ReCaptcha extension ReCaptchaWebViewManager { @@ -18,7 +17,7 @@ extension ReCaptchaWebViewManager { .path(forResource: "mock", ofType: "html") .flatMap { try? String(contentsOfFile: $0) } .map { String(format: $0, arguments: ["message": messageBody]) } - + self.init( html: html!, apiKey: apiKey ?? String(arc4random()), diff --git a/Example/ReCaptcha_Tests/Helpers/Result+Helpers.swift b/Example/ReCaptcha_Tests/Helpers/Result+Helpers.swift index 5e7eb26..94e9000 100644 --- a/Example/ReCaptcha_Tests/Helpers/Result+Helpers.swift +++ b/Example/ReCaptcha_Tests/Helpers/Result+Helpers.swift @@ -14,7 +14,7 @@ extension Result { guard case .success(let value) = self else { return nil } return value } - + var error: Error? { guard case .failure(let error) = self else { return nil } return error diff --git a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift index 3fd2226..432b3f3 100644 --- a/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift +++ b/Example/ReCaptcha_Tests/RxSwift/ReCaptcha+Rx__Tests.swift @@ -8,153 +8,153 @@ @testable import ReCaptcha -import XCTest import RxSwift +import XCTest class ReCaptcha_Rx__Tests: XCTestCase { - + fileprivate var disposeBag: DisposeBag! fileprivate var apiKey: String! fileprivate var presenterView: UIView! - + override func setUp() { super.setUp() - + disposeBag = DisposeBag() presenterView = UIApplication.shared.keyWindow! apiKey = String(arc4random()) } - + override func tearDown() { disposeBag = nil presenterView = nil apiKey = nil - + super.tearDown() } - - + + func test__Validate__Token() { let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey) manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - - + + var result: ReCaptchaWebViewManager.Response? let exp = expectation(description: "validate token") - + // Validate manager.rx.validate(on: presenterView) .subscribe { event in switch event { case .next(let value): result = value - + case .error(let error): XCTFail(error.localizedDescription) - + case .completed: exp.fulfill() } } .disposed(by: disposeBag) - + waitForExpectations(timeout: 3) - + // Verify XCTAssertNotNil(result) XCTAssertEqual(result?.value, apiKey) XCTAssertNil(result?.error) } - - + + func test__Validate__Show_ReCaptcha() { let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}", apiKey: apiKey) let exp = expectation(description: "show recaptcha") - + manager.configureWebView { _ in exp.fulfill() } - + // Validate manager.rx.validate(on: presenterView) .timeout(2, scheduler: MainScheduler.instance) .subscribe { event in switch event { - case .next(_): + case .next: XCTFail("should not have validated") - + case .error(let error): XCTAssertEqual(String(describing: error), RxError.timeout.debugDescription) - + case .completed: XCTFail("should not have completed") } } .disposed(by: disposeBag) - + waitForExpectations(timeout: 3) } - - + + func test__Validate__Error() { let manager = ReCaptchaWebViewManager(messageBody: "\"foobar\"", apiKey: apiKey) manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - - + + var result: ReCaptchaWebViewManager.Response? let exp = expectation(description: "validate token") - + // Validate manager.rx.validate(on: presenterView) .subscribe { event in switch event { case .next(let value): result = value - + case .error(let error): XCTFail(error.localizedDescription) - + case .completed: exp.fulfill() } } .disposed(by: disposeBag) - + waitForExpectations(timeout: 3) - + // Verify XCTAssertNotNil(result) XCTAssertNil(result?.value) XCTAssertNotNil(result?.error) XCTAssertEqual(result?.error, .wrongMessageFormat) } - + // MARK: Dispose - + func test__Dispose() { let exp = expectation(description: "stop loading") - + // Stop let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") manager.configureWebView { _ in XCTFail("should not ask to configure the webview") } - + let disposable = manager.rx.validate(on: presenterView) .subscribe { _ in XCTFail("should not validate") } disposable.dispose() - + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { exp.fulfill() } - + waitForExpectations(timeout: 3) } } diff --git a/LICENSE b/LICENSE index 366ce78..1c7afdc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 Flávio Caetano +Copyright © 2017 Flávio Caetano Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/ReCaptcha/Classes/ReCaptcha.swift b/ReCaptcha/Classes/ReCaptcha.swift index 96e79b3..190424c 100644 --- a/ReCaptcha/Classes/ReCaptcha.swift +++ b/ReCaptcha/Classes/ReCaptcha.swift @@ -3,7 +3,7 @@ // ReCaptcha // // Created by Flávio Caetano on 22/03/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // import Foundation @@ -22,7 +22,9 @@ open class ReCaptcha: ReCaptchaWebViewManager { /// The JS API endpoint to be loaded onto the HTML file. public enum Endpoint { - /// Google's default endpoint. Points to https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit + /** Google's default endpoint. Points to + https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit + */ case `default` /// Alternate endpoint. Points to https://www.recaptcha.net/recaptcha/api.js @@ -37,17 +39,18 @@ open class ReCaptcha: ReCaptchaWebViewManager { } /** Internal data model for DI in unit tests - */ + */ struct Config { /// The raw unformated HTML file content let html: String - // The API key that will be sent to the ReCaptcha API + /// The API key that will be sent to the ReCaptcha API let apiKey: String - // The base url to be used to resolve relative URLs in the webview + /// The base url to be used to resolve relative URLs in the webview let baseURL: URL + /// The Bundle that holds ReCaptcha's assets private static let bundle: Bundle = { let bundle = Bundle(for: ReCaptcha.self) guard let cocoapodsBundle = bundle @@ -60,17 +63,19 @@ open class ReCaptcha: ReCaptchaWebViewManager { }() /** - - parameter apiKey: The API key sent to the ReCaptcha init - - parameter infoPlistKey: The API key retrived from the application's Info.plist - - parameter baseURL: The base URL sent to the ReCaptcha init - - parameter infoPlistURL: The base URL retrieved from the application's Info.plist + - parameters: + - apiKey: The API key sent to the ReCaptcha init + - infoPlistKey: The API key retrived from the application's Info.plist + - baseURL: The base URL sent to the ReCaptcha init + - infoPlistURL: The base URL retrieved from the application's Info.plist - - Throws: - - `ReCaptchaError.htmlLoadError` if is unable to load the HTML embedded in the bundle. - - `ReCaptchaError.apiKeyNotFound` if an `apiKey` is not provided and can't find one in the project's Info.plist. - - `ReCaptchaError.baseURLNotFound` if a `baseURL` is not provided and can't find one in the project's Info.plist. - - Rethrows any exceptions thrown by `String(contentsOfFile:)` - */ + - Throws: `ReCaptchaError.htmlLoadError`: if is unable to load the HTML embedded in the bundle. + - Throws: `ReCaptchaError.apiKeyNotFound`: if an `apiKey` is not provided and can't find one in the project's + Info.plist. + - Throws: `ReCaptchaError.baseURLNotFound`: if a `baseURL` is not provided and can't find one in the project's + Info.plist. + - Throws: Rethrows any exceptions thrown by `String(contentsOfFile:)` + */ public init(apiKey: String?, infoPlistKey: String?, baseURL: URL?, infoPlistURL: URL?) throws { guard let filePath = Config.bundle.path(forResource: "recaptcha", ofType: "html") else { throw ReCaptchaError.htmlLoadError @@ -85,30 +90,34 @@ open class ReCaptcha: ReCaptchaWebViewManager { } let rawHTML = try String(contentsOfFile: filePath) - + self.html = rawHTML self.apiKey = apiKey self.baseURL = domain } } - - /** Initializes a ReCaptcha object + + /** + - parameters: + - apiKey: The API key sent to the ReCaptcha init + - infoPlistKey: The API key retrived from the application's Info.plist + - baseURL: The base URL sent to the ReCaptcha init + - infoPlistURL: The base URL retrieved from the application's Info.plist - Both `apiKey` and `baseURL` may be nil, in which case the lib will look for entries of `ReCaptchaKey` and + Initializes a ReCaptcha object + + Both `apiKey` and `baseURL` may be nil, in which case the lib will look for entries of `ReCaptchaKey` and `ReCaptchaDomain`, respectively, in the project's Info.plist - + A key may be aquired here: https://www.google.com/recaptcha/admin#list - - - parameter apiKey: The API key to be provided to Google's ReCaptcha. Overrides the Info.plist entry. Defaults to `nil`. - - parameter baseURL: A url domain to be load onto the webview. Overrides the Info.plist entry. Defaults to `nil`. - - parameter endpoint: The JS API endpoint to be loaded onto the HTML file. Defaults to `.default`. - - - Throws: - - `ReCaptchaError.htmlLoadError` if is unable to load the HTML embedded in the bundle. - - `ReCaptchaError.apiKeyNotFound` if an `apiKey` is not provided and can't find one in the project's Info.plist. - - `ReCaptchaError.baseURLNotFound` if a `baseURL` is not provided and can't find one in the project's Info.plist. - - Rethrows any exceptions thrown by `String(contentsOfFile:)` - */ + + - Throws: `ReCaptchaError.htmlLoadError`: if is unable to load the HTML embedded in the bundle. + - Throws: `ReCaptchaError.apiKeyNotFound`: if an `apiKey` is not provided and can't find one in the project's + Info.plist. + - Throws: `ReCaptchaError.baseURLNotFound`: if a `baseURL` is not provided and can't find one in the project's + Info.plist. + - Throws: Rethrows any exceptions thrown by `String(contentsOfFile:)` + */ public init(apiKey: String? = nil, baseURL: URL? = nil, endpoint: Endpoint = .default) throws { let infoDict = Bundle.main.infoDictionary diff --git a/ReCaptcha/Classes/ReCaptchaDecoder.swift b/ReCaptcha/Classes/ReCaptchaDecoder.swift index 5f180dc..12c6611 100644 --- a/ReCaptcha/Classes/ReCaptchaDecoder.swift +++ b/ReCaptcha/Classes/ReCaptchaDecoder.swift @@ -3,7 +3,7 @@ // ReCaptcha // // Created by Flávio Caetano on 22/03/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // import Foundation @@ -11,40 +11,41 @@ import WebKit /** The Decoder of javascript messages from the webview -*/ -class ReCaptchaDecoder: NSObject { + */ +internal class ReCaptchaDecoder: NSObject { /** The decoder result. - - - token(String): A result token, if any - - showReCaptcha: Indicates that the webview containing the challenge should be displayed. - - error(NSError): Any errors - */ + */ enum Result { /// A result token, if any case token(String) - + /// Indicates that the webview containing the challenge should be displayed. case showReCaptcha - + /// Any errors case error(ReCaptchaError) } - + + /// The closure that receives messages fileprivate let sendMessage: ((Result) -> Void) - - /** Initializes a decoder with a completion closure. + + /** - parameter didReceiveMessage: A closure that receives a ReCaptchaDecoder.Result + + Initializes a decoder with a completion closure. */ init(didReceiveMessage: @escaping (Result) -> Void) { sendMessage = didReceiveMessage - + super.init() } - - - /** Sends an error to the completion closure + + + /** - parameter error: The error to be sent. - */ + + Sends an error to the completion closure + */ func send(error: ReCaptchaError) { sendMessage(.error(error)) } @@ -60,7 +61,7 @@ extension ReCaptchaDecoder: WKScriptMessageHandler { guard let dict = message.body as? [String: Any] else { return sendMessage(.error(.wrongMessageFormat)) } - + sendMessage(Result.from(response: dict)) } } @@ -71,20 +72,22 @@ extension ReCaptchaDecoder: WKScriptMessageHandler { /** Private methods on `ReCaptchaDecoder.Result` */ fileprivate extension ReCaptchaDecoder.Result { - - /** Parses a dict received from the webview onto a `ReCaptchaDecoder.Result` + + /** - parameter response: A dictionary containing the message to be parsed - returns: A decoded ReCaptchaDecoder.Result + + Parses a dict received from the webview onto a `ReCaptchaDecoder.Result` */ static func from(response: [String: Any]) -> ReCaptchaDecoder.Result { if let token = response["token"] as? String { return .token(token) } - + if let action = response["action"] as? String, action == "showReCaptcha" { return .showReCaptcha } - + return .error(.wrongMessageFormat) } } diff --git a/ReCaptcha/Classes/ReCaptchaError.swift b/ReCaptcha/Classes/ReCaptchaError.swift index 0b8055a..f6c8be3 100644 --- a/ReCaptcha/Classes/ReCaptchaError.swift +++ b/ReCaptcha/Classes/ReCaptchaError.swift @@ -3,7 +3,7 @@ // ReCaptcha // // Created by Flávio Caetano on 22/03/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // import Foundation diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index 922c358..c964822 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -3,31 +3,36 @@ // ReCaptcha // // Created by Flávio Caetano on 22/03/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // import Foundation -import WebKit import Result +import WebKit /** Handles comunications with the webview containing the ReCaptcha challenge. -*/ + */ open class ReCaptchaWebViewManager { public typealias Response = Result - /** The webview delegate object that performs execution uppon script loading + /** The `webView` delegate object that performs execution uppon script loading */ fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate { + /// The parent manager private weak var manager: ReCaptchaWebViewManager? + + /// - parameter manager: The parent manager init(manager: ReCaptchaWebViewManager) { self.manager = manager } - /** Called when the navigation is complete. - - - parameter webView: The web view invoking the delegate method. - - parameter navigation: The navigation object that finished. + /** + - 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 @@ -40,35 +45,50 @@ open class ReCaptchaWebViewManager { } } } - + fileprivate struct Constants { static let ExecuteJSCommand = "execute();" } + /// Sends the result message fileprivate var completion: ((Response) -> Void)? + + /// Configures the webview for display when required fileprivate var configureWebView: ((WKWebView) -> Void)? + + /// The JS message recoder fileprivate var decoder: ReCaptchaDecoder! + + /// Indicates if the script has already been loaded by the `webView` fileprivate var didFinishLoading = false // webView.isLoading does not work in this case + /// The observer for `.UIWindowDidBecomeVisible` + fileprivate var observer: NSObjectProtocol? + + /// The `webView` delegate implementation fileprivate lazy var webviewDelegate: WebViewDelegate = { - return WebViewDelegate(manager: self) + WebViewDelegate(manager: self) }() - + + /// The webview that executes JS code fileprivate lazy var webView: WKWebView = { - let webview = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: self.buildConfiguration()) + let webview = WKWebView( + frame: CGRect(x: 0, y: 0, width: 1, height: 1), + configuration: self.buildConfiguration() + ) webview.navigationDelegate = self.webviewDelegate webview.isHidden = true - + return webview }() - - /** Initializes the manager + + /** - parameters: - - html: The HTML string to be loaded onto the webview - - apiKey: The Google's ReCaptcha API Key - - baseURL: The URL configured with the API Key - - endpoint: The JS API endpoint to be loaded onto the HTML file. - */ + - html: The HTML string to be loaded onto the webview + - apiKey: The Google's ReCaptcha API Key + - baseURL: The URL configured with the API Key + - 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?.handle(result: result) @@ -80,20 +100,25 @@ open class ReCaptchaWebViewManager { setupWebview(on: window, html: formattedHTML, url: baseURL) } else { - NotificationCenter.default.addObserver(forName: .UIWindowDidBecomeVisible, object: nil, queue: nil) - { [weak self] notification in + observer = NotificationCenter.default.addObserver( + forName: .UIWindowDidBecomeVisible, + object: nil, + queue: nil + ) { [weak self] notification in guard let window = notification.object as? UIWindow else { return } self?.setupWebview(on: window, html: formattedHTML, url: baseURL) } } } - - - /** Starts the challenge validation + + + /** - parameters: - - view: The view that presents the webview. - - completion: A closure that receives a Result which may contain a valid result token. - */ + - view: The view that should present the webview. + - completion: A closure that receives a Result which may contain a valid result token. + + Starts the challenge validation + */ open func validate(on view: UIView, completion: @escaping (Response) -> Void) { self.completion = completion @@ -103,21 +128,22 @@ open class ReCaptchaWebViewManager { execute() } - - + + /// Stops the execution of the webview open func stop() { webView.stopLoading() } - - - /** 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. - - - parameter configure: A closure that receives an instance of `WKWebView` for configuration. - */ + + + /** + - 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 } @@ -128,7 +154,7 @@ 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. */ @@ -137,52 +163,61 @@ fileprivate extension ReCaptchaWebViewManager { // Hasn't finished loading the HTML yet return } - - webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] result, error in + + webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in if let error = error { self?.decoder.send(error: .unexpected(error)) } } } - - /** Creates a `WKWebViewConfiguration` to be added to the `WKWebView` instance. + + /** - returns: An instance of `WKWebViewConfiguration` + + Creates a `WKWebViewConfiguration` to be added to the `WKWebView` instance. */ func buildConfiguration() -> WKWebViewConfiguration { let controller = WKUserContentController() controller.add(decoder, name: "recaptcha") - + let conf = WKWebViewConfiguration() conf.userContentController = controller - + return conf } - - /** Handles the decoder results received from the webview - - Parameter result: A `ReCaptchaDecoder.Result` with the decoded message. + + /** + - parameter result: A `ReCaptchaDecoder.Result` with the decoded message. + + Handles the decoder results received from the webview */ func handle(result: ReCaptchaDecoder.Result) { switch result { case .token(let token): completion?(.success(token)) - + case .error(let error): completion?(.failure(error)) - + case .showReCaptcha: configureWebView?(webView) } } - /** Adds the webview to a valid UIView and loads the initial HTML file - - parameter window: The window in which to add the webview - - parameter html: The embedded HTML file - - parameter url: The base URL given to the webview - */ + /** + - parameters: + - window: The window in which to add the webview + - html: The embedded HTML file + - url: The base URL given to the webview + + Adds the webview to a valid UIView and loads the initial HTML file + */ func setupWebview(on window: UIWindow, html: String, url: URL) { window.addSubview(webView) webView.loadHTMLString(html, baseURL: url) - NotificationCenter.default.removeObserver(self) + if let observer = observer { + NotificationCenter.default.removeObserver(observer) + } } } diff --git a/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift b/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift index 1420f7a..4ae660b 100644 --- a/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift +++ b/ReCaptcha/Classes/Rx/ReCaptcha+Rx.swift @@ -3,34 +3,37 @@ // ReCaptcha // // Created by Flávio Caetano on 11/04/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // -import UIKit import RxSwift +import UIKit /// Makes ReCaptchaWebViewManager compatible with RxSwift extensions extension ReCaptchaWebViewManager: ReactiveCompatible {} /// Provides a public extension on ReCaptchaWebViewManager that makes it reactive. public extension Reactive where Base: ReCaptchaWebViewManager { - - /** Starts the challenge validation uppon subscription. + + /** + - parameter view: The view that should present the webview. + Starts the challenge validation uppon subscription. + The stream's element is a `Result` that may contain a valid token. - + Sends `stop()` uppon disposal. - - - See: - [ReCaptchaWebViewManager.validate(on:completion:)](../Classes/ReCaptchaWebViewManager.html#/s:9ReCaptcha0aB14WebViewManagerC8validateySo6UIViewC2on_y6ResultAHOySSAA0aB5ErrorOGc10completiontF) - */ - public func validate(on view: UIView) -> Observable { + + - See: `ReCaptchaWebViewManager.validate(on:completion:)` + - See: `ReCaptchaWebViewManager.stop()` + */ + func validate(on view: UIView) -> Observable { return Observable.create { [weak base] (observer: AnyObserver) in base?.validate(on: view) { response in observer.onNext(response) observer.onCompleted() } - + return Disposables.create { [weak base] in base?.stop() } diff --git a/ReCaptcha/Classes/String+Dict.swift b/ReCaptcha/Classes/String+Dict.swift index 38c3521..2935520 100644 --- a/ReCaptcha/Classes/String+Dict.swift +++ b/ReCaptcha/Classes/String+Dict.swift @@ -3,13 +3,27 @@ // ReCaptcha // // Created by Flávio Caetano on 10/10/17. -// +// Copyright © 2017 ReCaptcha. All rights reserved. // import Foundation extension String { + /** + - parameters: + - format: The string to be formatted. + - arguments: A dictionary containing the which keys should be replaced by which values. + - returns: A formatted string + + Parses a format string using a dictionary of arguments + + Replaces occurrences of `"${key}"` with their respective values. + + ``` + String(format: "Hello, ${user}", ["user": "Flavio"]) // Hello, Flavio + ``` + */ init(format: String, arguments: [String: CustomStringConvertible]) { self.init(describing: arguments.reduce(format) { (format: String, args: (key: String, value: CustomStringConvertible)) -> String in