Skip to content

Commit 76af5d2

Browse files
CAMOBAPe271828-
andauthored
#28 add addOnOpenListener for open-callback events (#30)
* #28 add addOnOpenListener for open-callback events * Improve documentation for addOnOpenListener API Co-authored-by: e271828- <[email protected]> * #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- <[email protected]> * Bump version 2.2.0 Co-authored-by: e271828- <[email protected]>
1 parent 5ac6b08 commit 76af5d2

File tree

13 files changed

+142
-15
lines changed

13 files changed

+142
-15
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,19 @@ HCaptcha hCaptcha = HCaptcha.getClient(this).setup()
105105

106106
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`.
107107

108+
The SDK also provides a listener to track hCaptcha open events, e.g. for analytics:
109+
110+
```java
111+
HCaptcha.getClient(this).verifyWithHCaptcha(YOUR_API_SITE_KEY)
112+
...
113+
.addOnOpenListener(new OnOpenListener() {
114+
@Override
115+
public void onOpen() {
116+
Log.d("MainActivity", "hCaptcha has been displayed");
117+
}
118+
});
119+
```
120+
108121
##### Config params
109122

110123

example-app/src/main/java/com/hcaptcha/example/MainActivity.java

+9
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
import android.widget.CheckBox;
88
import android.widget.RadioGroup;
99
import android.widget.TextView;
10+
import android.widget.Toast;
1011

1112
import androidx.annotation.NonNull;
1213
import androidx.annotation.Nullable;
1314
import androidx.annotation.RequiresApi;
1415
import androidx.appcompat.app.AppCompatActivity;
1516
import com.hcaptcha.sdk.*;
1617
import com.hcaptcha.sdk.tasks.OnFailureListener;
18+
import com.hcaptcha.sdk.tasks.OnOpenListener;
1719
import com.hcaptcha.sdk.tasks.OnSuccessListener;
1820

1921

@@ -112,7 +114,14 @@ public void onFailure(HCaptchaException e) {
112114
Log.d(TAG, "hCaptcha failed: " + e.getMessage() + "(" + e.getStatusCode() + ")");
113115
setErrorTextView(e.getMessage());
114116
}
117+
})
118+
.addOnOpenListener(new OnOpenListener() {
119+
@Override
120+
public void onOpen() {
121+
Toast.makeText(MainActivity.this, "hCaptcha shown", Toast.LENGTH_SHORT).show();
122+
}
115123
});
124+
116125
}
117126

118127
}

sdk/build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ android {
1212
// See https://developer.android.com/studio/publish/versioning
1313
// versionCode must be integer and be incremented by one for every new update
1414
// android system uses this to prevent downgrades
15-
versionCode 11
15+
versionCode 12
1616

1717
// version number visible to the user
1818
// should follow semantic versioning (See https://semver.org)
19-
versionName "2.1.0"
19+
versionName "2.2.0"
2020

2121
buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\""
2222

sdk/src/androidTest/assets/hcaptcha-form.html

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
} catch (e) {
2222
BridgeObject.onError(29);
2323
}
24+
setTimeout(function() {
25+
BridgeObject.onOpen();
26+
}, 200);
2427
}
2528

2629
function onPass() {

sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java

+29
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
@RunWith(AndroidJUnit4.class)
3636
public class HCaptchaDialogFragmentTest {
3737
public class HCaptchaDialogTestAdapter extends HCaptchaDialogListener {
38+
@Override
39+
void onOpen() {
40+
}
41+
3842
@Override
3943
void onSuccess(HCaptchaTokenResponse hCaptchaTokenResponse) {
4044
}
@@ -138,4 +142,29 @@ void onFailure(HCaptchaException hCaptchaException) {
138142

139143
assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback
140144
}
145+
146+
@Test
147+
public void onOpenCallbackWorks() throws Exception {
148+
CountDownLatch latch = new CountDownLatch(1);
149+
final HCaptchaDialogListener listener = new HCaptchaDialogTestAdapter() {
150+
@Override
151+
void onOpen() {
152+
latch.countDown();
153+
}
154+
};
155+
156+
final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(listener);
157+
onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000));
158+
159+
onWebView(withId(R.id.webView)).forceJavascriptEnabled();
160+
161+
onWebView().withElement(findElement(Locator.ID, "input-text"))
162+
.perform(clearElement())
163+
.perform(DriverAtoms.webKeys("test-token"));
164+
165+
onWebView().withElement(findElement(Locator.ID, "on-pass"))
166+
.perform(webClick());
167+
168+
assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback
169+
}
141170
}

sdk/src/main/assets/hcaptcha-form.html

+6-5
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@
5454
return console.log("error: code ".concat(errCode));
5555
},
5656
onLoaded: function onLoaded() {
57-
return console.log('cb: challenge or checkbox is visible');
57+
return console.log('cb: api is loaded');
58+
},
59+
onOpen: function onOpen() {
60+
return console.log('cb: challenge is visible');
5861
}
5962
};
6063
var bridgeConfig = JSON.parse(BridgeObject.getConfig());
@@ -70,8 +73,6 @@
7073

7174
if (renderConfig.size === 'invisible') {
7275
hcaptcha.execute(hCaptchaID);
73-
} else {
74-
BridgeObject.onLoaded();
7576
}
7677
}
7778

@@ -132,7 +133,7 @@
132133
}
133134
},
134135
'open-callback': function openCallback() {
135-
return BridgeObject.onLoaded();
136+
return BridgeObject.onOpen();
136137
}
137138
};
138139
}
@@ -141,7 +142,7 @@
141142
try {
142143
var renderConfig = getRenderConfig();
143144
hCaptchaID = hcaptcha.render('hcaptcha-container', renderConfig);
144-
145+
BridgeObject.onLoaded();
145146
execute(bridgeConfig, renderConfig);
146147
} catch (e) {
147148
console.error(e);

sdk/src/main/java/com/hcaptcha/sdk/HCaptcha.java

+5
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ public HCaptcha setup(@NonNull final String siteKey) {
113113
public HCaptcha setup(@NonNull final HCaptchaConfig hCaptchaConfig) {
114114
this.hCaptchaConfig = hCaptchaConfig;
115115
this.hCaptchaDialogFragment = HCaptchaDialogFragment.newInstance(hCaptchaConfig, new HCaptchaDialogListener() {
116+
@Override
117+
void onOpen() {
118+
captchaOpened();
119+
}
120+
116121
@Override
117122
void onSuccess(final HCaptchaTokenResponse hCaptchaTokenResponse) {
118123
setResult(hCaptchaTokenResponse);

sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import androidx.fragment.app.DialogFragment;
1919
import com.hcaptcha.sdk.tasks.OnFailureListener;
2020
import com.hcaptcha.sdk.tasks.OnLoadedListener;
21+
import com.hcaptcha.sdk.tasks.OnOpenListener;
2122
import com.hcaptcha.sdk.tasks.OnSuccessListener;
2223

2324

@@ -26,6 +27,7 @@
2627
*/
2728
public class HCaptchaDialogFragment extends DialogFragment implements
2829
OnLoadedListener,
30+
OnOpenListener,
2931
OnSuccessListener<HCaptchaTokenResponse>,
3032
OnFailureListener {
3133

@@ -95,7 +97,7 @@ public void onCreate(Bundle savedInstanceState) {
9597
}
9698
final HCaptchaConfig hCaptchaConfig = (HCaptchaConfig) getArguments().getSerializable(KEY_CONFIG);
9799
this.resetOnTimeout = hCaptchaConfig.getResetOnTimeout();
98-
this.hCaptchaJsInterface = new HCaptchaJSInterface(hCaptchaConfig, this, this, this);
100+
this.hCaptchaJsInterface = new HCaptchaJSInterface(hCaptchaConfig, this, this, this, this);
99101
this.hCaptchaDebugInfo = new HCaptchaDebugInfo(getContext());
100102
this.showLoader = hCaptchaConfig.getLoading();
101103
setStyle(STYLE_NO_FRAME, R.style.HCaptchaDialogTheme);
@@ -192,6 +194,16 @@ public void onAnimationEnd(Animator animation) {
192194
});
193195
}
194196

197+
@Override
198+
public void onOpen() {
199+
handler.post(new Runnable() {
200+
@Override
201+
public void run() {
202+
hCaptchaDialogListener.onOpen();
203+
}
204+
});
205+
}
206+
195207
@Override
196208
public void onFailure(@NonNull final HCaptchaException hCaptchaException) {
197209
final boolean silentRetry = this.resetOnTimeout && hCaptchaException.getHCaptchaError() == HCaptchaError.SESSION_TIMEOUT;

sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogListener.java

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ abstract class HCaptchaDialogListener implements Parcelable {
99

1010
abstract void onFailure(HCaptchaException hCaptchaException);
1111

12+
abstract void onOpen();
13+
1214
@Override
1315
public int describeContents() {
1416
return 0;

sdk/src/main/java/com/hcaptcha/sdk/HCaptchaJSInterface.java

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import com.hcaptcha.sdk.tasks.OnFailureListener;
77
import com.hcaptcha.sdk.tasks.OnLoadedListener;
8+
import com.hcaptcha.sdk.tasks.OnOpenListener;
89
import com.hcaptcha.sdk.tasks.OnSuccessListener;
910
import lombok.AllArgsConstructor;
1011
import lombok.Data;
@@ -25,6 +26,8 @@ class HCaptchaJSInterface implements Serializable {
2526

2627
private final OnLoadedListener onLoadedListener;
2728

29+
private final OnOpenListener onOpenListener;
30+
2831
private final OnSuccessListener<HCaptchaTokenResponse> onSuccessListener;
2932

3033
private final OnFailureListener onFailureListener;
@@ -51,4 +54,8 @@ public void onLoaded() {
5154
this.onLoadedListener.onLoaded();
5255
}
5356

57+
@JavascriptInterface
58+
public void onOpen() {
59+
this.onOpenListener.onOpen();
60+
}
5461
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.hcaptcha.sdk.tasks;
2+
3+
/**
4+
* A hCaptcha open listener class
5+
*/
6+
public interface OnOpenListener {
7+
8+
/**
9+
* Called when the hCaptcha challenge is displayed on the html page
10+
*/
11+
void onOpen();
12+
}

sdk/src/main/java/com/hcaptcha/sdk/tasks/Task.java

+24
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public abstract class Task<TResult> {
2828

2929
private final List<OnFailureListener> onFailureListeners;
3030

31+
private final List<OnOpenListener> onOpenListeners;
32+
3133
/**
3234
* Creates a new Task object
3335
*/
@@ -36,6 +38,7 @@ protected Task() {
3638
this.successful = false;
3739
this.onSuccessListeners = new ArrayList<>();
3840
this.onFailureListeners = new ArrayList<>();
41+
this.onOpenListeners = new ArrayList<>();
3942
}
4043

4144
/**
@@ -92,6 +95,15 @@ protected void setException(@NonNull HCaptchaException hCaptchaException) {
9295
tryCb();
9396
}
9497

98+
/**
99+
* Internal callback which called once 'open-callback' fired in js SDK
100+
*/
101+
protected void captchaOpened() {
102+
for (OnOpenListener listener : onOpenListeners) {
103+
listener.onOpen();
104+
}
105+
}
106+
95107
/**
96108
* Add a success listener triggered when the task finishes successfully
97109
*
@@ -116,6 +128,18 @@ public Task<TResult> addOnFailureListener(@NonNull final OnFailureListener onFai
116128
return this;
117129
}
118130

131+
/**
132+
* Add a hCaptcha open listener triggered when the hCaptcha View is displayed
133+
*
134+
* @param onOpenListener the open listener to be triggered
135+
* @return current object
136+
*/
137+
public Task<TResult> addOnOpenListener(@NonNull final OnOpenListener onOpenListener) {
138+
this.onOpenListeners.add(onOpenListener);
139+
tryCb();
140+
return this;
141+
}
142+
119143
private void tryCb() {
120144
if (getResult() != null) {
121145
final Iterator<OnSuccessListener<TResult>> iterator = onSuccessListeners.iterator();

sdk/src/test/java/com/hcaptcha/sdk/HCaptchaJSInterfaceTest.java

+17-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.hcaptcha.sdk.tasks.OnFailureListener;
55
import com.hcaptcha.sdk.tasks.OnLoadedListener;
6+
import com.hcaptcha.sdk.tasks.OnOpenListener;
67
import com.hcaptcha.sdk.tasks.OnSuccessListener;
78

89
import org.json.JSONException;
@@ -27,6 +28,9 @@ public class HCaptchaJSInterfaceTest {
2728
@Spy
2829
OnLoadedListener onLoadedListener;
2930

31+
@Spy
32+
OnOpenListener onOpenListener;
33+
3034
@Spy
3135
OnSuccessListener<HCaptchaTokenResponse> onSuccessListener;
3236

@@ -66,7 +70,7 @@ public void full_config_serialization() throws JsonProcessingException, JSONExce
6670
.host(host)
6771
.resetOnTimeout(true)
6872
.build();
69-
final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null);
73+
final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null, null);
7074

7175
JSONObject expected = new JSONObject();
7276
expected.put("siteKey", siteKey);
@@ -101,7 +105,7 @@ public void subset_config_serialization() throws JsonProcessingException, JSONEx
101105
.theme(HCaptchaTheme.DARK)
102106
.rqdata(rqdata)
103107
.build();
104-
final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null);
108+
final HCaptchaJSInterface HCaptchaJsInterface = new HCaptchaJSInterface(config, null, null, null, null);
105109

106110
JSONObject expected = new JSONObject();
107111
expected.put("siteKey", siteKey);
@@ -124,16 +128,23 @@ public void subset_config_serialization() throws JsonProcessingException, JSONEx
124128
}
125129

126130
@Test
127-
public void calls_on_challenge_visible_cb() {
128-
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, onLoadedListener, null, null);
131+
public void calls_on_challenge_ready() {
132+
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, onLoadedListener, null, null, null);
129133
hCaptchaJSInterface.onLoaded();
130134
verify(onLoadedListener, times(1)).onLoaded();
131135
}
132136

137+
@Test
138+
public void calls_on_challenge_visible_cb() {
139+
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, onOpenListener, null, null);
140+
hCaptchaJSInterface.onOpen();
141+
verify(onOpenListener, times(1)).onOpen();
142+
}
143+
133144
@Test
134145
public void on_pass_forwards_token_to_listeners() {
135146
final String token = "mock-token";
136-
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, onSuccessListener, null);
147+
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, onSuccessListener, null);
137148
hCaptchaJSInterface.onPass(token);
138149
verify(onSuccessListener, times(1)).onSuccess(tokenCaptor.capture());
139150
assertEquals(token, tokenCaptor.getValue().getTokenResult());
@@ -142,11 +153,10 @@ public void on_pass_forwards_token_to_listeners() {
142153
@Test
143154
public void on_error_forwards_error_to_listeners() {
144155
final HCaptchaError error = HCaptchaError.CHALLENGE_CLOSED;
145-
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, onFailureListener);
156+
final HCaptchaJSInterface hCaptchaJSInterface = new HCaptchaJSInterface(null, null, null, null, onFailureListener);
146157
hCaptchaJSInterface.onError(error.getErrorId());
147158
verify(onFailureListener, times(1)).onFailure(exceptionCaptor.capture());
148159
assertEquals(error.getMessage(), exceptionCaptor.getValue().getMessage());
149160
assertNotNull(exceptionCaptor.getValue());
150161
}
151-
152162
}

0 commit comments

Comments
 (0)