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 type { LicenseCheckStatus } from "@yaakapp-internal/license";
import { useEffect, useState } from "react";
import { useKeyValue } from "../hooks/useKeyValue";
import { appInfo } from "../lib/appInfo";
import { pricingUrl } from "../lib/pricingUrl";
import { DismissibleBanner } from "./core/DismissibleBanner";
const COMMERCIAL_USE_SNOOZE_DAYS = 7;
export function CommercialUseBanner({
children,
source,
@@ -15,6 +19,15 @@ export function CommercialUseBanner({
title: string;
}) {
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(() => {
let canceled = false;
@@ -30,19 +43,25 @@ export function CommercialUseBanner({
};
}, [source]);
if (!visible) return null;
if (
!visible ||
isSnoozeLoading ||
isWithinDays(snoozedAt, COMMERCIAL_USE_SNOOZE_DAYS)
) {
return null;
}
return (
<div className="w-full">
<DismissibleBanner
id="commercial-use"
color="primary"
id={`commercial-use:${source}`}
color="info"
className="w-full"
dismissForDays={7}
onDismiss={() => setSnoozedAt(new Date().toISOString())}
actions={[
{
label: "View plans",
color: "primary",
color: "info",
variant: "solid",
onClick: () => {
openCommercialUsePricing(source).catch(console.error);
@@ -75,5 +94,14 @@ async function shouldShowCommercialUsePrompt(): Promise<boolean> {
}
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 { 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 }) {
</p>
<p>
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>
</VStack>
),
@@ -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() {
<span className="opacity-50">Personal use is always free, forever.</span>
<Separator className="my-2" />
<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
</Link>
</div>
@@ -68,7 +69,7 @@ function SettingsLicenseCmp() {
</span>
<Separator className="my-2" />
<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
</Link>
</div>
@@ -134,7 +135,7 @@ function SettingsLicenseCmp() {
<Button
color="secondary"
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" />}
>
Direct Support
@@ -150,9 +151,7 @@ function SettingsLicenseCmp() {
color="primary"
rightSlot={<Icon icon="external_link" />}
onClick={() =>
openUrl(
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
)
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
}
>
Purchase License
@@ -7,6 +7,7 @@ import { useExportData } from "../hooks/useExportData";
import { appInfo } from "../lib/appInfo";
import { showDialog } from "../lib/dialog";
import { importData } from "../lib/importData";
import { pricingUrl } from "../lib/pricingUrl";
import type { DropdownRef } from "./core/Dropdown";
import { Dropdown } from "./core/Dropdown";
import { Icon } from "@yaakapp-internal/ui";
@@ -76,7 +77,8 @@ export function SettingsDropdown() {
hidden: check.data == null || check.data.status === "active",
leftSlot: <Icon icon="circle_dollar_sign" />,
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",
@@ -9,13 +9,13 @@ import { Button } from "./Button";
export function DismissibleBanner({
children,
className,
dismissForDays,
id,
onDismiss,
actions,
...props
}: BannerProps & {
id: string;
dismissForDays?: number;
onDismiss?: () => void | Promise<void>;
actions?: {
label: string;
onClick: () => void;
@@ -27,13 +27,13 @@ export function DismissibleBanner({
isLoading,
set: setDismissed,
value: dismissed,
} = useKeyValue<boolean | string>({
} = useKeyValue<boolean>({
namespace: "global",
key: ["dismiss-banner", id],
fallback: false,
});
if (isLoading || isDismissed(dismissed, dismissForDays)) return null;
if (isLoading || dismissed) return null;
return (
<Banner className={classNames(className, "relative")} {...props}>
@@ -45,7 +45,10 @@ export function DismissibleBanner({
variant="border"
color={props.color}
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"
>
Dismiss
@@ -68,17 +71,3 @@ export function DismissibleBanner({
</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",
inputs: [
{ 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");
+3
View File
@@ -0,0 +1,3 @@
export function pricingUrl(intent: string): string {
return `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`;
}