Yandex SmartCaptcha in iOS apps
To embed SmartCaptcha in an iOS app:
Getting started
- Add HTML code to work with SmartCaptcha (or use a ready-made
https://smartcaptcha.yandexcloud.net/webview
). - Create a CAPTCHA by following this guide.
- Retrieve the CAPTCHA keys. Copy the Client key and Server key field values from the Overview tab of the CAPTCHA you created. You will need the Client key to load the page with CAPTCHA, and the Server key to get the CAPTCHA test results.
Configure your website's JS part
If not using https://smartcaptcha.yandexcloud.net/webview
, follow these steps:
-
Add a SmartCaptcha widget to the web page.
-
Create a method to communicate with the app's native part:
function sendIos(...args) { if (args.length == 0) { return; } const message = { method: args[0], data: args[1] !== undefined ? args[1] : "" }; // Check for a call from WKWebView. if (window.webkit) { window.webkit.messageHandlers.NativeClient.postMessage(message); } }
With the following message format:
{ method: "captchaDidFinish" | "challengeDidAppear" | "challengeDidDisappear" data: "tokenName" | "" }
The method returns the following:
success
: Successful user validation.challenge-visible
: Opening the challenge pop-up window.challenge-hidden
: Closing the challenge pop-up window.
Configure your website's native part
-
In WKUserContentController, register a handler named
WKScriptMessageHandler
for theNativeClient
key. -
Implement the following method in the handler:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let jsData = message.body as? [String: String] else { return } guard let methodName = jsData["method"] else { return } doSomething(name: methodName, params: jsData["data"]) }
-
Once you get the token from the
captchaDidFinish
method, send a POST request to the server to validatehttps://smartcaptcha.yandexcloud.net/validate
providing parameters in thex-www-form-urlencoded
format:secret
: Server key.token
: One-time token received after passing the check.ip
: IP address of the user that originated the request to validate the token. This is an optional parameter, but we ask you to provide the user IP when making requests. This helps improve SmartCaptcha performance.
challengeDidAppear method for invisible CAPTCHA
CAPTCHA will not be shown in the HTML code of the page if called with the invisible
parameter. Make sure WKWebView is loaded but not available to the user until the challengeDidAppear
method is called. Here is one of the ways to ensure this:
UIApplication.shared.windows.first?.addSubview(webControllerView)
If the validation results in captchaDidFinish
, remove webControllerView
from the hierarchy. If the result does not contain captchaDidFinish
, move webControllerView
to the hierarchy for displaying to the user.
challengeDidDisappear method for invisible CAPTCHA
If CAPTCHA is swiped off the screen, it cannot be recovered by the user unassisted. In this case, call the WKWebView content reload on the challengeDidDisappear
event:
webControllerView.reload()
Sample implementation in Swift using https://smartcaptcha.yandexcloud.net/webview
This section describes the steps to follow to create an app with a CAPTCHA for iOS. See the example of a ready-to-use app with all components configured: Yandex SmartCaptcha for iOS
-
Create a class to store WKWebView:
final class WebNativeBridge: NSObject { private(set) var view: WKWebView? private var userContentController = WKUserContentController() func load(_ request: URLRequest?) { guard let request = request else { return } view?.load(request) } func reload() { view?.reload() } private func close() { view?.removeFromSuperview() } private func getConfiguration() -> WKWebViewConfiguration { let configuration = WKWebViewConfiguration() configuration.userContentController = userContentController return configuration } }
-
Add a property to store the WKUserContentController handler:
private var handlers = [String: WebContentHandlerBase]() func setup(handlers: [String: WebContentHandlerBase]) { handlers.forEach { userContentController.add($1, name: $0) } view = WKWebView(frame: .zero, configuration: getConfiguration()) }
-
Create a handler implementation for the SmartCaptcha page methods:
class WebContentHandlerBase: NSObject, WKScriptMessageHandler { var handlerName: String { "" } func execMethod(name: String, params: Any?...) {} func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard let jsData = message.body as? [String: String] else { return } guard let methodName = jsData["method"] else { return } execMethod(name: methodName, params: jsData["data"]) } } final class CaptchaHandler: WebContentHandlerBase { private enum Methods: String { case captchaDidFinish case challengeDidAppear case challengeDidDisappear } override var handlerName: String { "NativeClient" } weak var delegate: CaptchaHandlerDelegate? private var validator: CaptchaValidatorProtocol init(_ validator) { self.validator = validator } override func execMethod(name: String, params: Any?...) { guard let method = Methods(rawValue: name) else { return } switch method { case .captchaDidFinish: guard let token = params.first as? String else { return } onSuccess(token: token) case .challengeDidDisappear: onChallengeHide() case .challengeDidAppear: onChallengeVisible() } } private func onSuccess(token: String) { validator.validateCaptcha(token: token) { result in DispatchQueue.main.async { switch result { case .success(_): self.delegate?.onSuccess() case .failure(let err): self.delegate?.onError(err) } } } } private func onChallengeVisible() { delegate?.onShow() } private func onChallengeHide() { delegate?.onHide() } }
-
Create a class to validate the token from SmartCaptcha
final class CaptchaValidator: CaptchaValidatorProtocol { private var host: String private var secret: String private var session: URLSession init(host: String, secret: String) { self.host = host self.secret = secret session = URLSession(configuration: .default) } func validateCaptcha(token: String, callback: @escaping (Result<String, Error>) -> Void) { guard let url = URL(string: host), var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } components.queryItems = [ URLQueryItem(name: "secret", value: secret), URLQueryItem(name: "token", value: token), URLQueryItem(name: "ip", value: getIPAddress()), ] let task = session.dataTask(with: URLRequest(url: components.url!)) { data, response, error in guard let code = (response as? HTTPURLResponse)?.statusCode, code == 200 else { return } guard let data = data, let result = try? JSONDecoder().decode(YACValidationResponse.self, from: data) else { return } if result.status == "ok" { callback(.success("ok")) } else { callback(.failure(NSError(domain: result.message ?? "", code: code))) } } task.resume() } private func getIPAddress() -> String { var address: String = "" var ifaddr: UnsafeMutablePointer<ifaddrs>? = nil if getifaddrs(&ifaddr) == 0 { var ptr = ifaddr while ptr != nil { defer { ptr = ptr?.pointee.ifa_next } let interface = ptr?.pointee let addrFamily = interface?.ifa_addr.pointee.sa_family if addrFamily == UInt8(AF_INET) { if String(cString: (interface?.ifa_name)!) == "en0" { var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) getnameinfo(interface?.ifa_addr, socklen_t((interface?.ifa_addr.pointee.sa_len)!), &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) address = String(cString: hostname) print(address) } } } freeifaddrs(ifaddr) } return address } }