1import { useAtomValue } from "@effect/atom-react";
2import { sessionHasLegacyPermissions } from "@t3tools/contracts";
3import { Atom } from "effect/reactivity";
4import * as SecureStore from "expo-secure-store";
5import { useEffect, useMemo, useRef, useState } from "react";
6import { Alert } from "react-native";
7
8import { useEnvironments } from "../state/environments";
9import { environmentSession } from "../state/session";
10
11const storageKey = "t3code.permission-update.v1";
12const dismissedThisLaunch = new Set<string>();
13const shownThisLaunch = new Set<string>();
14
15export function PermissionUpdateNotice() {
16 const { environments } = useEnvironments();
17 const [dismissed, setDismissed] = useState<ReadonlySet<string> | null>(null);
18 const showing = useRef(false);
19 const affected = useAtomValue(
20 useMemo(
21 () =>
22 Atom.make((get) =>
23 environments.filter(({ environmentId }) => {
24 const session = get(environmentSession.sessionStateAtom(environmentId));
25 return (
26 session._tag === "Success" &&
27 !session.waiting &&
28 sessionHasLegacyPermissions(session.value)
29 );
30 }),
31 ),
32 [environments],
33 ),
34 );
35
36 useEffect(() => {
37 let active = true;
38 void SecureStore.getItemAsync(storageKey)
39 .then((raw) => {
40 const value: unknown = raw === null ? [] : JSON.parse(raw);
41 if (active)
42 setDismissed(
43 new Set(
44 Array.isArray(value)
45 ? value.filter((id): id is string => typeof id === "string")
46 : [],
47 ),
48 );
49 })
50 .catch(() => {
51 if (active) setDismissed(new Set());
52 });
53 return () => {
54 active = false;
55 };
56 }, []);
57
58 useEffect(() => {
59 if (dismissed === null || showing.current) return;
60 const environment = affected.find(
61 ({ environmentId }) => !dismissed.has(environmentId) && !shownThisLaunch.has(environmentId),
62 );
63 if (!environment) return;
64 showing.current = true;
65 shownThisLaunch.add(environment.environmentId);
66 const dismiss = () => {
67 dismissedThisLaunch.add(environment.environmentId);
68 const next = new Set([...dismissed, ...dismissedThisLaunch]);
69 // Keep notices serial when several environments have old grants.
70 showing.current = false;
71 setDismissed(next);
72 void SecureStore.setItemAsync(storageKey, JSON.stringify([...next])).catch(() => {
73 // Remember for this launch even if persistent storage is unavailable.
74 });
75 };
76 Alert.alert(
77 `Permissions have changed for ${environment.label}`,
78 "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.",
79 [{ text: "Got it", onPress: dismiss }],
80 { cancelable: false },
81 );
82 }, [affected, dismissed]);
83 return null;
84}