@autobusal/providers 1.6.7 → 1.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ All notable changes to `@autobusal/providers` are documented here. This project
4
4
  adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.6.9] - 2026-07-31
8
+
9
+ ### Added (re-added)
10
+
11
+ - **`useRecoverFromDomCorruption()`'s global `removeChild`/`insertBefore`
12
+ listener is back**, generalized. Removed in 1.6.8 as a backstop
13
+ specifically for `GoogleReCaptchaProvider`'s remount corruption, on the
14
+ assumption that removing that provider removed the trigger. It didn't:
15
+ `@autobusal/common` 1.8.3 fixed a second, independent cause
16
+ (`Meta.tsx`'s stale-tag cleanup racing React 19's own `<title>`/`<meta>`
17
+ hoisting), and the exact same uncaught error still reproduced
18
+ intermittently with BOTH fixes shipped - some remaining React/router
19
+ timing edge case neither touches. Reinstated as a permanent safety net
20
+ rather than a fix for one specific bug: whatever the next contributing
21
+ cause turns out to be, a reload of the current URL has recovered
22
+ cleanly in every reproduction so far.
23
+
24
+ ## [1.6.8] - 2026-07-31
25
+
26
+ ### Removed
27
+
28
+ - **`Recaptcha/Recaptcha.tsx` and its `<Recaptcha>` wrapper around the
29
+ app in `Providers.tsx`.** This mounted `GoogleReCaptchaProvider`
30
+ (`react-google-recaptcha-v3`) as a persistent context provider inside
31
+ the same Suspense tree as `Styles`/`Notifications`/`Setup`; that
32
+ provider's script/badge injection is async and not remount-safe, and
33
+ any remount of it left the DOM in a state that could throw an
34
+ uncaught `removeChild`/`insertBefore` error on the next reconciliation
35
+ anywhere near the root - the actual root cause of the frozen-
36
+ navigation bug 1.6.6/1.6.7 worked around. `@autobusal/hooks` 1.3.1
37
+ moved reCAPTCHA to the same on-demand, no-provider pattern Turnstile
38
+ already used (`useRecaptcha`'s external API is unchanged), so nothing
39
+ needs this wrapper anymore - removing it removes the remount risk at
40
+ the root instead of continuing to recover from it. `useGetSettings()`
41
+ et al are otherwise unaffected: `Styles`/`Notifications`/`Setup` still
42
+ share the outer boundary exactly as before, just without `Recaptcha`
43
+ in the tree.
44
+ - **`useRecoverFromDomCorruption()`'s global `removeChild`/`insertBefore`
45
+ listener**, added in 1.6.7 as a backstop specifically for the
46
+ `GoogleReCaptchaProvider` remount corruption above. With that provider
47
+ gone, its trigger no longer exists.
48
+
7
49
  ## [1.6.7] - 2026-07-31
8
50
 
9
51
  ### Added
package/Providers.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Suspense, useEffect } from 'react';
2
- import Recaptcha from './Recaptcha/Recaptcha';
3
2
  import Queries from './Queries/Queries';
4
3
  import Styles from './Styles/Styles';
5
4
  import Notifications from './Notifications/Notifications';
@@ -12,17 +11,25 @@ interface Props {
12
11
  }
13
12
 
14
13
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
15
- // Defensive backstop for the removeChild corruption described below: even
16
- // with Setup's suspension isolated, GoogleReCaptchaProvider's script/badge
17
- // injection is inherently async (a script tag's onload race against React's
18
- // synchronous mount/cleanup), so a rare remount can still leave the DOM in a
19
- // state that throws on the next reconciliation - and that throw is NOT
20
- // render-phase, so no ErrorBoundary catches it. Left alone, this strands the
21
- // user on whatever was on screen with a URL that no longer matches it and no
22
- // working navigation (the exact "stuck, can't log out" symptom). A hard
23
- // reload of the current URL always recovers cleanly (proven: the router's
24
- // history.pushState already applied), so self-heal with one, guarded against
25
- // looping if the error turns out to be persistent rather than transient.
14
+ // Permanent safety net, not a fix for one specific bug. Two real, separate
15
+ // causes of a client-side navigation silently never landing were found and
16
+ // fixed - GoogleReCaptchaProvider's remount corruption (removed entirely,
17
+ // see this package's 1.6.6-1.6.8 and @autobusal/hooks 1.3.1) and Meta.tsx's
18
+ // stale-tag cleanup racing React 19's own <title>/<meta> hoisting (see
19
+ // @autobusal/common 1.8.3) - and the exact same uncaught "Cannot read
20
+ // properties of null (reading 'removeChild')" still reproduced
21
+ // intermittently with both gone: some remaining React/router timing edge
22
+ // case neither fix touches. That class of error is thrown outside React's
23
+ // render phase, so no ErrorBoundary catches it, and it silently aborts
24
+ // whatever navigation was in flight - the URL updates (the router's
25
+ // history.pushState already ran) but the page itself never re-renders, with
26
+ // no visible error and no way to navigate out. A reload of the current URL
27
+ // has recovered cleanly in every reproduction so far, so: watch for this
28
+ // exact error signature anywhere in the app and self-heal with one reload,
29
+ // cooldown-guarded so a genuinely persistent error can't loop. Logout.tsx/
30
+ // LoginAs.tsx carry a second, more targeted watchdog (a plain timer, not
31
+ // error-triggered) for the two pages users actually get stuck on during
32
+ // impersonation - this one is the general backstop for everywhere else.
26
33
  const useRecoverFromDomCorruption = (): void => {
27
34
  useEffect(() => {
28
35
  const RECOVERY_KEY = 'autobusalDomRecoveryAt';
@@ -55,59 +62,38 @@ const Providers = ({ type, loading, children }: Props): JSX.Element => {
55
62
 
56
63
  return (
57
64
  // Styles and Setup both call useGetSettings() (a useSuspenseQuery) and
58
- // sit ABOVE the inner <Suspense> below, so on a cold settings cache their
59
- // suspension had no boundary to catch it and React threw error #426
60
- // ("a component suspended while responding to synchronous input"). This
61
- // outer boundary catches those two suspensions (showing `loading` during
62
- // the initial settings fetch). The QueryClient in <Queries> is created
63
- // at module scope, so remounting this subtree while suspended keeps the
64
- // same client and cache - no refetch loop.
65
- // Edited: Ferjolt Ozuni - Date: 2026-07-25
66
- // Recaptcha moved INSIDE Queries: it now reads the label's runtime
67
- // captcha settings via useGetSettings(), which needs the
68
- // QueryClientProvider (and suspends into the outer boundary on a cold
69
- // cache, like Styles/Setup).
65
+ // sit ABOVE the inner <Suspense> below, so on a cold settings cache
66
+ // their suspension had no boundary to catch it and React threw error
67
+ // #426 ("a component suspended while responding to synchronous input").
68
+ // This outer boundary catches those two suspensions (showing `loading`
69
+ // during the initial settings fetch). The QueryClient in <Queries> is
70
+ // created at module scope, so remounting this subtree while suspended
71
+ // keeps the same client and cache - no refetch loop.
70
72
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
71
- // Bug: Setup also calls useGetMenu() (a SEPARATE useSuspenseQuery, its
72
- // own cache key/promise) in its own render body, still inside this same
73
- // outer boundary. On a cold menu cache, that suspends AFTER Recaptcha/
74
- // Styles/Notifications above it have already committed - and a Suspense
75
- // boundary re-suspending after its content is already committed hides/
76
- // discards that whole committed subtree and mounts it again once ready.
77
- // Recaptcha's GoogleReCaptchaProvider isn't remount-safe: its script/
78
- // badge injection is async and outside React's DOM bookkeeping, so
79
- // remounting it left a duplicate, half-torn-down
80
- // <div class="grecaptcha-badge"> behind. That corrupted DOM state then
81
- // threw an uncaught "Cannot read properties of null (reading
82
- // 'removeChild')" the next time React reconciled anywhere near the root
83
- // - which is every route change, since Recaptcha wraps the whole router
84
- // - silently aborting the commit. The URL would update (the router's
85
- // history.pushState already ran) but the page itself would never
86
- // re-render, freezing navigation app-wide with no console-visible error
87
- // (uncaught DOM exceptions like this don't hit the render-phase-only
88
- // ErrorBoundary). Giving Setup its own inner boundary means its menu
89
- // suspension can only hide/retry ITSELF, not the already-committed
90
- // Recaptcha/Styles/Notifications above it - this closed off the
91
- // deterministic trigger, and useRecoverFromDomCorruption() above is the
92
- // backstop for whatever async-timing sliver still gets through.
73
+ // Setup also calls useGetMenu() (a SEPARATE useSuspenseQuery, its own
74
+ // cache key/promise) in its own render body. Kept in its own inner
75
+ // <Suspense> rather than sharing the outer one: a cold menu cache
76
+ // suspending after Styles/Notifications above it have already committed
77
+ // would otherwise hide/discard that already-committed content and
78
+ // remount it once ready - harmless for Styles/Notifications themselves,
79
+ // but a needless remount of anything stateful sitting there is worth
80
+ // avoiding on principle.
93
81
  <Suspense fallback={ loading }>
94
82
  <Queries>
95
- <Recaptcha>
96
- <Styles>
97
- <Notifications>
98
- <Suspense fallback={ loading }>
99
- <Setup type={ type }>
100
- <Suspense fallback={ loading }>
101
- { children }
102
- </Suspense>
103
- </Setup>
104
- </Suspense>
105
- </Notifications>
106
- </Styles>
107
- </Recaptcha>
83
+ <Styles>
84
+ <Notifications>
85
+ <Suspense fallback={ loading }>
86
+ <Setup type={ type }>
87
+ <Suspense fallback={ loading }>
88
+ { children }
89
+ </Suspense>
90
+ </Setup>
91
+ </Suspense>
92
+ </Notifications>
93
+ </Styles>
108
94
  </Queries>
109
95
  </Suspense>
110
96
  );
111
97
  };
112
98
 
113
- export default Providers;
99
+ export default Providers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.6.7",
3
+ "version": "1.6.9",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"
@@ -1,36 +0,0 @@
1
- import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
2
- import { useGetSettings } from '../services';
3
-
4
- interface Props {
5
- children: JSX.Element
6
- }
7
-
8
- // Edited: Ferjolt Ozuni - Date: 2026-07-25
9
- // The captcha provider + site key are now the label's runtime settings
10
- // (/api/settings/get, managed at /admin/label?type=captcha) instead of the
11
- // build-time VITE_RECAPTCHA_KEY - which stays only as a fallback for labels
12
- // without a captcha config yet. The Google provider script is mounted ONLY
13
- // when reCAPTCHA is the active provider; Cloudflare Turnstile needs no
14
- // app-wide provider (its script is loaded on demand by useRecaptcha in
15
- // @autobusal/hooks). NB: this component now calls useGetSettings(), so it
16
- // must sit INSIDE <Queries> (see Providers.tsx).
17
- const Recaptcha = ({ children }: Props): JSX.Element => {
18
- const { data } = useGetSettings();
19
-
20
- const provider = data?.captcha?.provider
21
- ?? (import.meta.env.VITE_RECAPTCHA_KEY ? 'recaptcha' : 'none');
22
-
23
- const siteKey = data?.captcha?.site_key ?? import.meta.env.VITE_RECAPTCHA_KEY;
24
-
25
- if (provider !== 'recaptcha' || !siteKey) {
26
- return children;
27
- }
28
-
29
- return (
30
- <GoogleReCaptchaProvider reCaptchaKey={ siteKey }>
31
- { children }
32
- </GoogleReCaptchaProvider>
33
- );
34
- };
35
-
36
- export default Recaptcha;