mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-01 10:31:41 +02:00
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
||
import { useCallback, useEffect, useRef, 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_MS = 7 * 24 * 60 * 60 * 1000;
|
||
const COMMERCIAL_USE_BANNER_MESSAGE =
|
||
"Personal use of Yaak is free. If you’re using Yaak at work, please purchase a license.";
|
||
|
||
export function CommercialUseBanner({
|
||
source,
|
||
title,
|
||
}: {
|
||
source: string;
|
||
title: string;
|
||
}) {
|
||
const [visible, setVisible] = useState(false);
|
||
const snoozeStartedRef = useRef(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;
|
||
|
||
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 (
|
||
<div className="w-full">
|
||
<DismissibleBanner
|
||
id={`commercial-use:${source}`}
|
||
color="info"
|
||
className="w-full"
|
||
onDismiss={() =>
|
||
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() }))
|
||
}
|
||
onShow={handleShow}
|
||
actions={[
|
||
{
|
||
label: "Purchase License",
|
||
color: "info",
|
||
variant: "solid",
|
||
onClick: () => {
|
||
openCommercialUsePricing(source).catch(console.error);
|
||
},
|
||
},
|
||
]}
|
||
>
|
||
<div className="text-sm">
|
||
<p className="font-semibold text-text">{title}</p>
|
||
<p className="mt-0.5 text-text-subtle">{COMMERCIAL_USE_BANNER_MESSAGE}</p>
|
||
</div>
|
||
</DismissibleBanner>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
||
// 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<LicenseCheckStatus>("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<void> {
|
||
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;
|
||
}
|