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 { useState } from "react";
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir"; import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
import { appInfo } from "../lib/appInfo"; import { appInfo } from "../lib/appInfo";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { showErrorToast } from "../lib/toast"; import { showErrorToast } from "../lib/toast";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox"; import { Checkbox } from "./core/Checkbox";
@@ -89,6 +90,10 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
</Banner> </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 <PlainInput
required required
label="Repository URL" 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 { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { pluralizeCount } from "../lib/pluralize"; import { pluralizeCount } from "../lib/pluralize";
import { invokeCmd } from "../lib/tauri"; import { invokeCmd } from "../lib/tauri";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { Checkbox } from "./core/Checkbox"; import { Checkbox } from "./core/Checkbox";
import { DetailsBanner } from "./core/DetailsBanner"; import { DetailsBanner } from "./core/DetailsBanner";
@@ -85,8 +86,12 @@ function ExportDataDialogContent({
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length; const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
const noneSelected = numSelected === 0; const noneSelected = numSelected === 0;
return ( 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"> <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"> <table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
<thead> <thead>
<tr> <tr>
@@ -137,9 +142,9 @@ function ExportDataDialogContent({
/> />
</DetailsBanner> </DetailsBanner>
</VStack> </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> <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 Create Run Button
</Link> </Link>
</div> </div>
@@ -1,6 +1,7 @@
import { VStack } from "@yaakapp-internal/ui"; import { VStack } from "@yaakapp-internal/ui";
import { useState } from "react"; import { useState } from "react";
import { useLocalStorage } from "react-use"; import { useLocalStorage } from "react-use";
import { CommercialUseBanner } from "./CommercialUseBanner";
import { Button } from "./core/Button"; import { Button } from "./core/Button";
import { SelectFile } from "./SelectFile"; import { SelectFile } from "./SelectFile";
@@ -14,6 +15,10 @@ export function ImportDataDialog({ importData }: Props) {
return ( return (
<VStack space={5} className="pb-4"> <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}> <VStack space={1}>
<ul className="list-disc pl-5"> <ul className="list-disc pl-5">
<li>OpenAPI 3.0, 3.1</li> <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 { useAtomValue } from "jotai";
import { useRef } from "react"; import { useRef } from "react";
import { showConfirmDelete } from "../../lib/confirm"; import { showConfirmDelete } from "../../lib/confirm";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
import { Checkbox } from "../core/Checkbox"; import { Checkbox } from "../core/Checkbox";
import { DetailsBanner } from "../core/DetailsBanner"; import { DetailsBanner } from "../core/DetailsBanner";
@@ -232,6 +233,10 @@ export function SettingsCertificates() {
</HStack> </HStack>
</div> </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 && ( {certificates.length > 0 && (
<VStack space={3}> <VStack space={3}>
{certificates.map((cert, index) => ( {certificates.map((cert, index) => (
@@ -14,6 +14,7 @@ import {
} from "../../lib/requestSettings"; } from "../../lib/requestSettings";
import { revealInFinderText } from "../../lib/reveal"; import { revealInFinderText } from "../../lib/reveal";
import { CargoFeature } from "../CargoFeature"; import { CargoFeature } from "../CargoFeature";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { IconButton } from "../core/IconButton"; import { IconButton } from "../core/IconButton";
import { import {
ModelSettingRowBoolean, ModelSettingRowBoolean,
@@ -38,10 +39,15 @@ export function SettingsGeneral() {
return ( return (
<VStack space={1.5} className="mb-4"> <VStack space={1.5} className="mb-4">
<div className="mb-4"> <div>
<Heading>General</Heading> <Heading>General</Heading>
<p className="text-text-subtle">Configure general settings for update behavior and more.</p> <p className="text-text-subtle">Configure general settings for update behavior and more.</p>
</div> </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"> <SettingsList className="space-y-8">
<CargoFeature feature="updater"> <CargoFeature feature="updater">
<SettingsSection title="Updates"> <SettingsSection title="Updates">
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
import type { ProxySetting } from "@yaakapp-internal/models"; import type { ProxySetting } from "@yaakapp-internal/models";
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui"; import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { import {
SettingRowBoolean, SettingRowBoolean,
SettingRowSelect, SettingRowSelect,
@@ -33,6 +34,9 @@ export function SettingsProxy() {
traffic, or routing through specific infrastructure. traffic, or routing through specific infrastructure.
</p> </p>
</div> </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"> <SettingsList className="space-y-8">
<SettingsSection title="Proxy"> <SettingsSection title="Proxy">
<SettingRowSelect <SettingRowSelect
@@ -1,57 +1,84 @@
import type { Color } from "@yaakapp-internal/plugins"; import type { Color } from "@yaakapp-internal/plugins";
import type { BannerProps } from "@yaakapp-internal/ui"; 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 classNames from "classnames";
import { useKeyValue } from "../../hooks/useKeyValue"; import { useKeyValue } from "../../hooks/useKeyValue";
import type { ButtonProps } from "./Button";
import { Button } from "./Button"; import { Button } from "./Button";
export function DismissibleBanner({ export function DismissibleBanner({
children, children,
className, className,
dismissForDays,
id, id,
actions, actions,
...props ...props
}: BannerProps & { }: BannerProps & {
id: string; 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", namespace: "global",
key: ["dismiss-banner", id], key: ["dismiss-banner", id],
fallback: false, fallback: false,
}); });
if (dismissed) return null; if (isLoading || isDismissed(dismissed, dismissForDays)) return null;
return ( return (
<Banner <Banner className={classNames(className, "relative")} {...props}>
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")} <div className="@container">
{...props} <div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
> {children}
{children} <div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
<HStack space={1.5}> <Button
{actions?.map((a) => ( variant="border"
<Button color={props.color}
key={a.label} size="xs"
variant="border" onClick={() => setDismissed(dismissForDays == null ? true : new Date().toISOString())}
color={a.color ?? props.color} title="Dismiss message"
size="xs" >
onClick={a.onClick} Dismiss
title={a.label} </Button>
> {actions?.map((a) => (
{a.label} <Button
</Button> key={a.label}
))} variant={a.variant ?? "border"}
<Button color={a.color ?? props.color}
variant="border" size="xs"
color={props.color} onClick={a.onClick}
size="xs" title={a.label}
onClick={() => setDismissed((d) => !d)} >
title="Dismiss message" {a.label}
> </Button>
Dismiss ))}
</Button> </div>
</HStack> </div>
</div>
</Banner> </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 { showConfirm } from "../../lib/confirm";
import { showErrorToast } from "../../lib/toast"; import { showErrorToast } from "../../lib/toast";
import { sync } from "../../init/sync"; import { sync } from "../../init/sync";
import { CommercialUseBanner } from "../CommercialUseBanner";
import { Button } from "../core/Button"; import { Button } from "../core/Button";
import type { CheckboxProps } from "../core/Checkbox"; import type { CheckboxProps } from "../core/Checkbox";
import { Checkbox } from "../core/Checkbox"; import { Checkbox } from "../core/Checkbox";
@@ -205,7 +206,10 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
layout="horizontal" layout="horizontal"
defaultRatio={0.6} defaultRatio={0.6}
firstSlot={({ style }) => ( 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 <SplitLayout
storageKey="commit-vertical" storageKey="commit-vertical"
layout="vertical" layout="vertical"
+4 -3
View File
@@ -125,10 +125,11 @@ function bannerColorVariables(color: YaakColor | null): Partial<CSSVariables> {
if (color == null) return {}; if (color == null) return {};
return { return {
text: color.lift(0.8).css(), text: color.desaturate(0.5).lift(0.12).css(),
textSubtle: color.translucify(0.3).css(), textSubtle: color.desaturate(0.58).lift(0.04).translucify(0.04).css(),
textSubtlest: color.translucify(0.6).css(), textSubtlest: color.desaturate(0.65).translucify(0.18).css(),
surface: color.translucify(0.95).css(), surface: color.translucify(0.95).css(),
surfaceHighlight: color.translucify(0.85).css(),
border: color.lift(0.3).translucify(0.8).css(), border: color.lift(0.3).translucify(0.8).css(),
}; };
} }
+3
View File
@@ -69,6 +69,9 @@ const config = JSON.stringify({
const normalizedAdditionalArgs = []; const normalizedAdditionalArgs = [];
for (let i = 0; i < additionalArgs.length; i++) { for (let i = 0; i < additionalArgs.length; i++) {
const arg = additionalArgs[i]; const arg = additionalArgs[i];
if (arg === "--") {
continue;
}
if (arg === "--config" && i + 1 < additionalArgs.length) { if (arg === "--config" && i + 1 < additionalArgs.length) {
const value = additionalArgs[i + 1]; const value = additionalArgs[i + 1];
const isInlineJson = value.trimStart().startsWith("{"); const isInlineJson = value.trimStart().startsWith("{");