Feature: Swiftlint
This commit is contained in:
parent
c3517c32b1
commit
33fa4efc19
|
|
@ -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\.
|
||||
\/\/
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
F206BAD41F8D3FEB00A25807 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = "<group>"; };
|
||||
F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; };
|
||||
F288E9441F9537760018688D /* ReCaptchaError+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReCaptchaError+Equatable.swift"; sourceTree = "<group>"; };
|
||||
F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; name = .swiftlint.yml; path = ../.swiftlint.yml; sourceTree = "<group>"; };
|
||||
F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReCaptcha__Tests.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -143,6 +144,7 @@
|
|||
F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */,
|
||||
F2ECCF871E9FCE930097B199 /* .travis.yml */,
|
||||
F2ECCF8F1EA008C20097B199 /* codecov.yml */,
|
||||
F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */,
|
||||
);
|
||||
name = "Podspec Metadata";
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
disabled_rules:
|
||||
- explicit_top_level_acl
|
||||
|
|
@ -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:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
disabled_rules:
|
||||
- type_name
|
||||
- nesting
|
||||
- force_unwrapping
|
||||
- explicit_top_level_acl
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
LICENSE
2
LICENSE
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2017 Flávio Caetano <flavio@vieiracaetano.com>
|
||||
Copyright © 2017 Flávio Caetano <flavio@vieiracaetano.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// ReCaptcha
|
||||
//
|
||||
// Created by Flávio Caetano on 22/03/17.
|
||||
//
|
||||
// Copyright © 2017 ReCaptcha. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
|
|
|||
|
|
@ -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<String, ReCaptchaError>
|
||||
|
||||
/** 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<String, NSError> which may contain a valid result token.
|
||||
*/
|
||||
- view: The view that should present the webview.
|
||||
- completion: A closure that receives a Result<String, NSError> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, ReCaptchaError>` 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<Base.Response> {
|
||||
|
||||
- See: `ReCaptchaWebViewManager.validate(on:completion:)`
|
||||
- See: `ReCaptchaWebViewManager.stop()`
|
||||
*/
|
||||
func validate(on view: UIView) -> Observable<Base.Response> {
|
||||
return Observable<Base.Response>.create { [weak base] (observer: AnyObserver<Base.Response>) in
|
||||
base?.validate(on: view) { response in
|
||||
observer.onNext(response)
|
||||
observer.onCompleted()
|
||||
}
|
||||
|
||||
|
||||
return Disposables.create { [weak base] in
|
||||
base?.stop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue