2

I'm trying to use Google's recaptcha in a Clojurescript/Reagent SPA as seen in the code below.

(ns myapp.captcha
  (:require  [reagent.core :as r]
             [cljs.core.async :refer [<! >! chan]])
  (:require-macros [cljs.core.async.macros :refer [go go-loop]]))

(def captcha-ch (chan))

(defn ^:export data-callback [human-proof]
  (go (>! captcha-ch {:captcha-data human-proof})))

(defn ^:export data-expired-callback []
  (go (>! captcha-ch {:captcha-expired true})))

(defn captcha [site-key]
  (let [grecaptcha-script (doto (.createElement js/document "script")
                            (.setAttribute "id" "grecaptcha-script")
                            (.setAttribute "src" "https://www.google.com/recaptcha/api.js"))
        out-ch (chan)
        comp (r/create-class
              {:component-did-mount (fn [this]
                                      (.appendChild (.-body js/document)
                                                    grecaptcha-script))
               :component-will-unmount (fn [this]
                                         (.removeChild (.-body js/document)
                                                       (.getElementById js/document "grecaptcha-script"))
                                         (go (>! captcha-ch {:exit true})))
               :reagent-render (fn [this]
                                 [:div.g-recaptcha
                                  {:data-sitekey site-key
                                   :data-callback "myapp.captcha.data_callback"
                                   :data-expired-callback "myapp.captcha.data_expired_callback"}])})]
    (go-loop []
      (let [msg (<! captcha-ch)]
        (if-not (:exit msg)
          (>! out-ch msg)
          (recur))))

    {:chan out-ch :comp comp}))

When the captcha is solved and data-callback is supposed to be called I get an error saying:

ReCAPTCHA couldn't find user-provided function: myapp.captcha.data_callback

On the other hand if I call myapp.captcha.data_callback from the browser's debugger console the function is visible and executes correctly.

PS: For now please ignore the global chan, which is a different matter. In order to fix that I have to call captcha render explicitly and that puts me in some race conditions apparently related to the order of script loading. I admit that it might be a cleaner approach but for now it's interesting to see what the problem is here.

2
  • Have you tried "myapp.captcha.data-callback" rather than the underscore version? Have you tried it with your namespace just being "callback" i.e. without any namespace issues, that could be the cause of the problem? Commented Feb 14, 2017 at 20:04
  • Sorry, I forgot to answer your question. Yes. I tried with & without namespace but it behaves the same. Commented Feb 21, 2017 at 15:36

4 Answers 4

1

This is because the Closure compiler obfuscates your code during compilation, including renaming your functions. The easiest solution is first ensuring the compiler optimization respects your function names (or simply disabling the optimization such as via :optimization :none with shadow cljs.

Next, you want to make sure the function you want to use is exported. This is done with ^:export, such as: (defn ^:export my-exported-fun [] ...)

Lastly, pass the full namespace when refering to the function, such as myapp.frontend.my-exported-fun.

Hope this helps the future travelers :)

Sign up to request clarification or add additional context in comments.

Comments

1

I have a workaround but it's a bit of a hack.

I added a script element just before recaptcha script. In this script element I define callbacks which forward the calls to my clojurescript functions.

See the code below.

Still it would be nice to understand why I can't use my Clojurescript callbacks directly.

(defn captcha [handler]
  (let [callback-hooks (let [s (.createElement js/document "script")]
                         (.setAttribute s "id" "captcha-callbacks")
                         (set! (.-text s)
                               (str "var captcha_data_callback = function(x) { myapp.captcha.data_callback(x)};"
                                    "var captcha_data_expired_callback = function() { myapp.captcha.data_expired_callback()};"))
                         s)
        grecaptcha-script (doto (.createElement js/document "script")
                            (.setAttribute "id" "grecaptcha-script")
                            (.setAttribute "src" "https://www.google.com/recaptcha/api.js"))
        captcha-div [:div.g-recaptcha
                     {:data-sitekey config/grecaptcha-client-key
                      :data-callback "captcha_data_callback"
                      :data-expired-callback "captcha_data_expired_callback"}]]

    (go-loop []
      (let [msg (<! captcha-ch)]
        (handler msg)

        (if-not (:end msg)
          (recur))))

    (r/create-class
     {:component-did-mount (fn [this]
                             (doto (.-body js/document)
                               (.appendChild callback-hooks)
                               (.appendChild grecaptcha-script)))
      :component-will-unmount (fn [this]
                                (doto (.-body js/document)
                                  (.removeChild (.getElementById js/document "captcha-callbacks"))
                                  (.removeChild (.getElementById js/document "grecaptcha-script")))
                                (go (>! captcha-ch {:end true})))
      :reagent-render (fn [this] captcha-div)})))

Comments

1

I'm in the same situation as you and have only been able to solve it in the way that you listed - by supplying a JS script that passes the function call along to CLJS.

But, I found out though that if you define a callback function as an object property (in JS) Recaptcha is still unable to find that function, even though the function does exist.

I define a script tag in the the <head> of my index.html

<script type="text/javascript">
  // This function will be called correctly when the captcha is loaded
  var onloadCallback = function() { myapp.captcha.onloadCallback() };
  // This function will not be found by recaptcha.js
  var testObject = { onDataCallback: function(x) { myapp.captcha.onDataCallback(x) };
</script>

So with that in mind, it seems that the issue is not a ClojureScript problem, but a recaptcha problem. If there was a way to export ClojureScript functions directly into the global scope outside of their namespace (I'm not sure if this is possible), then in theory you should be able to access your CLJS callbacks directly from recaptcha.

Hope this helps!

Comments

0

I've just come across this very issue and have solved it as follows.

First I want to confirm that Nima's solution, while generally useful advice, doesn't work in this specific situation. As Rory How has correctly observed, you simply cannot pass a function which is an object property (which any var we define in ClojureScript is) to the Google reCaptcha API.

My solution is to take your callback function and set a JavaScript global variable to your function. In code, it looks like this:

(defn data-callback [token]
 ,,,)

(set! js/my-data-callback data-callback)

Then in your element, set your the value of the "data-callback" attribute to "my-data-callback" instead of "myapp.captcha.data_callback". This works in all optimization modes.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.