@autobusal/providers 1.6.6 → 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,51 @@ 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
+
32
+ ## [1.6.7] - 2026-07-31
33
+
34
+ ### Added
35
+
36
+ - **`Providers.tsx`: self-healing reload if the DOM ever corrupts near the
37
+ root.** 1.6.6 closed the deterministic trigger for the
38
+ `GoogleReCaptchaProvider` remount corruption (see 1.6.6 below), but its
39
+ script/badge injection is still fundamentally async - a script tag's
40
+ `onload` racing React's synchronous mount/cleanup - so a rare remount
41
+ can still leave the DOM in a state that throws an uncaught
42
+ `removeChild`/`insertBefore` error on the next reconciliation anywhere
43
+ near the root. That class of error isn't render-phase, so no
44
+ `ErrorBoundary` catches it, and it silently aborts whatever navigation
45
+ was in flight: the URL updates but the page never re-renders, with no
46
+ visible error and no way to navigate out. A reload of the current URL
47
+ always recovers cleanly (the router's `history.pushState` already
48
+ applied), so `Providers` now listens for exactly this error signature
49
+ and self-heals with one reload, guarded by a 10s `sessionStorage`
50
+ cooldown so a genuinely persistent error can't loop.
51
+
7
52
  ## [1.6.6] - 2026-07-31
8
53
 
9
54
  ### Fixed
package/Providers.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  import { Suspense } 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';
@@ -20,47 +19,30 @@ const Providers = ({ type, loading, children }: Props): JSX.Element => (
20
19
  // the initial settings fetch). The QueryClient in <Queries> is created at
21
20
  // module scope, so remounting this subtree while suspended keeps the same
22
21
  // client and cache - no refetch loop.
23
- // Edited: Ferjolt Ozuni - Date: 2026-07-25
24
- // Recaptcha moved INSIDE Queries: it now reads the label's runtime captcha
25
- // settings via useGetSettings(), which needs the QueryClientProvider (and
26
- // suspends into the outer boundary on a cold cache, like Styles/Setup).
27
22
  // Edited: Ferjolt Ozuni - Date: 2026-07-31
28
- // Bug: Setup also calls useGetMenu() (a SEPARATE useSuspenseQuery, its own
29
- // cache key/promise) in its own render body, still inside this same outer
30
- // boundary. On a cold menu cache, that suspends AFTER Recaptcha/Styles/
31
- // Notifications above it have already committed - and a Suspense boundary
32
- // re-suspending after its content is already committed hides/discards that
33
- // whole committed subtree and mounts it again once ready. Recaptcha's
34
- // GoogleReCaptchaProvider isn't remount-safe: its script/badge injection is
35
- // async and outside React's DOM bookkeeping, so remounting it left a
36
- // duplicate, half-torn-down <div class="grecaptcha-badge"> behind. That
37
- // corrupted DOM state then threw an uncaught "Cannot read properties of
38
- // null (reading 'removeChild')" the next time React reconciled anywhere
39
- // near the root - which is every route change, since Recaptcha wraps the
40
- // whole router - silently aborting the commit. The URL would update (the
41
- // router's history.pushState already ran) but the page itself would never
42
- // re-render, freezing navigation app-wide with no console-visible error
43
- // (uncaught DOM exceptions like this don't hit the render-phase-only
44
- // ErrorBoundary). Giving Setup its own inner boundary means its menu
45
- // suspension can only hide/retry ITSELF, not the already-committed
46
- // Recaptcha/Styles/Notifications above it.
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.
47
31
  <Suspense fallback={ loading }>
48
32
  <Queries>
49
- <Recaptcha>
50
- <Styles>
51
- <Notifications>
52
- <Suspense fallback={ loading }>
53
- <Setup type={ type }>
54
- <Suspense fallback={ loading }>
55
- { children }
56
- </Suspense>
57
- </Setup>
58
- </Suspense>
59
- </Notifications>
60
- </Styles>
61
- </Recaptcha>
33
+ <Styles>
34
+ <Notifications>
35
+ <Suspense fallback={ loading }>
36
+ <Setup type={ type }>
37
+ <Suspense fallback={ loading }>
38
+ { children }
39
+ </Suspense>
40
+ </Setup>
41
+ </Suspense>
42
+ </Notifications>
43
+ </Styles>
62
44
  </Queries>
63
45
  </Suspense>
64
46
  );
65
47
 
66
- export default Providers;
48
+ export default Providers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.6.6",
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;