Refine commercial use banner attribution

This commit is contained in:
Gregory Schier
2026-06-20 23:12:42 -07:00
parent 98794fa031
commit 693768ffc6
7 changed files with 58 additions and 34 deletions
@@ -2,9 +2,13 @@ import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener"; import { openUrl } from "@tauri-apps/plugin-opener";
import type { LicenseCheckStatus } from "@yaakapp-internal/license"; import type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useKeyValue } from "../hooks/useKeyValue";
import { appInfo } from "../lib/appInfo"; import { appInfo } from "../lib/appInfo";
import { pricingUrl } from "../lib/pricingUrl";
import { DismissibleBanner } from "./core/DismissibleBanner"; import { DismissibleBanner } from "./core/DismissibleBanner";
const COMMERCIAL_USE_SNOOZE_DAYS = 7;
export function CommercialUseBanner({ export function CommercialUseBanner({
children, children,
source, source,
@@ -15,6 +19,15 @@ export function CommercialUseBanner({
title: string; title: string;
}) { }) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const {
isLoading: isSnoozeLoading,
set: setSnoozedAt,
value: snoozedAt,
} = useKeyValue<string | null>({
namespace: "global",
key: "commercial-use-banner-snoozed-at",
fallback: null,
});
useEffect(() => { useEffect(() => {
let canceled = false; let canceled = false;
@@ -30,19 +43,25 @@ export function CommercialUseBanner({
}; };
}, [source]); }, [source]);
if (!visible) return null; if (
!visible ||
isSnoozeLoading ||
isWithinDays(snoozedAt, COMMERCIAL_USE_SNOOZE_DAYS)
) {
return null;
}
return ( return (
<div className="w-full"> <div className="w-full">
<DismissibleBanner <DismissibleBanner
id="commercial-use" id={`commercial-use:${source}`}
color="primary" color="info"
className="w-full" className="w-full"
dismissForDays={7} onDismiss={() => setSnoozedAt(new Date().toISOString())}
actions={[ actions={[
{ {
label: "View plans", label: "View plans",
color: "primary", color: "info",
variant: "solid", variant: "solid",
onClick: () => { onClick: () => {
openCommercialUsePricing(source).catch(console.error); openCommercialUsePricing(source).catch(console.error);
@@ -75,5 +94,14 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
} }
async function openCommercialUsePricing(source: string): Promise<void> { async function openCommercialUsePricing(source: string): Promise<void> {
await openUrl(`https://yaak.app/pricing?s=${source}&ref=app.yaak.desktop`).catch(console.error); await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
}
function isWithinDays(date: string | null, days: number): boolean {
if (date == null) return false;
const time = new Date(date).getTime();
if (Number.isNaN(time)) return false;
return Date.now() - time < days * 24 * 60 * 60 * 1000;
} }
@@ -8,6 +8,7 @@ import { useAtomValue } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace"; import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
import { showConfirm } from "../../lib/confirm"; import { showConfirm } from "../../lib/confirm";
import { pricingUrl } from "../../lib/pricingUrl";
import { invokeCmd } from "../../lib/tauri"; import { invokeCmd } from "../../lib/tauri";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
@@ -252,7 +253,9 @@ function LicenseSettings({ settings }: { settings: Settings }) {
</p> </p>
<p> <p>
Licenses help keep Yaak independent and sustainable.{" "} Licenses help keep Yaak independent and sustainable.{" "}
<Link href="https://yaak.app/pricing?s=badge">Purchase a License </Link> <Link href={pricingUrl("app.license.badge-hide-confirm")}>
Purchase a License
</Link>
</p> </p>
</VStack> </VStack>
), ),
@@ -6,6 +6,7 @@ import { formatDate } from "date-fns/format";
import { useState } from "react"; import { useState } from "react";
import { useToggle } from "../../hooks/useToggle"; import { useToggle } from "../../hooks/useToggle";
import { pluralizeCount } from "../../lib/pluralize"; import { pluralizeCount } from "../../lib/pluralize";
import { pricingUrl } from "../../lib/pricingUrl";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
import { Link } from "../core/Link"; import { Link } from "../core/Link";
@@ -48,7 +49,7 @@ function SettingsLicenseCmp() {
<span className="opacity-50">Personal use is always free, forever.</span> <span className="opacity-50">Personal use is always free, forever.</span>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice"> <div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}> <Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
Learn More Learn More
</Link> </Link>
</div> </div>
@@ -68,7 +69,7 @@ function SettingsLicenseCmp() {
</span> </span>
<Separator className="my-2" /> <Separator className="my-2" />
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice"> <div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}> <Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
Learn More Learn More
</Link> </Link>
</div> </div>
@@ -134,7 +135,7 @@ function SettingsLicenseCmp() {
<Button <Button
color="secondary" color="secondary"
size="sm" size="sm"
onClick={() => openUrl("https://yaak.app/dashboard?s=support&ref=app.yaak.desktop")} onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")}
rightSlot={<Icon icon="external_link" />} rightSlot={<Icon icon="external_link" />}
> >
Direct Support Direct Support
@@ -150,9 +151,7 @@ function SettingsLicenseCmp() {
color="primary" color="primary"
rightSlot={<Icon icon="external_link" />} rightSlot={<Icon icon="external_link" />}
onClick={() => onClick={() =>
openUrl( openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
)
} }
> >
Purchase License Purchase License
@@ -7,6 +7,7 @@ import { useExportData } from "../hooks/useExportData";
import { appInfo } from "../lib/appInfo"; import { appInfo } from "../lib/appInfo";
import { showDialog } from "../lib/dialog"; import { showDialog } from "../lib/dialog";
import { importData } from "../lib/importData"; import { importData } from "../lib/importData";
import { pricingUrl } from "../lib/pricingUrl";
import type { DropdownRef } from "./core/Dropdown"; import type { DropdownRef } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown"; import { Dropdown } from "./core/Dropdown";
import { Icon } from "@yaakapp-internal/ui"; import { Icon } from "@yaakapp-internal/ui";
@@ -76,7 +77,8 @@ export function SettingsDropdown() {
hidden: check.data == null || check.data.status === "active", hidden: check.data == null || check.data.status === "active",
leftSlot: <Icon icon="circle_dollar_sign" />, leftSlot: <Icon icon="circle_dollar_sign" />,
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />, rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
onSelect: () => openUrl("https://yaak.app/pricing"), onSelect: () =>
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
}, },
{ {
label: "Install CLI", label: "Install CLI",
@@ -9,13 +9,13 @@ import { Button } from "./Button";
export function DismissibleBanner({ export function DismissibleBanner({
children, children,
className, className,
dismissForDays,
id, id,
onDismiss,
actions, actions,
...props ...props
}: BannerProps & { }: BannerProps & {
id: string; id: string;
dismissForDays?: number; onDismiss?: () => void | Promise<void>;
actions?: { actions?: {
label: string; label: string;
onClick: () => void; onClick: () => void;
@@ -27,13 +27,13 @@ export function DismissibleBanner({
isLoading, isLoading,
set: setDismissed, set: setDismissed,
value: dismissed, value: dismissed,
} = useKeyValue<boolean | string>({ } = useKeyValue<boolean>({
namespace: "global", namespace: "global",
key: ["dismiss-banner", id], key: ["dismiss-banner", id],
fallback: false, fallback: false,
}); });
if (isLoading || isDismissed(dismissed, dismissForDays)) return null; if (isLoading || dismissed) return null;
return ( return (
<Banner className={classNames(className, "relative")} {...props}> <Banner className={classNames(className, "relative")} {...props}>
@@ -45,7 +45,10 @@ export function DismissibleBanner({
variant="border" variant="border"
color={props.color} color={props.color}
size="xs" size="xs"
onClick={() => setDismissed(dismissForDays == null ? true : new Date().toISOString())} onClick={() => {
setDismissed(true).catch(console.error);
Promise.resolve(onDismiss?.()).catch(console.error);
}}
title="Dismiss message" title="Dismiss message"
> >
Dismiss Dismiss
@@ -68,17 +71,3 @@ export function DismissibleBanner({
</Banner> </Banner>
); );
} }
function isDismissed(
dismissed: boolean | string | null,
dismissForDays: number | undefined,
): boolean {
if (dismissed === false || dismissed == null) return false;
if (dismissed === true) return true;
if (dismissForDays == null) return dismissed.length > 0;
const dismissedAt = new Date(dismissed).getTime();
if (Number.isNaN(dismissedAt)) return false;
return Date.now() - dismissedAt < dismissForDays * 24 * 60 * 60 * 1000;
}
@@ -9,7 +9,7 @@ export async function addGitRemote(dir: string, defaultName?: string): Promise<G
title: "Add Remote", title: "Add Remote",
inputs: [ inputs: [
{ type: "text", label: "Name", name: "name", defaultValue: defaultName }, { type: "text", label: "Name", name: "name", defaultValue: defaultName },
{ type: "text", label: "URL", name: "url" }, { type: "text", label: "URL", name: "url", placeholder: "git@github.com:org/repo.git" },
], ],
}); });
if (r == null) throw new Error("Cancelled remote prompt"); if (r == null) throw new Error("Cancelled remote prompt");
+3
View File
@@ -0,0 +1,3 @@
export function pricingUrl(intent: string): string {
return `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`;
}