Skip to content

Commit

Permalink
feat: add orientation param #62 (#63)
Browse files Browse the repository at this point in the history
* feat: add orientation param #62

* feat: expose reset into even object to allow retry #62

* feat: emit expired after 120 sec if token is not markUsed #62

* docs: update README.md #62

* docs: add orientatin param to table and markUsed snippet
  • Loading branch information
CAMOBAP authored Oct 6, 2024
1 parent d54659e commit e4df66f
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 22 deletions.
11 changes: 8 additions & 3 deletions Example.App.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ export default class App extends React.Component {
if (event && event.nativeEvent.data) {
if (['cancel'].includes(event.nativeEvent.data)) {
this.captchaForm.hide();
this.setState({ code: event.nativeEvent.data});
} else if (['error', 'expired'].includes(event.nativeEvent.data)) {
this.setState({ code: event.nativeEvent.data });
} else if (['error'].includes(event.nativeEvent.data)) {
this.captchaForm.hide();
this.setState({ code: event.nativeEvent.data});
this.setState({ code: event.nativeEvent.data });
console.log('Verification failed', event.nativeEvent.data);
} else if (event.nativeEvent.data === 'expired') {
event.reset();
console.log('Visual challenge expired, reset...', event.nativeEvent.data);
} else if (event.nativeEvent.data === 'open') {
console.log('Visual challenge opened');
} else {
console.log('Verified code from hCaptcha', event.nativeEvent.data);
this.captchaForm.hide();
event.markUsed();
this.setState({ code: event.nativeEvent.data });
}
}
Expand Down
5 changes: 5 additions & 0 deletions Hcaptcha.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ type HcaptchaProps = {
* hCaptcha SDK host identifier. null value means that it will be generated by SDK
*/
host?: string;
/**
* The orientation of the challenge.
* Default: portrait
*/
orientation?: 'portrait' | 'landscape';
}

export default class Hcaptcha extends React.Component<HcaptchaProps> {}
31 changes: 25 additions & 6 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from 'react';
import React, { useMemo, useCallback, useRef } from 'react';
import WebView from 'react-native-webview';
import { Linking, StyleSheet, View, ActivityIndicator } from 'react-native';
import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';
Expand All @@ -20,7 +20,7 @@ const patchPostMessageJsCode = `(${String(function () {
window.ReactNativeWebView.postMessage = patchedPostMessage;
})})();`;

const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi) => {
const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation) => {
var url = `${jsSrc || "https://hcaptcha.com/1/api.js"}?render=explicit&onload=onloadCallback`;

let effectiveHost;
Expand All @@ -30,7 +30,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
host = (siteKey || 'missing-sitekey') + '.react-native.hcaptcha.com';
}

for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi })) {
for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi, orientation })) {
if (value) {
url += `&${key}=${encodeURIComponent(value)}`
}
Expand Down Expand Up @@ -60,6 +60,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
* @param {string} imghost: Points loaded hCaptcha challenge images to a user defined image location, used for proxies. Default: https://imgs.hcaptcha.com (Override only if using first-party hosting feature.)
* @param {string} host: hCaptcha SDK host identifier. null value means that it will be generated by SDK
* @param {object} debug: debug information
* @parem {string} hCaptcha challenge orientation
*/
const Hcaptcha = ({
onMessage,
Expand All @@ -81,8 +82,10 @@ const Hcaptcha = ({
imghost,
host,
debug,
orientation,
}) => {
const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi);
const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation);
const tokenTimeout = 120000;

if (theme && typeof theme === 'string') {
theme = `"${theme}"`;
Expand Down Expand Up @@ -151,7 +154,7 @@ const Hcaptcha = ({
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down Expand Up @@ -199,8 +202,17 @@ const Hcaptcha = ({
[loadingIndicatorColor]
);

const webViewRef = useRef(null);

const reset = () => {
if (webViewRef.current) {
webViewRef.current.injectJavaScript('onloadCallback();');
}
};

return (
<WebView
ref={webViewRef}
originWhitelist={['*']}
onShouldStartLoadWithRequest={(event) => {
if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
Expand All @@ -210,7 +222,14 @@ const Hcaptcha = ({
return true;
}}
mixedContentMode={'always'}
onMessage={onMessage}
onMessage={(e) => {
e.reset = reset;
if (e.nativeEvent.data.length > 16) {
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, reset }), tokenTimeout);
e.markUsed = () => clearTimeout(expiredTokenTimerId);
}
onMessage(e);
}}
javaScriptEnabled
injectedJavaScript={patchPostMessageJsCode}
automaticallyAdjustContentInsets
Expand Down
43 changes: 35 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,54 @@ Also, please note the following special message strings that can be returned via

| name | purpose |
| --- | --- |
| expired | passcode response expired and the user must re-verify |
| expired | passcode response expired and the user must re-verify, or did not answer before session expired |
| error | there was an error displaying the challenge |
| cancel | the user closed the challenge, or did not answer before session expired |
| cancel | the user closed the challenge |
| open | the visual challenge was opened |


Any other string returned by `onMessage` will be a passcode.


### Handling the post-issuance expiration lifecycle

This extension is a lightweight wrapper, and does not currently attempt to manage post-verification state in the same way as the web JS API, e.g. with an on-expire callback.

In particular, if you do **not** plan to immediately consume the passcode returned by submitting it to your backend, you should start a timer to let your application state know that a new passcode is required when it expires.

By default, this value is 120 seconds. Thus, you would want code similar to the following in your app when handling `onMessage` responses that return a passcode:

```
this.timeoutCheck = setTimeout(() => {
this.setPasscodeExpired();
}, 120000);
By default, this value is 120 seconds. So, an `expired` error will be emitted to `onMessage` if you haven't called `event.markUsed()`.

Once you've utilized hCaptcha's token, call `markUsed` on the event object in `onMessage`:

```js
onMessage = event => {
if (event && event.nativeEvent.data) {
if (['cancel'].includes(event.nativeEvent.data)) {
this.captchaForm.hide();
} else if (['error'].includes(event.nativeEvent.data)) {
this.captchaForm.hide();
// handle error
} else {
this.captchaForm.hide();
const token = event.nativeEvent.data;
// utlize token and call markUsed once you done with it
event.markUsed();
}
}
};
...
<ConfirmHcaptcha
ref={_ref => (this.captchaForm = _ref)}
siteKey={siteKey}
languageCode="en"
onMessage={this.onMessage}
/>
```

### Handling errors and retry

If your app encounters an `error` event, you can reset the hCaptcha SDK flow by calling `event.reset()`` to perform another attempt at verification.

## Dependencies

1. [react-native-modal](https://github.com/react-native-community/react-native-modal)
Expand Down Expand Up @@ -129,6 +155,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
| baseUrl _(modal component only)_ | string | The url domain defined on your hCaptcha. You generally will not need to change this. |
| passiveSiteKey _(modal component only)_ | boolean | Indicates whether the passive mode is enabled; when true, the modal won't be shown at all |
| hasBackdrop _(modal component only)_ | boolean | Defines if the modal backdrop is shown (true by default) |
| orientation | string | This specifies the "orientation" of the challenge. It can be `portrait`, `landscape`. Default: `portrait` |


## Status
Expand Down
6 changes: 4 additions & 2 deletions __tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
})();"
javaScriptEnabled={true}
mixedContentMode="always"
onMessage={[Function]}
onShouldStartLoadWithRequest={[Function]}
originWhitelist={
[
Expand Down Expand Up @@ -103,7 +104,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down Expand Up @@ -208,6 +209,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
})();"
javaScriptEnabled={true}
mixedContentMode="always"
onMessage={[Function]}
onShouldStartLoadWithRequest={[Function]}
originWhitelist={
[
Expand Down Expand Up @@ -258,7 +260,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down
9 changes: 6 additions & 3 deletions __tests__/__snapshots__/Hcaptcha.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
})();"
javaScriptEnabled={true}
mixedContentMode="always"
onMessage={[Function]}
onShouldStartLoadWithRequest={[Function]}
originWhitelist={
[
Expand Down Expand Up @@ -65,7 +66,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down Expand Up @@ -130,6 +131,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
})();"
javaScriptEnabled={true}
mixedContentMode="always"
onMessage={[Function]}
onShouldStartLoadWithRequest={[Function]}
originWhitelist={
[
Expand Down Expand Up @@ -180,7 +182,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down Expand Up @@ -244,6 +246,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
})();"
javaScriptEnabled={true}
mixedContentMode="always"
onMessage={[Function]}
onShouldStartLoadWithRequest={[Function]}
originWhitelist={
[
Expand Down Expand Up @@ -294,7 +297,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
console.log("challenge opened");
};
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
var onDataErrorCallback = function(error) {
console.log("challenge error callback fired");
window.ReactNativeWebView.postMessage("error");
Expand Down
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class ConfirmHcaptcha extends PureComponent {
passiveSiteKey,
baseUrl,
languageCode,
orientation,
onMessage,
showLoading,
backgroundColor,
Expand Down Expand Up @@ -110,6 +111,7 @@ ConfirmHcaptcha.propTypes = {
baseUrl: PropTypes.string,
onMessage: PropTypes.func,
languageCode: PropTypes.string,
orientation: PropTypes.string,
backgroundColor: PropTypes.string,
showLoading: PropTypes.bool,
loadingIndicatorColor: PropTypes.string,
Expand All @@ -130,6 +132,7 @@ ConfirmHcaptcha.defaultProps = {
size: 'invisible',
passiveSiteKey: false,
showLoading: false,
orientation: 'portrait',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
loadingIndicatorColor: null,
theme: 'light',
Expand Down

0 comments on commit e4df66f

Please sign in to comment.