@mohasinac/appkit 4.5.0 → 4.5.2
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.
|
@@ -12,13 +12,34 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
12
12
|
* route overrides. Everything else defaults to appkit's ROUTES.
|
|
13
13
|
*/
|
|
14
14
|
import { useRouter } from "next/navigation";
|
|
15
|
+
import { useEffect, useRef, useState } from "react";
|
|
15
16
|
import { ProtectedRoute } from "../../../../features/auth/components/Guards";
|
|
16
17
|
import { useSession } from "../../../../react/contexts/SessionContext";
|
|
17
18
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
18
19
|
export function RoleGuard({ role, requireAuth = true, loginPath, unauthorizedPath, loadingComponent, children, }) {
|
|
19
|
-
const { user, loading } = useSession();
|
|
20
|
+
const { user, loading, refreshUser } = useSession();
|
|
20
21
|
const router = useRouter();
|
|
21
|
-
|
|
22
|
+
// SessionContext only refreshes role/disabled periodically (every 5
|
|
23
|
+
// minutes) or on a hard reload/re-login — never on ordinary client-side
|
|
24
|
+
// navigation. A user just approved as a seller, or just un-banned, would
|
|
25
|
+
// otherwise keep getting redirected to /unauthorized by this exact guard
|
|
26
|
+
// for up to 5 minutes even though the same check against live Firestore
|
|
27
|
+
// data would already pass. Force one fresh check per mount (i.e. once per
|
|
28
|
+
// navigation into a role-gated layout, since Next.js layouts persist
|
|
29
|
+
// across sibling route changes) before trusting a denial.
|
|
30
|
+
const hasRefreshedRef = useRef(false);
|
|
31
|
+
const [verifying, setVerifying] = useState(true);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (loading || hasRefreshedRef.current)
|
|
34
|
+
return;
|
|
35
|
+
hasRefreshedRef.current = true;
|
|
36
|
+
if (!user) {
|
|
37
|
+
setVerifying(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
refreshUser().finally(() => setVerifying(false));
|
|
41
|
+
}, [loading, user, refreshUser]);
|
|
42
|
+
return (_jsx(ProtectedRoute, { user: user, loading: loading || verifying, requireAuth: requireAuth, requireRole: role, onNavigate: (path) => router.push(path), routes: {
|
|
22
43
|
loginPath: loginPath ?? String(ROUTES.AUTH.LOGIN),
|
|
23
44
|
unauthorizedPath: unauthorizedPath ?? String(ROUTES.ERRORS.UNAUTHORIZED),
|
|
24
45
|
}, loadingComponent: loadingComponent, children: children }));
|
|
@@ -7,7 +7,6 @@ import { apiClient } from "../../../http";
|
|
|
7
7
|
import { useApiMutation } from "../../../client";
|
|
8
8
|
import { useSession } from "../../../react";
|
|
9
9
|
import { ACCOUNT_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
|
-
import { isAdminUser } from "../../auth/role-predicates";
|
|
11
10
|
import { TesterChecklistStepRow } from "./TesterChecklistStepRow";
|
|
12
11
|
function matchesQuery(item, query) {
|
|
13
12
|
if (!query)
|
|
@@ -40,6 +39,11 @@ export function TesterHubView({ sandboxExpiresAt }) {
|
|
|
40
39
|
const res = await apiClient.get(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST);
|
|
41
40
|
return res?.data ?? res;
|
|
42
41
|
},
|
|
42
|
+
// The API route resolves isTester/canTestAdmin fresh from Firestore on
|
|
43
|
+
// every request, so retrying a 403 (a role that genuinely lacks access)
|
|
44
|
+
// wastes calls for no benefit — but a real access grant should still be
|
|
45
|
+
// visible on the very next request, not cached as a permanent failure.
|
|
46
|
+
retry: false,
|
|
43
47
|
});
|
|
44
48
|
const upsertMutation = useApiMutation({
|
|
45
49
|
mutationFn: async (vars) => apiClient.put(ACCOUNT_ENDPOINTS.TESTER_CHECKLIST_ITEM_BY_ID(vars.checklistItemId), vars.patch),
|
|
@@ -47,9 +51,21 @@ export function TesterHubView({ sandboxExpiresAt }) {
|
|
|
47
51
|
void queryClient.invalidateQueries({ queryKey: ["user", "tester-checklist"] });
|
|
48
52
|
},
|
|
49
53
|
});
|
|
50
|
-
|
|
54
|
+
// Gate on the live API response, not the client-cached session snapshot.
|
|
55
|
+
// SessionContext only refreshes role/isTester/canTestAdmin periodically
|
|
56
|
+
// (every 5 minutes) or on a hard reload/re-login — never on ordinary
|
|
57
|
+
// client-side navigation — so a flag an admin just granted would
|
|
58
|
+
// otherwise still read as "Testers only" here for up to 5 minutes even
|
|
59
|
+
// though the same request already succeeds server-side. The tester-
|
|
60
|
+
// checklist API resolves these fields fresh from Firestore on every call,
|
|
61
|
+
// so trusting its actual response is both more accurate and immediate.
|
|
62
|
+
const isForbidden = query.isError && query.error?.status === 403;
|
|
63
|
+
if (isForbidden) {
|
|
51
64
|
return (_jsx(Alert, { variant: "warning", title: "Testers only", children: "This page is only available to accounts flagged as testers (and admins). Contact an admin if you believe this is a mistake." }));
|
|
52
65
|
}
|
|
66
|
+
if (query.isLoading) {
|
|
67
|
+
return _jsx(Text, { color: "muted", children: "Loading checklist\u2026" });
|
|
68
|
+
}
|
|
53
69
|
// Items arrive pre-sorted by catalog `order` (listActive() orders by it).
|
|
54
70
|
const items = query.data?.items ?? [];
|
|
55
71
|
const filtered = items.filter((item) => matchesQuery(item, search));
|
|
@@ -72,7 +88,7 @@ export function TesterHubView({ sandboxExpiresAt }) {
|
|
|
72
88
|
const handleSaveNote = async (checklistItemId, comment, screenshotUrl) => {
|
|
73
89
|
await upsertMutation.mutateAsync({ checklistItemId, patch: { comment, screenshotUrl } });
|
|
74
90
|
};
|
|
75
|
-
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }),
|
|
91
|
+
return (_jsxs(Stack, { gap: "lg", children: [_jsx(Heading, { level: 1, children: "Tester Hub" }), _jsx(Text, { color: "muted", children: "Work through the checklist below and answer Yes/No for each test case. Add a comment and screenshot wherever something looks off \u2014 colors, styles, readability, bugs. Your answers save automatically." }), sandboxExpiresAt && (_jsxs(Alert, { variant: "info", title: "Shared test sandbox", children: ["The shared test store/products/categories expire on ", new Date(sandboxExpiresAt).toLocaleDateString(), "."] })), _jsx(FieldInput, { name: "checklist-search", label: "Search test cases", value: search, onChange: setSearch, placeholder: "Search by title or route, e.g. checkout, /store/payouts..." }), sortedPhaseNumbers.map((phaseNumber) => {
|
|
76
92
|
const groups = phases.get(phaseNumber);
|
|
77
93
|
const phaseItemCount = Array.from(groups.values())
|
|
78
94
|
.flatMap((g) => Array.from(g.pages.values()))
|