mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-21 14:09:41 +02:00
Refine commercial use banner attribution
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function pricingUrl(intent: string): string {
|
||||
return `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user