Rework licensing flows to be more friendly

This commit is contained in:
Gregory Schier
2025-09-29 15:40:15 -07:00
parent 7262eccac5
commit 6c79c1ef3f
15 changed files with 133 additions and 79 deletions

View File

@@ -1,8 +1,9 @@
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license';
import { settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings';
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
import { appInfo } from '../lib/appInfo';
import { BadgeButton } from './core/BadgeButton';
import type { ButtonProps } from './core/Button';
@@ -19,7 +20,7 @@ const details: Record<
export function LicenseBadge() {
const { check } = useLicense();
const [licenseDetails, setLicenseDetails] = useLicenseConfirmation();
const settings = useAtomValue(settingsAtom);
if (appInfo.isDev) {
return null;
@@ -32,17 +33,17 @@ export function LicenseBadge() {
}
// Hasn't loaded yet
if (licenseDetails == null || check.data == null) {
if (check.data == null) {
return null;
}
// User has confirmed they are using Yaak for personal use only, so hide badge
if (licenseDetails.confirmedPersonalUse) {
// Dismissed license badge
if (settings.hideLicenseBadge) {
return null;
}
// User is trialing but has already seen the message, so hide badge
if (check.data.type === 'trialing' && licenseDetails.hasDismissedTrial) {
if (check.data.type === 'trialing') {
return null;
}
@@ -55,12 +56,6 @@ export function LicenseBadge() {
<BadgeButton
color={detail.color}
onClick={async () => {
if (check.data.type === 'trialing') {
await setLicenseDetails((v) => ({
...v,
hasDismissedTrial: true,
}));
}
openSettings.mutate('license');
}}
>

View File

@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { convertFileSrc } from '@tauri-apps/api/core';
import { resolveResource } from '@tauri-apps/api/path';
import classNames from 'classnames';
import React from 'react';
interface Props {
src: string;
className?: string;
}
export function LocalImage({ src: srcPath, className }: Props) {
const src = useQuery({
queryKey: ['local-image', srcPath],
queryFn: async () => {
const p = await resolveResource(srcPath);
console.log("LOADING SRC", srcPath, p)
return convertFileSrc(p);
},
});
return (
<img
src={src.data}
alt="Response preview"
className={classNames(
className,
'transition-opacity',
src.data == null ? 'opacity-0' : 'opacity-100',
)}
/>
);
}

View File

@@ -74,22 +74,22 @@ export default function Settings({ hide }: Props) {
onChangeValue={setTab}
tabs={tabs.map((value) => ({ value, label: capitalize(value) }))}
>
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-4">
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full p-8">
<SettingsGeneral />
</TabContent>
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-4">
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full p-8">
<SettingsInterface />
</TabContent>
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-4">
<TabContent value={TAB_THEME} className="overflow-y-auto h-full p-8">
<SettingsTheme />
</TabContent>
<TabContent value={TAB_PLUGINS} className="h-full px-4 grid grid-rows-1">
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 p-8">
<SettingsPlugins />
</TabContent>
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-4">
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full p-8!">
<SettingsProxy />
</TabContent>
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-4">
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-8 !py-4">
<SettingsLicense />
</TabContent>
</Tabs>

View File

@@ -1,13 +1,16 @@
import { type } from '@tauri-apps/plugin-os';
import { useFonts } from '@yaakapp-internal/fonts';
import { useLicense } from '@yaakapp-internal/license';
import type { EditorKeymap } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { showConfirm } from '../../lib/confirm';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
@@ -28,6 +31,7 @@ export function SettingsInterface() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const fonts = useFonts();
const license = useLicense();
if (settings == null || workspace == null) {
return null;
@@ -123,6 +127,31 @@ export function SettingsInterface() {
title="Colorize Request Methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
{license.check.data?.type === 'personal_use' && (
<Checkbox
checked={!settings.licenseBadge}
title="Hide personal use badge"
onChange={async (hide) => {
if (hide) {
const confirmed = await showConfirm({
id: 'hide-license-badge',
title: 'Hide License Badge',
confirmText: 'Hide Badge',
description: (
<>
Only proceed if youre using Yaak for personal projects only. If youre using it
at work, please <Link href="https://yaak.app/">Purchase a License</Link>.
</>
),
requireTyping: 'Personal Use',
color: 'notice',
});
if (!confirmed) return;
}
await patchModel(settings, { licenseBadge: !hide });
}}
/>
)}
{type() !== 'macos' && (
<Checkbox

View File

@@ -2,88 +2,80 @@ import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { differenceInDays } from 'date-fns';
import React, { useState } from 'react';
import { useLicenseConfirmation } from '../../hooks/useLicenseConfirmation';
import { useToggle } from '../../hooks/useToggle';
import { pluralizeCount } from '../../lib/pluralize';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Link } from '../core/Link';
import { PlainInput } from '../core/PlainInput';
import { HStack, VStack } from '../core/Stacks';
import { LocalImage } from '../LocalImage';
export function SettingsLicense() {
const { check, activate, deactivate } = useLicense();
const [key, setKey] = useState<string>('');
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
const [licenseDetails, setLicenseDetails] = useLicenseConfirmation();
const [checked, setChecked] = useState<boolean>(false);
if (check.isPending) {
return null;
}
return (
<div className="flex flex-col gap-6 max-w-lg">
<div className="flex flex-col gap-6 max-w-xl">
{check.data?.type === 'commercial_use' ? (
<Banner color="success">
<strong>License active!</strong> Enjoy using Yaak for commercial use.
</Banner>
<Banner color="success">Your license is active 🥳</Banner>
) : check.data?.type == 'trialing' ? (
<Banner color="success" className="flex flex-col gap-3 max-w-lg">
<p className="select-text">
You have{' '}
<strong>
{pluralizeCount('day', differenceInDays(check.data.end, new Date()))} remaining
</strong>{' '}
on your commercial use trial. Once the trial ends you agree to only use Yaak for
personal use until a license is activated.
on trial
</p>
</Banner>
) : check.data?.type == 'personal_use' && !licenseDetails?.confirmedPersonalUse ? (
) : check.data?.type == 'personal_use' ? (
<Banner color="success" className="flex flex-col gap-3 max-w-lg">
<p className="select-text">
Your 30-day trial has ended. Please activate a license or confirm how you&apos;re using
Yaak.
</p>
<form
className="flex flex-col gap-3 items-start"
onSubmit={async (e) => {
e.preventDefault();
await setLicenseDetails((v) => ({
...v,
confirmedPersonalUse: true,
}));
}}
>
<Checkbox
checked={checked}
onChange={setChecked}
title="I am only using Yaak for personal use"
/>
<Button type="submit" disabled={!checked} size="xs" variant="border" color="success">
Confirm
</Button>
</form>
<p>Your free trial has ended</p>
</Banner>
) : null}
<p className="select-text">
A commercial license is required if using Yaak within a for-profit organization.{' '}
<Link href="https://yaak.app/pricing" className="text-notice">
Learn More
</Link>
</p>
{check.data?.type !== 'commercial_use' && (
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-6 items-center my-3 ">
<LocalImage src="static/greg.jpeg" className="rounded-full h-20 w-20" />
<div className="flex flex-col gap-2">
<h2 className="text-lg font-bold">Hey, I&apos;m Greg 👋🏼</h2>
<p>
Yaak is free for personal projects and learning.{' '}
{check.data?.type === 'trialing' ? 'After your trial, a ' : 'A '}
license is required for work or commercial use.
</p>
<p>
<Link
noUnderline
href="https://yaak.app/pricing"
className="text-sm text-notice opacity-80 hover:opacity-100"
>
Learn More
</Link>
</p>
</div>
</div>
)}
{check.error && <Banner color="danger">{check.error}</Banner>}
{activate.error && <Banner color="danger">{activate.error}</Banner>}
{check.data?.type === 'commercial_use' ? (
<HStack space={2}>
<Button variant="border" color="secondary" size="sm" onClick={() => {
deactivate.mutate();
}}>
<Button
variant="border"
color="secondary"
size="sm"
onClick={() => {
deactivate.mutate();
}}
>
Deactivate License
</Button>
<Button

View File

@@ -39,6 +39,7 @@ const icons = {
code: lucide.CodeIcon,
columns_2: lucide.Columns2Icon,
command: lucide.CommandIcon,
credit_card: lucide.CreditCardIcon,
cookie: lucide.CookieIcon,
copy: lucide.CopyIcon,
copy_check: lucide.CopyCheck,