mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:14:03 +01:00
Changes for commercial use (#138)
This commit is contained in:
@@ -44,11 +44,11 @@ const router = createBrowserRouter([
|
||||
element: <RedirectLegacyEnvironmentURLs />,
|
||||
},
|
||||
{
|
||||
path: paths.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
environmentId: null,
|
||||
cookieJarId: null,
|
||||
}),
|
||||
path: paths
|
||||
.workspaceSettings({
|
||||
workspaceId: ':workspaceId',
|
||||
})
|
||||
.replace(/\?.*/, ''),
|
||||
element: <LazySettings />,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -33,7 +33,7 @@ export function HeaderSize({
|
||||
style={{
|
||||
...style,
|
||||
// Add padding for macOS stoplights, but keep it the same width (account for the interface scale)
|
||||
paddingLeft: stoplightsVisible ? 72 / settings.interfaceScale : undefined,
|
||||
paddingLeft: (stoplightsVisible && !ignoreControlsSpacing) ? 72 / settings.interfaceScale : undefined,
|
||||
...(size === 'md' ? { height: HEADER_SIZE_MD } : {}),
|
||||
...(size === 'lg' ? { height: HEADER_SIZE_LG } : {}),
|
||||
...(osInfo.osType === 'macos' || ignoreControlsSpacing
|
||||
|
||||
@@ -28,9 +28,10 @@ export function ImportCurlButton() {
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
size="2xs"
|
||||
variant="border"
|
||||
color="primary"
|
||||
color="success"
|
||||
className="rounded-full"
|
||||
leftSlot={<Icon icon="paste" size="sm" />}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
|
||||
42
src-web/components/LicenseBadge.tsx
Normal file
42
src-web/components/LicenseBadge.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { useOpenSettings } from '../hooks/useOpenSettings';
|
||||
import { Button } from './core/Button';
|
||||
import { SettingsTab } from './Settings/Settings';
|
||||
|
||||
const labels: Record<LicenseCheckStatus['type'], string | null> = {
|
||||
commercial_use: null,
|
||||
personal_use: 'Personal Use',
|
||||
trial_ended: 'Personal Use',
|
||||
trialing: 'Active Trial',
|
||||
};
|
||||
|
||||
export function LicenseBadge() {
|
||||
const openSettings = useOpenSettings();
|
||||
const { check } = useLicense();
|
||||
|
||||
if (check.data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = labels[check.data.type];
|
||||
if (label == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="2xs"
|
||||
variant="border"
|
||||
className="!rounded-full mx-1"
|
||||
onClick={() => openSettings.mutate(SettingsTab.License)}
|
||||
color={
|
||||
check.data.type == 'trial_ended' || check.data.type === 'personal_use'
|
||||
? 'primary'
|
||||
: 'success'
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import { capitalize } from '../../lib/capitalize';
|
||||
@@ -9,25 +10,34 @@ import { TabContent, Tabs } from '../core/Tabs/Tabs';
|
||||
import { HeaderSize } from '../HeaderSize';
|
||||
import { SettingsAppearance } from './SettingsAppearance';
|
||||
import { SettingsGeneral } from './SettingsGeneral';
|
||||
import { SettingsLicense } from './SettingsLicense';
|
||||
import { SettingsPlugins } from './SettingsPlugins';
|
||||
import {SettingsProxy} from "./SettingsProxy";
|
||||
import { SettingsProxy } from './SettingsProxy';
|
||||
|
||||
interface Props {
|
||||
hide?: () => void;
|
||||
}
|
||||
|
||||
enum Tab {
|
||||
export enum SettingsTab {
|
||||
General = 'general',
|
||||
Proxy = 'proxy',
|
||||
Appearance = 'appearance',
|
||||
Plugins = 'plugins',
|
||||
License = 'license',
|
||||
}
|
||||
|
||||
const tabs = [Tab.General, Tab.Appearance, Tab.Proxy, Tab.Plugins];
|
||||
const tabs = [
|
||||
SettingsTab.General,
|
||||
SettingsTab.Appearance,
|
||||
SettingsTab.Proxy,
|
||||
SettingsTab.Plugins,
|
||||
SettingsTab.License,
|
||||
];
|
||||
|
||||
export default function Settings({ hide }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const [tab, setTab] = useState<string>(Tab.General);
|
||||
const [params] = useSearchParams();
|
||||
const [tab, setTab] = useState<string>(params.get('tab') ?? SettingsTab.General);
|
||||
|
||||
// Close settings window on escape
|
||||
// TODO: Could this be put in a better place? Eg. in Rust key listener when creating the window
|
||||
@@ -71,18 +81,21 @@ export default function Settings({ hide }: Props) {
|
||||
onChangeValue={setTab}
|
||||
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
|
||||
>
|
||||
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.General} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsGeneral />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsAppearance />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Plugins} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsPlugins />
|
||||
</TabContent>
|
||||
<TabContent value={Tab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<TabContent value={SettingsTab.Proxy} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsProxy />
|
||||
</TabContent>
|
||||
<TabContent value={SettingsTab.License} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<SettingsLicense />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
125
src-web/components/Settings/SettingsLicense.tsx
Normal file
125
src-web/components/Settings/SettingsLicense.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import React, { useState } from 'react';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { InlineCode } from '../core/InlineCode';
|
||||
import { Link } from '../core/Link';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
|
||||
export function SettingsLicense() {
|
||||
const { check, activate } = useLicense();
|
||||
const [key, setKey] = useState<string>('');
|
||||
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
|
||||
const settings = useSettings();
|
||||
const specialAnnouncement =
|
||||
settings.createdAt < '2024-12-02' && check.data?.type === 'trial_ended';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{check.data?.type === 'trialing' && (
|
||||
<Banner color="success">
|
||||
<strong>Your trial ends in {formatDistanceToNow(check.data.end)}</strong>. If you're
|
||||
using Yaak for commercial use, please purchase a commercial use license.
|
||||
</Banner>
|
||||
)}
|
||||
{check.data?.type === 'trial_ended' && (
|
||||
<Banner color={'primary'}>
|
||||
<strong>Your trial ended on {format(check.data.end, 'MMMM dd, yyyy')}</strong>. A
|
||||
commercial-use license is required if you use Yaak within a for-profit organization of two
|
||||
or more people.
|
||||
</Banner>
|
||||
)}
|
||||
{check.data?.type === 'personal_use' && <Banner color="info">You're</Banner>}
|
||||
{check.data?.type === 'commercial_use' && (
|
||||
<Banner color="success">
|
||||
<strong>License active!</strong> Enjoy using Yaak for commercial use.
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
{check.error && <Banner color="danger">{check.error}</Banner>}
|
||||
{activate.error && <Banner color="danger">{activate.error}</Banner>}
|
||||
|
||||
{specialAnnouncement && (
|
||||
<VStack className="my-4 max-w-lg" space={4}>
|
||||
<p>
|
||||
<strong>Thank you for being an early supporter of Yaak!</strong>
|
||||
</p>
|
||||
<p>
|
||||
To support the ongoing development of the best local-first API client, Yaak now requires
|
||||
a paid license for the commercial use of prebuilt binaries (personal use and running the
|
||||
open-source code remains free.)
|
||||
</p>
|
||||
<p>
|
||||
For details, see the{' '}
|
||||
<Link href="https://yaak.app/blog/commercial-use">Announcement Post</Link>.
|
||||
</p>
|
||||
<p>
|
||||
As a thank-you, enter code <InlineCode>EARLYAAK</InlineCode> at checkout for 50% off
|
||||
your first year of the individual plan.
|
||||
</p>
|
||||
<p>~ Greg</p>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{check.data?.type === 'commercial_use' ? (
|
||||
<HStack space={2}>
|
||||
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate Another License
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => open('https://yaak.app/dashboard')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
>
|
||||
Direct Support
|
||||
</Button>
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack space={2}>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => open('https://yaak.app/pricing')}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
>
|
||||
Purchase
|
||||
</Button>
|
||||
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
|
||||
Activate License
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{activateFormVisible && (
|
||||
<VStack
|
||||
as="form"
|
||||
space={3}
|
||||
className="max-w-sm"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
toggleActivateFormVisible();
|
||||
activate.mutate({ licenseKey: key });
|
||||
}}
|
||||
>
|
||||
<PlainInput
|
||||
autoFocus
|
||||
label="License Key"
|
||||
name="key"
|
||||
onChange={setKey}
|
||||
placeholder="YK1-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||
/>
|
||||
<Button type="submit" color="primary" size="sm" isLoading={activate.isPending}>
|
||||
Submit
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { memo } from 'react';
|
||||
import { appInfo } from '../hooks/useAppInfo';
|
||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||
import { CookieDropdown } from './CookieDropdown';
|
||||
import { Button } from './core/Button';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { ImportCurlButton } from './ImportCurlButton';
|
||||
import { LicenseBadge } from './LicenseBadge';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
@@ -37,7 +40,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div className="pointer-events-none w-full max-w-[30vw] mx-auto">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-0.5">
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||
<ImportCurlButton />
|
||||
<IconButton
|
||||
icon="search"
|
||||
@@ -46,6 +49,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
onClick={togglePalette}
|
||||
/>
|
||||
<SettingsDropdown />
|
||||
<LicenseBadge />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
size === 'md' && 'h-md px-3 rounded-md',
|
||||
size === 'sm' && 'h-sm px-2.5 rounded-md',
|
||||
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
|
||||
size === '2xs' && 'h-2xs px-1 text-xs rounded',
|
||||
size === '2xs' && 'h-2xs px-2 text-xs rounded',
|
||||
|
||||
// Solids
|
||||
variant === 'solid' && 'border-transparent',
|
||||
|
||||
@@ -18,11 +18,11 @@ export function Link({ href, children, className, ...other }: Props) {
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classNames(className, 'pr-4')}
|
||||
className={classNames(className, 'pr-4 inline-flex items-center')}
|
||||
{...other}
|
||||
>
|
||||
<span className="underline">{children}</span>
|
||||
<Icon className="inline absolute right-0.5 top-0.5" size="xs" icon="external_link" />
|
||||
<Icon className="inline absolute right-0.5 top-1.5" size="xs" icon="external_link" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user