Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve UX when user is on a slow network #70

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading