mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:14:03 +01:00
Better license flows
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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'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"
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
15
src-web/hooks/useLicenseConfirmation.ts
Normal file
15
src-web/hooks/useLicenseConfirmation.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user