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

Merged
merged 5 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Loading
Loading