Skip to content

feat!: add prewarm lifecycle APIs in v5 - #122

Merged
e271828- merged 15 commits into
masterfrom
feat/prewarm
Oct 24, 2026
Merged

e271828- merged 15 commits into
masterfrom
feat/prewarm

Conversation

@e271828-

@e271828- e271828- commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Multi-step auth flows pay WebView and SDK initialization costs when verification starts. Add opt-in prewarm so apps initialize at startup, call execute() on the credentials screen, and submit the token with the form. prewarm defaults to false; execute() is equivalent to show().

Release as 5.0.0, with contract changes and upgrade steps. Legacy top-level rqdata/phone props retain serialization and SDK reloads. Use verifyParams for changing data while preserving the warmed SDK.

Changes

  • Retain the prewarmed WebView across hide/show, queue one request until readiness, and bind async outcomes to execution IDs. Cancelled tokens/errors and retired document callbacks cannot affect a later request, including config A→B→A and batched cancellation/re-execution.
  • Add destroy() to wrapper and inline refs. Silently cancel requests and loading/token timers, detach journey consumers, and unmount the native WebView. Repeated calls are safe. Prop changes do not recreate it; the next show()/execute() creates a fresh WebView. Retained event reset hooks become inert. Explicit destruction also cancels legacy token timers from prior default-mode hide cycles.
  • Destroy does not revoke an issued token. Apps retaining tokens after destruction must track expiry because local notifications stop. markUsed() remains scoped to its token.
  • Classify initialization failures and async rejections with separate internal message types. Long errors and rejected Error objects from these paths cannot become tokens. Prewarm idle loading failures/timeouts stay silent and are retried on execute; active errors cancel the request and expose a working reset hook. Informational messages do not cancel loading deadlines.
  • Make event.reset request fresh verification and restore a hidden wrapper overlay unless passive. Retire callbacks synchronously on native reloads.
  • Recover loading deadlines and renderer termination, preserve default automatic verification after document reloads, retain warmed documents/held-token expiry across presentation changes, and safely copy native events with inherited read-only fields.
  • Inject current verification/journey data per execution. Document initialization versus execution props, cancellation/retry distinctions, root placement, custom SDK async requirements, and the prewarm overlay's inability to appear above another native Modal.
  • Preserve overlay, backdrop, and loader layout on React Native 0.88 using the supported StyleSheet.absoluteFill API. Three regression cases remove the deprecated alias; each fails before the fix and passes afterward.
  • Add native multi-screen signup/login, original-example smoke, transport faults, and lifecycle fixtures. Configure Harness runtime deadlines. Persist controlled SDK release until reset, including release-before-load and concurrent requests; three real-HTTP regressions fail with the old gate and pass with the fix. These regressions run without a generated native host; renderer debugger dependencies load only when used, and child startup errors fail immediately. Acknowledge original-example launch input so native modal dismissal cannot silently swallow the next tap.

Validation

  • 254 Jest tests, 8 suites, 2 snapshots, lint, and diff checks pass. No expected failures or search exemptions.
  • Independent contract oracle against real React components, generated HTML/injected JavaScript, and the published loader: 84,220 unique executed scenarios, zero violations. All 71,259 eligible traces of length 0–5 are exercised across wrapper overlay/passive and inline components. The 814,359-word raw domain excludes 743,100 words through declared preconditions.
  • A second depth-4 lifecycle search exercises all 11,529 eligible traces out of 92,823 raw words, excluding 81,294 through declared preconditions. It adds initialization/render/script failures, event.reset, native reload, informational messages, and duplicate readiness. Both search domains overlap; the primary scenario total is deduplicated.
  • Also exercise 192 deterministic 32-action traces across all three component variants, 899 metamorphic pairs, 1,228 boundary/isolation scenarios, and 10 prior reduced regressions. Boundary coverage includes all 90 execute/hide/destroy command words of length 1–4 containing destroy, across four initial phases and three component variants: 1,080 React batches. Other cases deliver stale callbacks before the destroy commit, deliver queued native outcomes after recreation, and destroy at deadlines ±1 ms. Counts overlap; definitions/exclusions are in the audit.
  • React Native 0.88.0 / Harness 1.6.0: 38 native checks per platform on Android API 33 and iPhone 16 / iOS 26. Each full suite includes 28 controlled-SDK lifecycle/payload cases, six live-SDK auth cases with backend token verification, two original-example smoke tests, and two theme snapshots. Hidden-reset checks verify both overlay touch interception and touch restoration after hide. Auth fixtures wait for actual SDK readiness and the native retry button to become enabled. Native readiness uses a bounded 30-second test budget; the product loading deadline remains 15 seconds. A controlled 16-second idle SDK load reproduces the old test-budget failure and verifies silent late readiness followed by successful verification without reloading.
  • Stateful coverage gates require all 25 declared phase/request/token states and 21 state-conditioned adjacent transition pairs per variant. Every long trace must accept a token. Per variant, long traces accept 273 tokens and execute 107 consume/103 expire commands; they observe 246 distinct transitions and 792 adjacent transition pairs. The previous token-starving generator is replaced with lifecycle witness prefixes plus weighted exploration.
  • Exhaust every completion order and success/error assignment for two/three outstanding attempts across hide/destroy/config replacement/renderer loss: 1,008 eligible scenarios, all exercised, no exclusions within this domain. This includes newer tokens before older success/errors and verifies expiry or consumption afterward; 792 permutation comparisons also satisfy metamorphic equivalence. Model/driver independently address SDK attempts. The latest review follow-up adds 93 regression executions and nine payload/retry metamorphic relations, together with the production fixes they validate.
  • Original Example.App.js remains unchanged. Its native smoke tests cover actual challenge launch/dismiss/reopen and an auto-pass-key variant with token display and backend verification. See __e2e__/example.smoke.md for the fixture design.
  • Earlier transport-fault, latency, soak, and long-expiry results remain recorded in the audit; those profiles were not rerun for destroy. Fresh CI also passes all 38 native checks per platform on Android API 34 and iPhone 16 / iOS 18.5 (run).

Exhaustiveness applies to the declared finite grammar and selected batching domain. Native tests use public integration keys and controlled SDK fixtures; they do not prove arbitrary OS/network scheduling, enterprise payload acceptance, physical-device performance, or leak freedom. Native views are unmounted, but OS/WebView process caches may prevent an immediate RSS decrease.

@e271828- e271828- changed the title feat: add opt-in WebView prewarming and deferred execution feat!: add prewarm support in v5 with migration guide Oct 22, 2026
Comment thread Hcaptcha.js Outdated
return;
}

if (e.nativeEvent.data === 'script-error') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the literal 'script-error' marks a load failure. Other pre-ready failures go through the generic error branch instead: the loader .catch posts error.message / 'error', and onloadCallback posts e.name when render throws. Because readyRef is still false there, executeRequestedRef stays true and loadFailedRef stays false, and the loading timer has already been cleared above.

After that:

  • execute() returns early at if (executeRequestedRef.current && !loadFailedRef.current)
  • reset() clears the flag, but the next execute() is neither ready nor failed, so it does nothing

With prewarm the component stays mounted, so the captcha is stuck until it is remounted. Could any error received before READY be treated as a load failure (set loadFailedRef and wire e.reset = retryApiLoad)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea1dcd8: initialization failures now recover through execute() or event.reset() without stalling; long error messages cannot become tokens.

Comment thread index.js

if (prewarm) {
return (
<View

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With prewarm, the full-screen Modal becomes an absoluteFill View. That only covers ConfirmHcaptcha's parent container, not the window. If the component sits in a nested View, under a navigator header, or next to sibling screens, the challenge and backdrop can be clipped or drawn underneath other UI. Without prewarm, Modal always covered the whole screen.

If this is intended, it should be documented in UPGRADING.md and the README (where to mount it). Alternatively, could the hidden/prewarming state stay a View while the visible state is shown in a Modal?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed: README and upgrade guide document mounting after navigation in a full-screen root View and the parent/Modal limitations. Switching View/Modal would remount the warmed WebView.

Comment thread Hcaptcha.js
injectVerifyData(true);
// Event hooks can be retained across renders while a token is held.
const reset = useCallback(() => resetRef.current(), []);
resetRef.current = () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In prewarm mode, event.reset() can start a challenge nobody can see. Example: the app gets a token and calls hide(). Later 'expired' fires and the app calls event.reset(), as the README suggests. This calls setExecuteRequested(true) and injectVerifyData(true), but ConfirmHcaptcha's state.show is still false, so the challenge opens in the container at opacity 0 with pointerEvents="none". Without prewarm, the WebView was unmounted after hide(), so the same call did nothing.

Should ConfirmHcaptcha show itself again when a reset is requested while hidden? Or should the reset only reset the widget, not run it, while hidden?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea1dcd8: event.reset() restores the hidden overlay when retrying; passive mode stays hidden.

Comment thread Hcaptcha.js Outdated
setExecuteRequested(false);
}
setIsLoading(false);
onMessageRef.current({ nativeEvent: { data: 'error', description: 'loading timeout' } });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With prewarm, this timeout (and any script-error) reaches the app's onMessage while the component is mounting in the background, before the user asked for verification. If a prewarmed ConfirmHcaptcha mounts on app start while offline or on a slow network, the app gets { data: 'error', description: 'loading timeout' } about 15s later. Typical handlers show an error toast or call hide(), so the user sees a captcha error they never triggered.

Could prewarm load failures be held back until show()/execute() is called, and then either reported or retried?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ea1dcd8: idle preload failures/timeouts stay silent; show()/execute() retries them. Active-request failures still reach onMessage.

@e271828- e271828- changed the title feat!: add prewarm support in v5 with migration guide feat!: add prewarm lifecycle APIs in v5 Oct 23, 2026
@e271828-
e271828- requested a review from CAMOBAP October 23, 2026 13:38
@e271828-
e271828- merged commit 113aa05 into master Oct 24, 2026
9 checks passed
@e271828-
e271828- deleted the feat/prewarm branch October 24, 2026 10:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants