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({ 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: "Purchase License", 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; }