Open History to see the last commit
1import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts";
2import { ArrowRightIcon, DownloadIcon } from "lucide-react";
3
4import { ensureLocalApi } from "../localApi";
5import {
6 getDesktopUpdateActionError,
7 getDesktopUpdateDownloadedVersion,
8 getDesktopUpdateInstallConfirmationMessage,
9 getDesktopUpdateReleaseUrl,
10 shouldToastDesktopUpdateActionResult,
11} from "./desktopUpdate.logic";
12import { toastManager } from "./ui/toast";
13import { stackedThreadToast } from "./ui/toastHelpers";
14
15type DesktopUpdateShell = Pick<DesktopBridge, "openExternal"> &
16 Partial<Pick<DesktopBridge, "installUpdate">>;
17
18export async function openDesktopUpdateReleaseNotes(
19 shell: Pick<DesktopBridge, "openExternal"> | undefined,
20 releaseUrl: string,
21): Promise<void> {
22 try {
23 if (shell && (await shell.openExternal(releaseUrl))) return;
24 } catch {}
25 toastManager.add({ type: "error", title: "Unable to open release notes" });
26}
27
28/**
29 * Confirms, then restarts into the downloaded update. Shared by the sidebar
30 * update control and the update-downloaded toast's Restart now action.
31 */
32export async function installDesktopUpdate(
33 bridge: Pick<DesktopBridge, "installUpdate" | "openExternal">,
34 state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
35): Promise<void> {
36 let confirmed = false;
37 try {
38 confirmed = await ensureLocalApi().dialogs.confirm(
39 getDesktopUpdateInstallConfirmationMessage(state),
40 { initialFocus: "confirm" },
41 );
42 } catch (error) {
43 toastManager.add(
44 stackedThreadToast({
45 type: "error",
46 title: "Could not confirm update",
47 description: error instanceof Error ? error.message : "Update confirmation failed.",
48 }),
49 );
50 return;
51 }
52 if (!confirmed) return;
53
54 try {
55 const result = await bridge.installUpdate();
56 if (!result.accepted) {
57 showDesktopUpdateDownloadedToast(bridge, result.state);
58 return;
59 }
60 if (!shouldToastDesktopUpdateActionResult(result)) return;
61 const actionError = getDesktopUpdateActionError(result);
62 if (!actionError) return;
63 toastManager.add(
64 stackedThreadToast({
65 type: "error",
66 title: "Could not install update",
67 description: actionError,
68 }),
69 );
70 } catch (error) {
71 toastManager.add(
72 stackedThreadToast({
73 type: "error",
74 title: "Could not install update",
75 description: error instanceof Error ? error.message : "An unexpected error occurred.",
76 }),
77 );
78 }
79}
80
81function ReleaseNotesLink({
82 shell,
83 releaseUrl,
84}: {
85 shell: DesktopUpdateShell;
86 releaseUrl: string;
87}) {
88 return (
89 <button
90 className="ml-2 inline cursor-pointer text-muted-foreground underline decoration-dotted underline-offset-4 transition-colors hover:text-foreground"
91 onClick={() => {
92 void openDesktopUpdateReleaseNotes(shell, releaseUrl);
93 }}
94 type="button"
95 >
96 Release notes
97 <ArrowRightIcon
98 aria-hidden
99 className="ml-1 inline size-3 -rotate-45 align-[-0.125em]"
100 strokeWidth={2.25}
101 />
102 </button>
103 );
104}
105
106export function showDesktopUpdateDownloadedToast(
107 shell: DesktopUpdateShell,
108 state: DesktopUpdateState,
109): void {
110 const version = getDesktopUpdateDownloadedVersion(state);
111 const releaseUrl = getDesktopUpdateReleaseUrl(version);
112 const installBridge = shell.installUpdate
113 ? {
114 installUpdate: shell.installUpdate.bind(shell),
115 openExternal: shell.openExternal.bind(shell),
116 }
117 : null;
118 toastManager.add(
119 stackedThreadToast({
120 type: "success",
121 title: "Update downloaded",
122 description: (
123 <>
124 {version ? `T3 Code ${version} is ready.` : "The update is ready."} Restart to finish
125 updating.
126 {releaseUrl ? <ReleaseNotesLink releaseUrl={releaseUrl} shell={shell} /> : null}
127 </>
128 ),
129 ...(installBridge
130 ? {
131 actionProps: {
132 children: "Restart now",
133 onClick: () => {
134 void installDesktopUpdate(installBridge, state);
135 },
136 },
137 actionVariant: "secondary" as const,
138 }
139 : {}),
140 data: {
141 leadingIcon: (
142 <DownloadIcon aria-hidden="true" className="size-[15px] text-success-foreground" />
143 ),
144 },
145 }),
146 );
147}