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()