Skip to content

Instantly share code, notes, and snippets.

@jkereako
Created September 8, 2020 13:15
Show Gist options
  • Save jkereako/d111ff6f4594873d409d3194371d6732 to your computer and use it in GitHub Desktop.
Save jkereako/d111ff6f4594873d409d3194371d6732 to your computer and use it in GitHub Desktop.
ReCAPTCHA v2 in Swift
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://www.google.com/recaptcha/api.js?onload=onLoad&render=explicit&hl=en" async defer></script>
<title></title>
<script type="text/javascript">
const post = function(value) {
window.webkit.messageHandlers.recaptcha.postMessage(value);
};
console.log = function(message) {
post(message);
};
var onLoad = function() {
grecaptcha.render(
"recaptcha",
{
sitekey: "${siteKey}",
callback: function(token) {
post(token);
},
size: "normal"
}
);
};
</script>
</head>
<body>
<div id="recaptcha"></div>
</body>
</html>
import UIKit
import WebKit
final class ReCAPTCHAViewController: UIViewController {
private var webView: WKWebView!
private let viewModel: ReCAPTCHAViewModel
init(viewModel: ReCAPTCHAViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
let contentController = WKUserContentController()
contentController.add(viewModel, name: "recaptcha")
webConfiguration.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: webConfiguration)
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(didSelectCloseButton)
)
webView.loadHTMLString(viewModel.html, baseURL: viewModel.url)
}
}
// MARK: - Target-Actions
private extension ReCAPTCHAViewController {
@IBAction func didSelectCloseButton() {
dismiss(animated: true)
}
}
import WebKit
protocol ReCAPTCHAViewModelDelegate: class {
func didSolveCAPTCHA(token: String)
}
final class ReCAPTCHAViewModel: NSObject {
weak var delegate: ReCAPTCHAViewModelDelegate?
var html: String {
guard let filePath = Bundle.main.path(
forResource: "recaptcha", ofType: "html"
) else {
assertionFailure("Unable to find the file.")
return ""
}
let contents = try! String(
contentsOfFile: filePath, encoding: .utf8
)
return parse(contents, with: ["siteKey": siteKey])
}
let siteKey: String
let url: URL
/// Creates a ReCAPTCHAViewModel
/// - Parameters:
/// - siteKey: ReCAPTCHA's site key
/// - url: The URL for registered with Google
init(siteKey: String, url: URL) {
self.siteKey = siteKey
self.url = url
super.init()
}
}
// MARK: - WKScriptMessageHandler
extension ReCAPTCHAViewModel: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let message = message.body as? String else {
assertionFailure("Expected a string")
return
}
delegate?.didSolveCAPTCHA(token: message)
}
}
private extension ReCAPTCHAViewModel {
func parse(_ string: String, with valueMap: [String: String]) -> String {
var parsedString = string
valueMap.forEach { key, value in
parsedString = parsedString.replacingOccurrences(
of: "${\(key)}", with: value
)
}
return parsedString
}
}
@jkereako
Copy link
Author

jkereako commented Sep 8, 2020

Description

This is a simple, MVVM implementation of ReCAPTCHA v2 in Swift. I adapted the idea from fjcaetano's ReCaptcha. His solution is robust but complex and accounts for cases (an alternate endpoint) that I think most folks won't run into. I wanted something much simpler.

I built it using the MVVM pattern. Configuration of the form is done through ReCAPTCHAViewModel. The HTML file itself is tokenized with ${siteKey} and parsed thusly:

parse(contents, with: ["siteKey": "foobarbaz"])

You can add more tokens to the HTML file and parse them by adding the name to the dictionary passed to parse(: with:).

There is a delegate on ReCAPTCHAViewModel which is called only if the ReCAPTCHA is solved.

Usage

First, make sure you add recaptcha.html to your bundled resources. Click on the Xcode project, select your target, select the tab Build Phases and add recaptcha.html to Copy Bundle Resources.

import UIKit

final class MyViewController: UIViewController {
    private var reCAPTCHAViewModel: ReCAPTCHAViewModel?

    override viewDidLoad() {
        super.viewDidLoad()
        let viewModel = ReCAPTCHAViewModel(
            siteKey: "your_site_key",
            url: URL(string: "https://yourdomain.com")!
        )

        viewModel.delegate = self

        let vc = ReCAPTCHAViewController(viewModel: viewModel)

        // Optional: present the ReCAPTCHAViewController so you have a navigation bar
        let nav = UINavigationController(rootViewController: vc)

        // Keep a reference to the View Model so we can be alerted when the user
        // solves the CAPTCHA.
        reCAPTCHAViewModel = viewModel

        present(nav, animated: true)
    }
}

// MARK: - ReCAPTCHAViewModelDelegate
extension MyViewController: ReCAPTCHAViewModelDelegate {
    func didSolveCAPTCHA(token: String) {
        print("Token: \(token)")
    }
}

@dittmar
Copy link

dittmar commented Nov 24, 2020

Hi @jkereako. I recently came across this gist for reCAPTCHA support, and I forked it as a basis for a Swift Package. I made some additions to make it a little better suited for my use-case within an iOS app. Since there aren't too many packages available that handle reCAPTCHA support on iOS, I was hoping to get your approval to make that package publicly available. Since I used your code as a basis, I wanted to ask for your permission to prevent a scenario where I use a derivative of your code in a way that you didn't intend.

@jkereako
Copy link
Author

@dittma75 permission granted! Do what you will with this code!

I appreciate your honesty. I look forward to checking out the Swift package you're making!

@dittmar
Copy link

dittmar commented Nov 25, 2020

@jkereako, thank you very much for responding, and thank you for allowing me to use your code as a base for the package. I'll be sure to post a link to the package here once everything is ready.

@Hokila
Copy link

Hokila commented Dec 14, 2020

I write a sample project from @jkereako 's code.
https://github.com/Hokila/recapcha

for anyone want to integrate recapcha into exist project.

@jkereako
Copy link
Author

@Hokila nice work!

@chavitos
Copy link

Hi guys! The solution is very nice! But my recaptcha is not working as invisible (i registered as invisible recaptcha and using fjcaetano's pod with the same key it works as invisible captcha). Do you know what can be happen? I already tried to change data-size to invisible in the html but it not worked, the webview shows a box with terms of recaptcha and nothing happen (i can't retrieve my token cuz the images didn't appear too).

@dittmar
Copy link

dittmar commented Jan 14, 2021

Hey, @chavitos. I'm still working on the Swift Package, which uses invisible reCAPTCHA. I'm hoping to get posted as soon as possible. In the meantime, this is the modified recaptcha.html file that I'm using for invisible reCAPTCHA. Hope it helps:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="https://www.google.com/recaptcha/api.js?onload=onLoad" async defer></script>
    <title></title>
    <style>.grecaptcha-badge { visibility: hidden; }</style>
    <script type="text/javascript">
      const post = function(value) {
          window.webkit.messageHandlers.recaptcha.postMessage(value);
      };

      console.log = function(message) {
          post(message);
      };

      var onLoad = function() {
          grecaptcha.render(
            "recaptcha",
            {
              sitekey: "${siteKey}",
              callback: function(token) {
                  post(token);
              },
              size: "invisible"
            }
          );
          grecaptcha.ready(function() {
            grecaptcha.execute();
          });
      };
    </script>
  </head>
  <body>
      <div id="recaptcha"></div>
  </body>
</html>

If replacing the recaptcha.html file with this one won't work for you, I think adding the line <style>.grecaptcha-badge { visibility: hidden; }</style> and changing the <script src> to <script src="https://www.google.com/recaptcha/api.js?onload=onLoad" async defer></script> did it for me.

@chavitos
Copy link

Hi @dittma75! It worked perfectly, thank you!

And please, let me know when you finish with SPM version!

@dittmar
Copy link

dittmar commented Jan 14, 2021

No problem. I'll post here if and when I finish 😄.

@pmahend1
Copy link

pmahend1 commented May 6, 2021

@dittma75 did you finish it up?

@dittmar
Copy link

dittmar commented May 6, 2021

Unfortunately, work has gotten really busy for me, and I haven't been able to get back to it. I'm not hopeful that I will get back to it any time soon, so please don't wait for it to happen. Sorry to disappoint 😞.
In the meantime, @jkereako's gist works pretty well, and the modified HTML that I posted above helps to make it work for invisible reCAPTCHAs.

@safranashereen
Copy link

Do you have code for Objective C???

@dittmar
Copy link

dittmar commented May 31, 2021

Hi, @safranashereen. I threw a Swift Package together that's essentially @jkereako's gist with some very minor tweaks to make it visible to Objective-C. I wasn't able to test it to make sure that it fully works, but I can verify that I'm able to load the Swift Package into an Objective-C project, and I am able to call the code to initialize and present a ReCAPTCHAViewController.
This is the repository: https://github.com/dittma75/ReCAPTCHA_Package. Give it a try and let me know if it helps.

For clarity, this isn't the improved package I was working on months ago. That one isn't ready. This is just a straight copy of @jkereako's code wrapped up in a package so that Swift Package Manager can be used to install it, and it should work for Objective-C as well. Hopefully that's at least a bit helpful to the people in this thread (and future visitors to this gist).

@safranashereen
Copy link

safranashereen commented Jun 1, 2021 via email

@dleyvaabrahantes
Copy link

Hola estoy desarrollando un app y necesito dibujar un recaptcha que se encuentra en un sitio web. me ayudan por favor? Algun consejo

@m-misha93
Copy link

is it possible to hide the white window with visible or invisible captcha ( so that the code works in the background. to use version 3) ?

@hcgharish
Copy link

Hello anyone,
can you tell me why i am getting "Error for site owner, invalid key type."
I have used above sample code and trying to integrate recaptcha v3, my code working fine in recaptcha v2.
i did not do anything code on my server side.
I am integrating it in ios, swift.
please help me, i am stuck on it.

@sanjaywv
Copy link

@Hokila nice work!
Thanks

@sofent
Copy link

sofent commented Apr 25, 2022

thanks it work fine in my case,but it show retapcha code everytime while access via web did not completely invisibe. any suggestion why?

@CodeWithOz
Copy link

@dittmar @jkereako hey folks do you know if it's possible to make this usable in a cordova/ionic app? I'm not familiar with native development so I'm not sure what the necessary steps would be if it's possible.

@michzio
Copy link

michzio commented Mar 13, 2023

What is the reason to not use reCAPTCHA Enterprise ? there is the same limit as I can see 1,000,000 views per month?

@shanmugam105
Copy link

I think need iOS Target 14.x for reCAPTCHA Enterprise

@ZaynaxiOSDev
Copy link

How can I Observe Expired Sessions?

@shanmugam105
Copy link

shanmugam105 commented May 18, 2023

@ZaynaxiOSDev

https://www.google.com/recaptcha/api/siteverify?secret=your_secret&response=response_token

Use this GET api for timeout status validation. Replace your_secret & response_token

@ZaynaxiOSDev
Copy link

Is this possible to generate a delegate method like didSolveCAPTCHA?

If yes, then how can I achieve that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment