diff --git a/Hcaptcha.d.ts b/Hcaptcha.d.ts index 8268b70..7a1d8ec 100644 --- a/Hcaptcha.d.ts +++ b/Hcaptcha.d.ts @@ -13,6 +13,14 @@ export type HcaptchaProps = { * The callback function that runs after receiving a response, error, or when user cancels. */ onMessage: (event: CustomWebViewMessageEvent) => void; + /** + * Runs after the hCaptcha API loads. + */ + onLoad?: () => void; + /** + * Runs when hCaptcha is ready to execute. + */ + onReady?: () => void; /** * The size of the checkbox. */ diff --git a/Hcaptcha.js b/Hcaptcha.js index f4cb9d3..dca6269 100644 --- a/Hcaptcha.js +++ b/Hcaptcha.js @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import hCaptchaLoaderInlineScript from '@hcaptcha/loader/inline'; import WebView from 'react-native-webview'; import { ActivityIndicator, Linking, Platform, StyleSheet, TouchableWithoutFeedback, View } from 'react-native'; import ReactNativeVersion from 'react-native/Libraries/Core/ReactNativeVersion'; @@ -26,6 +27,7 @@ const patchPostMessageJsCode = `(${String(function () { window.ReactNativeWebView.postMessage = patchedPostMessage; })})();`; +const HCAPTCHA_LOAD_EVENT = '__hcaptcha_load__'; const HCAPTCHA_READY_EVENT = '__hcaptcha_ready__'; const serializeForInlineScript = (value) => @@ -135,28 +137,45 @@ const buildVerifyData = ({ const buildVerifyInjectionScript = (payload, resetFirst = false) => `try { ${resetFirst ? 'reset(); ' : ''}setData(${serializeForInlineScript(payload)}); execute(); } catch (e) { window.ReactNativeWebView.postMessage((e && e.name) || 'error'); } true;`; -const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, assethost, imghost, reportapi, orientation) => { - var url = `${jsSrc || 'https://hcaptcha.com/1/api.js'}?render=explicit&onload=onloadCallback`; - - let effectiveHost; +const getHcaptchaHost = (host, siteKey) => { if (host) { - effectiveHost = encodeURIComponent(host); + return host; + } else if (siteKey) { + return `${siteKey}.react-native.hcaptcha.com`; } else { - effectiveHost = (siteKey || 'missing-sitekey') + '.react-native.hcaptcha.com'; - } - - for (let [key, value] of Object.entries({ host: effectiveHost, hl, custom: typeof theme === 'object', sentry, endpoint, assethost, imghost, reportapi, orientation })) { - if (value) { - url += `&${key}=${encodeURIComponent(value)}`; - } + return 'missing-sitekey.react-native.hcaptcha.com'; } - - return url; }; +const buildHcaptchaLoaderConfig = ({ + scriptSource, + siteKey, + hl, + theme, + host, + sentry, + endpoint, + assethost, + imghost, + reportapi, +}) => ({ + scriptSource, + render: 'explicit', + host: getHcaptchaHost(host, siteKey), + hl, + custom: typeof theme === 'object', + sentry: Boolean(sentry), + endpoint, + assethost, + imghost, + reportapi, +}); + /** * * @param {*} onMessage: callback after receiving response, error, or when user cancels + * @param {function} onLoad: callback after the hCaptcha API loads + * @param {function} onReady: callback when hCaptcha is ready to execute * @param {*} siteKey: your hCaptcha sitekey * @param {string} size: The size of the widget, can be 'invisible', 'compact' or 'normal'. 'checkbox' is kept as a legacy alias for 'normal'. Default: 'invisible' * @param {*} style: custom style @@ -184,6 +203,8 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint, */ const Hcaptcha = ({ onMessage, + onLoad, + onReady, size, siteKey, style, @@ -217,9 +238,20 @@ const Hcaptcha = ({ const hasJourneyConsumerRef = useRef(false); const normalizedTheme = useMemo(() => normalizeTheme(theme), [theme]); const normalizedSize = useMemo(() => normalizeSize(size), [size]); - const apiUrl = useMemo( - () => buildHcaptchaApiUrl(jsSrc, siteKey, languageCode, normalizedTheme, host, sentry, endpoint, assethost, imghost, reportapi, orientation), - [jsSrc, siteKey, languageCode, normalizedTheme, host, sentry, endpoint, assethost, imghost, reportapi, orientation] + const loaderConfig = useMemo( + () => buildHcaptchaLoaderConfig({ + scriptSource: jsSrc, + siteKey, + hl: languageCode, + theme: normalizedTheme, + host, + sentry, + endpoint, + assethost, + imghost, + reportapi, + }), + [jsSrc, siteKey, languageCode, normalizedTheme, host, sentry, endpoint, assethost, imghost, reportapi] ); const debugInfo = useMemo( @@ -229,17 +261,18 @@ const Hcaptcha = ({ const serializedWebViewConfig = useMemo( () => serializeForInlineScript({ - apiUrl, + loaderConfig, backgroundColor: backgroundColor ?? '', debugInfo, phoneNumber: phoneNumber ?? null, phonePrefix: phonePrefix ?? null, + orientation: orientation ?? null, rqdata: rqdata ?? null, siteKey: siteKey || '', size: normalizedSize, theme: normalizedTheme, }), - [apiUrl, backgroundColor, debugInfo, normalizedSize, normalizedTheme, phoneNumber, phonePrefix, rqdata, siteKey] + [loaderConfig, backgroundColor, debugInfo, normalizedSize, normalizedTheme, orientation, phoneNumber, phonePrefix, rqdata, siteKey] ); const generateTheWebViewContent = useMemo( @@ -254,13 +287,19 @@ const Hcaptcha = ({ var hcaptchaConfig = ${serializedWebViewConfig}; Object.entries(hcaptchaConfig.debugInfo || {}).forEach(function (entry) { window[entry[0]] = entry[1] }); +