Skip to content

Commit

Permalink
feat: add token timeout error (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
CAMOBAP authored Jul 13, 2022
1 parent d3007a2 commit 7966fea
Show file tree
Hide file tree
Showing 21 changed files with 256 additions and 93 deletions.
40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,30 +108,31 @@ 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.
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
The following list contains configuration properties to allows customization of the hCaptcha verification flow.
| Name | Values/Type | Required | Default | Description |
|------------------|-------------------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `siteKey` | String | **Yes** | - | This is your sitekey, this allows you to load challenges. If you need a sitekey, please visit [hCaptcha](https://www.hcaptcha.com), and sign up to get your sitekey. |
| `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-2 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.) |
| `sentry` | Boolean | No | True | See Enterprise docs. |
| `rqdata` | String | No | - | See Enterprise docs. |
| `apiEndpoint` | String | No | - | See Enterprise docs. |
| `endpoint` | String | No | - | See Enterprise docs. |
| `reportapi` | String | No | - | See Enterprise docs. |
| `assethost` | String | No | - | See Enterprise docs. |
| `imghost` | String | No | - | See Enterprise docs. |
| `customTheme` | Stringified JSON | No | - | See Enterprise docs. |
| `host` | String | No | - | See Enterprise docs. |
| `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. |
| Name | Values/Type | Required | Default | Description |
|---------------------|-------------------------|----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `siteKey` | String | **Yes** | - | This is your sitekey, this allows you to load challenges. If you need a sitekey, please visit [hCaptcha](https://www.hcaptcha.com), and sign up to get your sitekey. |
| `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-2 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.) |
| `sentry` | Boolean | No | True | See Enterprise docs. |
| `rqdata` | String | No | - | See Enterprise docs. |
| `apiEndpoint` | String | No | - | See Enterprise docs. |
| `endpoint` | String | No | - | See Enterprise docs. |
| `reportapi` | String | No | - | See Enterprise docs. |
| `assethost` | String | No | - | See Enterprise docs. |
| `imghost` | String | No | - | See Enterprise docs. |
| `customTheme` | Stringified JSON | No | - | See Enterprise docs. |
| `host` | String | No | - | See Enterprise docs. |
| `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). |
### Config Examples
Expand Down Expand Up @@ -168,6 +169,7 @@ The following is a list of possible error codes:
| `CHALLENGE_ERROR` | 9 | JS client encountered an error on challenge setup. |
| `INTERNAL_ERROR` | 10 | JS client encountered an internal error. |
| `SESSION_TIMEOUT` | 15 | The challenge expired. |
| `TOKEN_TIMEOUT` | 16 | The token expired. |
| `CHALLENGE_CLOSED` | 30 | The challenge was closed by the user. |
| `RATE_LIMITED` | 31 | Spam detected. |
| `INVALID_CUSTOM_THEME` | 32 | Invalid custom theme. |
Expand Down
10 changes: 10 additions & 0 deletions example-app/src/main/java/com/hcaptcha/example/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class MainActivity extends AppCompatActivity {
private TextView tokenTextView;
private TextView errorTextView;
private HCaptcha hCaptcha;
private HCaptchaTokenResponse tokenResponse;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
Expand Down Expand Up @@ -65,6 +66,7 @@ private HCaptchaConfig getConfig() {
.size(size)
.loading(loading.isChecked())
.hideDialog(hideDialog.isChecked())
.tokenExpiration(10)
.build();
}

Expand Down Expand Up @@ -98,11 +100,18 @@ public void onClickVerify(final View v) {
}
}

public void onMarkUsed(final View v) {
if (tokenResponse != null) {
tokenResponse.markUsed();
}
}

private void setupClient(final HCaptcha hCaptcha) {
hCaptcha
.addOnSuccessListener(new OnSuccessListener<HCaptchaTokenResponse>() {
@Override
public void onSuccess(HCaptchaTokenResponse response) {
tokenResponse = response;
String userResponseToken = response.getTokenResult();
setTokenTextView(userResponseToken);
}
Expand All @@ -112,6 +121,7 @@ public void onSuccess(HCaptchaTokenResponse response) {
public void onFailure(HCaptchaException e) {
Log.d(TAG, "hCaptcha failed: " + e.getMessage() + "(" + e.getStatusCode() + ")");
setErrorTextView(e.getMessage());
tokenResponse = null;
}
})
.addOnOpenListener(new OnOpenListener() {
Expand Down
17 changes: 11 additions & 6 deletions example-app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
android:checked="false"
android:text="@string/hide_dialog"
style="@style/CheckBoxText" />
<CheckBox
android:id="@+id/webViewDebug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:checked="false"
android:text="@string/web_view_debug"
style="@style/CheckBoxText" />
</LinearLayout>

<LinearLayout
Expand All @@ -57,14 +65,11 @@
android:text="@string/verify"
android:onClick="onClickVerify" />

<CheckBox
android:id="@+id/webViewDebug"
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:checked="false"
android:text="@string/web_view_debug"
style="@style/CheckBoxText" />
android:text="@string/mark_used"
android:onClick="onMarkUsed" />
</LinearLayout>

<LinearLayout
Expand Down
3 changes: 2 additions & 1 deletion example-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<string name="clear">Clear</string>
<string name="setup">Setup</string>
<string name="verify">Verify</string>
<string name="mark_used">Mark used</string>
<string name="loading">Loading</string>
<string name="web_view_debug">WebView Debug</string>
<string name="web_view_debug">Web Debug</string>
<string name="hide_dialog">Hide Dialog</string>
</resources>
4 changes: 4 additions & 0 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ android {
minifyEnabled true
}
}

testOptions {
unitTests.returnDefaultValues = true
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,50 @@
@RunWith(AndroidJUnit4.class)
public class HCaptchaDialogFragmentTest {
private static final long AWAIT_CALLBACK_MS = 1000;

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment() {
private static final String TEST_TOKEN = "test-token";

final HCaptchaConfig config = HCaptchaConfig.builder()
.siteKey("10000000-ffff-ffff-ffff-000000000001")
.endpoint("https://js.hcaptcha.com/1/api.js")
.locale("en")
.loading(true)
.size(HCaptchaSize.INVISIBLE)
.theme(HCaptchaTheme.LIGHT)
.build();

private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment() {
return launchCaptchaFragment(true);
}

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader) {
return launchCaptchaFragment(showLoader, new HCaptchaStateTestAdapter());
private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader) {
return launchCaptchaFragment(config.toBuilder().loading(showLoader).build(), new HCaptchaStateTestAdapter());
}

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(HCaptchaStateListener listener) {
return launchCaptchaFragment(true, listener);
private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(HCaptchaStateListener listener) {
return launchCaptchaFragment(config, listener);
}

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader,
HCaptchaStateListener listener) {
final HCaptchaConfig hCaptchaConfig = HCaptchaConfig.builder()
.siteKey("10000000-ffff-ffff-ffff-000000000001")
.endpoint("https://js.hcaptcha.com/1/api.js")
.locale("en")
.loading(showLoader)
.size(HCaptchaSize.INVISIBLE)
.theme(HCaptchaTheme.LIGHT)
.build();
private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(final HCaptchaConfig captchaConfig,
HCaptchaStateListener listener) {
final Bundle args = new Bundle();
args.putSerializable(KEY_CONFIG, hCaptchaConfig);
args.putSerializable(KEY_CONFIG, captchaConfig);
args.putParcelable(KEY_LISTENER, listener);
return FragmentScenario.launchInContainer(HCaptchaDialogFragment.class, args);
}

private void makeWebViewToEmitToken(String token) {
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(token));

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

@Test
public void loaderVisible() {
launchCaptchaFragment();
Expand All @@ -82,23 +97,14 @@ public void webViewReturnToken() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {
@Override
void onSuccess(HCaptchaTokenResponse response) {
assertEquals("test-token", response.getTokenResult());
void onSuccess(String token) {
assertEquals(TEST_TOKEN, token);
latch.countDown();
}
};

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("test-token"));

onWebView().withElement(findElement(Locator.ID, "on-pass"))
.perform(webClick());
makeWebViewToEmitToken(TEST_TOKEN);

assertTrue(latch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); // wait for callback
}
Expand All @@ -109,7 +115,7 @@ public void webViewReturnsError() throws Exception {
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {
@Override
void onFailure(HCaptchaException exception) {
assertEquals(HCaptchaError.SESSION_TIMEOUT, exception.getHCaptchaError());
assertEquals(HCaptchaError.CHALLENGE_ERROR, exception.getHCaptchaError());
latch.countDown();
}
};
Expand All @@ -122,7 +128,7 @@ void onFailure(HCaptchaException exception) {
onWebView().withElement(findElement(Locator.ID, "input-text"))
.perform(clearElement())
.perform(DriverAtoms.webKeys(
String.valueOf(HCaptchaError.SESSION_TIMEOUT.getErrorId())));
String.valueOf(HCaptchaError.CHALLENGE_ERROR.getErrorId())));

onWebView().withElement(findElement(Locator.ID, "on-error"))
.perform(webClick());
Expand All @@ -141,16 +147,7 @@ void onOpen() {
};

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("test-token"));

onWebView().withElement(findElement(Locator.ID, "on-pass"))
.perform(webClick());
makeWebViewToEmitToken(TEST_TOKEN);

assertTrue(latch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); // wait for callback
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void onOpen() {
}

@Override
void onSuccess(HCaptchaTokenResponse response) {
void onSuccess(String token) {
latch.countDown();
}

Expand Down Expand Up @@ -73,7 +73,7 @@ public void testFailure() throws Exception {
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

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

Expand Down
Loading

0 comments on commit 7966fea

Please sign in to comment.