Changes for commercial use (#138)

This commit is contained in:
Gregory Schier
2024-12-03 09:28:27 -08:00
committed by GitHub
parent 2b076c90e4
commit 88bcfb9e66
49 changed files with 1072 additions and 96 deletions

View File

@@ -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 />,
},
],

View File

@@ -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

View File

@@ -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 () => {

View 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>
);
}

View File

@@ -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>
);

View 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&apos;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&apos;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>
);
}

View File

@@ -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>
);

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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>
);
}

View File

@@ -8,7 +8,7 @@ export interface AppInfo {
appLogDir: string;
}
const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
export const appInfo = (await invokeCmd('cmd_metadata')) as AppInfo;
export function useAppInfo() {
return appInfo;

View File

@@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { SettingsTab } from '../components/Settings/Settings';
import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar';
import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment';
@@ -13,12 +14,18 @@ export type RouteParamsRequest = RouteParamsWorkspace & {
requestId: string;
};
export type RouteParamsSettings = {
workspaceId: string;
tab?: SettingsTab;
};
export const paths = {
workspaces() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
workspaces(_ = {}) {
return '/workspaces';
},
workspaceSettings({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) {
return `/workspaces/${workspaceId}/settings`;
workspaceSettings({ workspaceId, tab } = { workspaceId: ':workspaceId' } as RouteParamsSettings) {
return `/workspaces/${workspaceId}/settings?tab=${tab ?? SettingsTab.General}`;
},
workspace(
{ workspaceId, environmentId, cookieJarId } = {

View File

@@ -19,7 +19,7 @@ export function useKeyValue<T extends object | boolean | number | string | null>
key,
fallback,
}: {
namespace?: 'global' | 'no_sync';
namespace?: 'global' | 'no_sync' | 'license';
key: string | string[];
fallback: T;
}) {

View File

@@ -1,4 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { SettingsTab } from '../components/Settings/Settings';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { useAppRoutes } from './useAppRoutes';
@@ -8,15 +9,11 @@ export function useOpenSettings() {
const workspace = useActiveWorkspace();
return useMutation({
mutationKey: ['open_settings'],
mutationFn: async () => {
mutationFn: async (tab?: SettingsTab) => {
if (workspace == null) return;
await invokeCmd('cmd_new_child_window', {
url: routes.paths.workspaceSettings({
workspaceId: workspace.id,
cookieJarId: null,
environmentId: null,
}),
url: routes.paths.workspaceSettings({ workspaceId: workspace.id, tab }),
label: 'settings',
title: 'Yaak Settings',
innerSize: [600, 550],

View File

@@ -28,6 +28,7 @@
"@tauri-apps/plugin-log": "^2.0.0",
"@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@yaakapp-internal/license": "^1.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"cm6-graphql": "^0.0.9",

View File

@@ -1,7 +1,7 @@
const plugin = require('tailwindcss/plugin');
const sizes = {
'2xs': '1.25rem',
'2xs': '1.4rem',
xs: '1.8rem',
sm: '2.0rem',
md: '2.5rem',
@@ -58,6 +58,7 @@ module.exports = {
xs: '0.8rem',
sm: '0.9rem',
base: '1rem',
lg: '1.12rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '2rem',