1import * as Dialog from "../../components/ui/Dialog";
2import { sessionHasLegacyPermissions } from "@t3tools/contracts";
3import { Atom } from "effect/reactivity";
4import * as SecureStore from "expo-secure-store";
5
6import { whenProtectedDataAvailable } from "../../lib/protectedData";
7import { appAtomRegistry } from "../../state/atom-registry";
8import { projectEnvironmentPresentation } from "../../state/environments";
9import { environmentPresentations } from "../../state/presentation";
10import { environmentSession } from "../../state/session";
11
12const storageKey = "t3code.permission-update.v1";
13
14const legacyPermissionEnvironmentsAtom = Atom.make((get) =>
15 [...get(environmentPresentations.presentationsAtom).entries()].flatMap(
16 ([environmentId, presentation]) => {
17 const session = get(environmentSession.sessionStateAtom(environmentId));
18 return session._tag === "Success" &&
19 !session.waiting &&
20 sessionHasLegacyPermissions(session.value)
21 ? [projectEnvironmentPresentation(environmentId, presentation)]
22 : [];
23 },
24 ),
25).pipe(Atom.withLabel("mobile:legacy-permission-environments"));
26
27async function loadDismissedEnvironmentIds(): Promise<Set<string>> {
28 try {
29 await whenProtectedDataAvailable();
30 const raw = await SecureStore.getItemAsync(storageKey);
31 const value: unknown = raw === null ? [] : JSON.parse(raw);
32 return new Set(
33 Array.isArray(value) ? value.filter((id): id is string => typeof id === "string") : [],
34 );
35 } catch {
36 return new Set();
37 }
38}
39
40/**
41 * Tells the user, once per environment, that a paired session still carries
42 * the legacy permission set. App-lifetime; presents one dialog at a time.
43 */
44export function installPermissionUpdateNotice(): () => void {
45 let dismissed: Set<string> | null = null;
46 const shownThisLaunch = new Set<string>();
47 let showing = false;
48 let disposed = false;
49
50 const presentNext = () => {
51 if (disposed || dismissed === null || showing) return;
52 const known = dismissed;
53 const environment = appAtomRegistry
54 .get(legacyPermissionEnvironmentsAtom)
55 .find(
56 ({ environmentId }) => !known.has(environmentId) && !shownThisLaunch.has(environmentId),
57 );
58 if (!environment) return;
59 showing = true;
60 shownThisLaunch.add(environment.environmentId);
61 const dismiss = () => {
62 known.add(environment.environmentId);
63 showing = false;
64 void SecureStore.setItemAsync(storageKey, JSON.stringify([...known])).catch(() => {});
65 presentNext();
66 };
67 Dialog.alert(
68 `Permissions have changed for ${environment.label}`,
69 "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.",
70 [{ text: "Got it", onPress: dismiss }],
71 { cancelable: false },
72 );
73 };
74
75 const unsubscribe = appAtomRegistry.subscribe(legacyPermissionEnvironmentsAtom, presentNext, {
76 immediate: true,
77 });
78 void loadDismissedEnvironmentIds().then((ids) => {
79 dismissed = ids;
80 presentNext();
81 });
82 return () => {
83 disposed = true;
84 unsubscribe();
85 };
86}