diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 2c90e8f..06fb474 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -14,10 +14,10 @@ jobs:
actions: write
runs-on: ubuntu-latest
steps:
- - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- run: npm install
- run: npm test
- run: npm run lint
@@ -26,10 +26,10 @@ jobs:
runs-on: ubuntu-latest
needs: build
steps:
- - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: npm release
run: |
npm ci
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index d1caf41..4c6296f 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -17,12 +17,12 @@ jobs:
actions: write
runs-on: ubuntu-latest
steps:
- - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- run: npm install
- - run: npm test
+ - run: npm run test
- run: npm run lint
- if: github.ref == 'refs/heads/master' && github.event_name == 'push'
run: npm run perf:baseline
@@ -40,9 +40,9 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- run: npm install
- - uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
+ - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-reassure
with:
path: .reassure
@@ -91,17 +91,17 @@ jobs:
platform: ios
pm: yarn
steps:
- - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
path: react-native-hcaptcha
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
- if: matrix.platform == 'android'
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: adopt
@@ -140,12 +140,12 @@ jobs:
- os: macos-latest
platform: ios
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
- if: matrix.platform == 'android'
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
java-version: 17
distribution: adopt
@@ -174,7 +174,7 @@ jobs:
name: Run iOS E2E tests
run: npm run test:e2e:ios
- - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-results-${{ matrix.platform }}
@@ -189,7 +189,7 @@ jobs:
needs: test
if: always() && github.event_name == 'schedule' && needs.test.result == 'failure'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- run: |
RN_VERSION="${{ needs.test.outputs.rn-version }}"
GHA_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
diff --git a/.mise.toml b/.mise.toml
new file mode 100644
index 0000000..022e974
--- /dev/null
+++ b/.mise.toml
@@ -0,0 +1,6 @@
+[env]
+_.path = "{{ cwd }}/bin"
+
+[tools]
+node = "24.16.0"
+yarn = "1.22.22"
diff --git a/Hcaptcha.js b/Hcaptcha.js
index f4cb9d3..dd03d98 100644
--- a/Hcaptcha.js
+++ b/Hcaptcha.js
@@ -213,6 +213,7 @@ const Hcaptcha = ({
const tokenTimeout = 120000;
const loadingTimeout = 15000;
const [isLoading, setIsLoading] = useState(true);
+ const isLoadingRef = useRef(true);
const journeyEnabled = Boolean(userJourney);
const hasJourneyConsumerRef = useRef(false);
const normalizedTheme = useMemo(() => normalizeTheme(theme), [theme]);
@@ -345,13 +346,13 @@ const Hcaptcha = ({
useEffect(() => {
const timeoutId = setTimeout(() => {
- if (isLoading) {
+ if (isLoadingRef.current) {
onMessage({ nativeEvent: { data: 'error', description: 'loading timeout' } });
}
}, loadingTimeout);
return () => clearTimeout(timeoutId);
- }, [isLoading, onMessage]);
+ }, [onMessage]);
const webViewRef = useRef(null);
const injectVerifyData = (resetFirst = false) => {
@@ -407,6 +408,9 @@ const Hcaptcha = ({
}}
mixedContentMode={'always'}
onMessage={(e) => {
+ isLoadingRef.current = false;
+ setIsLoading(false);
+
if (e.nativeEvent.data === HCAPTCHA_READY_EVENT) {
injectVerifyData();
return;
@@ -415,7 +419,6 @@ const Hcaptcha = ({
e.reset = reset;
e.success = true;
if (e.nativeEvent.data === 'open') {
- setIsLoading(false);
} else if (e.nativeEvent.data.length > 35) {
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, success: false, reset }), tokenTimeout);
e.markUsed = () => clearTimeout(expiredTokenTimerId);
diff --git a/MAINTAINER.md b/MAINTAINER.md
index c253f7e..63670a6 100644
--- a/MAINTAINER.md
+++ b/MAINTAINER.md
@@ -42,29 +42,22 @@ PATCH: bugfix only.
### Generate test app
-For `expo` test app
+For `expo` test app:
+- `yarn example --expo`
+- `yarn run android`
-- `cd react-native-hcaptcha`
-- `yarn example --expo
-- `yarn android` or `npm run android`
-
-For `react-native` test app
-
-- `cd react-native-hcaptcha`
+For `react-native` test app:
- `yarn example`
-- `yarn android` or `npm run android`
+- `yarn 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`
+- run `yarn 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`
-- `yarn ios` or `npm run ios`
+- `yarn run ios`
### Known issues
diff --git a/README.md b/README.md
index 7ae6765..48c9ad8 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ hCaptcha wrapper for React Native (Android and iOS)
1. Install package:
- Using NPM
- `npm install @hcaptcha/react-native-hcaptcha`
+ `npm install @hcaptcha/react-native-hcaptcha`
- Using Yarn
`yarn add @hcaptcha/react-native-hcaptcha`
2. Import package:
@@ -21,7 +21,7 @@ Full examples for expo and react-native, as well as debugging guides, are in [MA
## Demo
-See live demo in [Snack](https://snack.expo.io/rTUn6wTjW).
+See live demo in [Snack](https://snack.expo.dev/@ds-imi/example-app-react-native-hcaptcha?platform=ios).
## Usage
diff --git a/__scripts__/generate-example.js b/__scripts__/generate-example.js
index e049bc1..bf24658 100644
--- a/__scripts__/generate-example.js
+++ b/__scripts__/generate-example.js
@@ -158,8 +158,28 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat
} else {
// https://github.com/facebook/react-native/issues/29977 - react-native doesn't work with symlinks so `cp` instead
const destLibDir = path.join(projectPath, 'react-native-hcaptcha');
- const excludes = ['__e2e__/host', '__tests__', '__mocks__', 'node_modules', '.git', 'output', '.reassure'].map(e => `--exclude=${e}`).join(' ');
+ const excludes = [
+ '__e2e__/host',
+ '__tests__',
+ '__mocks__',
+ 'node_modules',
+ '.git',
+ 'output',
+ '.reassure',
+ 'package-lock.json',
+ 'yarn.lock',
+ ].map(e => `--exclude=${e}`).join(' ');
execSync(`rsync -a ${excludes} ${libRoot}/ ${destLibDir}/`, { stdio: 'inherit' });
+
+ const copiedPkgPath = path.join(destLibDir, 'package.json');
+ const copiedPkg = JSON.parse(fs.readFileSync(copiedPkgPath, 'utf8'));
+ delete copiedPkg.devDependencies;
+ delete copiedPkg.packageManager;
+ if (copiedPkg.scripts?.prepare) {
+ delete copiedPkg.scripts.prepare;
+ }
+ fs.writeFileSync(copiedPkgPath, JSON.stringify(copiedPkg, null, 2) + '\n');
+
execSync('npm i --save file:./react-native-hcaptcha', packageManagerOptions);
execSync(`npm i --save --dev ${devPackages}`, packageManagerOptions);
execSync(`npm i --save ${peerPackages}`, packageManagerOptions);
diff --git a/__tests__/Hcaptcha.test.js b/__tests__/Hcaptcha.test.js
index f7c71b1..bb8f648 100644
--- a/__tests__/Hcaptcha.test.js
+++ b/__tests__/Hcaptcha.test.js
@@ -103,7 +103,7 @@ describe('Hcaptcha', () => {
expect(config.debugInfo).toMatchObject({
customDebug: 'enabled',
'dep_mocked-md5': true,
- sdk_4_0_0: true,
+ 'sdk_4_0_1-alpha': true,
});
expect(query).toMatchObject({
@@ -396,6 +396,31 @@ describe('Hcaptcha', () => {
expect(component.UNSAFE_queryByType(TouchableWithoutFeedback)).toBeNull();
});
+ it('does not emit a loading timeout after the widget becomes ready in passive flows', () => {
+ jest.useFakeTimers();
+ const onMessage = jest.fn();
+ const component = render(
+
+ );
+
+ act(() => {
+ getWebView(component).props.onMessage({ nativeEvent: { data: HCAPTCHA_READY_EVENT } });
+ jest.advanceTimersByTime(15000);
+ });
+
+ expect(onMessage).not.toHaveBeenCalledWith({
+ nativeEvent: {
+ data: 'error',
+ description: 'loading timeout',
+ },
+ });
+ expect(getLastInjectJavaScriptMock()).toHaveBeenCalledWith(expect.stringContaining('execute();'));
+ });
+
it('forwards token messages with reset and markUsed hooks', async () => {
jest.useFakeTimers();
const onMessage = jest.fn();
diff --git a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
index f7c227f..ec1c005 100644
--- a/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
+++ b/__tests__/__snapshots__/ConfirmHcaptcha.test.js.snap
@@ -112,7 +112,7 @@ exports[`ConfirmHcaptcha renders ConfirmHcaptcha with minimum props after show()