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 +}