Skip to content
Source
Sign inSign up

PermissionUpdateNotice.ts

83 lines2.9 KB

1import { sessionHasLegacyPermissions, type EnvironmentId } from "@t3tools/contracts";
2import type { AnyRouter } from "@tanstack/react-router";
3import * as Schema from "effect/Schema";
4import { Atom } from "effect/reactivity";
5​
6import { getLocalStorageItem, setLocalStorageItem } from "../hooks/useLocalStorage";
7import { appAtomRegistry } from "../rpc/atomRegistry";
8import { environmentSummaries } from "../state/presentation";
9import { environmentSession } from "../state/session";
10import { toastManager } from "./ui/toast";
11​
12interface LegacyPermissionEnvironment {
13 readonly environmentId: EnvironmentId;
14 readonly label: string;
16​
17/** Environments whose settled session still carries pre-scope permissions. */
18const legacyPermissionEnvironmentsAtom = Atom.make((get) =>
19 get(environmentSummaries.identitiesAtom).filter(({ environmentId }) => {
20 const session = get(environmentSession.sessionStateAtom(environmentId));
21 return (
22 session._tag === "Success" && !session.waiting && sessionHasLegacyPermissions(session.value)
23 );
24 }),
25).pipe(Atom.withLabel("web-legacy-permission-environments"));
26​
27const dismissalKey = (environmentId: EnvironmentId) =>
28 `t3code:permission-update:v1:${environmentId}`;
29​
30function showPermissionNotice(
31 router: AnyRouter,
32 { environmentId, label }: LegacyPermissionEnvironment,
33) {
34 const key = dismissalKey(environmentId);
35 const persistDismissal = () => {
36 try {
37 setLocalStorageItem(key, true, Schema.Boolean);
38 } catch {}
39 };
40 const dismiss = () => {
41 persistDismissal();
42 toastManager.close(id);
43 };
44 const id = toastManager.add({
45 title: `Permissions have changed for ${label}`,
46 description:
47 "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.",
48 timeout: 0,
49 onClose: persistDismissal,
50 actionProps: {
51 children: "Open Sharing & access",
52 onClick: () => {
53 dismiss();
54 void router.navigate({ to: "/settings/access" });
55 },
56 },
57 data: {
58 actionLayout: "stacked-end",
59 secondaryActionProps: { children: "Dismiss", onClick: dismiss },
60 secondaryActionVariant: "ghost",
61 },
62 });
64​
65/** Asks once per environment, until dismissed, to re-pair a connection that predates scoped permissions. */
66export function startPermissionUpdateNotices(router: AnyRouter): () => void {
67 const shown = new Set<EnvironmentId>();
68 return appAtomRegistry.subscribe(
69 legacyPermissionEnvironmentsAtom,
70 (environments) => {
71 for (const environment of environments) {
72 if (shown.has(environment.environmentId)) continue;
73 try {
74 if (getLocalStorageItem(dismissalKey(environment.environmentId), Schema.Boolean))
75 continue;
76 } catch {}
77 shown.add(environment.environmentId);
78 showPermissionNotice(router, environment);
79 }
80 },
81 { immediate: true },
82 );