@autobusal/providers 1.6.7 → 1.6.8

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,31 @@ 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.8] - 2026-07-31
8
+
9
+ ### Removed
10
+
11
+ - **`Recaptcha/Recaptcha.tsx` and its `<Recaptcha>` wrapper around the
12
+ app in `Providers.tsx`.** This mounted `GoogleReCaptchaProvider`
13
+ (`react-google-recaptcha-v3`) as a persistent context provider inside
14
+ the same Suspense tree as `Styles`/`Notifications`/`Setup`; that
15
+ provider's script/badge injection is async and not remount-safe, and
16
+ any remount of it left the DOM in a state that could throw an
17
+ uncaught `removeChild`/`insertBefore` error on the next reconciliation
18
+ anywhere near the root - the actual root cause of the frozen-
19
+ navigation bug 1.6.6/1.6.7 worked around. `@autobusal/hooks` 1.3.1
20
+ moved reCAPTCHA to the same on-demand, no-provider pattern Turnstile
21
+ already used (`useRecaptcha`'s external API is unchanged), so nothing
22
+ needs this wrapper anymore - removing it removes the remount risk at
23
+ the root instead of continuing to recover from it. `useGetSettings()`
24
+ et al are otherwise unaffected: `Styles`/`Notifications`/`Setup` still
25
+ share the outer boundary exactly as before, just without `Recaptcha`
26
+ in the tree.
27
+ - **`useRecoverFromDomCorruption()`'s global `removeChild`/`insertBefore`
28
+ listener**, added in 1.6.7 as a backstop specifically for the
29
+ `GoogleReCaptchaProvider` remount corruption above. With that provider
30
+ gone, its trigger no longer exists.
31
+
7
32
  ## [1.6.7] - 2026-07-31
8
33
 
9
34
  ### Added
package/Providers.tsx CHANGED
@@ -1,5 +1,4 @@
1
- import { Suspense, useEffect } from 'react';
2
- import Recaptcha from './Recaptcha/Recaptcha';
1
+ import { Suspense } from 'react';
3
2
  import Queries from './Queries/Queries';
4
3
  import Styles from './Styles/Styles';
5
4
  import Notifications from './Notifications/Notifications';
@@ -11,103 +10,39 @@ interface Props {
11
10
  children: JSX.Element
12
11
  }
13
12
 
14
- // 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.
26
- const useRecoverFromDomCorruption = (): void => {
27
- useEffect(() => {
28
- const RECOVERY_KEY = 'autobusalDomRecoveryAt';
29
-
30
- const onError = (event: ErrorEvent): void => {
31
- const message = event.message || '';
32
-
33
- if (!message.includes('removeChild') && !message.includes('insertBefore')) {
34
- return;
35
- }
36
-
37
- const last = Number(sessionStorage.getItem(RECOVERY_KEY) || 0);
38
-
39
- if (Date.now() - last < 10000) {
40
- return;
41
- }
42
-
43
- sessionStorage.setItem(RECOVERY_KEY, String(Date.now()));
44
- window.location.reload();
45
- };
46
-
47
- window.addEventListener('error', onError);
48
-
49
- return () => window.removeEventListener('error', onError);
50
- }, []);
51
- };
52
-
53
- const Providers = ({ type, loading, children }: Props): JSX.Element => {
54
- useRecoverFromDomCorruption();
55
-
56
- return (
57
- // 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).
70
- // 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.
93
- <Suspense fallback={ loading }>
94
- <Queries>
95
- <Recaptcha>
96
- <Styles>
97
- <Notifications>
13
+ const Providers = ({ type, loading, children }: Props): JSX.Element => (
14
+ // Styles and Setup both call useGetSettings() (a useSuspenseQuery) and sit
15
+ // ABOVE the inner <Suspense> below, so on a cold settings cache their
16
+ // suspension had no boundary to catch it and React threw error #426
17
+ // ("a component suspended while responding to synchronous input"). This
18
+ // outer boundary catches those two suspensions (showing `loading` during
19
+ // the initial settings fetch). The QueryClient in <Queries> is created at
20
+ // module scope, so remounting this subtree while suspended keeps the same
21
+ // client and cache - no refetch loop.
22
+ // Edited: Ferjolt Ozuni - Date: 2026-07-31
23
+ // Setup also calls useGetMenu() (a SEPARATE useSuspenseQuery, its own
24
+ // cache key/promise) in its own render body. Kept in its own inner
25
+ // <Suspense> rather than sharing the outer one: a cold menu cache
26
+ // suspending after Styles/Notifications above it have already committed
27
+ // would otherwise hide/discard that already-committed content and remount
28
+ // it once ready - harmless for Styles/Notifications themselves, but a
29
+ // needless remount of anything stateful sitting there is worth avoiding on
30
+ // principle.
31
+ <Suspense fallback={ loading }>
32
+ <Queries>
33
+ <Styles>
34
+ <Notifications>
35
+ <Suspense fallback={ loading }>
36
+ <Setup type={ type }>
98
37
  <Suspense fallback={ loading }>
99
- <Setup type={ type }>
100
- <Suspense fallback={ loading }>
101
- { children }
102
- </Suspense>
103
- </Setup>
38
+ { children }
104
39
  </Suspense>
105
- </Notifications>
106
- </Styles>
107
- </Recaptcha>
108
- </Queries>
109
- </Suspense>
110
- );
111
- };
112
-
113
- export default Providers;
40
+ </Setup>
41
+ </Suspense>
42
+ </Notifications>
43
+ </Styles>
44
+ </Queries>
45
+ </Suspense>
46
+ );
47
+
48
+ 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.8",
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;