Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
73 changes: 66 additions & 7 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,31 +93,90 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: react-native-hcaptcha
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- if: matrix.platform == 'android'
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: 17
distribution: adopt
- if: contains(matrix.os, 'macos')
- if: matrix.platform == 'ios'
run: sudo xcode-select -s /Applications/Xcode_16.4.app
- run: |
npm run example -- --pm ${{ matrix.pm }}
- run: 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: |
RN_VERSION=$(cat package.json | jq ".dependencies.\"react-native\"" -r)
RN_VERSION=$(jq -r ".dependencies.\"react-native\"" package.json)
echo "value=${RN_VERSION}" >> $GITHUB_OUTPUT
- run: cat package.json
working-directory: react-native-hcaptcha-example
- run: yarn test --config ./jest.config.js
working-directory: react-native-hcaptcha-example
- run: npx react-native build-${{ matrix.platform }}
working-directory: react-native-hcaptcha-example
- run: npx --yes check-peer-dependencies --yarn --runOnlyOnRootDependencies
working-directory: react-native-hcaptcha-example

e2e:
needs: build
permissions:
contents: read
runs-on: ${{ matrix.os }}
concurrency:
group: 'e2e-${{ matrix.platform }}-${{ github.head_ref || github.ref_name }}'
cancel-in-progress: true
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: android
- os: macos-latest
platform: ios
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- if: matrix.platform == 'android'
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: 17
distribution: adopt
- run: npm install
- name: Generate E2E host app
run: npm run test:e2e:setup -- --pm npm --platform ${{ matrix.platform }}

- if: matrix.platform == 'android'
name: Run Android E2E tests
uses: hCaptcha/hcaptcha-android-sdk/.github/actions/android-emulator-run@dc2e0fc978424322c977b68ae06f8dd54e571e22
with:
target: google_apis
api-level: '34'
profile: pixel_7
script: npm run test:e2e:android

- if: matrix.platform == 'ios'
name: Boot iOS simulator
run: |
UDID=$(xcrun simctl list devices available --json | \
jq -r '.devices["com.apple.CoreSimulator.SimRuntime.iOS-18-5"][] | select(.name=="iPhone 16") | .udid')
echo "Booting iPhone 16 (iOS 18.5): $UDID"
xcrun simctl bootstatus "$UDID" -b

- if: matrix.platform == 'ios'
name: Run iOS E2E tests
run: npm run test:e2e:ios

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: e2e-results-${{ matrix.platform }}
path: __e2e__/__image_snapshots__/
retention-days: 14

create-an-issue:
permissions:
contents: read
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
node_modules/*
yarn.lock
.DS_Store

# Reassure performance testing files
.reassure/
output/

# Generated E2E host app
__e2e__/host/
102 changes: 75 additions & 27 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@ const patchPostMessageJsCode = `(${String(function () {
window.ReactNativeWebView.postMessage = patchedPostMessage;
})})();`;

const serializeForInlineScript = (value) =>
JSON.stringify(value)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');

const normalizeTheme = (value) => {
if (value == null) {
return null;
}

if (typeof value === 'object') {
return value;
}

if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch (_) {
return value;
}
}

return value;
};

const normalizeSize = (value) => {
if (value == null) {
return 'invisible';
}

return value === 'checkbox' ? 'normal' : value;
};

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`;

Expand All @@ -43,7 +79,7 @@ const buildHcaptchaApiUrl = (jsSrc, siteKey, hl, theme, host, sentry, endpoint,
*
* @param {*} onMessage: callback after receiving response, error, or when user cancels
* @param {*} siteKey: your hCaptcha sitekey
* @param {string} size: The size of the checkbox, can be 'invisible', 'compact' or 'checkbox', Default: 'invisible'
* @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
* @param {*} url: base url
* @param {*} languageCode: can be found at https://docs.hcaptcha.com/languages
Expand Down Expand Up @@ -90,26 +126,15 @@ const Hcaptcha = ({
phonePrefix,
phoneNumber,
}) => {
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') {
try {
JSON.parse(theme);
} catch (_) {
theme = `"${theme}"`;
}
}

if (theme && typeof theme === 'object') {
theme = `${JSON.stringify(theme)}`;
}

if (rqdata && typeof rqdata === 'string') {
rqdata = `"${rqdata}"`;
}
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 debugInfo = useMemo(
() => {
Expand All @@ -128,6 +153,21 @@ const Hcaptcha = ({
[debug]
);

const serializedWebViewConfig = useMemo(
() => serializeForInlineScript({
apiUrl,
backgroundColor: backgroundColor ?? '',
debugInfo,
phoneNumber: phoneNumber ?? null,
phonePrefix: phonePrefix ?? null,
rqdata: rqdata ?? null,
siteKey: siteKey || '',
size: normalizedSize,
theme: normalizedTheme,
}),
[apiUrl, backgroundColor, debugInfo, normalizedSize, normalizedTheme, phoneNumber, phonePrefix, rqdata, siteKey]
);

const generateTheWebViewContent = useMemo(
() =>
`<!DOCTYPE html>
Expand All @@ -137,14 +177,21 @@ const Hcaptcha = ({
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script type="text/javascript">
Object.entries(${JSON.stringify(debugInfo)}).forEach(function (entry) { window[entry[0]] = entry[1] })
var hcaptchaConfig = ${serializedWebViewConfig};
Object.entries(hcaptchaConfig.debugInfo || {}).forEach(function (entry) { window[entry[0]] = entry[1] });
</script>
<script src="${apiUrl}" async defer></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);
};
var onloadCallback = function() {
try {
console.log("challenge onload starting");
hcaptcha.render("hcaptcha-container", getRenderConfig("${siteKey || ''}", ${theme}, "${size || 'invisible'}"));
hcaptcha.render("hcaptcha-container", getRenderConfig(hcaptchaConfig.siteKey, hcaptchaConfig.theme, hcaptchaConfig.size));
// have loaded by this point; render is sync.
console.log("challenge render complete");
} catch (e) {
Expand All @@ -166,7 +213,7 @@ const Hcaptcha = ({
window.ReactNativeWebView.postMessage("challenge-closed");
};
var onOpen = function() {
document.body.style.backgroundColor = '${backgroundColor}';
document.body.style.backgroundColor = hcaptchaConfig.backgroundColor;
window.ReactNativeWebView.postMessage("open");
console.log("challenge opened");
};
Expand Down Expand Up @@ -194,9 +241,9 @@ const Hcaptcha = ({
};
const getExecuteOpts = function() {
var opts = {};
const rqdata = ${rqdata};
const phonePrefix = ${phonePrefix || 'null'};
const phoneNumber = ${phoneNumber || 'null'};
const rqdata = hcaptchaConfig.rqdata;
const phonePrefix = hcaptchaConfig.phonePrefix;
const phoneNumber = hcaptchaConfig.phoneNumber;

if (rqdata) {
opts.rqdata = rqdata;
Expand All @@ -209,13 +256,14 @@ const Hcaptcha = ({
}
return opts;
};
loadApiScript();
</script>
</head>
<body>
<div id="hcaptcha-container"></div>
</body>
</html>`,
[debugInfo, apiUrl, siteKey, theme, size, backgroundColor, rqdata, phonePrefix, phoneNumber]
[serializedWebViewConfig]
);

useEffect(() => {
Expand Down Expand Up @@ -261,7 +309,7 @@ const Hcaptcha = ({
data: 'sms-open-failed',
description: err.message,
},
success: false
success: false,
});
});
return false;
Expand Down
31 changes: 29 additions & 2 deletions MAINTAINER.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,33 @@ PATCH: bugfix only.

- bump [`package.json's`](./package.json) version
- run `npm i` to update `package-lock.json`
- update [`MAINTAINER.md`](./MAINTAINER.md) if release or verification steps changed
- verify:
- `npm test`
- `npm run lint`
- `CI=1 npm run test:e2e` to confirm CI skips local-only device E2E cleanly
- `npm run test:e2e:android-local` if you want the full Android emulator verification locally
- commit `package.json` and `package-lock.json`
- commit any maintainer or release-documentation updates in the same PR
- open the PR for review
- once the PR is approved and merged to master:
- set the tag on master matching your version: git tag `vM.M.P`
- set the tag on master matching your version: `git tag vM.m.p`
- push the tag: `git push origin vM.m.p`
- draft a new release https://github.com/hCaptcha/react-native-hcaptcha/releases
- summarize functional changes in the release notes, including:
- safer WebView config handling for HTML-facing props
- `ConfirmHcaptcha` forwarding of `phonePrefix` and `phoneNumber`
- `checkbox` size alias normalization to `normal`
- expanded unit coverage and local Android theme E2E coverage
- once the release is created, CI will release the new version to https://www.npmjs.com/package/@hcaptcha/react-native-hcaptcha?activeTab=versions

3. Post-release verification:

- confirm the GitHub release published successfully
- confirm npm lists the new version:
- `npm view @hcaptcha/react-native-hcaptcha version`
- smoke-test install in a disposable app if the release changed packaging or generated assets

### Generate test app

For `expo` test app
Expand All @@ -33,6 +53,13 @@ For `react-native` test app
- `yarn example`
- `yarn android` or `npm run android`

For the local Android emulator regression E2E added in this repo:

- `cd react-native-hcaptcha`
- ensure Android SDK, emulator, and an AVD are installed
- run `npm run test:e2e:android-local`
- inspect artifacts in [`output/android-e2e`](./output/android-e2e) if the run fails

For iOS instead the last step do:

- `pushd ios; env USE_HERMES=0 pod install; popd`
Expand Down Expand Up @@ -155,4 +182,4 @@ Usage Error: The file:../react-native-hcaptcha string didn't match the required

Solution: `yarn add @hcaptcha/react-native-hcaptcha@file:../react-native-hcaptcha`

Yarn 2.10.x and above doesn't require `file:` scheme prefix https://stackoverflow.com/questions/40102686/how-to-install-package-with-local-path-by-yarn-it-couldnt-find-package
Yarn 2.10.x and above doesn't require `file:` scheme prefix https://stackoverflow.com/questions/40102686/how-to-install-package-with-local-path-by-yarn-it-couldnt-find-package
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ The SDK supports phone prefix and phone number parameters for MFA (Multi-Factor
| **Name** | **Type** | **Description** |
|:---|:---|:---|
| siteKey _(required)_ | string | The hCaptcha siteKey |
| size | string | The size of the checkbox, can be 'invisible', 'compact' or 'checkbox', Default: 'invisible' |
| size | string | The size of the widget, can be 'invisible', 'compact' or 'normal'. `checkbox` is also accepted as a legacy alias for `normal`. Default: 'invisible' |
| 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 |
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading