From 76af5d2cab967f075caccd4a885f42be8b886603 Mon Sep 17 00:00:00 2001 From: Alex Bobrikovich Date: Wed, 4 May 2022 14:34:14 +0200 Subject: [PATCH] #28 add addOnOpenListener for open-callback events (#30) * #28 add addOnOpenListener for open-callback events * Improve documentation for addOnOpenListener API Co-authored-by: e271828- * #28 put back iternal OnLoadedListener * #28 fix PR suggestions - wrong function name for onLoaded - logging statement for onLoaded stub function * #28 remove debug logging from JS and don't drop listeners after open event is happens * Improve open-callback docs Co-authored-by: e271828- * Bump version 2.2.0 Co-authored-by: e271828- --- README.md | 13 +++++++++ .../com/hcaptcha/example/MainActivity.java | 9 ++++++ sdk/build.gradle | 4 +-- sdk/src/androidTest/assets/hcaptcha-form.html | 3 ++ .../sdk/HCaptchaDialogFragmentTest.java | 29 +++++++++++++++++++ sdk/src/main/assets/hcaptcha-form.html | 11 +++---- .../main/java/com/hcaptcha/sdk/HCaptcha.java | 5 ++++ .../hcaptcha/sdk/HCaptchaDialogFragment.java | 14 ++++++++- .../hcaptcha/sdk/HCaptchaDialogListener.java | 2 ++ .../com/hcaptcha/sdk/HCaptchaJSInterface.java | 7 +++++ .../hcaptcha/sdk/tasks/OnOpenListener.java | 12 ++++++++ .../java/com/hcaptcha/sdk/tasks/Task.java | 24 +++++++++++++++ .../hcaptcha/sdk/HCaptchaJSInterfaceTest.java | 24 ++++++++++----- 13 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 sdk/src/main/java/com/hcaptcha/sdk/tasks/OnOpenListener.java diff --git a/README.md b/README.md index 0ec4e06e..2c9ce741 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,19 @@ HCaptcha hCaptcha = HCaptcha.getClient(this).setup() If `verifyWithHCaptcha` is called with different arguments than `setup` the SDK will handle this by re-configuring hCaptcha. Note that this will reduce some of the performance benefit of using `setup`. +The SDK also provides a listener to track hCaptcha open events, e.g. for analytics: + +```java +HCaptcha.getClient(this).verifyWithHCaptcha(YOUR_API_SITE_KEY) + ... + .addOnOpenListener(new OnOpenListener() { + @Override + public void onOpen() { + Log.d("MainActivity", "hCaptcha has been displayed"); + } + }); +``` + ##### Config params diff --git a/example-app/src/main/java/com/hcaptcha/example/MainActivity.java b/example-app/src/main/java/com/hcaptcha/example/MainActivity.java index a1a1f740..391f192f 100644 --- a/example-app/src/main/java/com/hcaptcha/example/MainActivity.java +++ b/example-app/src/main/java/com/hcaptcha/example/MainActivity.java @@ -7,6 +7,7 @@ import android.widget.CheckBox; import android.widget.RadioGroup; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -14,6 +15,7 @@ import androidx.appcompat.app.AppCompatActivity; import com.hcaptcha.sdk.*; import com.hcaptcha.sdk.tasks.OnFailureListener; +import com.hcaptcha.sdk.tasks.OnOpenListener; import com.hcaptcha.sdk.tasks.OnSuccessListener; @@ -112,7 +114,14 @@ public void onFailure(HCaptchaException e) { Log.d(TAG, "hCaptcha failed: " + e.getMessage() + "(" + e.getStatusCode() + ")"); setErrorTextView(e.getMessage()); } + }) + .addOnOpenListener(new OnOpenListener() { + @Override + public void onOpen() { + Toast.makeText(MainActivity.this, "hCaptcha shown", Toast.LENGTH_SHORT).show(); + } }); + } } diff --git a/sdk/build.gradle b/sdk/build.gradle index f7830c54..3e6fc1df 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -12,11 +12,11 @@ android { // See https://developer.android.com/studio/publish/versioning // versionCode must be integer and be incremented by one for every new update // android system uses this to prevent downgrades - versionCode 11 + versionCode 12 // version number visible to the user // should follow semantic versioning (See https://semver.org) - versionName "2.1.0" + versionName "2.2.0" buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\"" diff --git a/sdk/src/androidTest/assets/hcaptcha-form.html b/sdk/src/androidTest/assets/hcaptcha-form.html index fe3818cb..c75f4e3e 100644 --- a/sdk/src/androidTest/assets/hcaptcha-form.html +++ b/sdk/src/androidTest/assets/hcaptcha-form.html @@ -21,6 +21,9 @@ } catch (e) { BridgeObject.onError(29); } + setTimeout(function() { + BridgeObject.onOpen(); + }, 200); } function onPass() { diff --git a/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java b/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java index ed6a07ba..0efa74de 100644 --- a/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java +++ b/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java @@ -35,6 +35,10 @@ @RunWith(AndroidJUnit4.class) public class HCaptchaDialogFragmentTest { public class HCaptchaDialogTestAdapter extends HCaptchaDialogListener { + @Override + void onOpen() { + } + @Override void onSuccess(HCaptchaTokenResponse hCaptchaTokenResponse) { } @@ -138,4 +142,29 @@ void onFailure(HCaptchaException hCaptchaException) { assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback } + + @Test + public void onOpenCallbackWorks() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + final HCaptchaDialogListener listener = new HCaptchaDialogTestAdapter() { + @Override + void onOpen() { + latch.countDown(); + } + }; + + final FragmentScenario scenario = launchCaptchaFragment(listener); + onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000)); + + onWebView(withId(R.id.webView)).forceJavascriptEnabled(); + + onWebView().withElement(findElement(Locator.ID, "input-text")) + .perform(clearElement()) + .perform(DriverAtoms.webKeys("test-token")); + + onWebView().withElement(findElement(Locator.ID, "on-pass")) + .perform(webClick()); + + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback + } } diff --git a/sdk/src/main/assets/hcaptcha-form.html b/sdk/src/main/assets/hcaptcha-form.html index 4dc4677b..1cadeb08 100644 --- a/sdk/src/main/assets/hcaptcha-form.html +++ b/sdk/src/main/assets/hcaptcha-form.html @@ -54,7 +54,10 @@ return console.log("error: code ".concat(errCode)); }, onLoaded: function onLoaded() { - return console.log('cb: challenge or checkbox is visible'); + return console.log('cb: api is loaded'); + }, + onOpen: function onOpen() { + return console.log('cb: challenge is visible'); } }; var bridgeConfig = JSON.parse(BridgeObject.getConfig()); @@ -70,8 +73,6 @@ if (renderConfig.size === 'invisible') { hcaptcha.execute(hCaptchaID); - } else { - BridgeObject.onLoaded(); } } @@ -132,7 +133,7 @@ } }, 'open-callback': function openCallback() { - return BridgeObject.onLoaded(); + return BridgeObject.onOpen(); } }; } @@ -141,7 +142,7 @@ try { var renderConfig = getRenderConfig(); hCaptchaID = hcaptcha.render('hcaptcha-container', renderConfig); - + BridgeObject.onLoaded(); execute(bridgeConfig, renderConfig); } catch (e) { console.error(e); diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java index bb918e77..6d40ddb5 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java @@ -113,6 +113,11 @@ public HCaptcha setup(@NonNull final String siteKey) { public HCaptcha setup(@NonNull final HCaptchaConfig hCaptchaConfig) { this.hCaptchaConfig = hCaptchaConfig; this.hCaptchaDialogFragment = HCaptchaDialogFragment.newInstance(hCaptchaConfig, new HCaptchaDialogListener() { + @Override + void onOpen() { + captchaOpened(); + } + @Override void onSuccess(final HCaptchaTokenResponse hCaptchaTokenResponse) { setResult(hCaptchaTokenResponse); diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java index e0cdaf9c..087f2111 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java @@ -18,6 +18,7 @@ import androidx.fragment.app.DialogFragment; import com.hcaptcha.sdk.tasks.OnFailureListener; import com.hcaptcha.sdk.tasks.OnLoadedListener; +import com.hcaptcha.sdk.tasks.OnOpenListener; import com.hcaptcha.sdk.tasks.OnSuccessListener; @@ -26,6 +27,7 @@ */ public class HCaptchaDialogFragment extends DialogFragment implements OnLoadedListener, + OnOpenListener, OnSuccessListener, OnFailureListener { @@ -95,7 +97,7 @@ public void onCreate(Bundle savedInstanceState) { } final HCaptchaConfig hCaptchaConfig = (HCaptchaConfig) getArguments().getSerializable(KEY_CONFIG); this.resetOnTimeout = hCaptchaConfig.getResetOnTimeout(); - this.hCaptchaJsInterface = new HCaptchaJSInterface(hCaptchaConfig, this, this, this); + this.hCaptchaJsInterface = new HCaptchaJSInterface(hCaptchaConfig, this, this, this, this); this.hCaptchaDebugInfo = new HCaptchaDebugInfo(getContext()); this.showLoader = hCaptchaConfig.getLoading(); setStyle(STYLE_NO_FRAME, R.style.HCaptchaDialogTheme); @@ -192,6 +194,16 @@ public void onAnimationEnd(Animator animation) { }); } + @Override + public void onOpen() { + handler.post(new Runnable() { + @Override + public void run() { + hCaptchaDialogListener.onOpen(); + } + }); + } + @Override public void onFailure(@NonNull final HCaptchaException hCaptchaException) { final boolean silentRetry = this.resetOnTimeout && hCaptchaException.getHCaptchaError() == HCaptchaError.SESSION_TIMEOUT; diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogListener.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogListener.java index f1a924a2..ba9229ee 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogListener.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogListener.java @@ -9,6 +9,8 @@ abstract class HCaptchaDialogListener implements Parcelable { abstract void onFailure(HCaptchaException hCaptchaException); + abstract void onOpen(); + @Override public int describeContents() { return 0; diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java index 380789f8..09c6f40c 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.hcaptcha.sdk.tasks.OnFailureListener; import com.hcaptcha.sdk.tasks.OnLoadedListener; +import com.hcaptcha.sdk.tasks.OnOpenListener; import com.hcaptcha.sdk.tasks.OnSuccessListener; import lombok.AllArgsConstructor; import lombok.Data; @@ -25,6 +26,8 @@ class HCaptchaJSInterface implements Serializable { private final OnLoadedListener onLoadedListener; + private final OnOpenListener onOpenListener; + private final OnSuccessListener onSuccessListener; private final OnFailureListener onFailureListener; @@ -51,4 +54,8 @@ public void onLoaded() { this.onLoadedListener.onLoaded(); } + @JavascriptInterface + public void onOpen() { + this.onOpenListener.onOpen(); + } } diff --git a/sdk/src/main/java/com/hcaptcha/sdk/tasks/OnOpenListener.java b/sdk/src/main/java/com/hcaptcha/sdk/tasks/OnOpenListener.java new file mode 100644 index 00000000..a3a4f527 --- /dev/null +++ b/sdk/src/main/java/com/hcaptcha/sdk/tasks/OnOpenListener.java @@ -0,0 +1,12 @@ +package com.hcaptcha.sdk.tasks; + +/** + * A hCaptcha open listener class + */ +public interface OnOpenListener { + + /** + * Called when the hCaptcha challenge is displayed on the html page + */ + void onOpen(); +} diff --git a/sdk/src/main/java/com/hcaptcha/sdk/tasks/Task.java b/sdk/src/main/java/com/hcaptcha/sdk/tasks/Task.java index f0cf6bb8..c780b66e 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/tasks/Task.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/tasks/Task.java @@ -28,6 +28,8 @@ public abstract class Task { private final List onFailureListeners; + private final List onOpenListeners; + /** * Creates a new Task object */ @@ -36,6 +38,7 @@ protected Task() { this.successful = false; this.onSuccessListeners = new ArrayList<>(); this.onFailureListeners = new ArrayList<>(); + this.onOpenListeners = new ArrayList<>(); } /** @@ -92,6 +95,15 @@ protected void setException(@NonNull HCaptchaException hCaptchaException) { tryCb(); } + /** + * Internal callback which called once 'open-callback' fired in js SDK + */ + protected void captchaOpened() { + for (OnOpenListener listener : onOpenListeners) { + listener.onOpen(); + } + } + /** * Add a success listener triggered when the task finishes successfully * @@ -116,6 +128,18 @@ public Task addOnFailureListener(@NonNull final OnFailureListener onFai return this; } + /** + * Add a hCaptcha open listener triggered when the hCaptcha View is displayed + * + * @param onOpenListener the open listener to be triggered + * @return current object + */ + public Task addOnOpenListener(@NonNull final OnOpenListener onOpenListener) { + this.onOpenListeners.add(onOpenListener); + tryCb(); + return this; + } + private void tryCb() { if (getResult() != null) { final Iterator> iterator = onSuccessListeners.iterator(); diff --git a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaJSInterfaceTest.java b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaJSInterfaceTest.java index 0125b7ac..d2f6f9e5 100644 --- a/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaJSInterfaceTest.java +++ b/sdk/src/test/java/com/hcaptcha/sdk/HCaptchaJSInterfaceTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.hcaptcha.sdk.tasks.OnFailureListener; import com.hcaptcha.sdk.tasks.OnLoadedListener; +import com.hcaptcha.sdk.tasks.OnOpenListener; import com.hcaptcha.sdk.tasks.OnSuccessListener; import org.json.JSONException; @@ -27,6 +28,9 @@ public class HCaptchaJSInterfaceTest { @Spy OnLoadedListener onLoadedListener; + @Spy + OnOpenListener onOpenListener; + @Spy OnSuccessListener onSuccessListener; @@ -66,7 +70,7 @@ public void full_config_serialization() throws JsonProcessingException, JSONExce .host(host) .resetOnTimeout(true) .build(); - final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null); + final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null, null); JSONObject expected = new JSONObject(); expected.put("siteKey", siteKey); @@ -101,7 +105,7 @@ public void subset_config_serialization() throws JsonProcessingException, JSONEx .theme(HCaptchaTheme.DARK) .rqdata(rqdata) .build(); - final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null); + final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null, null); JSONObject expected = new JSONObject(); expected.put("siteKey", siteKey); @@ -124,16 +128,23 @@ public void subset_config_serialization() throws JsonProcessingException, JSONEx } @Test - public void calls_on_challenge_visible_cb() { - final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, onLoadedListener, null, null); + public void calls_on_challenge_ready() { + final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, onLoadedListener, null, null, null); hCaptchaJSInterface.onLoaded(); verify(onLoadedListener, times(1)).onLoaded(); } + @Test + public void calls_on_challenge_visible_cb() { + final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, onOpenListener, null, null); + hCaptchaJSInterface.onOpen(); + verify(onOpenListener, times(1)).onOpen(); + } + @Test public void on_pass_forwards_token_to_listeners() { final String token = "mock-token"; - final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, onSuccessListener, null); + final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, onSuccessListener, null); hCaptchaJSInterface.onPass(token); verify(onSuccessListener, times(1)).onSuccess(tokenCaptor.capture()); assertEquals(token, tokenCaptor.getValue().getTokenResult()); @@ -142,11 +153,10 @@ public void on_pass_forwards_token_to_listeners() { @Test public void on_error_forwards_error_to_listeners() { final HCaptchaError error = HCaptchaError.CHALLENGE_CLOSED; - final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, onFailureListener); + final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, null, onFailureListener); hCaptchaJSInterface.onError(error.getErrorId()); verify(onFailureListener, times(1)).onFailure(exceptionCaptor.capture()); assertEquals(error.getMessage(), exceptionCaptor.getValue().getMessage()); assertNotNull(exceptionCaptor.getValue()); } - }