Repository navigation
Conversation
| orientation, | ||
| callbacks: callbacksRef.current, | ||
| })); | ||
| hasRenderedRef.current = true; |
There was a problem hiding this comment.
Clear isLoadingRef and isLoading after api.render() succeeds, matching native’s ready handler. Otherwise a loaded widget that has not emitted open/token/error still shows the loading overlay and reports a false loading timeout after 15s.
There was a problem hiding this comment.
Fixed in 5c11121: isLoadingRef/isLoading are now cleared right after api.render() succeeds, matching native's ready handler. Added a test for an invisible widget that renders without emitting anything: no overlay, and no loading timeout after 15s. The render-count guard now expects one extra commit on load, which is the same as native's ready message.
| } | ||
| }) | ||
| .catch((error) => { | ||
| emitRef.current((error && error.message) || (error && error.name) || 'error'); |
There was a problem hiding this comment.
Ignore loader rejection after unmount. If the modal is hidden while api.js is loading, this still calls onMessage; reopening before that rejection lets the old instance’s error handler close the new challenge.
There was a problem hiding this comment.
Fixed in 31a01b2: each load gets a generation number, and both unmount and any newer load (including retryApiLoad) invalidate it, so a stale resolve or reject does nothing. Tests cover a rejection after unmount and a stale load from a previous instance racing a remounted one.
| phoneNumber: pn, | ||
| phonePrefix: pp, | ||
| rqdata: rq, | ||
| userJourney: journeyEnabled ? peekJourneyEvents() : undefined, |
There was a problem hiding this comment.
Read the current journey flag at execution time. The pending loader retains the initial applyVerifyData closure, so disabling userJourney or calling stopEvents() before it resolves can still attach buffered journey events.
There was a problem hiding this comment.
Fixed in e50dbe6: the flag is now read through journeyEnabledRef when verify data is applied, the same way verifyInputsRef works, and applyVerifyData no longer depends on it. Added a test that turns userJourney off while the loader is pending and asserts the buffered event is not in setData.
Moves the platform-agnostic helpers (theme/size normalization, debug info, verify data, loader config, render config, shared timeouts) into hcaptchaShared.js, and isolates the `react-native/Libraries/Core/ReactNativeVersion` deep import behind reactNativeVersion.js. That import is the one thing in the library no web bundler can resolve once `react-native` is aliased to `react-native-web`, so it needs a platform split before a web build is possible. Hcaptcha.js still re-exports buildDebugInfo, buildVerifyData and HCAPTCHA_READY_EVENT, so the native behaviour and the existing tests are unchanged. Also fixes one latent bug carried over in the move: `custom` was computed as `typeof theme === 'object'`, and because `typeof null === 'object'` an absent theme told the loader to expect a custom theme that never arrives. Now guarded with an explicit null check. No test pinned the old behaviour, and ConfirmHcaptcha masked it by defaulting `theme` to 'light'; only direct `Hcaptcha` consumers without a theme were affected. Revert this hunk alone if the wire change is unwanted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a web build so the same ConfirmHcaptcha / Hcaptcha code runs under
react-native-web with no application changes.
Rather than emulating a WebView in the browser, Hcaptcha.web.js loads api.js
with @hcaptcha/loader — already a dependency, and the same loader the native
inline HTML uses — and renders the widget directly into the document. That keeps
setData/execute/reset as direct API calls instead of messages that need an
injectJavaScript transport, so the full native feature set carries over: the
{ nativeEvent: { data } } event shape, success/reset/markUsed, size and theme
normalization, rqdata, verifyParams, MFA phone props, User Journeys, the
15s loading timeout, the 120s token expiry and script-error retry.
react-native-web supplies Modal, SafeAreaView and the rest, so no Modal,
animation or WebView shims are needed; `react-native` -> `react-native-web` is
the only bundler alias. react-native-web and react-dom are declared as optional
peer dependencies, so native-only installs are unaffected.
Testing:
- __tests_web__/ holds a jsdom Jest project (`npm run test:web`, 27 tests). It
stubs only the network fetch of api.js and the widget API it installs on
`window`; the component renders for real. It lives outside __tests__ so the
native project's default testMatch cannot pick it up.
- `npm test` now runs both projects.
- Verified end to end in a real browser against live hCaptcha via the new
`npm run web` example: the widget loads, a visual challenge opens inside the
RNW modal, and with hCaptcha's always-pass test key the token reaches
onMessage and markUsed()/hide() fire.
Also adds `modulePathIgnorePatterns` for __e2e__ to the native Jest config. A
generated __e2e__/host app carries its own node_modules including a second copy
of React, which Jest resolved into, failing 32 unit tests locally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The config file has been inert since it was added in #95: nothing imported it, and reassure has no config-file auto-discovery — `configure()` is meant to be called from a Jest setup file. Every setting in it was dead, including the `testingLibrary` pin. That pin matters now. Adding @testing-library/react for the web suite means both it and @testing-library/react-native are installed, and reassure was falling back to auto-detection, warning once per perf test: Both '@testing-library/react-native' and '@testing-library/react' are installed. Using '@testing-library/react-native' by default. Wiring the file into setupFilesAfterEnv makes the pin take effect and silences the warning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`npm run test:web -- --coverage` writes a coverage/ report that showed up as untracked noise in git status and, because eslint does not read .gitignore, also tripped lint on the generated lcov-report sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the example in a browser logged two warnings from our own code:
TouchableWithoutFeedback is deprecated. Please use Pressable.
props.pointerEvents is deprecated. Use style.pointerEvents
Both are shared native/web files, so the fix applies to both platforms.
TouchableWithoutFeedback cloned its single child to attach handlers; Pressable
renders the view itself, so the wrapper View collapses into it. On native the
rendered element gains only inert props — accessibilityValue with all-undefined
fields, collapsable={false}, and Pressable's internal onFocus/onBlur. Notably
accessible={true} and focusable={true} were already set by
TouchableWithoutFeedback, so screen-reader and focus behaviour is unchanged.
On web the backdrop additionally gets cursor:pointer and touch-action.
Moving pointerEvents into the style broke a test that located the passive
container with UNSAFE_getByProps({pointerEvents:'none'}); the container now
carries a testID, matching the existing confirm-hcaptcha-backdrop convention.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The web suite never passed userJourney, so every journey path was unexecuted under react-native-web: the enable/disable effect in Hcaptcha.web.js and syncJourneyConsumer/stopEvents in index.js. The native suite covers all of it. Nothing here is mocked — __mocks__/web.js stubs only the api.js fetch, so the shared journey runtime runs for real, as it does on native. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Catches a dropped useMemo/useCallback in Hcaptcha.web.js turning a mount into a mount-plus-rerender. Uses React's own Profiler rather than reassure. These are absolute assertions on commit counts, so there is no baseline to diff against and no second perf:compare pipeline to maintain; running reassure's measureRenders outside its CLI also prints an "incorrect Node.js configuration" banner on every test:web run. Documents one existing redundancy rather than changing behaviour: emit() calls setIsLoading(false) on every message, so React renders once more before bailing out on the unchanged value even though isLoadingRef already tracks it. Hcaptcha.js has the same shape, so any fix belongs on both at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jest fakes bundler resolution with moduleNameMapper and moduleFileExtensions, so nothing in CI ever proved the web build actually links. A broken .web.js platform extension or react-native-web alias would pass every test — which is the exact failure reactNativeVersion.web.js exists to prevent, since react-native-web ships no Libraries/Core/ReactNativeVersion for the native file to resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dev server logged a 404 for /favicon.ico on every load. An inline empty icon avoids the request without adding an asset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Native clears isLoading on its ready event; web only did so on the first emitted message. An invisible widget that rendered but had not opened yet kept the overlay up and reported a false loading timeout after 15s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A loader rejection that settled after the modal was hidden still called onMessage; if the modal had been reopened meanwhile, the old instance's error could close the new challenge. Each load now carries a generation that unmount and later loads invalidate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pending loader kept the first render's applyVerifyData, so turning userJourney off before api.js resolved still attached buffered journey events to the verify payload. The flag is now read through a ref, like the other verify inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e50dbe6 to
22ee283
Compare
ConfirmHcaptcha drives prewarm itself: it mounts the widget hidden before show() and executes it through the Hcaptcha ref. The web widget has no ref API and executes on mount, so prewarm on web ran a hidden verification at mount and made show()/hide()/destroy() no-ops on the widget. Prewarm is now gated to native in the wrapper, so web takes the default mount-on-show() path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm installs a local directory dependency's devDependencies, unlike a registry install. The web tooling added unpinned react-dom, which resolves to the newest React and nests a copy inside the library whenever the host template pins an older one (RN 0.88 pins react 19.3.0, react-dom@* needs ^19.3.1). Metro then bundled two Reacts and every Hcaptcha hook call failed with "Invalid hook call", failing 36 of 38 e2e tests on both platforms. The copied library's devDependencies are now dropped before install, matching what a consumer gets from the registry. Also refresh the new shared modules alongside Hcaptcha.js when re-running e2e on an existing host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ec55f23 to
fa24207
Compare
Fixes #66.
What
Adds react-native-web support:
ConfirmHcaptchaandHcaptchanow render in a browser, with the public API, prop names andonMessagecontract unchanged.On native the widget lives in a WebView and the two sides talk over
postMessage/injectJavaScript. On web the host page is a browser, so the widget renders straight into the document via@hcaptcha/loader— the same loader the native HTML already uses — and the message channel collapses into direct calls.How
hcaptchaShared.jsholds the logic both platforms share (loader config, verify payload, size/theme normalization, timeouts), extracted fromHcaptcha.jswith no behaviour change.Hcaptcha.web.jsis picked up by bundler platform-extension resolution;index.jsand the type definitions are untouched.reactNativeVersion.web.jsexists becausereact-native-webships noLibraries/Core/ReactNativeVersion, and that deep import fails to resolve oncereact-nativeis aliased. This is why the split is two files rather than one file with a runtimePlatform.OScheck — static imports ofreact-native-webviewand that version path are resolved at bundle time, before any runtime branch could run.Testing
test:nativetest:web__tests_web__/runs under jsdom withreact-nativealiased toreact-native-web, exactly as a consumer's bundler would alias it. Only the api.js network fetch is stubbed — the component, loader wiring and event mapping all run for real.stopEvents(), multiple simultaneous consumers, buffered events reaching the verify payload). The shared journey runtime is unmocked.Profiler, so a droppeduseMemo/useCallbackthat adds a re-render fails the build.npm run build:web. Jest fakes bundler resolution, so only a real webpack build proves the.web.jsextensions and the alias actually link.Verified manually in Chrome against live hCaptcha: the visual challenge renders and returns a token,
markUsed()andhide()fire, and the loader is configured correctly (render=explicit, sitekey-derivedhost,hlfromlanguageCode).Also in here
reassure.config.jshas been inert since feat: add reassure testing to control render count #95 — nothing imported it, and reassure has no config-file auto-discovery. Adding@testing-library/reactmade that matter, because reassure then had two testing libraries to choose between and fell back to auto-detection. It is now loaded viasetupFilesAfterEnv.TouchableWithoutFeedbackwithPressableand movedpointerEventsinto the style — both logged deprecation warnings from react-native-web in a real browser.accessibleandfocusablewere already set byTouchableWithoutFeedback, so native accessibility behaviour is unchanged.Note for reviewers
npm run perf:baselinefails onmasterwith master's own lockfile — 11 of 24 tests fail withTypeError: Cannot read properties of null (reading 'useMemo'). It is not introduced by this branch (the perf suite passes 24/24 here), but it means CI's baseline step on master push fails, no baseline is cached, and the PRperformancejob'sif: cache-hit == 'true'guard silently skipsperf:compare. So no perf comparison is running on PRs today. Worth a separate issue.