1import { AuthProvidersManageScope } from "@t3tools/contracts";
2import { readEnvironmentScope, useEnvironmentScope } from "../../state/session";
3import { useAtomValue } from "@effect/atom-react";
4import {
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type EnvironmentId,
8 type ServerProvider,
9} from "@t3tools/contracts";
10import { useEffect, useEffectEvent, useState, type ReactNode } from "react";
11import { usesChatGptSharing } from "@t3tools/shared/usageLimits";
12
13import { useEnvironmentSettings } from "../../hooks/useSettings";
14import { randomUUID } from "../../lib/utils";
15import { serverEnvironment } from "../../state/server";
16import { useAtomCommand } from "../../state/use-atom-command";
17import { Button } from "../ui/button";
18import { Dialog } from "../ui/dialog";
19import { Input } from "../ui/input";
20import { WizardFooter, WizardHeader, WizardPanel, WizardPopup } from "../ui/wizard";
21import { SettingsRow } from "./settingsLayout";
22
23export function AddCodexAccountDialog({
24 environmentId,
25 onClose,
26 renderSetup,
27 onAccountCreated,
28}: {
29 readonly environmentId: EnvironmentId;
30 readonly onClose: () => void;
31 readonly onAccountCreated?:
32 | ((instanceId: ProviderInstanceId, displayName: string) => void)
33 | undefined;
34 readonly renderSetup: (instanceId: ProviderInstanceId, provider: ServerProvider) => ReactNode;
35}) {
36 const canManageProviders = useEnvironmentScope(environmentId, AuthProvidersManageScope);
37 const settings = useEnvironmentSettings(environmentId);
38 const providers = useAtomValue(serverEnvironment.providersValueAtom(environmentId));
39 const update = useAtomCommand(serverEnvironment.updateSettings, "Add ChatGPT account");
40 const [name, setName] = useState("Personal");
41 const displayName = `ChatGPT - ${name.trim()}`;
42 const [instanceId, setInstanceId] = useState<ProviderInstanceId | null>(null);
43 const [pending, setPending] = useState(false);
44 const provider = providers?.find((candidate) => candidate.instanceId === instanceId);
45 const connected = usesChatGptSharing(provider);
46 const closeAfterConnection = useEffectEvent(onClose);
47 useEffect(() => {
48 // The destination snapshot confirms remote transfer as well as local sign-in.
49 if (connected) closeAfterConnection();
50 }, [connected]);
51
52 const createAccount = async () => {
53 if (pending || !name.trim() || !readEnvironmentScope(environmentId, AuthProvidersManageScope))
54 return;
55 setPending(true);
56 // The ID is routing identity; the name is editable and need not be unique.
57 const id = ProviderInstanceId.make(`codex_${randomUUID()}`);
58 const result = await update({
59 environmentId,
60 input: {
61 patch: {
62 providerInstances: {
63 ...settings.providerInstances,
64 [id]: {
65 driver: ProviderDriverKind.make("codex"),
66 displayName,
67 enabled: true,
68 config: { enabled: true, setupMode: "managed" },
69 },
70 },
71 },
72 },
73 });
74 if (result._tag === "Success") {
75 if (onAccountCreated) {
76 onAccountCreated(id, displayName);
77 onClose();
78 } else {
79 setInstanceId(id);
80 }
81 }
82 setPending(false);
83 };
84
85 return (
86 <Dialog
87 open={!connected}
88 onOpenChange={(open) => {
89 if (!open) onClose();
90 }}
91 >
92 <WizardPopup size="wide">
93 <WizardHeader
94 title={instanceId ? displayName : "Add ChatGPT account"}
95 description="Each account has its own Codex instance and sign-in. Choose the other account on the sign-in page."
96 />
97 <WizardPanel>
98 {instanceId ? (
99 provider?.setup ? (
100 renderSetup(instanceId, provider)
101 ) : (
102 <SettingsRow title="Codex runtime" description="Preparing managed setup." />
103 )
104 ) : (
105 <form
106 id="add-codex-account"
107 onSubmit={(event) => {
108 event.preventDefault();
109 void createAccount();
110 }}
111 >
112 <SettingsRow
113 title="Account name"
114 description="Shown in the provider list and model picker."
115 control={
116 <Input
117 aria-label="Account name"
118 value={name}
119 disabled={pending}
120 onChange={(event) => setName(event.target.value)}
121 placeholder="e.g. Personal or Work"
122 />
123 }
124 />
125 </form>
126 )}
127 </WizardPanel>
128 <WizardFooter>
129 {instanceId ? (
130 <Button variant="outline" onClick={onClose}>
131 Finish later
132 </Button>
133 ) : (
134 <>
135 <Button variant="outline" disabled={pending} onClick={onClose}>
136 Cancel
137 </Button>
138 <Button
139 type="submit"
140 form="add-codex-account"
141 disabled={pending || !name.trim() || !canManageProviders}
142 >
143 {pending ? "Adding account…" : "Continue"}
144 </Button>
145 </>
146 )}
147 </WizardFooter>
148 </WizardPopup>
149 </Dialog>
150 );
151}