Skip to content

Commit e329552

Browse files
authored
Merge pull request #70 from hCaptcha/feature/improve-slow-network-experience
feat: improve UX when user is on a slow network
2 parents dfcfd77 + 2d9339c commit e329552

File tree

9 files changed

+315
-216
lines changed

9 files changed

+315
-216
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ jobs:
5151
# npm install -g ${{ matrix.pm }} react-native
5252
npm run example -- --pm ${{ matrix.pm }}
5353
working-directory: react-native-hcaptcha
54+
env:
55+
YARN_ENABLE_IMMUTABLE_INSTALLS: false
5456
- id: rn-version
5557
working-directory: react-native-hcaptcha-example
5658
run: |

Hcaptcha.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ type HcaptchaProps = {
3232
* Whether to show a loading indicator while the hCaptcha web content loads
3333
*/
3434
showLoading?: boolean;
35+
/**
36+
* Allow user to cancel hcaptcha during loading by touch loader overlay
37+
*/
38+
closableLoading?: boolean;
3539
/**
3640
* Color of the ActivityIndicator
3741
*/

Hcaptcha.js

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useMemo, useCallback, useRef } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import WebView from 'react-native-webview';
3-
import { Linking, StyleSheet, View, ActivityIndicator } from 'react-native';
3+
import { ActivityIndicator, Linking, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
44
import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';
55

66
import md5 from './md5';
@@ -48,6 +48,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
4848
* @param {*} url: base url
4949
* @param {*} languageCode: can be found at https://docs.hcaptcha.com/languages
5050
* @param {*} showLoading: loading indicator for webview till hCaptcha web content loads
51+
* @param {*} closableLoading: allow user to cancel hcaptcha during loading by touch loader overlay
5152
* @param {*} loadingIndicatorColor: color for the ActivityIndicator
5253
* @param {*} backgroundColor: backgroundColor which can be injected into HTML to alter css backdrop colour
5354
* @param {string|object} theme: can be 'light', 'dark', 'contrast' or custom theme object
@@ -70,6 +71,7 @@ const Hcaptcha = ({
7071
url,
7172
languageCode,
7273
showLoading,
74+
closableLoading,
7375
loadingIndicatorColor,
7476
backgroundColor,
7577
theme,
@@ -86,6 +88,8 @@ const Hcaptcha = ({
8688
}) => {
8789
const apiUrl = buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation);
8890
const tokenTimeout = 120000;
91+
const loadingTimeout = 15000;
92+
const [isLoading, setIsLoading] = useState(true);
8993

9094
if (theme && typeof theme === 'string') {
9195
theme = `"${theme}"`;
@@ -128,7 +132,7 @@ const Hcaptcha = ({
128132
var onloadCallback = function() {
129133
try {
130134
console.log("challenge onload starting");
131-
hcaptcha.render("submit", getRenderConfig("${siteKey || ''}", ${theme}, "${size || 'invisible'}"));
135+
hcaptcha.render("hcaptcha-container", getRenderConfig("${siteKey || ''}", ${theme}, "${size || 'invisible'}"));
132136
// have loaded by this point; render is sync.
133137
console.log("challenge render complete");
134138
} catch (e) {
@@ -150,6 +154,7 @@ const Hcaptcha = ({
150154
window.ReactNativeWebView.postMessage("cancel");
151155
};
152156
var onOpen = function() {
157+
document.body.style.backgroundColor = '${backgroundColor}';
153158
window.ReactNativeWebView.postMessage("open");
154159
console.log("challenge opened");
155160
};
@@ -185,73 +190,81 @@ const Hcaptcha = ({
185190
};
186191
</script>
187192
</head>
188-
<body style="background-color: ${backgroundColor};">
189-
<div id="submit"></div>
193+
<body>
194+
<div id="hcaptcha-container"></div>
190195
</body>
191196
</html>`,
192197
[siteKey, backgroundColor, theme, debugInfo]
193198
);
194199

200+
useEffect(() => {
201+
const timeoutId = setTimeout(() => {
202+
if (isLoading) {
203+
onMessage({ nativeEvent: { data: 'error', description: 'loading timeout' } });
204+
}
205+
}, loadingTimeout);
206+
207+
return () => clearTimeout(timeoutId);
208+
}, [isLoading, onMessage]);
209+
210+
const webViewRef = useRef(null);
211+
195212
// This shows ActivityIndicator till webview loads hCaptcha images
196-
const renderLoading = useCallback(
197-
() => (
198-
<View style={[styles.loadingOverlay]}>
213+
const renderLoading = () => (
214+
<TouchableWithoutFeedback onPress={() => closableLoading && onMessage({ nativeEvent: { data: 'cancel' } })}>
215+
<View style={styles.loadingOverlay}>
199216
<ActivityIndicator size="large" color={loadingIndicatorColor} />
200217
</View>
201-
),
202-
[loadingIndicatorColor]
218+
</TouchableWithoutFeedback>
203219
);
204220

205-
const webViewRef = useRef(null);
206-
207221
const reset = () => {
208222
if (webViewRef.current) {
209223
webViewRef.current.injectJavaScript('onloadCallback();');
210224
}
211225
};
212226

213227
return (
214-
<WebView
215-
ref={webViewRef}
216-
originWhitelist={['*']}
217-
onShouldStartLoadWithRequest={(event) => {
218-
if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
219-
Linking.openURL(event.url);
220-
return false;
221-
}
222-
return true;
223-
}}
224-
mixedContentMode={'always'}
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-
}}
233-
javaScriptEnabled
234-
injectedJavaScript={patchPostMessageJsCode}
235-
automaticallyAdjustContentInsets
236-
style={[{ backgroundColor: 'transparent', width: '100%' }, style]}
237-
source={{
238-
html: generateTheWebViewContent,
239-
baseUrl: `${url}`,
240-
}}
241-
renderLoading={renderLoading}
242-
startInLoadingState={showLoading}
243-
/>
228+
<View style={{ flex: 1 }}>
229+
<WebView
230+
ref={webViewRef}
231+
originWhitelist={['*']}
232+
onShouldStartLoadWithRequest={(event) => {
233+
if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
234+
Linking.openURL(event.url);
235+
return false;
236+
}
237+
return true;
238+
}}
239+
mixedContentMode={'always'}
240+
onMessage={(e) => {
241+
e.reset = reset;
242+
if (e.nativeEvent.data === 'open') {
243+
setIsLoading(false);
244+
} else if (e.nativeEvent.data.length > 16) {
245+
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, reset }), tokenTimeout);
246+
e.markUsed = () => clearTimeout(expiredTokenTimerId);
247+
}
248+
onMessage(e);
249+
}}
250+
javaScriptEnabled
251+
injectedJavaScript={patchPostMessageJsCode}
252+
automaticallyAdjustContentInsets
253+
style={[{ backgroundColor: 'transparent', width: '100%' }, style]}
254+
source={{
255+
html: generateTheWebViewContent,
256+
baseUrl: `${url}`,
257+
}}
258+
/>
259+
{showLoading && isLoading && renderLoading()}
260+
</View>
244261
);
245262
};
246263

247264
const styles = StyleSheet.create({
248265
loadingOverlay: {
249-
bottom: 0,
266+
...StyleSheet.absoluteFillObject,
250267
justifyContent: 'center',
251-
left: 0,
252-
position: 'absolute',
253-
right: 0,
254-
top: 0,
255268
},
256269
});
257270

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,10 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
127127

128128

129129
### Notes
130+
130131
- The UI defaults to the "invisible" mode of the JS SDK, i.e. no checkbox is displayed.
131-
- You can `import Hcaptcha from '@hcaptcha/react-native-hcaptcha/Hcaptcha';` to customize the UI yourself.
132+
- You can `import Hcaptcha from '@hcaptcha/react-native-hcaptcha/Hcaptcha';` to customize the UI yourself.
133+
- hCaptcha loading is restricted to a 15-second timeout; an `error` will be sent via `onMessage` if it fails to load due to network issues.
132134

133135
## Properties
134136

@@ -139,6 +141,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
139141
| onMessage | Function (see [here](https://github.com/react-native-webview/react-native-webview/blob/master/src/WebViewTypes.ts#L299)) | The callback function that runs after receiving a response, error, or when user cancels. |
140142
| languageCode | string | Default language for hCaptcha; overrides phone defaults. A complete list of supported languages and their codes can be found [here](https://docs.hcaptcha.com/languages/) |
141143
| showLoading | boolean | Whether to show a loading indicator while the hCaptcha web content loads |
144+
| closableLoading | boolean | Allow user to cancel hcaptcha during loading by touch loader overlay |
142145
| loadingIndicatorColor | string | Color of the ActivityIndicator |
143146
| backgroundColor | string | The background color code that will be applied to the main HTML element |
144147
| theme | string\|object | The theme can be 'light', 'dark', 'contrast' or a custom theme object (see Enterprise docs) |
@@ -154,7 +157,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
154157
| style _(inline component only)_ | ViewStyle (see [here](https://reactnative.dev/docs/view-style-props)) | The webview style |
155158
| baseUrl _(modal component only)_ | string | The url domain defined on your hCaptcha. You generally will not need to change this. |
156159
| passiveSiteKey _(modal component only)_ | boolean | Indicates whether the passive mode is enabled; when true, the modal won't be shown at all |
157-
| hasBackdrop _(modal component only)_ | boolean | Defines if the modal backdrop is shown (true by default) |
160+
| hasBackdrop _(modal component only)_ | boolean | Defines if the modal backdrop is shown (true by default). If `hasBackdrop=false`, `backgroundColor` will apply only after the hCaptcha visual challenge is presented. |
158161
| orientation | string | This specifies the "orientation" of the challenge. It can be `portrait`, `landscape`. Default: `portrait` |
159162

160163

0 commit comments

Comments
 (0)