Skip to content
Open
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
8 changes: 4 additions & 4 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}"
Expand Down
6 changes: 6 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[env]
_.path = "{{ cwd }}/bin"

[tools]
node = "24.16.0"
yarn = "1.22.22"
9 changes: 6 additions & 3 deletions Hcaptcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -407,6 +408,9 @@ const Hcaptcha = ({
}}
mixedContentMode={'always'}
onMessage={(e) => {
isLoadingRef.current = false;
setIsLoading(false);

if (e.nativeEvent.data === HCAPTCHA_READY_EVENT) {
injectVerifyData();
return;
Expand All @@ -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);
Expand Down
21 changes: 7 additions & 14 deletions MAINTAINER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
22 changes: 21 additions & 1 deletion __scripts__/generate-example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions __tests__/Hcaptcha.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Hcaptcha
siteKey="00000000-0000-0000-0000-000000000000"
url="https://hcaptcha.com"
onMessage={onMessage}
/>
);

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();
Expand Down
Loading
Loading