Skip to content

Commit d4541f7

Browse files
committed
fix(hcaptcha): iOS timeout when passive mode
1 parent 17ab909 commit d4541f7

9 files changed

Lines changed: 594 additions & 527 deletions

File tree

.github/workflows/publish.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ jobs:
1414
actions: write
1515
runs-on: ubuntu-latest
1616
steps:
17-
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
17+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
1818
with:
1919
node-version: 24
20-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
20+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
2121
- run: npm install
2222
- run: npm test
2323
- run: npm run lint
@@ -26,10 +26,10 @@ jobs:
2626
runs-on: ubuntu-latest
2727
needs: build
2828
steps:
29-
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
29+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3030
with:
3131
node-version: 24
32-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
32+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
3333
- name: npm release
3434
run: |
3535
npm ci

.github/workflows/tests.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ jobs:
1717
actions: write
1818
runs-on: ubuntu-latest
1919
steps:
20-
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
20+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
2121
with:
2222
node-version: 24
23-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
23+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
2424
- run: npm install
25-
- run: npm test
25+
- run: npm run test
2626
- run: npm run lint
2727
- if: github.ref == 'refs/heads/master' && github.event_name == 'push'
2828
run: npm run perf:baseline
@@ -40,9 +40,9 @@ jobs:
4040
runs-on: ubuntu-latest
4141
if: github.event_name == 'pull_request'
4242
steps:
43-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
43+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
4444
- run: npm install
45-
- uses: actions/cache/restore@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
45+
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
4646
id: cache-reassure
4747
with:
4848
path: .reassure
@@ -91,10 +91,10 @@ jobs:
9191
platform: ios
9292
pm: yarn
9393
steps:
94-
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
94+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
9595
with:
9696
node-version: 24
97-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
97+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
9898
with:
9999
path: react-native-hcaptcha
100100
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
@@ -140,7 +140,7 @@ jobs:
140140
- os: macos-latest
141141
platform: ios
142142
steps:
143-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
143+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
144144
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
145145
with:
146146
node-version: 22
@@ -174,7 +174,7 @@ jobs:
174174
name: Run iOS E2E tests
175175
run: npm run test:e2e:ios
176176

177-
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
177+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
178178
if: always()
179179
with:
180180
name: e2e-results-${{ matrix.platform }}
@@ -189,7 +189,7 @@ jobs:
189189
needs: test
190190
if: always() && github.event_name == 'schedule' && needs.test.result == 'failure'
191191
steps:
192-
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
192+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
193193
- run: |
194194
RN_VERSION="${{ needs.test.outputs.rn-version }}"
195195
GHA_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

.mise.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[env]
2+
_.path = "{{ cwd }}/bin"
3+
4+
[tools]
5+
node = "24.16.0"
6+
yarn = "1.22.22"

Hcaptcha.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ const Hcaptcha = ({
213213
const tokenTimeout = 120000;
214214
const loadingTimeout = 15000;
215215
const [isLoading, setIsLoading] = useState(true);
216+
const isLoadingRef = useRef(true);
216217
const journeyEnabled = Boolean(userJourney);
217218
const hasJourneyConsumerRef = useRef(false);
218219
const normalizedTheme = useMemo(() => normalizeTheme(theme), [theme]);
@@ -345,13 +346,13 @@ const Hcaptcha = ({
345346

346347
useEffect(() => {
347348
const timeoutId = setTimeout(() => {
348-
if (isLoading) {
349+
if (isLoadingRef.current) {
349350
onMessage({ nativeEvent: { data: 'error', description: 'loading timeout' } });
350351
}
351352
}, loadingTimeout);
352353

353354
return () => clearTimeout(timeoutId);
354-
}, [isLoading, onMessage]);
355+
}, [onMessage]);
355356

356357
const webViewRef = useRef(null);
357358
const injectVerifyData = (resetFirst = false) => {
@@ -407,6 +408,9 @@ const Hcaptcha = ({
407408
}}
408409
mixedContentMode={'always'}
409410
onMessage={(e) => {
411+
isLoadingRef.current = false;
412+
setIsLoading(false);
413+
410414
if (e.nativeEvent.data === HCAPTCHA_READY_EVENT) {
411415
injectVerifyData();
412416
return;
@@ -415,7 +419,6 @@ const Hcaptcha = ({
415419
e.reset = reset;
416420
e.success = true;
417421
if (e.nativeEvent.data === 'open') {
418-
setIsLoading(false);
419422
} else if (e.nativeEvent.data.length > 35) {
420423
const expiredTokenTimerId = setTimeout(() => onMessage({ nativeEvent: { data: 'expired' }, success: false, reset }), tokenTimeout);
421424
e.markUsed = () => clearTimeout(expiredTokenTimerId);

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ hCaptcha wrapper for React Native (Android and iOS)
1111

1212
1. Install package:
1313
- Using NPM
14-
`npm install @hcaptcha/react-native-hcaptcha`
14+
`npm install @hcaptcha/react-native-hcaptcha`
1515
- Using Yarn
1616
`yarn add @hcaptcha/react-native-hcaptcha`
1717
2. Import package:
@@ -21,7 +21,7 @@ Full examples for expo and react-native, as well as debugging guides, are in [MA
2121

2222
## Demo
2323

24-
See live demo in [Snack](https://snack.expo.io/rTUn6wTjW).
24+
See live demo in [Snack](https://snack.expo.dev/@ds-imi/example-app-react-native-hcaptcha?platform=ios).
2525

2626
## Usage
2727

__scripts__/generate-example.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,28 @@ function main({ cliName, projectRelativeProjectPath, projectName, projectTemplat
158158
} else {
159159
// https://github.com/facebook/react-native/issues/29977 - react-native doesn't work with symlinks so `cp` instead
160160
const destLibDir = path.join(projectPath, 'react-native-hcaptcha');
161-
const excludes = ['__e2e__/host', '__tests__', '__mocks__', 'node_modules', '.git', 'output', '.reassure'].map(e => `--exclude=${e}`).join(' ');
161+
const excludes = [
162+
'__e2e__/host',
163+
'__tests__',
164+
'__mocks__',
165+
'node_modules',
166+
'.git',
167+
'output',
168+
'.reassure',
169+
'package-lock.json',
170+
'yarn.lock',
171+
].map(e => `--exclude=${e}`).join(' ');
162172
execSync(`rsync -a ${excludes} ${libRoot}/ ${destLibDir}/`, { stdio: 'inherit' });
173+
174+
const copiedPkgPath = path.join(destLibDir, 'package.json');
175+
const copiedPkg = JSON.parse(fs.readFileSync(copiedPkgPath, 'utf8'));
176+
delete copiedPkg.devDependencies;
177+
delete copiedPkg.packageManager;
178+
if (copiedPkg.scripts?.prepare) {
179+
delete copiedPkg.scripts.prepare;
180+
}
181+
fs.writeFileSync(copiedPkgPath, JSON.stringify(copiedPkg, null, 2) + '\n');
182+
163183
execSync('npm i --save file:./react-native-hcaptcha', packageManagerOptions);
164184
execSync(`npm i --save --dev ${devPackages}`, packageManagerOptions);
165185
execSync(`npm i --save ${peerPackages}`, packageManagerOptions);

__tests__/Hcaptcha.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,31 @@ describe('Hcaptcha', () => {
396396
expect(component.UNSAFE_queryByType(TouchableWithoutFeedback)).toBeNull();
397397
});
398398

399+
it('does not emit a loading timeout after the widget becomes ready in passive flows', () => {
400+
jest.useFakeTimers();
401+
const onMessage = jest.fn();
402+
const component = render(
403+
<Hcaptcha
404+
siteKey="00000000-0000-0000-0000-000000000000"
405+
url="https://hcaptcha.com"
406+
onMessage={onMessage}
407+
/>
408+
);
409+
410+
act(() => {
411+
getWebView(component).props.onMessage({ nativeEvent: { data: HCAPTCHA_READY_EVENT } });
412+
jest.advanceTimersByTime(15000);
413+
});
414+
415+
expect(onMessage).not.toHaveBeenCalledWith({
416+
nativeEvent: {
417+
data: 'error',
418+
description: 'loading timeout',
419+
},
420+
});
421+
expect(getLastInjectJavaScriptMock()).toHaveBeenCalledWith(expect.stringContaining('execute();'));
422+
});
423+
399424
it('forwards token messages with reset and markUsed hooks', async () => {
400425
jest.useFakeTimers();
401426
const onMessage = jest.fn();

0 commit comments

Comments
 (0)