Compare commits

...

1 Commits

Author SHA1 Message Date
Gregory Schier 4092511f22 Add commercial use upsell banners 2026-06-19 16:09:43 -07:00
11 changed files with 184 additions and 40 deletions
@@ -4,6 +4,7 @@ import { Banner, VStack } from "@yaakapp-internal/ui";
import { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { appInfo } from "../lib/appInfo";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { showErrorToast } from "../lib/toast";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
@@ -89,6 +90,10 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
</Banner>
)}
<CommercialUseBanner source="git-clone" title="Using Git for work?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
<PlainInput
required
label="Repository URL"
@@ -0,0 +1,79 @@
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 { appInfo } from "../lib/appInfo";
import { DismissibleBanner } from "./core/DismissibleBanner";
export function CommercialUseBanner({
children,
source,
title,
}: {
children: string;
source: string;
title: string;
}) {
const [visible, setVisible] = useState(false);
useEffect(() => {
let canceled = false;
shouldShowCommercialUsePrompt()
.then((shouldShow) => {
if (!canceled) setVisible(shouldShow);
})
.catch(console.error);
return () => {
canceled = true;
};
}, [source]);
if (!visible) return null;
return (
<div className="w-full">
<DismissibleBanner
id="commercial-use"
color="primary"
className="w-full"
dismissForDays={7}
actions={[
{
label: "View plans",
color: "primary",
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">{children}</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(`https://yaak.app/pricing?s=${source}&ref=app.yaak.desktop`).catch(console.error);
}
@@ -8,6 +8,7 @@ import slugify from "slugify";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from "../lib/tauri";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner";
@@ -85,8 +86,12 @@ function ExportDataDialogContent({
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
const noneSelected = numSelected === 0;
return (
<div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
<div className="h-full w-full grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden rounded-b-lg">
<VStack space={3} className="overflow-auto px-5 pb-6">
<CommercialUseBanner source="data-export" title="Exporting work data?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead>
<tr>
@@ -137,9 +142,9 @@ function ExportDataDialogContent({
/>
</DetailsBanner>
</VStack>
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface py-3 border-t border-border-subtle">
<div>
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtlest">
Create Run Button
</Link>
</div>
@@ -1,6 +1,7 @@
import { VStack } from "@yaakapp-internal/ui";
import { useState } from "react";
import { useLocalStorage } from "react-use";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button";
import { SelectFile } from "./SelectFile";
@@ -14,6 +15,10 @@ export function ImportDataDialog({ importData }: Props) {
return (
<VStack space={5} className="pb-4">
<CommercialUseBanner source="data-import" title="Importing work data?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
<VStack space={1}>
<ul className="list-disc pl-5">
<li>OpenAPI 3.0, 3.1</li>
@@ -4,6 +4,7 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai";
import { useRef } from "react";
import { showConfirmDelete } from "../../lib/confirm";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox";
import { DetailsBanner } from "../core/DetailsBanner";
@@ -232,6 +233,10 @@ export function SettingsCertificates() {
</HStack>
</div>
<CommercialUseBanner source="client-certificates" title="Using certificates for work?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
{certificates.length > 0 && (
<VStack space={3}>
{certificates.map((cert, index) => (
@@ -14,6 +14,7 @@ import {
} from "../../lib/requestSettings";
import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from "../CargoFeature";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { IconButton } from "../core/IconButton";
import {
ModelSettingRowBoolean,
@@ -38,10 +39,15 @@ export function SettingsGeneral() {
return (
<VStack space={1.5} className="mb-4">
<div className="mb-4">
<div>
<Heading>General</Heading>
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
</div>
<div className="mt-3 mb-5">
<CommercialUseBanner source="settings-general" title="Using Yaak for work?">
A Yaak license is required for commercial use and helps support future development.
</CommercialUseBanner>
</div>
<SettingsList className="space-y-8">
<CargoFeature feature="updater">
<SettingsSection title="Updates">
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import type { ProxySetting } from "@yaakapp-internal/models";
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai";
import { CommercialUseBanner } from "../CommercialUseBanner";
import {
SettingRowBoolean,
SettingRowSelect,
@@ -33,6 +34,9 @@ export function SettingsProxy() {
traffic, or routing through specific infrastructure.
</p>
</div>
<CommercialUseBanner source="proxy-settings" title="Using a proxy for work?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
<SettingsList className="space-y-8">
<SettingsSection title="Proxy">
<SettingRowSelect
@@ -1,57 +1,84 @@
import type { Color } from "@yaakapp-internal/plugins";
import type { BannerProps } from "@yaakapp-internal/ui";
import { Banner, HStack } from "@yaakapp-internal/ui";
import { Banner } from "@yaakapp-internal/ui";
import classNames from "classnames";
import { useKeyValue } from "../../hooks/useKeyValue";
import type { ButtonProps } from "./Button";
import { Button } from "./Button";
export function DismissibleBanner({
children,
className,
dismissForDays,
id,
actions,
...props
}: BannerProps & {
id: string;
actions?: { label: string; onClick: () => void; color?: Color }[];
dismissForDays?: number;
actions?: {
label: string;
onClick: () => void;
color?: Color;
variant?: ButtonProps["variant"];
}[];
}) {
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
const {
isLoading,
set: setDismissed,
value: dismissed,
} = useKeyValue<boolean | string>({
namespace: "global",
key: ["dismiss-banner", id],
fallback: false,
});
if (dismissed) return null;
if (isLoading || isDismissed(dismissed, dismissForDays)) return null;
return (
<Banner
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
{...props}
>
{children}
<HStack space={1.5}>
{actions?.map((a) => (
<Button
key={a.label}
variant="border"
color={a.color ?? props.color}
size="xs"
onClick={a.onClick}
title={a.label}
>
{a.label}
</Button>
))}
<Button
variant="border"
color={props.color}
size="xs"
onClick={() => setDismissed((d) => !d)}
title="Dismiss message"
>
Dismiss
</Button>
</HStack>
<Banner className={classNames(className, "relative")} {...props}>
<div className="@container">
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
{children}
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
<Button
variant="border"
color={props.color}
size="xs"
onClick={() => setDismissed(dismissForDays == null ? true : new Date().toISOString())}
title="Dismiss message"
>
Dismiss
</Button>
{actions?.map((a) => (
<Button
key={a.label}
variant={a.variant ?? "border"}
color={a.color ?? props.color}
size="xs"
onClick={a.onClick}
title={a.label}
>
{a.label}
</Button>
))}
</div>
</div>
</div>
</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;
}
@@ -16,6 +16,7 @@ import { resolvedModelName } from "../../lib/resolvedModelName";
import { showConfirm } from "../../lib/confirm";
import { showErrorToast } from "../../lib/toast";
import { sync } from "../../init/sync";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { Button } from "../core/Button";
import type { CheckboxProps } from "../core/Checkbox";
import { Checkbox } from "../core/Checkbox";
@@ -205,7 +206,10 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
layout="horizontal"
defaultRatio={0.6}
firstSlot={({ style }) => (
<div style={style} className="h-full px-4">
<div style={style} className="h-full px-4 grid grid-rows-[auto_minmax(0,1fr)] gap-3">
<CommercialUseBanner source="git-commit" title="Using Git for work?">
A Yaak license is required for commercial use and helps support features like this.
</CommercialUseBanner>
<SplitLayout
storageKey="commit-vertical"
layout="vertical"
+4 -3
View File
@@ -125,10 +125,11 @@ function bannerColorVariables(color: YaakColor | null): Partial<CSSVariables> {
if (color == null) return {};
return {
text: color.lift(0.8).css(),
textSubtle: color.translucify(0.3).css(),
textSubtlest: color.translucify(0.6).css(),
text: color.desaturate(0.5).lift(0.12).css(),
textSubtle: color.desaturate(0.58).lift(0.04).translucify(0.04).css(),
textSubtlest: color.desaturate(0.65).translucify(0.18).css(),
surface: color.translucify(0.95).css(),
surfaceHighlight: color.translucify(0.85).css(),
border: color.lift(0.3).translucify(0.8).css(),
};
}
+3
View File
@@ -69,6 +69,9 @@ const config = JSON.stringify({
const normalizedAdditionalArgs = [];
for (let i = 0; i < additionalArgs.length; i++) {
const arg = additionalArgs[i];
if (arg === "--") {
continue;
}
if (arg === "--config" && i + 1 < additionalArgs.length) {
const value = additionalArgs[i + 1];
const isInlineJson = value.trimStart().startsWith("{");