diff --git a/templates/clips/app/components/library/library-layout.tsx b/templates/clips/app/components/library/library-layout.tsx index d4afc1f062..62384c6cf5 100644 --- a/templates/clips/app/components/library/library-layout.tsx +++ b/templates/clips/app/components/library/library-layout.tsx @@ -524,7 +524,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) { <>
{shouldShowSidebarLink && ( - + {t("navigation.desktopCta")} diff --git a/templates/clips/app/hooks/use-desktop-promo.ts b/templates/clips/app/hooks/use-desktop-promo.ts index 82ad06b317..45fb608278 100644 --- a/templates/clips/app/hooks/use-desktop-promo.ts +++ b/templates/clips/app/hooks/use-desktop-promo.ts @@ -1,8 +1,10 @@ import { useCallback, useEffect, useState } from "react"; import { useIsMobile } from "@/hooks/use-mobile"; - -const DISMISSED_KEY = "clips.desktop-promo.dismissed"; +import { + hasDownloadedDesktopApp, + markDesktopAppDownloaded, +} from "@/lib/capture-install-options"; function detectDesktopApp(): boolean { if (typeof navigator === "undefined") return false; @@ -25,20 +27,12 @@ export function useDesktopPromo() { useEffect(() => { setIsDesktopApp(detectDesktopApp()); - setDismissed( - typeof window !== "undefined" && - window.localStorage?.getItem(DISMISSED_KEY) === "1", - ); + setDismissed(hasDownloadedDesktopApp()); }, []); const dismiss = useCallback(() => { setDismissed(true); - try { - window.localStorage?.setItem(DISMISSED_KEY, "1"); - } catch { - // localStorage can throw in private browsing — ignore, dismissal - // still holds for the session via React state. - } + markDesktopAppDownloaded(); }, []); return { diff --git a/templates/clips/app/lib/capture-install-options.test.ts b/templates/clips/app/lib/capture-install-options.test.ts index 989d309c67..e964dd65c8 100644 --- a/templates/clips/app/lib/capture-install-options.test.ts +++ b/templates/clips/app/lib/capture-install-options.test.ts @@ -1,11 +1,31 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { + hasDownloadedDesktopApp, + markDesktopAppDownloaded, resolveClipsChromeExtensionEnabled, supportsPublishedClipsChromeExtensionHost, } from "./capture-install-options"; describe("capture install options", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("remembers when the desktop installer is downloaded", () => { + const values = new Map(); + vi.stubGlobal("window", { + localStorage: { + getItem: (key: string) => values.get(key) ?? null, + setItem: (key: string, value: string) => values.set(key, value), + }, + }); + + expect(hasDownloadedDesktopApp()).toBe(false); + markDesktopAppDownloaded(); + expect(hasDownloadedDesktopApp()).toBe(true); + }); + it("enables the published Chrome extension on supported first-party/local hosts", () => { expect( supportsPublishedClipsChromeExtensionHost("clips.agent-native.com"), diff --git a/templates/clips/app/lib/capture-install-options.ts b/templates/clips/app/lib/capture-install-options.ts index 01870d5e81..a8315afa8d 100644 --- a/templates/clips/app/lib/capture-install-options.ts +++ b/templates/clips/app/lib/capture-install-options.ts @@ -1,3 +1,24 @@ +const DESKTOP_DOWNLOAD_STORAGE_KEY = "clips.desktop-promo.dismissed"; + +export function hasDownloadedDesktopApp(): boolean { + try { + return ( + typeof window !== "undefined" && + window.localStorage?.getItem(DESKTOP_DOWNLOAD_STORAGE_KEY) === "1" + ); + } catch { + return false; + } +} + +export function markDesktopAppDownloaded(): void { + try { + window.localStorage?.setItem(DESKTOP_DOWNLOAD_STORAGE_KEY, "1"); + } catch { + // Download tracking is best-effort and must not block the installer. + } +} + function isFalsy(value: unknown): boolean { if (typeof value !== "string") return false; return ["0", "false", "no", "off"].includes(value.trim().toLowerCase()); diff --git a/templates/clips/app/routes/_app.dictate.tsx b/templates/clips/app/routes/_app.dictate.tsx index 728e1bac97..db16590381 100644 --- a/templates/clips/app/routes/_app.dictate.tsx +++ b/templates/clips/app/routes/_app.dictate.tsx @@ -679,10 +679,6 @@ function DownloadDesktopAppCard() { })}

- - - {t("dictateRoute.downloadDesktopApp")} - ); } diff --git a/templates/clips/app/routes/_app.meetings.$meetingId.tsx b/templates/clips/app/routes/_app.meetings.$meetingId.tsx index 4605caa386..a636e8f6c9 100644 --- a/templates/clips/app/routes/_app.meetings.$meetingId.tsx +++ b/templates/clips/app/routes/_app.meetings.$meetingId.tsx @@ -797,19 +797,11 @@ export default function MeetingDetailRoute() { {showDesktopRecordHint && ( -
- - {t("meetingDetail.desktopHint")} - {!isDesktopApp && ( - - - {t("meetingDetail.getDesktopApp")} - - )} +
+ + + {t("meetingDetail.desktopHint")} +
)} diff --git a/templates/clips/app/routes/_app.meetings._index.tsx b/templates/clips/app/routes/_app.meetings._index.tsx index 15c35af7bd..00af183a49 100644 --- a/templates/clips/app/routes/_app.meetings._index.tsx +++ b/templates/clips/app/routes/_app.meetings._index.tsx @@ -5,7 +5,6 @@ import { } from "@agent-native/core/client"; import { IconAlertTriangle, - IconAppWindow, IconBellRinging, IconCalendar, IconCheck, @@ -24,7 +23,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { NavLink, useSearchParams } from "react-router"; import { toast } from "sonner"; -import { CaptureInstallButton } from "@/components/capture-install-options"; import { PageHeader } from "@/components/library/page-header"; import type { AttendeeStackParticipant } from "@/components/meetings/attendee-stack"; import { DayHeader, formatDayLabel } from "@/components/meetings/day-header"; @@ -52,7 +50,6 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; -import { useDesktopPromo } from "@/hooks/use-desktop-promo"; import enMessages from "@/i18n/en-US"; export function meta() { @@ -465,7 +462,7 @@ function MeetingNotesSteps() { ); } -function MeetingNotesGuide({ showDesktopCta }: { showDesktopCta: boolean }) { +function MeetingNotesGuide() { const t = useT(); return (
@@ -478,16 +475,6 @@ function MeetingNotesGuide({ showDesktopCta }: { showDesktopCta: boolean }) { {t("meetingsRoute.howToTriggerDescription")}

- {showDesktopCta && ( - - - {t("meetingsRoute.getDesktopApp")} - - )} @@ -696,14 +683,12 @@ function CalendarAccountMenu({ function MeetingsHeader({ query, onQueryChange, - showDesktopCta, calendarAccounts, onConnected, onDisconnected, }: { query: string; onQueryChange: (next: string) => void; - showDesktopCta: boolean; calendarAccounts: CalendarAccount[]; onConnected?: () => void | Promise; onDisconnected?: () => void; @@ -748,21 +733,6 @@ function MeetingsHeader({ )} - {showDesktopCta && ( -
- - - {t("meetingsRoute.getDesktopApp")} - -

- {t("meetingsRoute.requiredForReminders")} -

-
- )} ); @@ -807,7 +777,6 @@ export default function MeetingsIndexRoute() { }, [query]); const queryClient = useQueryClient(); - const { shouldShowSidebarLink: showDesktopCta } = useDesktopPromo(); const accounts = useActionQuery<{ accounts: CalendarAccount[] } | undefined>( "list-calendar-accounts", @@ -1011,7 +980,6 @@ export default function MeetingsIndexRoute() { )} - {hasCalendar && meetings.length === 0 && ( - - )} + {hasCalendar && meetings.length === 0 && } {nothingAtAll ? (
diff --git a/templates/clips/app/routes/download.tsx b/templates/clips/app/routes/download.tsx index df1dec45b2..fee1e8430a 100644 --- a/templates/clips/app/routes/download.tsx +++ b/templates/clips/app/routes/download.tsx @@ -14,6 +14,7 @@ import enMessages from "@/i18n/en-US"; import { clipsChromeExtensionEnabled, clipsChromeExtensionUrl, + markDesktopAppDownloaded, } from "@/lib/capture-install-options"; export function meta() { @@ -118,7 +119,7 @@ function primaryDownloadButton( if (asset) { return (