Skip to content

Commit

Permalink
feat(config): retryPredicate (#87)
Browse files Browse the repository at this point in the history
* feat(config): retryPredicate

* bump(version): 3.4.0

---------

Co-authored-by: Sergiu Danalachi <danalachi.sergiu@gmail.com>
  • Loading branch information
CAMOBAP and DSergiu committed Feb 3, 2023
1 parent 0462c18 commit c636106
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 37 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

# 3.4.0

- Feat: new `HCaptchaConfig.retryPredicate` which allows conditional automatic retry
- Deprecated: `HCaptchaConfig.resetOnTimeout` replaced by more generic `HCaptchaConfig.retryPredicate` option

# 3.3.7

- Bugfix: handle Failed to load WebView provider: No WebView installed
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ hCaptcha.setup().verifyWithHCaptcha()
1. The listeners (`onSuccess`, `onFailure`, `onOpen`) can be called multiple times in the following cases:
1. the same client is used to invoke multiple verifications
2. the config option `resetOnTimeout(true)` is used which will automatically trigger a new verification when the current token expired. This will result in a new success or error callback.
* deprecated, please use [`HCaptchaConfig.retryPredicate`](#retry-failed-verification)
2. the config option [`HCaptchaConfig.retryPredicate`](#retry-failed-verification) is used to automatically trigger a new verification when some error occurs. If `HCaptchaConfig.retryPredicate` returns `true`, this will result in a new success or error callback.
3. `onFailure` with `TOKEN_TIMEOUT` will be called once the token is expired. To prevent this you can call `HCaptchaTokenResponse.markUsed` once the token is utilized. Also, you can change expiration timeout with `HCaptchaConfigBuilder.tokenExpiration(timeout)` (default 2 min.)
## Config Params
Expand All @@ -128,7 +130,8 @@ The following list contains configuration properties to allows customization of
| `size` | Enum | No | INVISIBLE | This specifies the "size" of the checkbox component. By default, the checkbox is invisible and the challenge is shown automatically. |
| `theme` | Enum | No | LIGHT | hCaptcha supports light, dark, and contrast themes. |
| `locale` | String (ISO 639-1 code) | No | AUTO | You can enforce a specific language or let hCaptcha auto-detect the local language based on user's device. |
| `resetOnTimeout` | Boolean | No | False | Automatically reload to fetch new challenge if user does not submit challenge. (Matches iOS SDK behavior.) |
| `resetOnTimeout` | Boolean | No | False | (DEPRECATED, use `retryPredicate`) Automatically reload to fetch new challenge if user does not submit challenge. (Matches iOS SDK behavior.) |
| `retryPredicate` | Lambda | No | - | Automatically trigger a new verification when some error occurs. |
| `sentry` | Boolean | No | True | See Enterprise docs. |
| `rqdata` | String | No | - | See Enterprise docs. |
| `apiEndpoint` | String | No | - | See Enterprise docs. |
Expand All @@ -141,7 +144,7 @@ The following list contains configuration properties to allows customization of
| `loading` | Boolean | No | True | Show or hide the loading dialog. |
| `hideDialog` | Boolean | No | False | To be used in combination with a passive sitekey when no user interaction is required. See Enterprise docs. |
| `tokenExpiration` | long | No | 120 | hCaptcha token expiration timeout (seconds). |
| `diagnosticLog` | Boolean | No | False | Emit detailed console logs for debugging |
| `diagnosticLog` | Boolean | No | False | Emit detailed console logs for debugging |
### Config Examples
Expand All @@ -157,7 +160,7 @@ final HCaptchaConfig config = HCaptchaConfig.builder()
2. Set a specific language, use a dark theme and a compact checkbox.
```java
final HCaptchaConfig config = HCaptchaConfig.builder()
.siteKey(YOUR_API_SITE_KEY)
.siteKey("YOUR_API_SITE_KEY")
.locale("ro")
.size(HCaptchaSize.COMPACT)
.theme(HCaptchaTheme.DARK)
Expand All @@ -184,6 +187,22 @@ The following is a list of possible error codes:
| `INVALID_CUSTOM_THEME` | 32 | Invalid custom theme. |
| `ERROR` | 29 | General failure. |
### Retry Failed Verification
You can indicate an automatic verification retry by setting the lambda config `HCaptchaConfig.retryPredicate`.
One must be careful to not introduce infinite retries and thus blocking the user from error recovering.
Example below will automatically retry in case of `CHALLENGE_CLOSED` error:
```java
final HCaptchaConfig config = HCaptchaConfig.builder()
.siteKey("YOUR_API_SITE_KEY")
.retryPredicate((config, hCaptchaException) -> {
return hCaptchaException.getHCaptchaError() == HCaptchaError.CHALLENGE_CLOSED;
})
.build();
...
```
## Debugging Tips
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ private HCaptchaConfig getConfig() {
.hideDialog(hideDialog.isChecked())
.tokenExpiration(10)
.diagnosticLog(true)
.retryPredicate((config, exception) -> exception.getHCaptchaError() == HCaptchaError.SESSION_TIMEOUT)
.build();
}

Expand Down
6 changes: 3 additions & 3 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,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 25
versionCode 26

// version number visible to the user
// should follow semantic versioning (See https://semver.org)
versionName "3.3.7"
versionName "3.4.0"

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

Expand Down Expand Up @@ -165,7 +165,7 @@ android.libraryVariants.all { variant ->

def packageName = "com.hcaptcha.sdk"
def outputDir = file("${project.buildDir}/generated/source/hcaptcha/${variant.name}/${packageName.replaceAll('\\.', '/')}")
def generateTask = project.task("genarate${variantName}JavaClassFromStaticHtml") {
def generateTask = project.task("generate${variantName}JavaClassFromStaticHtml") {
group 'Generate'
description "Generate HTML java class"

Expand Down
29 changes: 29 additions & 0 deletions sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.model.Atoms.getCurrentUrl;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static org.hamcrest.CoreMatchers.any;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.view.View;
import android.webkit.ValueCallback;
Expand All @@ -19,6 +23,8 @@
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import androidx.test.espresso.web.webdriver.DriverAtoms;
import androidx.test.espresso.web.webdriver.Locator;

import org.hamcrest.Matcher;

Expand Down Expand Up @@ -172,4 +178,27 @@ public static void waitHCaptchaWebViewError(final CountDownLatch latch,
"onError(" + error.getErrorId() + ")"));
assertTrue(latch.await(timeout, TimeUnit.MILLISECONDS));
}

public static void waitHCaptchaWebViewErrorByInput(final CountDownLatch latch,
final HCaptchaError error,
final long timeout)
throws InterruptedException {
onView(withId(R.id.webView)).perform(waitToBeDisplayed());

onWebView(withId(R.id.webView)).forceJavascriptEnabled();

onWebView().withElement(findElement(Locator.ID, "input-text"))
.perform(clearElement())
.perform(DriverAtoms.webKeys(
String.valueOf(error.getErrorId())));

onWebView().withElement(findElement(Locator.ID, "on-error"))
.perform(webClick());

assertTrue(latch.await(timeout, TimeUnit.MILLISECONDS)); // wait for callback
}

public static void failAsNonReachable() {
fail("Should not be called for this test");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static com.hcaptcha.sdk.AssertUtil.failAsNonReachable;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewErrorByInput;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken;
import static com.hcaptcha.sdk.AssertUtil.waitToBeDisplayed;
import static com.hcaptcha.sdk.AssertUtil.waitToDisappear;
import static com.hcaptcha.sdk.HCaptchaDialogFragment.KEY_CONFIG;
Expand Down Expand Up @@ -133,19 +136,8 @@ void onFailure(HCaptchaException exception) {
};

launchCaptchaFragment(listener);
onView(withId(R.id.webView)).perform(waitToBeDisplayed());

onWebView(withId(R.id.webView)).forceJavascriptEnabled();

onWebView().withElement(findElement(Locator.ID, "input-text"))
.perform(clearElement())
.perform(DriverAtoms.webKeys(
String.valueOf(HCaptchaError.CHALLENGE_ERROR.getErrorId())));

onWebView().withElement(findElement(Locator.ID, "on-error"))
.perform(webClick());

assertTrue(latch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); // wait for callback
waitHCaptchaWebViewErrorByInput(latch, HCaptchaError.CHALLENGE_ERROR, AWAIT_CALLBACK_MS);
}

@Test
Expand Down Expand Up @@ -210,4 +202,66 @@ void onFailure(HCaptchaException exception) {

assertTrue(latch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
}

@Test
public void testRetryPredicate() throws Exception {
final CountDownLatch failureLatch = new CountDownLatch(1);
final CountDownLatch successLatch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

@Override
void onSuccess(String token) {
successLatch.countDown();
}

@Override
void onFailure(HCaptchaException exception) {
failAsNonReachable();
}
};

final HCaptchaConfig updatedWithRetry = config.toBuilder()
.retryPredicate((c, e) -> {
failureLatch.countDown();
return e.getHCaptchaError() == HCaptchaError.NETWORK_ERROR;
})
.build();

launchCaptchaFragment(updatedWithRetry, listener);

waitHCaptchaWebViewErrorByInput(failureLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

waitHCaptchaWebViewToken(successLatch, AWAIT_CALLBACK_MS);
}

@Test
public void testNotRetryPredicate() throws Exception {
final CountDownLatch failureLatch = new CountDownLatch(1);
final CountDownLatch retryLatch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

@Override
void onSuccess(String token) {
failAsNonReachable();
}

@Override
void onFailure(HCaptchaException exception) {
failureLatch.countDown();
}
};

final HCaptchaConfig updatedWithRetry = config.toBuilder()
.retryPredicate((c, e) -> {
retryLatch.countDown();
return e.getHCaptchaError() != HCaptchaError.NETWORK_ERROR;
})
.build();

launchCaptchaFragment(updatedWithRetry, listener);

waitHCaptchaWebViewErrorByInput(retryLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.hcaptcha.sdk;

import static com.hcaptcha.sdk.AssertUtil.failAsNonReachable;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewError;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import androidx.test.core.app.ActivityScenario;
Expand All @@ -13,6 +15,7 @@
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
public class HCaptchaHeadlessWebViewTest {
Expand Down Expand Up @@ -48,7 +51,7 @@ void onSuccess(String token) {

@Override
void onFailure(HCaptchaException exception) {
fail("Should not be called for this test");
failAsNonReachable();
}
};

Expand All @@ -69,7 +72,7 @@ public void testFailure() throws Exception {

@Override
void onSuccess(String token) {
fail("Should not be called for this test");
failAsNonReachable();
}

@Override
Expand All @@ -87,4 +90,76 @@ void onFailure(HCaptchaException exception) {

waitHCaptchaWebViewError(latch, HCaptchaError.ERROR, AWAIT_CALLBACK_MS);
}

@Test
public void testRetryPredicate() throws Exception {
final CountDownLatch failureLatch = new CountDownLatch(1);
final CountDownLatch successLatch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

@Override
void onSuccess(String token) {
successLatch.countDown();
}

@Override
void onFailure(HCaptchaException exception) {
failAsNonReachable();
}
};

final HCaptchaConfig updatedWithRetry = config.toBuilder()
.retryPredicate((c, e) -> {
failureLatch.countDown();
return e.getHCaptchaError() == HCaptchaError.NETWORK_ERROR;
})
.build();

final ActivityScenario<TestActivity> scenario = rule.getScenario();
scenario.onActivity(activity -> {
final HCaptchaHeadlessWebView subject = new HCaptchaHeadlessWebView(
activity, updatedWithRetry, internalConfig, listener);
subject.startVerification(activity);
});

waitHCaptchaWebViewError(failureLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

waitHCaptchaWebViewToken(successLatch, AWAIT_CALLBACK_MS);
}

@Test
public void testNotRetryPredicate() throws Exception {
final CountDownLatch failureLatch = new CountDownLatch(1);
final CountDownLatch retryLatch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

@Override
void onSuccess(String token) {
failAsNonReachable();
}

@Override
void onFailure(HCaptchaException exception) {
failureLatch.countDown();
}
};

final HCaptchaConfig updatedWithRetry = config.toBuilder()
.retryPredicate((c, e) -> {
retryLatch.countDown();
return e.getHCaptchaError() != HCaptchaError.NETWORK_ERROR;
})
.build();

final ActivityScenario<TestActivity> scenario = rule.getScenario();
scenario.onActivity(activity -> {
final HCaptchaHeadlessWebView subject = new HCaptchaHeadlessWebView(
activity, updatedWithRetry, internalConfig, listener);
subject.startVerification(activity);
});

waitHCaptchaWebViewError(retryLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
}
}
6 changes: 3 additions & 3 deletions sdk/src/main/html/hcaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
};
var bridgeConfig = JSON.parse(BridgeObject.getConfig());
var hCaptchaID = null;
function execute() {
hcaptcha.execute(hCaptchaID);
}
/**
* Called programmatically from HCaptchaWebViewHelper.
*/
function resetAndExecute() {
hcaptcha.reset();
hcaptcha.execute(hCaptchaID);
Expand Down
Loading

0 comments on commit c636106

Please sign in to comment.