Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4ecea20
feat: load api.js through hCaptcha Loader
damian-rodriguez-imi Jul 10, 2026
21d9fe0
feat: add React Native loader diagnostics
damian-rodriguez-imi Jul 13, 2026
650acc9
chore: minimize loader diagnostics changes
damian-rodriguez-imi Jul 13, 2026
2e4f36a
chore: remove loader lifecycle bridge
damian-rodriguez-imi Jul 13, 2026
2284c55
fix: restore inline WebView handling
damian-rodriguez-imi Jul 13, 2026
5ee4969
chore: remove native loader timeout reporting
damian-rodriguez-imi Jul 13, 2026
679c216
feat: bundle hCaptcha Loader with the SDK
damian-rodriguez-imi Jul 13, 2026
6db9c6c
chore: remove loader test changes
damian-rodriguez-imi Jul 13, 2026
80a3468
chore: update hCaptcha Loader dependency
damian-rodriguez-imi Jul 14, 2026
7dbc6ea
feat: use structured hCaptcha loader config
damian-rodriguez-imi Jul 14, 2026
07733a3
refactor: finalize loader integration
damian-rodriguez-imi Jul 14, 2026
04dd7b2
refactor: preserve loading behavior
damian-rodriguez-imi Jul 14, 2026
39a2a0b
fix: use published hcaptcha loader
damian-rodriguez-imi Jul 15, 2026
edccef2
ci: pin Xcode runner image
damian-rodriguez-imi Jul 15, 2026
ce4ff65
ci: pin iOS E2E runner image
damian-rodriguez-imi Jul 15, 2026
8484367
fix: support React Native 0.86 Jest preset
damian-rodriguez-imi Jul 15, 2026
ee50790
fix: remove redundant Babel preset
damian-rodriguez-imi Jul 15, 2026
9827987
fix: update hcaptcha loader to 2.4.1
damian-rodriguez-imi Jul 15, 2026
85c942b
fix: preserve loader configuration parity
damian-rodriguez-imi Jul 15, 2026
30f26bd
chore: bump version to 4.1.0
damian-rodriguez-imi Jul 15, 2026
7ee98dc
chore: remove unrelated whitespace change
damian-rodriguez-imi Jul 15, 2026
5eb943e
chore: addressed feedback
damian-rodriguez-imi Jul 16, 2026
71e25fd
fix: update hcaptcha loader to 2.4.2
damian-rodriguez-imi Jul 16, 2026
a080c13
docs: document api loading retries
damian-rodriguez-imi Jul 17, 2026
e0eff69
fix: passive timeout
damian-rodriguez-imi Jul 17, 2026
81cbb73
fix: retry api loading after script error
damian-rodriguez-imi Jul 17, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ jobs:
platform: [ android ]
pm: [ npm, yarn ]
include:
- os: macos-latest
- os: macos-15
platform: ios
pm: npm
- os: macos-latest
- os: macos-15
platform: ios
pm: yarn
steps:
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
include:
- os: ubuntu-latest
platform: android
- os: macos-latest
- os: macos-15
platform: ios
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down
2 changes: 1 addition & 1 deletion Example.jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
preset: 'react-native',
preset: '@react-native/jest-preset',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testPathIgnorePatterns: ['/node_modules/'],
modulePathIgnorePatterns: ['<rootDir>/react-native-hcaptcha/'],
Expand Down
91 changes: 65 additions & 26 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -135,25 +136,42 @@ 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;
};

function buildHcaptchaLoaderConfig({
scriptSource,
siteKey,
hl,
theme,
host,
sentry,
endpoint,
assethost,
imghost,
reportapi,
}) {
return {
scriptSource: scriptSource || 'https://hcaptcha.com/1/api.js',
render: 'explicit',
host: getHcaptchaHost(host, siteKey),
hl,
custom: typeof theme === 'object',
sentry,
endpoint,
assethost,
imghost,
reportapi,
};
}

/**
*
* @param {*} onMessage: callback after receiving response, error, or when user cancels
Expand Down Expand Up @@ -217,9 +235,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(
Expand All @@ -229,17 +258,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(
Expand All @@ -254,13 +284,19 @@ const Hcaptcha = ({
var hcaptchaConfig = ${serializedWebViewConfig};
Object.entries(hcaptchaConfig.debugInfo || {}).forEach(function (entry) { window[entry[0]] = entry[1] });
</script>
<script type="text/javascript">
${hCaptchaLoaderInlineScript}
</script>
<script type="text/javascript">
var loadApiScript = function() {
var script = document.createElement('script');
script.async = true;
script.defer = true;
script.src = hcaptchaConfig.apiUrl;
document.head.appendChild(script);
if (typeof window.hCaptchaLoader !== 'function') {
window.ReactNativeWebView.postMessage('error');
return;
}

window.hCaptchaLoader(hcaptchaConfig.loaderConfig).then(onloadCallback).catch(function(error) {
window.ReactNativeWebView.postMessage((error && error.message) || (error && error.name) || 'error');
});
};
var hcaptchaWidgetId = null;
var setData = function(data) {
Expand All @@ -275,7 +311,7 @@ const Hcaptcha = ({
var onloadCallback = function() {
try {
console.log("challenge onload starting");
hcaptchaWidgetId = hcaptcha.render("hcaptcha-container", getRenderConfig(hcaptchaConfig.siteKey, hcaptchaConfig.theme, hcaptchaConfig.size));
hcaptchaWidgetId = hcaptcha.render("hcaptcha-container", getRenderConfig(hcaptchaConfig.siteKey, hcaptchaConfig.theme, hcaptchaConfig.size, hcaptchaConfig.orientation));
window.ReactNativeWebView.postMessage("${HCAPTCHA_READY_EVENT}");
// have loaded by this point; render is sync.
console.log("challenge render complete");
Expand All @@ -301,7 +337,7 @@ const Hcaptcha = ({
console.warn("challenge error callback fired");
window.ReactNativeWebView.postMessage(error);
};
const getRenderConfig = function(siteKey, theme, size) {
const getRenderConfig = function(siteKey, theme, size, orientation) {
var config = {
sitekey: siteKey,
size: size,
Expand All @@ -315,6 +351,9 @@ const Hcaptcha = ({
if (theme) {
config.theme = theme;
}
if (orientation) {
config.orientation = orientation;
}
return config;
};
loadApiScript();
Expand Down
3 changes: 3 additions & 0 deletions __mocks__/global.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
jest.mock('react-native-webview');
jest.mock('@hcaptcha/loader/inline', () => (
'window.hCaptchaLoader = function(config) { window.hCaptchaLoaderConfig = config; return Promise.resolve(window.hcaptcha); };'
));
jest.mock('react', () => {
let ActualReact = jest.requireActual('react');
return {
Expand Down
6 changes: 4 additions & 2 deletions __scripts__/generate-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat
const mainPackage = '@hcaptcha/react-native-hcaptcha';
const libRoot = process.cwd();
const libPathFromProject = path.relative(projectPath, libRoot);
const projectPackage = JSON.parse(fs.readFileSync(path.join(projectPath, 'package.json'), 'utf8'));
const reactNativeVersion = projectPackage.dependencies['react-native'];
const peerPackages = 'react-native-webview';
const devPackages = 'typescript @babel/preset-env';
const devPackages = `typescript @react-native/jest-preset@${reactNativeVersion}`;

console.warn('Installing dependencies...');
if (packageManager === 'yarn') {
Expand All @@ -161,7 +163,7 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat
const excludes = ['__e2e__/host', '__tests__', '__mocks__', 'node_modules', '.git', 'output', '.reassure'].map(e => `--exclude=${e}`).join(' ');
execSync(`rsync -a ${excludes} ${libRoot}/ ${destLibDir}/`, { stdio: 'inherit' });
execSync('npm i --save file:./react-native-hcaptcha', packageManagerOptions);
execSync(`npm i --save --dev ${devPackages}`, packageManagerOptions);
execSync(`npm i --save-dev ${devPackages}`, packageManagerOptions);
execSync(`npm i --save ${peerPackages}`, packageManagerOptions);
}

Expand Down
Loading
Loading