Skip to content

Release 2.4.0

Choose a tag to compare

@smitvalture smitvalture released this 22 May 17:12
· 3 commits to main since this release

v2.4.0 — Lifecycle hardening, security defaults, enterprise & abort signals

A comprehensive audit-and-improve pass. All changes are non-breaking — existing code continues to work; new features are additive; behavior changes have escape-hatch props.

See PR #6 for the full review.

✨ New features

  • AbortSignal support — cancel an in-flight getToken():
    const controller = new AbortController();
    recaptchaRef.current?.getToken('checkout', { signal: controller.signal });
    controller.abort(); // promise rejects with 'Token request was aborted.'
  • reCAPTCHA Enterprise — opt in with the new useEnterprise prop. Switches to enterprise.js and grecaptcha.enterprise.execute.
  • onVerify now receives the action — onVerify?: (token, action) => void. Useful when one component handles multiple actions. Existing handlers that ignore the extra arg keep working.

🐛 Bug fixes

  • onLoadEnd fires exactly once per load cycle (was 2–3×).
  • Concurrent reset() calls no longer leak the first promise — all queued resolvers now fire on the next load.
  • reset() rejects in-flight and queued token requests immediately with a clear RESET error (used to hang until the request's own timeout).
  • Superseded requests now reject with a SUPERSEDED error instead of the misleading TOKEN_REQUEST_TIMEOUT.
  • Force-READY timer no longer reads closure-stale isReady / hasError — uses refs so the latest values win.
  • handleLoadStart no longer wipes pendingRequests — previously dropped getToken() calls queued between mount and the WebView's first onLoadStart.
  • Unmount cleanup — all timers (init, force-READY, in-flight, queued) and abort listeners are now cleared on unmount, fixing a real memory leak when the component unmounts mid-flight.

🛡️ Security

These change defaults but each has an opt-out prop, so existing setups can pin the old behavior if needed.

  • mixedContentMode is now configurable, defaulting to 'never' (was hard-coded 'always'). Pass mixedContentMode="always" to restore.
  • originWhitelist is now configurable, defaulting to ['https://www.google.com', 'https://www.gstatic.com', baseUrl] (was ['*']). Pass originWhitelist={['*']} to restore.
  • siteKey is regex-validated at mount (/^[A-Za-z0-9_-]{20,80}$/). Invalid keys surface via onError instead of being interpolated into the WebView's HTML/URL.

🧱 Internal

  • HTML template memoized (was rebuilt every render).
  • Magic numbers extracted to named constants.
  • NodeJS.Timeout → portable ReturnType<typeof setTimeout>.

🧪 Tests & CI (new)

  • 32 unit tests covering all bug fixes and the public API, using jest + @testing-library/react-native with a controllable WebView mock.
  • GitHub Actions CI runs lint · typecheck · test · build on every PR across Node 20 and 22.

🔌 Migrating

- "@valture/react-native-recaptcha-v3": "^2.3.0"
+ "@valture/react-native-recaptcha-v3": "^2.4.0"

No source changes required.

Full Changelog: v2.3.0...v2.4.0