@mohasinac/appkit 4.35.3 → 4.35.4
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.
|
@@ -3,6 +3,8 @@ import { useEffect } from "react";
|
|
|
3
3
|
import { useQuery } from "@tanstack/react-query";
|
|
4
4
|
import { apiClient } from "../../../http";
|
|
5
5
|
import { useToast } from "../../../ui";
|
|
6
|
+
import { useOptionalSession } from "../../../react/contexts/SessionContext";
|
|
7
|
+
import { isAdminUser, isTesterUser } from "../../auth/role-predicates";
|
|
6
8
|
import { PRODUCT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
9
|
export function useProducts(params = {}, opts) {
|
|
8
10
|
const { showToast } = useToast();
|
|
@@ -73,11 +75,46 @@ export function useProducts(params = {}, opts) {
|
|
|
73
75
|
sp.set(key, value);
|
|
74
76
|
}
|
|
75
77
|
const qs = sp.toString();
|
|
78
|
+
/*
|
|
79
|
+
* 🛑 A TESTER MUST REFETCH; EVERYONE ELSE MUST KEEP THE CACHED SSR PAINT.
|
|
80
|
+
*
|
|
81
|
+
* Sandbox rows are correctly hidden from the public. But the SSR listing views
|
|
82
|
+
* pass no `viewer` to `listPublicProducts`, so the server-rendered grid is the
|
|
83
|
+
* anonymous one for EVERY visitor — and with `initialData` + `staleTime:
|
|
84
|
+
* Infinity` the client never refetches, so a signed-in tester saw a catalogue
|
|
85
|
+
* with no sandbox fixtures in it, on every listing page. The items were
|
|
86
|
+
* reachable by direct URL, which is what made it look like a data problem
|
|
87
|
+
* rather than a visibility one.
|
|
88
|
+
*
|
|
89
|
+
* The fix is NOT to read the session in those SSR views. `getServerSessionUser`
|
|
90
|
+
* is a dynamic API, and calling it there would make every public listing page
|
|
91
|
+
* per-request — no caching, a billed invocation per visitor, and every
|
|
92
|
+
* `revalidate` silently overridden. That is Root Cause #82, and paying it for a
|
|
93
|
+
* testing affordance would be a poor trade.
|
|
94
|
+
*
|
|
95
|
+
* `/api/products` ALREADY resolves the session and passes `viewer`. So the only
|
|
96
|
+
* thing missing was a reason for the client to ask again: the query key now
|
|
97
|
+
* carries the viewer class, giving testers their own cache entry, and their
|
|
98
|
+
* `staleTime` is not frozen. Anonymous visitors keep the cached SSR data
|
|
99
|
+
* untouched and issue no extra request.
|
|
100
|
+
*
|
|
101
|
+
* `useOptionalSession` rather than `useAuth()` on purpose — `useSession`
|
|
102
|
+
* THROWS outside a provider, and making a public appkit hook require one would
|
|
103
|
+
* be a breaking contract change (Root Cause #20). Absent provider degrades to
|
|
104
|
+
* "not a tester", which is the fail-closed answer.
|
|
105
|
+
*/
|
|
106
|
+
const session = useOptionalSession();
|
|
107
|
+
const viewer = session?.user;
|
|
108
|
+
const canSeeTestData = isTesterUser(viewer) || isAdminUser(viewer);
|
|
76
109
|
const query = useQuery({
|
|
77
|
-
queryKey: ["products", qs],
|
|
110
|
+
queryKey: ["products", qs, canSeeTestData ? "with-test-data" : "public"],
|
|
78
111
|
queryFn: () => apiClient.get(`${PRODUCT_ENDPOINTS.LIST}${qs ? `?${qs}` : ""}`),
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
// The SSR paint is the anonymous view, so it is only a valid starting point
|
|
113
|
+
// for an anonymous viewer. Handing it to a tester would show them the very
|
|
114
|
+
// rows this refetch exists to reveal.
|
|
115
|
+
initialData: canSeeTestData ? undefined : opts?.initialData,
|
|
116
|
+
staleTime: opts?.staleTime ??
|
|
117
|
+
(!canSeeTestData && opts?.initialData != null ? Infinity : 0),
|
|
81
118
|
enabled: opts?.enabled,
|
|
82
119
|
});
|
|
83
120
|
const warning = query.data?.warning;
|
|
@@ -95,6 +95,19 @@ export interface SessionProviderProps {
|
|
|
95
95
|
}
|
|
96
96
|
export declare function SessionProvider({ children, initialUser, endpoints: endpointOverrides, onSignOutInvalidate, signOutInvalidationKeys, }: SessionProviderProps): React.JSX.Element;
|
|
97
97
|
export declare function useSession(): SessionContextValue;
|
|
98
|
+
/**
|
|
99
|
+
* The session if a provider is mounted, `null` if not — never throws.
|
|
100
|
+
*
|
|
101
|
+
* `useSession` throwing is right for a component that genuinely cannot work
|
|
102
|
+
* signed-out. It is wrong for library code that merely wants to ADJUST for the
|
|
103
|
+
* viewer, because requiring a provider there turns an optional refinement into a
|
|
104
|
+
* breaking contract change for every consumer (Root Cause #20).
|
|
105
|
+
*
|
|
106
|
+
* Use this only where "no provider" has a sensible, fail-closed answer — e.g.
|
|
107
|
+
* `useProducts`, which treats an absent session as "not a tester" and keeps the
|
|
108
|
+
* public cached view.
|
|
109
|
+
*/
|
|
110
|
+
export declare function useOptionalSession(): SessionContextValue | null;
|
|
98
111
|
export declare function useAuth(): {
|
|
99
112
|
user: SessionUser | null;
|
|
100
113
|
loading: boolean;
|
|
@@ -394,6 +394,21 @@ export function useSession() {
|
|
|
394
394
|
}
|
|
395
395
|
return context;
|
|
396
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* The session if a provider is mounted, `null` if not — never throws.
|
|
399
|
+
*
|
|
400
|
+
* `useSession` throwing is right for a component that genuinely cannot work
|
|
401
|
+
* signed-out. It is wrong for library code that merely wants to ADJUST for the
|
|
402
|
+
* viewer, because requiring a provider there turns an optional refinement into a
|
|
403
|
+
* breaking contract change for every consumer (Root Cause #20).
|
|
404
|
+
*
|
|
405
|
+
* Use this only where "no provider" has a sensible, fail-closed answer — e.g.
|
|
406
|
+
* `useProducts`, which treats an absent session as "not a tester" and keeps the
|
|
407
|
+
* public cached view.
|
|
408
|
+
*/
|
|
409
|
+
export function useOptionalSession() {
|
|
410
|
+
return useContext(SessionContext) ?? null;
|
|
411
|
+
}
|
|
397
412
|
export function useAuth() {
|
|
398
413
|
const { user, loading, refreshUser } = useSession();
|
|
399
414
|
return { user, loading, refreshUser };
|
|
@@ -83,7 +83,26 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
|
|
|
83
83
|
adjustZoom(e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP);
|
|
84
84
|
}, [adjustZoom]);
|
|
85
85
|
const handleKeyDown = useCallback((e) => {
|
|
86
|
+
/*
|
|
87
|
+
* Escape unwinds ONE level: fullscreen first, the lightbox second.
|
|
88
|
+
*
|
|
89
|
+
* Unconditionally calling `onClose()` meant Escape in fullscreen tore the
|
|
90
|
+
* whole lightbox down while the browser was natively exiting fullscreen at
|
|
91
|
+
* the same moment — the user asked to leave fullscreen and lost the image
|
|
92
|
+
* they were looking at. The Exit-fullscreen BUTTON behaved correctly, which
|
|
93
|
+
* is what made this read as a keyboard quirk rather than a bug.
|
|
94
|
+
*
|
|
95
|
+
* Exiting explicitly rather than relying on the browser's own handling: the
|
|
96
|
+
* `fullscreenchange` listener above is what clears `isFullscreen`, so going
|
|
97
|
+
* through the same path keeps the button label and this state in agreement
|
|
98
|
+
* whichever way the user leaves.
|
|
99
|
+
*/
|
|
86
100
|
if (e.key === "Escape") {
|
|
101
|
+
if (isFullscreen) {
|
|
102
|
+
const doc = document;
|
|
103
|
+
(document.exitFullscreen?.() ?? doc.webkitExitFullscreen?.())?.catch?.(() => { });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
87
106
|
onClose();
|
|
88
107
|
return;
|
|
89
108
|
}
|
|
@@ -112,7 +131,11 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
|
|
|
112
131
|
setRotation(0);
|
|
113
132
|
return;
|
|
114
133
|
}
|
|
115
|
-
},
|
|
134
|
+
},
|
|
135
|
+
// `isFullscreen` is a real dependency now — Escape branches on it, and a
|
|
136
|
+
// stale closure would send the user back to the pre-fix behaviour exactly
|
|
137
|
+
// when they are in fullscreen.
|
|
138
|
+
[onClose, navigate, adjustZoom, isFullscreen]);
|
|
116
139
|
// Focus overlay on open for keyboard to work
|
|
117
140
|
useEffect(() => {
|
|
118
141
|
if (isOpen)
|