From 13a667a9b179044e272844706314fdd3ee25ec78 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 30 Jun 2026 10:23:13 -0700 Subject: [PATCH] Add commercial use nudge banners (#478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: Nguyễn Huỳnh Anh Khoa <113995598+anhkhoakz@users.noreply.github.com> Co-authored-by: startsevdenis Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../components/CloneGitRepositoryDialog.tsx | 3 + .../components/CommercialUseBanner.tsx | 130 ++++++++++++++++++ .../components/ExportDataDialog.tsx | 9 +- .../components/ImportDataDialog.tsx | 3 + .../Settings/SettingsCertificates.tsx | 3 + .../components/Settings/SettingsGeneral.tsx | 6 +- .../components/Settings/SettingsInterface.tsx | 5 +- .../components/Settings/SettingsLicense.tsx | 11 +- .../components/Settings/SettingsProxy.tsx | 2 + .../components/SettingsDropdown.tsx | 4 +- .../components/core/DismissibleBanner.tsx | 91 +++++++----- .../components/git/GitCommitDialog.tsx | 4 +- .../components/git/showAddRemoteDialog.tsx | 2 +- apps/yaak-client/lib/pricingUrl.ts | 3 + packages/theme/src/window.ts | 3 + scripts/run-dev.mjs | 3 + 16 files changed, 236 insertions(+), 46 deletions(-) create mode 100644 apps/yaak-client/components/CommercialUseBanner.tsx create mode 100644 apps/yaak-client/lib/pricingUrl.ts diff --git a/apps/yaak-client/components/CloneGitRepositoryDialog.tsx b/apps/yaak-client/components/CloneGitRepositoryDialog.tsx index 3b3e2af2..ccf14580 100644 --- a/apps/yaak-client/components/CloneGitRepositoryDialog.tsx +++ b/apps/yaak-client/components/CloneGitRepositoryDialog.tsx @@ -4,6 +4,7 @@ import { Banner, VStack } from "@yaakapp-internal/ui"; import { useState } from "react"; import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir"; import { appInfo } from "../lib/appInfo"; +import { CommercialUseBanner } from "./CommercialUseBanner"; import { showErrorToast } from "../lib/toast"; import { Button } from "./core/Button"; import { Checkbox } from "./core/Checkbox"; @@ -89,6 +90,8 @@ export function CloneGitRepositoryDialog({ hide }: Props) { )} + + ({ + namespace: "global", + key: "commercial-use-banner-snoozed-at", + fallback: null, + }); + + useEffect(() => { + let canceled = false; + + shouldShowCommercialUsePrompt() + .then((shouldShow) => { + if (!canceled) setVisible(shouldShow); + }) + .catch(console.error); + + return () => { + canceled = true; + }; + }, [source]); + + const snoozed = isSnoozed(snoozedAt, COMMERCIAL_USE_SNOOZE_MS); + const handleShow = useCallback(() => { + if (snoozeStartedRef.current || snoozed) { + return; + } + + snoozeStartedRef.current = true; + setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })).catch(console.error); + }, [setSnoozedAt, snoozed, source]); + + if (!visible || isSnoozeLoading || snoozed) { + return null; + } + + return ( +
+ + setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })) + } + onShow={handleShow} + actions={[ + { + label: "View plans", + color: "info", + variant: "solid", + onClick: () => { + openCommercialUsePricing(source).catch(console.error); + }, + }, + ]} + > +
+

{title}

+

{COMMERCIAL_USE_BANNER_MESSAGE}

+
+
+
+ ); +} + +async function shouldShowCommercialUsePrompt(): Promise { + // Open-source builds omit the Rust license plugin, so never show commercial-use prompts there. + if (appInfo.featureLicense !== true) { + return false; + } + + try { + const license = await invoke("plugin:yaak-license|check"); + return license.status !== "active" && license.status !== "trialing"; + } catch (err) { + console.log("Failed to check license before commercial-use prompt", err); + return true; + } +} + +async function openCommercialUsePricing(source: string): Promise { + await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error); +} + +function isSnoozed(value: string | null, ms: number): boolean { + if (value == null) return false; + + try { + const snooze = JSON.parse(value) as { at?: unknown }; + const at = typeof snooze.at === "string" ? snooze.at : null; + return isWithinMs(at, ms); + } catch { + // Older builds stored only the timestamp, so keep respecting that as a global snooze. + return isWithinMs(value, ms); + } +} + +function isWithinMs(date: string | null, ms: number): boolean { + if (date == null) return false; + + const time = new Date(date).getTime(); + if (Number.isNaN(time)) return false; + + return Date.now() - time < ms; +} diff --git a/apps/yaak-client/components/ExportDataDialog.tsx b/apps/yaak-client/components/ExportDataDialog.tsx index fe5263a5..620182be 100644 --- a/apps/yaak-client/components/ExportDataDialog.tsx +++ b/apps/yaak-client/components/ExportDataDialog.tsx @@ -8,6 +8,7 @@ import slugify from "slugify"; import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace"; import { pluralizeCount } from "../lib/pluralize"; import { invokeCmd } from "../lib/tauri"; +import { CommercialUseBanner } from "./CommercialUseBanner"; import { Button } from "./core/Button"; import { Checkbox } from "./core/Checkbox"; import { DetailsBanner } from "./core/DetailsBanner"; @@ -85,8 +86,10 @@ function ExportDataDialogContent({ const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length; const noneSelected = numSelected === 0; return ( -
+
+ + @@ -137,9 +140,9 @@ function ExportDataDialogContent({ /> -
+
- + Create Run Button
diff --git a/apps/yaak-client/components/ImportDataDialog.tsx b/apps/yaak-client/components/ImportDataDialog.tsx index 4c676c1c..087721c7 100644 --- a/apps/yaak-client/components/ImportDataDialog.tsx +++ b/apps/yaak-client/components/ImportDataDialog.tsx @@ -1,6 +1,7 @@ import { VStack } from "@yaakapp-internal/ui"; import { useState } from "react"; import { useLocalStorage } from "react-use"; +import { CommercialUseBanner } from "./CommercialUseBanner"; import { Button } from "./core/Button"; import { SelectFile } from "./SelectFile"; @@ -14,6 +15,8 @@ export function ImportDataDialog({ importData }: Props) { return ( + +
  • OpenAPI 3.0, 3.1
  • diff --git a/apps/yaak-client/components/Settings/SettingsCertificates.tsx b/apps/yaak-client/components/Settings/SettingsCertificates.tsx index b42b2593..b076918c 100644 --- a/apps/yaak-client/components/Settings/SettingsCertificates.tsx +++ b/apps/yaak-client/components/Settings/SettingsCertificates.tsx @@ -4,6 +4,7 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui"; import { useAtomValue } from "jotai"; import { useRef } from "react"; import { showConfirmDelete } from "../../lib/confirm"; +import { CommercialUseBanner } from "../CommercialUseBanner"; import { Button } from "../core/Button"; import { Checkbox } from "../core/Checkbox"; import { DetailsBanner } from "../core/DetailsBanner"; @@ -232,6 +233,8 @@ export function SettingsCertificates() { + + {certificates.length > 0 && ( {certificates.map((cert, index) => ( diff --git a/apps/yaak-client/components/Settings/SettingsGeneral.tsx b/apps/yaak-client/components/Settings/SettingsGeneral.tsx index 91c16f0c..add60afb 100644 --- a/apps/yaak-client/components/Settings/SettingsGeneral.tsx +++ b/apps/yaak-client/components/Settings/SettingsGeneral.tsx @@ -6,6 +6,7 @@ import { useCheckForUpdates } from "../../hooks/useCheckForUpdates"; import { appInfo } from "../../lib/appInfo"; import { revealInFinderText } from "../../lib/reveal"; import { CargoFeature } from "../CargoFeature"; +import { CommercialUseBanner } from "../CommercialUseBanner"; import { DismissibleBanner } from "../core/DismissibleBanner"; import { IconButton } from "../core/IconButton"; import { @@ -34,12 +35,15 @@ export function SettingsGeneral() { return ( -
    +
    General

    Configure general settings for update behavior and more.

    +
    + +
    diff --git a/apps/yaak-client/components/Settings/SettingsInterface.tsx b/apps/yaak-client/components/Settings/SettingsInterface.tsx index 1bfc92d2..764ab1e4 100644 --- a/apps/yaak-client/components/Settings/SettingsInterface.tsx +++ b/apps/yaak-client/components/Settings/SettingsInterface.tsx @@ -8,6 +8,7 @@ import { useAtomValue } from "jotai"; import { useState } from "react"; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace"; import { showConfirm } from "../../lib/confirm"; +import { pricingUrl } from "../../lib/pricingUrl"; import { invokeCmd } from "../../lib/tauri"; import { CargoFeature } from "../CargoFeature"; import { Button } from "../core/Button"; @@ -252,7 +253,9 @@ function LicenseSettings({ settings }: { settings: Settings }) {

    Licenses help keep Yaak independent and sustainable.{" "} - Purchase a License → + + Purchase a License → +

    ), diff --git a/apps/yaak-client/components/Settings/SettingsLicense.tsx b/apps/yaak-client/components/Settings/SettingsLicense.tsx index fc246efc..baa4bd6d 100644 --- a/apps/yaak-client/components/Settings/SettingsLicense.tsx +++ b/apps/yaak-client/components/Settings/SettingsLicense.tsx @@ -6,6 +6,7 @@ import { formatDate } from "date-fns/format"; import { useState } from "react"; import { useToggle } from "../../hooks/useToggle"; import { pluralizeCount } from "../../lib/pluralize"; +import { pricingUrl } from "../../lib/pricingUrl"; import { CargoFeature } from "../CargoFeature"; import { Button } from "../core/Button"; import { Link } from "../core/Link"; @@ -48,7 +49,7 @@ function SettingsLicenseCmp() { Personal use is always free, forever.
    - + Learn More
    @@ -68,7 +69,7 @@ function SettingsLicenseCmp() {
    - + Learn More
    @@ -134,7 +135,7 @@ function SettingsLicenseCmp() {
    + , rightSlot: , - onSelect: () => openUrl("https://yaak.app/pricing"), + onSelect: () => + openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)), }, { label: "Install CLI", diff --git a/apps/yaak-client/components/core/DismissibleBanner.tsx b/apps/yaak-client/components/core/DismissibleBanner.tsx index 8c3fe8f9..38eca586 100644 --- a/apps/yaak-client/components/core/DismissibleBanner.tsx +++ b/apps/yaak-client/components/core/DismissibleBanner.tsx @@ -1,57 +1,84 @@ import type { Color } from "@yaakapp-internal/plugins"; import type { BannerProps } from "@yaakapp-internal/ui"; -import { Banner, HStack } from "@yaakapp-internal/ui"; +import { Banner } from "@yaakapp-internal/ui"; import classNames from "classnames"; +import { useEffect } from "react"; import { useKeyValue } from "../../hooks/useKeyValue"; +import type { ButtonProps } from "./Button"; import { Button } from "./Button"; export function DismissibleBanner({ children, className, id, + onDismiss, + onShow, actions, ...props }: BannerProps & { id: string; - actions?: { label: string; onClick: () => void; color?: Color }[]; + onDismiss?: () => void | Promise; + onShow?: () => void | Promise; + actions?: { + label: string; + onClick: () => void; + color?: Color; + variant?: ButtonProps["variant"]; + }[]; }) { - const { set: setDismissed, value: dismissed } = useKeyValue({ + const { + isLoading, + set: setDismissed, + value: dismissed, + } = useKeyValue({ namespace: "global", key: ["dismiss-banner", id], fallback: false, }); - if (dismissed) return null; + const shouldShow = !isLoading && !dismissed; + + useEffect(() => { + if (shouldShow) { + Promise.resolve(onShow?.()).catch(console.error); + } + }, [onShow, shouldShow]); + + if (!shouldShow) return null; return ( - - {children} - - {actions?.map((a) => ( - - ))} - - + +
    +
    + {children} +
    + + {actions?.map((a) => ( + + ))} +
    +
    +
    ); } diff --git a/apps/yaak-client/components/git/GitCommitDialog.tsx b/apps/yaak-client/components/git/GitCommitDialog.tsx index 39d12092..cac73d78 100644 --- a/apps/yaak-client/components/git/GitCommitDialog.tsx +++ b/apps/yaak-client/components/git/GitCommitDialog.tsx @@ -16,6 +16,7 @@ import { resolvedModelName } from "../../lib/resolvedModelName"; import { showConfirm } from "../../lib/confirm"; import { showErrorToast } from "../../lib/toast"; import { sync } from "../../init/sync"; +import { CommercialUseBanner } from "../CommercialUseBanner"; import { Button } from "../core/Button"; import type { CheckboxProps } from "../core/Checkbox"; import { Checkbox } from "../core/Checkbox"; @@ -205,7 +206,8 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { layout="horizontal" defaultRatio={0.6} firstSlot={({ style }) => ( -
    +
    +