Better license flows

This commit is contained in:
Gregory Schier
2025-02-24 05:59:15 -08:00
parent 2b1431d041
commit af7782c93b
9 changed files with 184 additions and 70 deletions

View File

@@ -336,12 +336,14 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
label: banner,
},
{
color: 'success',
label: 'Open Workspace Settings',
leftSlot: <Icon icon="settings" />,
onSelect() {
openWorkspaceSettings.mutate({ openSyncMenu: true });
},
},
{ type: 'separator' },
{
label: 'Hide This Message',
leftSlot: <Icon icon="eye_closed" />,
@@ -396,8 +398,8 @@ function SetupGitDropdown({
leftSlot: <Icon icon="magic_wand" />,
onSelect: initRepo,
},
{ type: 'separator' },
{
color: 'warning',
label: 'Hide This Message',
leftSlot: <Icon icon="eye_closed" />,
async onSelect() {

View File

@@ -1,14 +1,14 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license';
import type { ReactNode } from 'react';
import { openSettings } from '../commands/openSettings';
import { appInfo } from '../hooks/useAppInfo';
import { useLicenseConfirmation } from '../hooks/useLicenseConfirmation';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import { Icon } from './core/Icon';
import { HStack } from './core/Stacks';
import { openSettings } from '../commands/openSettings';
import {SettingsTab} from "./Settings/SettingsTab";
import { SettingsTab } from './Settings/SettingsTab';
const details: Record<
LicenseCheckStatus['type'] | 'dev' | 'beta',
@@ -26,22 +26,30 @@ const details: Record<
dev: { label: 'Develop', color: 'secondary' },
commercial_use: null,
invalid_license: { label: 'License Error', color: 'danger' },
personal_use: { label: 'Personal Use', color: 'primary' },
trialing: { label: 'Personal Use', color: 'primary' },
personal_use: { label: 'Personal Use', color: 'success' },
trialing: { label: 'Active Trial', color: 'success' },
};
export function LicenseBadge() {
const { check } = useLicense();
const [licenseDetails, setLicenseDetails] = useLicenseConfirmation();
if (check.data == null) {
// Hasn't loaded yet
if (licenseDetails == null || check.data == null) {
return null;
}
const checkType = appInfo.version.includes('beta')
? 'beta'
: appInfo.isDev
? 'dev'
: check.data.type;
// User has confirmed they are using Yaak for personal use only, so hide badge
if (licenseDetails.confirmedPersonalUse) {
return null;
}
// User is trialing but has already seen the message, so hide badge
if (check.data.type === 'trialing' && licenseDetails.hasDismissedTrial) {
return null;
}
const checkType = appInfo.version.includes('beta') ? 'beta' : check.data.type;
const detail = details[checkType];
if (detail == null) {
return null;
@@ -53,11 +61,13 @@ export function LicenseBadge() {
variant="border"
className="!rounded-full mx-1"
onClick={async () => {
if (checkType === 'beta') {
await openUrl('https://feedback.yaak.app');
} else {
openSettings.mutate(SettingsTab.License);
if (check.data.type === 'trialing') {
await setLicenseDetails((v) => ({
...v,
dismissedTrial: true,
}));
}
openSettings.mutate(SettingsTab.License);
}}
color={detail.color}
event={{ id: 'license-badge', status: check.data.type }}

View File

@@ -1,47 +1,75 @@
import { openUrl } from '@tauri-apps/plugin-opener';
import { useLicense } from '@yaakapp-internal/license';
import { formatDistanceToNowStrict } from 'date-fns';
import React, { useState } from 'react';
import { useLicenseConfirmation } from '../../hooks/useLicenseConfirmation';
import { useToggle } from '../../hooks/useToggle';
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 { openUrl } from '@tauri-apps/plugin-opener';
export function SettingsLicense() {
const { check, activate } = 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">
<div className="flex flex-col gap-6 max-w-lg">
{check.data?.type === 'commercial_use' ? (
<Banner color="success">
<strong>License active!</strong> Enjoy using Yaak for commercial use.
</Banner>
) : (
<Banner color="primary" className="flex flex-col gap-3 max-w-lg">
{check.data?.type === 'trialing' && (
<p className="select-text">
<strong>
You have {formatDistanceToNowStrict(check.data.end)} remaining on your trial.
</strong>
</p>
)}
) : check.data?.type == 'trialing' ? (
<Banner color="success" className="flex flex-col gap-3 max-w-lg">
<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>
<strong>{formatDistanceToNowStrict(check.data.end)} days remaining</strong> on your
commercial use trial
</p>
</Banner>
)}
) : check.data?.type == 'personal_use' && !licenseDetails?.confirmedPersonalUse ? (
<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>
</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.error && <Banner color="danger">{check.error}</Banner>}
{activate.error && <Banner color="danger">{activate.error}</Banner>}
@@ -80,7 +108,7 @@ export function SettingsLicense() {
<Button
color="secondary"
size="sm"
onClick={() => open('https://yaak.app/pricing?ref=app.yaak.desktop')}
onClick={() => openUrl('https://yaak.app/pricing?ref=app.yaak.desktop')}
rightSlot={<Icon icon="external_link" />}
event="license.purchase"
>

View File

@@ -65,7 +65,7 @@ export function SettingsDropdown() {
label: 'Feedback',
leftSlot: <Icon icon="chat" />,
rightSlot: <Icon icon="external_link" />,
onSelect: () => openUrl('https://yaak.app/roadmap'),
onSelect: () => openUrl('https://yaak.app/feedback'),
},
{
label: 'Changelog',

View File

@@ -55,7 +55,7 @@ export type DropdownItemDefault = {
label: ReactNode;
hotKeyAction?: HotkeyAction;
hotKeyLabelOnly?: boolean;
color?: 'default' | 'danger' | 'info' | 'warning' | 'notice';
color?: 'default' | 'danger' | 'info' | 'warning' | 'notice' | 'success';
disabled?: boolean;
hidden?: boolean;
leftSlot?: ReactNode;
@@ -111,20 +111,23 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
const buttonRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
const setIsOpen = useCallback((o: SetStateAction<boolean>) => {
jotaiStore.set(openAtom, (prevId) => {
const prevIsOpen = prevId === id.current;
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
// Persist background color of button until we close the dropdown
if (newIsOpen) {
onOpen?.();
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
}
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
});
}, [onOpen]);
const setIsOpen = useCallback(
(o: SetStateAction<boolean>) => {
jotaiStore.set(openAtom, (prevId) => {
const prevIsOpen = prevId === id.current;
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
// Persist background color of button until we close the dropdown
if (newIsOpen) {
onOpen?.();
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
}
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
});
},
[onOpen],
);
// Because a different dropdown can cause ours to close, a useEffect([isOpen]) is the only method
// we have of detecting the dropdown closed, to do cleanup.
@@ -642,6 +645,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
'focus:bg-surface-highlight focus:text rounded',
item.color === 'danger' && '!text-danger',
item.color === 'success' && '!text-success',
item.color === 'warning' && '!text-warning',
item.color === 'notice' && '!text-notice',
item.color === 'info' && '!text-info',

View File

@@ -115,7 +115,6 @@ export function useHotKey(
if (e.metaKey) currentKeysWithModifiers.add('Meta');
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
console.log('down', currentKeysWithModifiers);
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
if (
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&

View File

@@ -0,0 +1,15 @@
import { useKeyValue } from './useKeyValue';
interface LicenseConfirmation {
hasDismissedTrial: boolean;
confirmedPersonalUse: boolean;
}
export function useLicenseConfirmation() {
const { set, value } = useKeyValue<LicenseConfirmation>({
key: 'license_confirmation',
fallback: { hasDismissedTrial: false, confirmedPersonalUse: false },
});
return [value, set] as const;
}