Skip to content

Commit e4df66f

Browse files
authored
feat: add orientation param #62 (#63)
* 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
1 parent d54659e commit e4df66f

7 files changed

Lines changed: 86 additions & 22 deletions

File tree

Example.App.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@ export default class App extends React.Component {
1414
if (event && event.nativeEvent.data) {
1515
if (['cancel'].includes(event.nativeEvent.data)) {
1616
this.captchaForm.hide();
17-
this.setState({ code: event.nativeEvent.data});
18-
} else if (['error', 'expired'].includes(event.nativeEvent.data)) {
17+
this.setState({ code: event.nativeEvent.data });
18+
} else if (['error'].includes(event.nativeEvent.data)) {
1919
this.captchaForm.hide();
20-
this.setState({ code: event.nativeEvent.data});
20+
this.setState({ code: event.nativeEvent.data });
21+
console.log('Verification failed', event.nativeEvent.data);
22+
} else if (event.nativeEvent.data === 'expired') {
23+
event.reset();
24+
console.log('Visual challenge expired, reset...', event.nativeEvent.data);
2125
} else if (event.nativeEvent.data === 'open') {
2226
console.log('Visual challenge opened');
2327
} else {
2428
console.log('Verified code from hCaptcha', event.nativeEvent.data);
2529
this.captchaForm.hide();
30+
event.markUsed();
2631
this.setState({ code: event.nativeEvent.data });
2732
}
2833
}

Hcaptcha.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ type HcaptchaProps = {
8181
* hCaptcha SDK host identifier. null value means that it will be generated by SDK
8282
*/
8383
host?: string;
84+
/**
85+
* The orientation of the challenge.
86+
* Default: portrait
87+
*/
88+
orientation?: 'portrait' | 'landscape';
8489
}
8590

8691
export default class Hcaptcha extends React.Component<HcaptchaProps> {}

Hcaptcha.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo, useCallback } from 'react';
1+
import React, { useMemo, useCallback, useRef } from 'react';
22
import WebView from 'react-native-webview';
33
import { Linking, StyleSheet, View, ActivityIndicator } from 'react-native';
44
import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';
@@ -20,7 +20,7 @@ const patchPostMessageJsCode = `(${String(function () {
2020
window.ReactNativeWebView.postMessage = patchedPostMessage;
2121
})})();`;
2222

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

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

33-
for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi })) {
33+
for (let [key, value] of Object.entries({ host, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi, orientation })) {
3434
if (value) {
3535
url += `&${key}=${encodeURIComponent(value)}`
3636
}
@@ -60,6 +60,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
6060
* @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.)
6161
* @param {string} host: hCaptcha SDK host identifier. null value means that it will be generated by SDK
6262
* @param {object} debug: debug information
63+
* @parem {string} hCaptcha challenge orientation
6364
*/
6465
const Hcaptcha = ({
6566
onMessage,
@@ -81,8 +82,10 @@ const Hcaptcha = ({
8182
imghost,
8283
host,
8384
debug,
85+
orientation,
8486
}) => {
85-
const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi);
87+
const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation);
88+
const tokenTimeout = 120000;
8689

8790
if (theme && typeof theme === 'string') {
8891
theme = `"${theme}"`;
@@ -151,7 +154,7 @@ const Hcaptcha = ({
151154
console.log("challenge opened");
152155
};
153156
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
154-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
157+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
155158
var onDataErrorCallback = function(error) {
156159
console.log("challenge error callback fired");
157160
window.ReactNativeWebView.postMessage("error");
@@ -199,8 +202,17 @@ const Hcaptcha = ({
199202
[loadingIndicatorColor]
200203
);
201204

205+
const webViewRef = useRef(null);
206+
207+
const reset = () => {
208+
if (webViewRef.current) {
209+
webViewRef.current.injectJavaScript('onloadCallback();');
210+
}
211+
};
212+
202213
return (
203214
<WebView
215+
ref={webViewRef}
204216
originWhitelist={['*']}
205217
onShouldStartLoadWithRequest={(event) => {
206218
if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
@@ -210,7 +222,14 @@ const Hcaptcha = ({
210222
return true;
211223
}}
212224
mixedContentMode={'always'}
213-
onMessage={onMessage}
225+
onMessage={(e) => {
226+
e.reset = reset;
227+
if (e.nativeEvent.data.length > 16) {
228+
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, reset }), tokenTimeout);
229+
e.markUsed = () => clearTimeout(expiredTokenTimerId);
230+
}
231+
onMessage(e);
232+
}}
214233
javaScriptEnabled
215234
injectedJavaScript={patchPostMessageJsCode}
216235
automaticallyAdjustContentInsets

README.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,54 @@ Also, please note the following special message strings that can be returned via
3535

3636
| name | purpose |
3737
| --- | --- |
38-
| expired | passcode response expired and the user must re-verify |
38+
| expired | passcode response expired and the user must re-verify, or did not answer before session expired |
3939
| error | there was an error displaying the challenge |
40-
| cancel | the user closed the challenge, or did not answer before session expired |
40+
| cancel | the user closed the challenge |
4141
| open | the visual challenge was opened |
4242

4343

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

46+
4647
### Handling the post-issuance expiration lifecycle
4748

4849
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.
4950

5051
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.
5152

52-
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:
53-
54-
```
55-
this.timeoutCheck = setTimeout(() => {
56-
this.setPasscodeExpired();
57-
}, 120000);
53+
By default, this value is 120 seconds. So, an `expired` error will be emitted to `onMessage` if you haven't called `event.markUsed()`.
54+
55+
Once you've utilized hCaptcha's token, call `markUsed` on the event object in `onMessage`:
56+
57+
```js
58+
onMessage = event => {
59+
if (event && event.nativeEvent.data) {
60+
if (['cancel'].includes(event.nativeEvent.data)) {
61+
this.captchaForm.hide();
62+
} else if (['error'].includes(event.nativeEvent.data)) {
63+
this.captchaForm.hide();
64+
// handle error
65+
} else {
66+
this.captchaForm.hide();
67+
const token = event.nativeEvent.data;
68+
// utlize token and call markUsed once you done with it
69+
event.markUsed();
70+
}
71+
}
72+
};
73+
...
74+
<ConfirmHcaptcha
75+
ref={_ref => (this.captchaForm = _ref)}
76+
siteKey={siteKey}
77+
languageCode="en"
78+
onMessage={this.onMessage}
79+
/>
5880
```
5981

82+
### Handling errors and retry
83+
84+
If your app encounters an `error` event, you can reset the hCaptcha SDK flow by calling `event.reset()`` to perform another attempt at verification.
85+
6086
## Dependencies
6187

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

133160

134161
## Status

__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
5353
})();"
5454
javaScriptEnabled={true}
5555
mixedContentMode="always"
56+
onMessage={[Function]}
5657
onShouldStartLoadWithRequest={[Function]}
5758
originWhitelist={
5859
[
@@ -103,7 +104,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
103104
console.log("challenge opened");
104105
};
105106
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
106-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
107+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
107108
var onDataErrorCallback = function(error) {
108109
console.log("challenge error callback fired");
109110
window.ReactNativeWebView.postMessage("error");
@@ -208,6 +209,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
208209
})();"
209210
javaScriptEnabled={true}
210211
mixedContentMode="always"
212+
onMessage={[Function]}
211213
onShouldStartLoadWithRequest={[Function]}
212214
originWhitelist={
213215
[
@@ -258,7 +260,7 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
258260
console.log("challenge opened");
259261
};
260262
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
261-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
263+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
262264
var onDataErrorCallback = function(error) {
263265
console.log("challenge error callback fired");
264266
window.ReactNativeWebView.postMessage("error");

__tests__/__snapshots__/Hcaptcha.test.js.snap

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
1515
})();"
1616
javaScriptEnabled={true}
1717
mixedContentMode="always"
18+
onMessage={[Function]}
1819
onShouldStartLoadWithRequest={[Function]}
1920
originWhitelist={
2021
[
@@ -65,7 +66,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
6566
console.log("challenge opened");
6667
};
6768
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
68-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
69+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
6970
var onDataErrorCallback = function(error) {
7071
console.log("challenge error callback fired");
7172
window.ReactNativeWebView.postMessage("error");
@@ -130,6 +131,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
130131
})();"
131132
javaScriptEnabled={true}
132133
mixedContentMode="always"
134+
onMessage={[Function]}
133135
onShouldStartLoadWithRequest={[Function]}
134136
originWhitelist={
135137
[
@@ -180,7 +182,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
180182
console.log("challenge opened");
181183
};
182184
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
183-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
185+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
184186
var onDataErrorCallback = function(error) {
185187
console.log("challenge error callback fired");
186188
window.ReactNativeWebView.postMessage("error");
@@ -244,6 +246,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
244246
})();"
245247
javaScriptEnabled={true}
246248
mixedContentMode="always"
249+
onMessage={[Function]}
247250
onShouldStartLoadWithRequest={[Function]}
248251
originWhitelist={
249252
[
@@ -294,7 +297,7 @@ exports[`Hcaptcha snapshot tests test debug 1`] = `
294297
console.log("challenge opened");
295298
};
296299
var onDataExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
297-
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("cancel"); };
300+
var onChalExpiredCallback = function(error) { window.ReactNativeWebView.postMessage("expired"); };
298301
var onDataErrorCallback = function(error) {
299302
console.log("challenge error callback fired");
300303
window.ReactNativeWebView.postMessage("error");

index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class ConfirmHcaptcha extends PureComponent {
2828
passiveSiteKey,
2929
baseUrl,
3030
languageCode,
31+
orientation,
3132
onMessage,
3233
showLoading,
3334
backgroundColor,
@@ -110,6 +111,7 @@ ConfirmHcaptcha.propTypes = {
110111
baseUrl: PropTypes.string,
111112
onMessage: PropTypes.func,
112113
languageCode: PropTypes.string,
114+
orientation: PropTypes.string,
113115
backgroundColor: PropTypes.string,
114116
showLoading: PropTypes.bool,
115117
loadingIndicatorColor: PropTypes.string,
@@ -130,6 +132,7 @@ ConfirmHcaptcha.defaultProps = {
130132
size: 'invisible',
131133
passiveSiteKey: false,
132134
showLoading: false,
135+
orientation: 'portrait',
133136
backgroundColor: 'rgba(0, 0, 0, 0.3)',
134137
loadingIndicatorColor: null,
135138
theme: 'light',

0 commit comments

Comments
 (0)