Skip to content

Commit

Permalink
Merge pull request #70 from hCaptcha/feature/improve-slow-network-exp…
Browse files Browse the repository at this point in the history
…erience

feat: improve UX when user is on a slow network
  • Loading branch information
CAMOBAP authored Nov 9, 2024
2 parents dfcfd77 + 2d9339c commit e329552
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 216 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
# npm install -g ${{ matrix.pm }} react-native
npm run example -- --pm ${{ matrix.pm }}
working-directory: react-native-hcaptcha
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
- id: rn-version
working-directory: react-native-hcaptcha-example
run: |
Expand Down
4 changes: 4 additions & 0 deletions Hcaptcha.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ type HcaptchaProps = {
* Whether to show a loading indicator while the hCaptcha web content loads
*/
showLoading?: boolean;
/**
* Allow user to cancel hcaptcha during loading by touch loader overlay
*/
closableLoading?: boolean;
/**
* Color of the ActivityIndicator
*/
Expand Down
107 changes: 60 additions & 47 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useMemo, useCallback, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import WebView from 'react-native-webview';
import { Linking, StyleSheet, View, ActivityIndicator } from 'react-native';
import { ActivityIndicator, Linking, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion';

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

if (theme && typeof theme === 'string') {
theme = `"${theme}"`;
Expand Down Expand Up @@ -128,7 +132,7 @@ const Hcaptcha = ({
var onloadCallback = function() {
try {
console.log("challenge onload starting");
hcaptcha.render("submit", getRenderConfig("${siteKey || ''}", ${theme}, "${size || 'invisible'}"));
hcaptcha.render("hcaptcha-container", getRenderConfig("${siteKey || ''}", ${theme}, "${size || 'invisible'}"));
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
Expand All @@ -150,6 +154,7 @@ const Hcaptcha = ({
window.ReactNativeWebView.postMessage("cancel");
};
var onOpen = function() {
document.body.style.backgroundColor = '${backgroundColor}';
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
Expand Down Expand Up @@ -185,73 +190,81 @@ const Hcaptcha = ({
};
</script>
</head>
<body style="background-color: ${backgroundColor};">
<div id="submit"></div>
<body>
<div id="hcaptcha-container"></div>
</body>
</html>`,
[siteKey, backgroundColor, theme, debugInfo]
);

useEffect(() => {
const timeoutId = setTimeout(() => {
if (isLoading) {
onMessage({ nativeEvent: { data: 'error', description: 'loading timeout' } });
}
}, loadingTimeout);

return () => clearTimeout(timeoutId);
}, [isLoading, onMessage]);

const webViewRef = useRef(null);

// This shows ActivityIndicator till webview loads hCaptcha images
const renderLoading = useCallback(
() => (
<View style={[styles.loadingOverlay]}>
const renderLoading = () => (
<TouchableWithoutFeedback onPress={() => closableLoading && onMessage({ nativeEvent: { data: 'cancel' } })}>
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={loadingIndicatorColor} />
</View>
),
[loadingIndicatorColor]
</TouchableWithoutFeedback>
);

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') {
Linking.openURL(event.url);
return false;
}
return true;
}}
mixedContentMode={'always'}
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
style={[{ backgroundColor: 'transparent', width: '100%' }, style]}
source={{
html: generateTheWebViewContent,
baseUrl: `${url}`,
}}
renderLoading={renderLoading}
startInLoadingState={showLoading}
/>
<View style={{ flex: 1 }}>
<WebView
ref={webViewRef}
originWhitelist={['*']}
onShouldStartLoadWithRequest={(event) => {
if (event.url.slice(0, 24) === 'https://www.hcaptcha.com') {
Linking.openURL(event.url);
return false;
}
return true;
}}
mixedContentMode={'always'}
onMessage={(e) => {
e.reset = reset;
if (e.nativeEvent.data === 'open') {
setIsLoading(false);
} else 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
style={[{ backgroundColor: 'transparent', width: '100%' }, style]}
source={{
html: generateTheWebViewContent,
baseUrl: `${url}`,
}}
/>
{showLoading && isLoading && renderLoading()}
</View>
);
};

const styles = StyleSheet.create({
loadingOverlay: {
bottom: 0,
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,10 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge


### Notes

- The UI defaults to the "invisible" mode of the JS SDK, i.e. no checkbox is displayed.
- You can `import Hcaptcha from '@hcaptcha/react-native-hcaptcha/Hcaptcha';` to customize the UI yourself.
- You can `import Hcaptcha from '@hcaptcha/react-native-hcaptcha/Hcaptcha';` to customize the UI yourself.
- 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.

## Properties

Expand All @@ -139,6 +141,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
| 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. |
| 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/) |
| showLoading | boolean | Whether to show a loading indicator while the hCaptcha web content loads |
| closableLoading | boolean | Allow user to cancel hcaptcha during loading by touch loader overlay |
| loadingIndicatorColor | string | Color of the ActivityIndicator |
| backgroundColor | string | The background color code that will be applied to the main HTML element |
| theme | string\|object | The theme can be 'light', 'dark', 'contrast' or a custom theme object (see Enterprise docs) |
Expand All @@ -154,7 +157,7 @@ Otherwise, you should pass in the preferred device locale, e.g. fetched from `ge
| style _(inline component only)_ | ViewStyle (see [here](https://reactnative.dev/docs/view-style-props)) | The webview style |
| 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) |
| 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. |
| orientation | string | This specifies the "orientation" of the challenge. It can be `portrait`, `landscape`. Default: `portrait` |


Expand Down
Loading

0 comments on commit e329552

Please sign in to comment.