feat: react-native-web support - #118
Open
CAMOBAP wants to merge 9 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.