Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions packages/ui/src/features/settings/sections/UpdatesSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export function UpdatesSettings() {
const setDownloadUpdatesAutomatically = useSettingsStore(
(state) => state.setDownloadUpdatesAutomatically,
);
const dismissibleUpdateBanners = useSettingsStore(
(state) => state.dismissibleUpdateBanners,
);
const setDismissibleUpdateBanners = useSettingsStore(
(state) => state.setDismissibleUpdateBanners,
);
const [checkingForUpdates, setCheckingForUpdates] = useState(false);
const [updatesDisabled, setUpdatesDisabled] = useState(false);
const [updateStatus, setUpdateStatus] = useState<{
Expand Down Expand Up @@ -80,6 +86,18 @@ export function UpdatesSettings() {
[setDownloadUpdatesAutomatically],
);

const handleDismissibleBannersChange = useCallback(
(checked: boolean) => {
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
setting_name: "dismissible_update_banners",
new_value: checked,
old_value: !checked,
});
setDismissibleUpdateBanners(checked);
},
[setDismissibleUpdateBanners],
);

useEffect(() => {
if (!hasCheckedRef.current) {
hasCheckedRef.current = true;
Expand Down Expand Up @@ -119,16 +137,28 @@ export function UpdatesSettings() {
</SettingRow>

{updatesEnabled?.enabled ? (
<SettingRow
label="Download updates automatically"
description="Download new versions in the background and install them on the next quit. When off, you choose when to download each update."
>
<Switch
checked={downloadUpdatesAutomatically}
onCheckedChange={handleAutoDownloadChange}
size="1"
/>
</SettingRow>
<>
<SettingRow
label="Download updates automatically"
description="Download new versions in the background and install them on the next quit. When off, you choose when to download each update."
>
<Switch
checked={downloadUpdatesAutomatically}
onCheckedChange={handleAutoDownloadChange}
size="1"
/>
</SettingRow>
<SettingRow
label="Dismissible update banners"
description="Show a dismiss button on update banners. A dismissed banner stays hidden until a new update arrives or the app restarts."
>
<Switch
checked={dismissibleUpdateBanners}
onCheckedChange={handleDismissibleBannersChange}
size="1"
/>
</SettingRow>
</>
) : null}

<SettingRow
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/features/settings/settingsStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe("feature settingsStore cloud selections", () => {
["lastUsedWorkspaceMode", "local", "cloud"],
["debugLogsCloudRuns", false, true],
["slotMachineMode", false, true],
["dismissibleUpdateBanners", false, true],
] as const)("rehydrates %s", async (field, initial, persisted) => {
getItem.mockResolvedValue(
JSON.stringify({ state: { [field]: persisted }, version: 0 }),
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/src/features/settings/settingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ interface SettingsStore {
brainrotMode: boolean;
mcpAppsDisabledServers: string[];
downloadUpdatesAutomatically: boolean;
dismissibleUpdateBanners: boolean;
lastSeenChangelogVersion: string | null;
// Renders the conversation with the new ChatX (quill) primitives instead of
// the virtualized ConversationView. Local A/B toggle while the rebuild bakes.
Expand All @@ -182,6 +183,7 @@ interface SettingsStore {
setBrainrotMode: (enabled: boolean) => void;
setMcpAppsDisabledServers: (servers: string[]) => void;
setDownloadUpdatesAutomatically: (enabled: boolean) => void;
setDismissibleUpdateBanners: (enabled: boolean) => void;
setLastSeenChangelogVersion: (version: string | null) => void;

// Onboarding hints
Expand Down Expand Up @@ -344,6 +346,7 @@ export const useSettingsStore = create<SettingsStore>()(
brainrotMode: false,
mcpAppsDisabledServers: [],
downloadUpdatesAutomatically: true,
dismissibleUpdateBanners: false,
lastSeenChangelogVersion: null,
useNewChatThread: false,
setUseNewChatThread: (enabled) => set({ useNewChatThread: enabled }),
Expand All @@ -352,6 +355,8 @@ export const useSettingsStore = create<SettingsStore>()(
setBrainrotMode: (enabled) => set({ brainrotMode: enabled }),
setDownloadUpdatesAutomatically: (enabled) =>
set({ downloadUpdatesAutomatically: enabled }),
setDismissibleUpdateBanners: (enabled) =>
set({ dismissibleUpdateBanners: enabled }),
setLastSeenChangelogVersion: (version) =>
set({ lastSeenChangelogVersion: version }),
setMcpAppsDisabledServers: (servers) =>
Expand Down Expand Up @@ -446,6 +451,7 @@ export const useSettingsStore = create<SettingsStore>()(
brainrotMode: state.brainrotMode,
mcpAppsDisabledServers: state.mcpAppsDisabledServers,
downloadUpdatesAutomatically: state.downloadUpdatesAutomatically,
dismissibleUpdateBanners: state.dismissibleUpdateBanners,
lastSeenChangelogVersion: state.lastSeenChangelogVersion,
useNewChatThread: state.useNewChatThread,

Expand Down
143 changes: 143 additions & 0 deletions packages/ui/src/features/sidebar/components/UpdateBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { updateStore } from "@posthog/core/updates/updateStore";
import { ServiceProvider } from "@posthog/di/react";
import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
import { useUpdateBannerStore } from "@posthog/ui/features/updates/updateBannerStore";
import {
UPDATES_CLIENT,
type UpdatesClient,
} from "@posthog/ui/features/updates/updatesClient";
import { registerRendererStateStorage } from "@posthog/ui/shell/rendererStorage";
import { Theme } from "@radix-ui/themes";
import {
act,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { Container } from "inversify";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { UpdateBanner } from "./UpdateBanner";

registerRendererStateStorage({
getItem: vi.fn().mockResolvedValue(null),
setItem: vi.fn().mockResolvedValue(undefined),
removeItem: vi.fn().mockResolvedValue(undefined),
});

const fakeUpdatesClient: UpdatesClient = {
install: vi.fn().mockResolvedValue({ installed: true }),
check: vi.fn(),
isEnabled: vi.fn().mockResolvedValue({ enabled: true }),
getStatus: vi.fn(),
onStatus: () => ({ unsubscribe: () => {} }),
onReady: () => ({ unsubscribe: () => {} }),
onCheckFromMenu: () => ({ unsubscribe: () => {} }),
};

function renderBanner(variant?: "sidebar" | "compact") {
const container = new Container();
container.bind(UPDATES_CLIENT).toConstantValue(fakeUpdatesClient);
return render(
<ServiceProvider container={container}>
<Theme>
<UpdateBanner variant={variant} />
</Theme>
</ServiceProvider>,
);
}

function dismissButton() {
return screen.queryByLabelText("Dismiss update banner");
}

async function expectBannerRemoved(text: string) {
await waitFor(() => {
expect(screen.queryByText(text)).toBeNull();
});
}

describe("UpdateBanner", () => {
beforeEach(() => {
updateStore.setState({
status: "ready",
version: "1.2.3",
availableVersion: "1.2.3",
isEnabled: true,
});
useUpdateBannerStore.setState({ dismissedVersion: null });
useSettingsStore.setState({ dismissibleUpdateBanners: false });
});

it("shows no dismiss button when the setting is off", () => {
renderBanner();

expect(screen.getByText("1.2.3 ready")).toBeInTheDocument();
expect(dismissButton()).toBeNull();
});

it("dismisses the banner when the setting is on", async () => {
useSettingsStore.setState({ dismissibleUpdateBanners: true });
renderBanner();

const dismiss = dismissButton();
expect(dismiss).not.toBeNull();
fireEvent.click(dismiss as HTMLElement);

await expectBannerRemoved("1.2.3 ready");
});
Comment thread
charlesvien marked this conversation as resolved.
Outdated

it("keeps a dismissed version hidden across the update cycle", async () => {
useSettingsStore.setState({ dismissibleUpdateBanners: true });
updateStore.setState({
status: "available",
version: null,
availableVersion: "1.2.3",
});
renderBanner();

fireEvent.click(dismissButton() as HTMLElement);
await expectBannerRemoved("Update available");

act(() => {
updateStore.setState({ status: "ready", version: "1.2.3" });
});
expect(screen.queryByText("1.2.3 ready")).toBeNull();
});

it("shows the banner again when a newer version arrives", async () => {
useSettingsStore.setState({ dismissibleUpdateBanners: true });
renderBanner();

fireEvent.click(dismissButton() as HTMLElement);
await expectBannerRemoved("1.2.3 ready");

act(() => {
updateStore.setState({ version: "1.2.4", availableVersion: "1.2.4" });
});
expect(screen.getByText("1.2.4 ready")).toBeInTheDocument();
});

it("restores a dismissed banner when the setting is turned off", async () => {
useSettingsStore.setState({ dismissibleUpdateBanners: true });
renderBanner();

fireEvent.click(dismissButton() as HTMLElement);
await expectBannerRemoved("1.2.3 ready");

act(() => {
useSettingsStore.setState({ dismissibleUpdateBanners: false });
});
expect(screen.getByText("1.2.3 ready")).toBeInTheDocument();
});

it("dismisses the compact variant too", async () => {
useSettingsStore.setState({ dismissibleUpdateBanners: true });
renderBanner("compact");

expect(screen.getByText("1.2.3 ready — Restart")).toBeInTheDocument();
fireEvent.click(dismissButton() as HTMLElement);

await expectBannerRemoved("1.2.3 ready — Restart");
});
});
Loading
Loading