diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e6e50ea..8716634 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -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: |
diff --git a/Hcaptcha.d.ts b/Hcaptcha.d.ts
index e4bc0d5..0ba1c9c 100644
--- a/Hcaptcha.d.ts
+++ b/Hcaptcha.d.ts
@@ -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
*/
diff --git a/Hcaptcha.js b/Hcaptcha.js
index 6b8463e..4b19ea5 100644
--- a/Hcaptcha.js
+++ b/Hcaptcha.js
@@ -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';
@@ -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
@@ -70,6 +71,7 @@ const Hcaptcha = ({
url,
languageCode,
showLoading,
+ closableLoading,
loadingIndicatorColor,
backgroundColor,
theme,
@@ -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}"`;
@@ -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) {
@@ -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");
};
@@ -185,25 +190,34 @@ const Hcaptcha = ({
};
-
-
+
+
`,
[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(
- () => (
-
+ const renderLoading = () => (
+ closableLoading && onMessage({ nativeEvent: { data: 'cancel' } })}>
+
- ),
- [loadingIndicatorColor]
+
);
- const webViewRef = useRef(null);
-
const reset = () => {
if (webViewRef.current) {
webViewRef.current.injectJavaScript('onloadCallback();');
@@ -211,47 +225,46 @@ const Hcaptcha = ({
};
return (
- {
- 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}
- />
+
+ {
+ 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()}
+
);
};
const styles = StyleSheet.create({
loadingOverlay: {
- bottom: 0,
+ ...StyleSheet.absoluteFillObject,
justifyContent: 'center',
- left: 0,
- position: 'absolute',
- right: 0,
- top: 0,
},
});
diff --git a/README.md b/README.md
index f0c30ec..d6f3b7a 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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) |
@@ -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` |
diff --git a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
index ea8b473..cec807a 100644
--- a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
+++ b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
@@ -39,9 +39,16 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with all props 1
]
}
>
-
+
+ javaScriptEnabled={true}
+ mixedContentMode="always"
+ onMessage={[Function]}
+ onShouldStartLoadWithRequest={[Function]}
+ originWhitelist={
+ [
+ "*",
+ ]
+ }
+ source={
+ {
+ "baseUrl": "https://hcaptcha.com",
+ "html": "
-
+
-
-
+
+
",
+ }
}
- }
- startInLoadingState={false}
- style={
- [
- {
- "backgroundColor": "transparent",
- "width": "100%",
- },
- undefined,
- ]
- }
- />
+ style={
+ [
+ {
+ "backgroundColor": "transparent",
+ "width": "100%",
+ },
+ undefined,
+ ]
+ }
+ />
+
`;
@@ -195,9 +202,16 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
]
}
>
-
+
+ javaScriptEnabled={true}
+ mixedContentMode="always"
+ onMessage={[Function]}
+ onShouldStartLoadWithRequest={[Function]}
+ originWhitelist={
+ [
+ "*",
+ ]
+ }
+ source={
+ {
+ "baseUrl": "https://hcaptcha.com",
+ "html": "
@@ -229,12 +242,12 @@ exports[`ConfirmHcaptcha snapshot tests renders ConfirmHcaptcha with minimum pro
-
+
-
-
+
+
",
+ }
}
- }
- startInLoadingState={false}
- style={
- [
- {
- "backgroundColor": "transparent",
- "width": "100%",
- },
- undefined,
- ]
- }
- />
+ style={
+ [
+ {
+ "backgroundColor": "transparent",
+ "width": "100%",
+ },
+ undefined,
+ ]
+ }
+ />
+
`;
diff --git a/__tests__/__snapshots__/Hcaptcha.test.js.snap b/__tests__/__snapshots__/Hcaptcha.test.js.snap
index 09e3405..4e81b7b 100644
--- a/__tests__/__snapshots__/Hcaptcha.test.js.snap
+++ b/__tests__/__snapshots__/Hcaptcha.test.js.snap
@@ -1,9 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Hcaptcha snapshot tests renders Hcaptcha with all props 1`] = `
-
+
+ javaScriptEnabled={true}
+ mixedContentMode="always"
+ onMessage={[Function]}
+ onShouldStartLoadWithRequest={[Function]}
+ originWhitelist={
+ [
+ "*",
+ ]
+ }
+ source={
+ {
+ "baseUrl": "https://hcaptcha.com",
+ "html": "
-
-
+
+
",
+ }
}
- }
- startInLoadingState={true}
- style={
- [
+ style={
+ [
+ {
+ "backgroundColor": "transparent",
+ "width": "100%",
+ },
+ undefined,
+ ]
+ }
+ />
+
+ "busy": undefined,
+ "checked": undefined,
+ "disabled": undefined,
+ "expanded": undefined,
+ "selected": undefined,
+ }
+ }
+ accessible={true}
+ focusable={true}
+ onClick={[Function]}
+ onResponderGrant={[Function]}
+ onResponderMove={[Function]}
+ onResponderRelease={[Function]}
+ onResponderTerminate={[Function]}
+ onResponderTerminationRequest={[Function]}
+ onStartShouldSetResponder={[Function]}
+ style={
+ {
+ "bottom": 0,
+ "justifyContent": "center",
+ "left": 0,
+ "position": "absolute",
+ "right": 0,
+ "top": 0,
+ }
+ }
+ >
+
+
+
`;
exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
-
+
+ javaScriptEnabled={true}
+ mixedContentMode="always"
+ onMessage={[Function]}
+ onShouldStartLoadWithRequest={[Function]}
+ originWhitelist={
+ [
+ "*",
+ ]
+ }
+ source={
+ {
+ "baseUrl": "https://hcaptcha.com",
+ "html": "
@@ -156,7 +204,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
var onloadCallback = function() {
try {
console.log("challenge onload starting");
- hcaptcha.render("submit", getRenderConfig("", undefined, "invisible"));
+ hcaptcha.render("hcaptcha-container", getRenderConfig("", undefined, "invisible"));
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
@@ -178,6 +226,7 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
window.ReactNativeWebView.postMessage("cancel");
};
var onOpen = function() {
+ document.body.style.backgroundColor = 'undefined';
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
@@ -213,28 +262,36 @@ exports[`Hcaptcha snapshot tests renders Hcaptcha with minimum props 1`] = `
};
-
-
+
+
",
+ }
}
- }
- style={
- [
- {
- "backgroundColor": "transparent",
- "width": "100%",
- },
- undefined,
- ]
- }
-/>
+ style={
+ [
+ {
+ "backgroundColor": "transparent",
+ "width": "100%",
+ },
+ undefined,
+ ]
+ }
+ />
+
`;
exports[`Hcaptcha snapshot tests test debug 1`] = `
-
+
+ javaScriptEnabled={true}
+ mixedContentMode="always"
+ onMessage={[Function]}
+ onShouldStartLoadWithRequest={[Function]}
+ originWhitelist={
+ [
+ "*",
+ ]
+ }
+ source={
+ {
+ "baseUrl": "https://hcaptcha.com",
+ "html": "
-
-
+
+
",
+ }
}
- }
- style={
- [
- {
- "backgroundColor": "transparent",
- "width": "100%",
- },
- undefined,
- ]
- }
-/>
+ style={
+ [
+ {
+ "backgroundColor": "transparent",
+ "width": "100%",
+ },
+ undefined,
+ ]
+ }
+ />
+
`;
diff --git a/index.js b/index.js
index 0298b47..0759e6b 100644
--- a/index.js
+++ b/index.js
@@ -31,6 +31,7 @@ class ConfirmHcaptcha extends PureComponent {
orientation,
onMessage,
showLoading,
+ closableLoading,
backgroundColor,
loadingIndicatorColor,
theme,
@@ -61,7 +62,7 @@ class ConfirmHcaptcha extends PureComponent {
hasBackdrop={!passiveSiteKey && hasBackdrop}
coverScreen={!passiveSiteKey}
>
-
+
@@ -114,6 +117,7 @@ ConfirmHcaptcha.propTypes = {
orientation: PropTypes.string,
backgroundColor: PropTypes.string,
showLoading: PropTypes.bool,
+ closableLoading: PropTypes.bool,
loadingIndicatorColor: PropTypes.string,
theme: PropTypes.string,
rqdata: PropTypes.string,
@@ -132,6 +136,7 @@ ConfirmHcaptcha.defaultProps = {
size: 'invisible',
passiveSiteKey: false,
showLoading: false,
+ closableLoading: false,
orientation: 'portrait',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
loadingIndicatorColor: null,
diff --git a/package-lock.json b/package-lock.json
index dac0e22..7321aae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@hcaptcha/react-native-hcaptcha",
- "version": "1.7.2",
+ "version": "1.8.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@hcaptcha/react-native-hcaptcha",
- "version": "1.7.2",
+ "version": "1.8.2",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.15.5",
diff --git a/package.json b/package.json
index b6e59d4..1b7f01d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@hcaptcha/react-native-hcaptcha",
- "version": "1.7.2",
+ "version": "1.8.2",
"description": "hCaptcha Library for React Native (both Android and iOS)",
"main": "index.js",
"scripts": {
@@ -61,4 +61,4 @@
"babel-jest": "^27.2.2",
"metro-react-native-babel-preset": "^0.77.0"
}
-}
\ No newline at end of file
+}