Compare commits

..

3 Commits

Author SHA1 Message Date
Gregory Schier 4092511f22 Add commercial use upsell banners 2026-06-19 16:09:43 -07:00
Gregory Schier 3de9a1edd4 Persist response filter per request 2026-06-11 09:09:12 -07:00
Gregory Schier 1b28dfd9d1 Actually fix overflowing text when Input has right slot items 2026-06-03 12:44:33 -07:00
15 changed files with 214 additions and 59 deletions
Generated
+9 -9
View File
@@ -215,7 +215,7 @@ dependencies = [
"objc2-foundation 0.3.1",
"parking_lot",
"percent-encoding",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"wl-clipboard-rs",
"x11rb",
]
@@ -1151,7 +1151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static 1.5.0",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1970,7 +1970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6534,7 +6534,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6547,7 +6547,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -7508,9 +7508,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.46"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
dependencies = [
"filetime",
"libc",
@@ -7988,7 +7988,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -9317,7 +9317,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -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;
}
+2 -2
View File
@@ -292,8 +292,8 @@ function BaseInput({
inputWrapperClassName,
"flex-1 min-w-0 px-2",
fullHeight && "h-full",
leftSlot ? "pl-0.5 -ml-2" : null,
rightSlot ? "pr-0.5 -mr-2" : null,
leftSlot ? "pl-0" : null,
rightSlot ? "pr-0" : null,
)}
>
<Editor
@@ -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"
@@ -69,6 +69,7 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex
text={text}
language={language}
stateKey={`response.body.${response.id}`}
filterStateKey={`response.body.${response.requestId}`}
pretty={pretty}
className={className}
onFilter={filterCallback}
@@ -16,6 +16,7 @@ interface Props {
text: string;
language: EditorProps["language"];
stateKey: string | null;
filterStateKey?: string | null;
pretty?: boolean;
className?: string;
onFilter?: (filter: string) => {
@@ -27,16 +28,25 @@ interface Props {
const useFilterText = createGlobalState<Record<string, string | null>>({});
export function TextViewer({ language, text, stateKey, pretty, className, onFilter }: Props) {
export function TextViewer({
language,
text,
stateKey,
filterStateKey,
pretty,
className,
onFilter,
}: Props) {
const filterKey = filterStateKey ?? stateKey;
const [filterTextMap, setFilterTextMap] = useFilterText();
const filterText = stateKey ? (filterTextMap[stateKey] ?? null) : null;
const filterText = filterKey ? (filterTextMap[filterKey] ?? null) : null;
const debouncedFilterText = useDebouncedValue(filterText);
const setFilterText = useCallback(
(v: string | null) => {
if (!stateKey) return;
setFilterTextMap((m) => ({ ...m, [stateKey]: v }));
if (!filterKey) return;
setFilterTextMap((m) => ({ ...m, [filterKey]: v }));
},
[setFilterTextMap, stateKey],
[filterKey, setFilterTextMap],
);
const isSearching = filterText != null;
@@ -64,7 +74,7 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
nodes.push(
<div key="input" className="w-full !opacity-100">
<Input
key={stateKey ?? "filter"}
key={filterKey ?? "filter"}
validate={!filteredResponse.error}
hideLabel
autoFocus
@@ -76,7 +86,7 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
defaultValue={filterText}
onKeyDown={(e) => e.key === "Escape" && toggleSearch()}
onChange={setFilterText}
stateKey={stateKey ? `filter.${stateKey}` : null}
stateKey={filterKey ? `filter.${filterKey}` : null}
/>
</div>,
);
@@ -97,12 +107,12 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt
return nodes;
}, [
canFilter,
filterKey,
filterText,
filteredResponse.error,
filteredResponse.isPending,
isSearching,
language,
stateKey,
setFilterText,
toggleSearch,
]);
+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("{");