1import { useAtomValue } from "@effect/atom-react";
2import { sessionHasLegacyPermissions, type EnvironmentId } from "@t3tools/contracts";
3import { useNavigate } from "@tanstack/react-router";
4import * as Schema from "effect/Schema";
5import { useEffect } from "react";
6
7import { getLocalStorageItem, setLocalStorageItem } from "../hooks/useLocalStorage";
8import { useEnvironments } from "../state/environments";
9import { environmentSession } from "../state/session";
10import { toastManager } from "./ui/toast";
11
12// Keep an active toast across remounts, including Strict Mode effect replay.
13const shown = new Set<EnvironmentId>();
14
15function EnvironmentPermissionNotice({
16 environmentId,
17 label,
18}: {
19 environmentId: EnvironmentId;
20 label: string;
21}) {
22 const session = useAtomValue(environmentSession.sessionStateAtom(environmentId));
23 const navigate = useNavigate();
24 useEffect(() => {
25 if (
26 session._tag !== "Success" ||
27 session.waiting ||
28 !sessionHasLegacyPermissions(session.value)
29 )
30 return;
31 if (shown.has(environmentId)) return;
32 const key = `t3code:permission-update:v1:${environmentId}`;
33 try {
34 if (getLocalStorageItem(key, Schema.Boolean)) return;
35 } catch {
36 // An unavailable store must not prevent the notice.
37 }
38 shown.add(environmentId);
39 const persistDismissal = () => {
40 try {
41 setLocalStorageItem(key, true, Schema.Boolean);
42 } catch {
43 // The in-memory marker still prevents repeats during this launch.
44 }
45 };
46 const dismiss = () => {
47 persistDismissal();
48 toastManager.close(id);
49 };
50 const id = toastManager.add({
51 title: `Permissions have changed for ${label}`,
52 description:
53 "This connection still uses the old permissions, so some actions may no longer be available. Pair again using a new link with the permissions you need.",
54 timeout: 0,
55 onClose: persistDismissal,
56 actionProps: {
57 children: "Open Connections",
58 onClick: () => {
59 dismiss();
60 void navigate({ to: "/settings/connections" });
61 },
62 },
63 data: {
64 actionLayout: "stacked-end",
65 secondaryActionProps: { children: "Dismiss", onClick: dismiss },
66 secondaryActionVariant: "ghost",
67 },
68 });
69 }, [environmentId, label, navigate, session]);
70 return null;
71}
72
73export function PermissionUpdateNotice() {
74 const { environments } = useEnvironments();
75 return environments.map(({ environmentId, label }) => (
76 <EnvironmentPermissionNotice key={environmentId} environmentId={environmentId} label={label} />
77 ));
78}