Compare commits
No commits in common. "master" and "1.4.1" have entirely different histories.
|
|
@ -1,13 +1,14 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
## Is it really a bug?
|
## Is it really a bug?
|
||||||
Before opening an issue, check the following:
|
Before opening an issue, check the following:
|
||||||
1. You are using the **SITE** key
|
1. You are using the **Client side integration** key
|
||||||
2. The correct domain, with protocol, is setup.
|
2. The correct domain, with protocol, is setup.
|
||||||
3. You are using an **Invisible** reCAPTCHA v2 key.
|
3. You are using an **Invisible** reCAPTCHA key.
|
||||||
4. If the widget doesn't appear, that is expected since the library will try to resolve the challenge _invisibly_.
|
4. If the widget doesn't appear, that is expected since the library will try to resolve the challenge _invisibly_.
|
||||||
|
|
||||||
https://www.google.com/recaptcha/admin#site
|
https://www.google.com/recaptcha/admin#site
|
||||||
|
|
@ -19,7 +20,7 @@ A clear and concise description of what the bug is.
|
||||||
## To Reproduce
|
## To Reproduce
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '...'
|
2. Click on '....'
|
||||||
3. ...
|
3. ...
|
||||||
4. Profit (jk See error)
|
4. Profit (jk See error)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
# 1.4.2
|
|
||||||
|
|
||||||
- Fix: Webview's resource loading detection (#56 #60)
|
|
||||||
|
|
||||||
# 1.4.1
|
# 1.4.1
|
||||||
|
|
||||||
- Fix RxSwift dependency version (#58)
|
- Fix RxSwift dependency version (#58)
|
||||||
|
|
|
||||||
2
Cartfile
2
Cartfile
|
|
@ -1 +1 @@
|
||||||
binary "https://raw.github.com/TouchInstinct/CarthageBinaries/master/RxSwift/RxSwift.json"
|
github "ReactiveX/RxSwift" ~> 4.3
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
binary "https://raw.github.com/TouchInstinct/CarthageBinaries/master/RxSwift/RxSwift.json" "4.5.0"
|
github "ReactiveX/RxSwift" "4.4.0"
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ class ReCaptcha_Rx__Tests: XCTestCase {
|
||||||
let exp = expectation(description: "stop loading")
|
let exp = expectation(description: "stop loading")
|
||||||
|
|
||||||
// Stop
|
// Stop
|
||||||
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{log: \"foo\"}"))
|
let recaptcha = ReCaptcha(manager: ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}"))
|
||||||
recaptcha.configureWebView { _ in
|
recaptcha.configureWebView { _ in
|
||||||
XCTFail("should not ask to configure the webview")
|
XCTFail("should not ask to configure the webview")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
post({action: "didLoad"});
|
||||||
|
};
|
||||||
|
|
||||||
var reset = function() {
|
var reset = function() {
|
||||||
shouldFail = false;
|
shouldFail = false;
|
||||||
post({action: "didLoad"});
|
post({action: "didLoad"});
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Add Google's [Invisible ReCaptcha v2](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
|
Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
|
||||||
automatically handles ReCaptcha's events and retrieves the validation token or notifies you to present the challenge if
|
automatically handles ReCaptcha's events and retrieves the validation token or notifies you to present the challenge if
|
||||||
invisibility is not possible.
|
invisibility is not possible.
|
||||||
|
|
||||||
|
|
@ -17,15 +17,8 @@ invisibility is not possible.
|
||||||
|
|
||||||
#### _Warning_ ⚠️
|
#### _Warning_ ⚠️
|
||||||
|
|
||||||
Beware that this library only works for ReCaptcha v2 Invisible keys! Make sure to check the reCAPTCHA
|
Beware that this library only works for Invisible ReCaptcha keys! Make sure to check the Invisible reCAPTCHA option
|
||||||
v2 Invisible badge option when creating your [API Key](https://www.google.com/recaptcha/admin/create).
|
when creating your [API Key](https://www.google.com/recaptcha/admin).
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
You won't be able to use a ReCaptcha v3 key because it requires server-side validation. On v3, all
|
|
||||||
challenges succeed into a token which is then validated in the server for a score. For this reason,
|
|
||||||
a frontend app can't know on its own wether or not a user is valid since the challenge will always
|
|
||||||
result in a valid token.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -49,7 +42,7 @@ extension for the ReCaptcha framework.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol ex. http:// or https://) to your Info.plist and run:
|
Simply add `ReCaptchaKey` and `ReCaptchaDomain` (with a protocol) to your Info.plist and run:
|
||||||
|
|
||||||
``` swift
|
``` swift
|
||||||
let recaptcha = try? ReCaptcha()
|
let recaptcha = try? ReCaptcha()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'ReCaptcha'
|
s.name = 'ReCaptcha'
|
||||||
s.version = '1.4.2'
|
s.version = '1.4.1'
|
||||||
s.summary = 'ReCaptcha for iOS'
|
s.summary = 'ReCaptcha for iOS'
|
||||||
s.swift_version = '4.2'
|
s.swift_version = '4.2'
|
||||||
|
|
||||||
s.description = <<-DESC
|
s.description = <<-DESC
|
||||||
Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
|
Add Google's [Invisible ReCaptcha](https://developers.google.com/recaptcha/docs/invisible) to your project. This library
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import WebKit
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public class ReCaptcha {
|
public class ReCaptcha {
|
||||||
public typealias BoolParameterClosure = (Bool) -> ()
|
|
||||||
|
|
||||||
fileprivate struct Constants {
|
fileprivate struct Constants {
|
||||||
struct InfoDictKeys {
|
struct InfoDictKeys {
|
||||||
static let APIKey = "ReCaptchaKey"
|
static let APIKey = "ReCaptchaKey"
|
||||||
|
|
@ -103,12 +101,6 @@ public class ReCaptcha {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for WebView loading state changing
|
|
||||||
public var onLoadingChanged: BoolParameterClosure? {
|
|
||||||
get { return manager.onLoadingChanged }
|
|
||||||
set { manager.onLoadingChanged = newValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The worker that handles webview events and communication
|
/// The worker that handles webview events and communication
|
||||||
let manager: ReCaptchaWebViewManager
|
let manager: ReCaptchaWebViewManager
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,93 @@ import WebKit
|
||||||
/** Handles comunications with the webview containing the ReCaptcha challenge.
|
/** Handles comunications with the webview containing the ReCaptcha challenge.
|
||||||
*/
|
*/
|
||||||
internal class ReCaptchaWebViewManager {
|
internal class ReCaptchaWebViewManager {
|
||||||
|
/** The `webView` delegate object that performs execution uppon script loading
|
||||||
|
*/
|
||||||
|
fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate {
|
||||||
|
struct Constants {
|
||||||
|
/// The host that loaded requests should have
|
||||||
|
static let apiURLHost = "www.google.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parent manager
|
||||||
|
private weak var manager: ReCaptchaWebViewManager?
|
||||||
|
|
||||||
|
/// The active requests' urls
|
||||||
|
private var activeRequests = Set<String>(minimumCapacity: 0)
|
||||||
|
|
||||||
|
/// - parameter manager: The parent manager
|
||||||
|
init(manager: ReCaptchaWebViewManager) {
|
||||||
|
self.manager = manager
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- parameters:
|
||||||
|
- webView: The web view invoking the delegate method.
|
||||||
|
- navigationAction: Descriptive information about the action triggering the navigation request.
|
||||||
|
- decisionHandler: The decision handler to call to allow or cancel the navigation. The argument is one of
|
||||||
|
the constants of the enumerated type WKNavigationActionPolicy.
|
||||||
|
|
||||||
|
Decides whether to allow or cancel a navigation.
|
||||||
|
*/
|
||||||
|
func webView(
|
||||||
|
_ webView: WKWebView,
|
||||||
|
decidePolicyFor navigationAction: WKNavigationAction,
|
||||||
|
decisionHandler: @escaping (WKNavigationActionPolicy
|
||||||
|
) -> Void) {
|
||||||
|
defer { decisionHandler(.allow) }
|
||||||
|
|
||||||
|
if let url = navigationAction.request.url, url.host == Constants.apiURLHost {
|
||||||
|
activeRequests.insert(url.absoluteString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- parameters:
|
||||||
|
- webView: The web view invoking the delegate method.
|
||||||
|
- navigationResponse: Descriptive information about the navigation response.
|
||||||
|
- decisionHandler: A block to be called when your app has decided whether to allow or cancel the navigation
|
||||||
|
|
||||||
|
Decides whether to allow or cancel a navigation after its response is known.
|
||||||
|
*/
|
||||||
|
func webView(
|
||||||
|
_ webView: WKWebView,
|
||||||
|
decidePolicyFor navigationResponse: WKNavigationResponse,
|
||||||
|
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
|
||||||
|
) {
|
||||||
|
defer { decisionHandler(.allow) }
|
||||||
|
guard let url = navigationResponse.response.url?.absoluteString,
|
||||||
|
activeRequests.remove(url) != nil, activeRequests.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flag the requests as finished and call ReCaptcha execution if necessary
|
||||||
|
func execute() {
|
||||||
|
guard manager?.didFinishLoading != true else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.throttle(deadline: .now() + 1, context: self) { [weak self] in
|
||||||
|
// Did finish loading the ReCaptcha JS source
|
||||||
|
self?.manager?.didFinishLoading = true
|
||||||
|
|
||||||
|
if self?.manager?.completion != nil {
|
||||||
|
// User has requested for validation
|
||||||
|
self?.manager?.execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flags all requests as finished
|
||||||
|
func reset() {
|
||||||
|
activeRequests.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate struct Constants {
|
fileprivate struct Constants {
|
||||||
static let ExecuteJSCommand = "execute();"
|
static let ExecuteJSCommand = "execute();"
|
||||||
static let ResetCommand = "reset();"
|
static let ResetCommand = "reset();"
|
||||||
static let BotUserAgent = "Googlebot/2.1"
|
static let BotUserAgent = "Googlebot/2.1"
|
||||||
static let NumberOfDivsCommand = "document.getElementsByTagName(\"div\").length"
|
|
||||||
|
|
||||||
static let MinNumberOfDivs = 5
|
|
||||||
static let NumberOfDivsFinishedLoadingAttempts = 10
|
|
||||||
|
|
||||||
// A page doesn't have enough time to load on old devices
|
|
||||||
static let NumberOfDivsLoadingDelay = 0.5
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
@ -44,9 +119,6 @@ internal class ReCaptchaWebViewManager {
|
||||||
public var shouldSkipForTests = false
|
public var shouldSkipForTests = false
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Callback for loading state changing
|
|
||||||
var onLoadingChanged: ReCaptcha.BoolParameterClosure?
|
|
||||||
|
|
||||||
/// Sends the result message
|
/// Sends the result message
|
||||||
var completion: ((ReCaptchaResult) -> Void)?
|
var completion: ((ReCaptchaResult) -> Void)?
|
||||||
|
|
||||||
|
|
@ -63,39 +135,29 @@ internal class ReCaptchaWebViewManager {
|
||||||
fileprivate var decoder: ReCaptchaDecoder!
|
fileprivate var decoder: ReCaptchaDecoder!
|
||||||
|
|
||||||
/// Indicates if the script has already been loaded by the `webView`
|
/// Indicates if the script has already been loaded by the `webView`
|
||||||
fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString
|
fileprivate var didFinishLoading = false // webView.isLoading does not work in this case
|
||||||
didSet {
|
|
||||||
if didFinishLoading && completion != nil {
|
|
||||||
// User has requested for validation
|
|
||||||
// A small delay is necessary to allow JS to wrap its operations and avoid errors.
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
||||||
self?.execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The observer for `.UIWindowDidBecomeVisible`
|
/// The observer for `.UIWindowDidBecomeVisible`
|
||||||
fileprivate var observer: NSObjectProtocol?
|
fileprivate var observer: NSObjectProtocol?
|
||||||
|
|
||||||
/// The observer for `\WKWebView.estimatedProgress`
|
|
||||||
fileprivate var loadingObservation: NSKeyValueObservation?
|
|
||||||
|
|
||||||
/// The endpoint url being used
|
/// The endpoint url being used
|
||||||
fileprivate var endpoint: String
|
fileprivate var endpoint: String
|
||||||
|
|
||||||
|
/// The `webView` delegate implementation
|
||||||
|
fileprivate lazy var webviewDelegate: WebViewDelegate = {
|
||||||
|
WebViewDelegate(manager: self)
|
||||||
|
}()
|
||||||
|
|
||||||
/// The webview that executes JS code
|
/// The webview that executes JS code
|
||||||
lazy var webView: WKWebView = {
|
lazy var webView: WKWebView = {
|
||||||
let webview = WKWebView(
|
let webview = WKWebView(
|
||||||
frame: CGRect(x: 0, y: 0, width: 1, height: 1),
|
frame: CGRect(x: 0, y: 0, width: 1, height: 1),
|
||||||
configuration: self.buildConfiguration()
|
configuration: self.buildConfiguration()
|
||||||
)
|
)
|
||||||
|
webview.navigationDelegate = self.webviewDelegate
|
||||||
webview.accessibilityIdentifier = "webview"
|
webview.accessibilityIdentifier = "webview"
|
||||||
webview.accessibilityTraits = UIAccessibilityTraits.link
|
webview.accessibilityTraits = UIAccessibilityTraits.link
|
||||||
webview.isHidden = true
|
webview.isHidden = true
|
||||||
self.loadingObservation = webview.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in
|
|
||||||
self?.didFinishLoading = change.newValue == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return webview
|
return webview
|
||||||
}()
|
}()
|
||||||
|
|
@ -109,7 +171,6 @@ internal class ReCaptchaWebViewManager {
|
||||||
*/
|
*/
|
||||||
init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
|
init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
|
|
||||||
self.decoder = ReCaptchaDecoder { [weak self] result in
|
self.decoder = ReCaptchaDecoder { [weak self] result in
|
||||||
self?.handle(result: result)
|
self?.handle(result: result)
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +198,6 @@ internal class ReCaptchaWebViewManager {
|
||||||
Starts the challenge validation
|
Starts the challenge validation
|
||||||
*/
|
*/
|
||||||
func validate(on view: UIView) {
|
func validate(on view: UIView) {
|
||||||
onLoadingChanged?(true)
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
guard !shouldSkipForTests else {
|
guard !shouldSkipForTests else {
|
||||||
completion?(.token(""))
|
completion?(.token(""))
|
||||||
|
|
@ -164,6 +224,7 @@ internal class ReCaptchaWebViewManager {
|
||||||
func reset() {
|
func reset() {
|
||||||
didFinishLoading = false
|
didFinishLoading = false
|
||||||
configureWebViewDispatchToken = UUID()
|
configureWebViewDispatchToken = UUID()
|
||||||
|
webviewDelegate.reset()
|
||||||
|
|
||||||
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
|
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
|
|
@ -178,7 +239,7 @@ internal class ReCaptchaWebViewManager {
|
||||||
/** Private methods for ReCaptchaWebViewManager
|
/** Private methods for ReCaptchaWebViewManager
|
||||||
*/
|
*/
|
||||||
fileprivate extension ReCaptchaWebViewManager {
|
fileprivate extension ReCaptchaWebViewManager {
|
||||||
/** Executes the JS command that loads the ReCaptcha challenge after a page finished loading.
|
/** Executes the JS command that loads the ReCaptcha challenge.
|
||||||
This method has no effect if the webview hasn't finished loading.
|
This method has no effect if the webview hasn't finished loading.
|
||||||
*/
|
*/
|
||||||
func execute() {
|
func execute() {
|
||||||
|
|
@ -187,65 +248,10 @@ fileprivate extension ReCaptchaWebViewManager {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluateExecuteWhenLoadingFinished(count: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
- parameter count: Number of checks of number of divs
|
|
||||||
|
|
||||||
Executes the JS command that returns number of divs.
|
|
||||||
*/
|
|
||||||
func evaluateExecuteWhenLoadingFinished(count: Int) {
|
|
||||||
webView.evaluateJavaScript(Constants.NumberOfDivsCommand) { [weak self] (result, error) -> Void in
|
|
||||||
if let error = error {
|
|
||||||
self?.decoder.send(error: .unexpected(error))
|
|
||||||
self?.onLoadingChanged?(false)
|
|
||||||
} else {
|
|
||||||
self?.handleNumberOfDivs(result: result, count: count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
- parameters:
|
|
||||||
- result: Result of number of divs command evaluation
|
|
||||||
- count: Number of checks of divs count
|
|
||||||
|
|
||||||
Handles number of divs command result.
|
|
||||||
*/
|
|
||||||
func handleNumberOfDivs(result: Any?, count: Int) {
|
|
||||||
if let result = result as? Int, result >= Constants.MinNumberOfDivs {
|
|
||||||
evaluateExecute()
|
|
||||||
} else {
|
|
||||||
handleInvalidNumberOfDivsResult(count: count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
- parameter count: Number of checks of number of divs
|
|
||||||
|
|
||||||
Handles invalid number of divs.
|
|
||||||
*/
|
|
||||||
func handleInvalidNumberOfDivsResult(count: Int) {
|
|
||||||
if count < Constants.NumberOfDivsFinishedLoadingAttempts {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.NumberOfDivsLoadingDelay) { [weak self] in
|
|
||||||
self?.evaluateExecuteWhenLoadingFinished(count: count + 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
decoder.send(error: .htmlLoadError)
|
|
||||||
onLoadingChanged?(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Executes the JS command that loads the ReCaptcha challenge.
|
|
||||||
*/
|
|
||||||
func evaluateExecute() {
|
|
||||||
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
|
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
self?.decoder.send(error: .unexpected(error))
|
self?.decoder.send(error: .unexpected(error))
|
||||||
}
|
}
|
||||||
self?.onLoadingChanged?(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +297,7 @@ fileprivate extension ReCaptchaWebViewManager {
|
||||||
|
|
||||||
case .didLoad:
|
case .didLoad:
|
||||||
// For testing purposes
|
// For testing purposes
|
||||||
didFinishLoading = true
|
webviewDelegate.execute()
|
||||||
|
|
||||||
case .log(let message):
|
case .log(let message):
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public enum ReCaptchaRxError: Error {
|
|
||||||
case baseWasReleased
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Makes ReCaptcha compatible with RxSwift extensions
|
/// Makes ReCaptcha compatible with RxSwift extensions
|
||||||
extension ReCaptcha: ReactiveCompatible {}
|
extension ReCaptcha: ReactiveCompatible {}
|
||||||
|
|
||||||
|
|
@ -68,20 +64,4 @@ public extension Reactive where Base: ReCaptcha {
|
||||||
base?.reset()
|
base?.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
Observable of loading state
|
|
||||||
(will not work if someone changes onLoadingChanged variable; current onLodinglChanged will not work after subscription)
|
|
||||||
*/
|
|
||||||
var loadingObservable: Observable<Bool> {
|
|
||||||
return .create { [weak base] observer -> Disposable in
|
|
||||||
guard let base = base else {
|
|
||||||
observer.onError(ReCaptchaRxError.baseWasReleased)
|
|
||||||
return Disposables.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
base.onLoadingChanged = { observer.onNext($0) }
|
|
||||||
return Disposables.create { [weak base] in base?.onLoadingChanged = nil }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
Loading…
Reference in New Issue