Tweak license flow

This commit is contained in:
Gregory Schier
2024-12-16 13:46:58 -08:00
parent 20140148bf
commit e2253786dc
4 changed files with 37 additions and 77 deletions

View File

@@ -81,18 +81,17 @@ pub async fn activate_license<R: Runtime>(
if let Err(e) = window.emit("license-activated", true) { if let Err(e) = window.emit("license-activated", true) {
warn!("Failed to emit check-license event: {}", e); warn!("Failed to emit check-license event: {}", e);
} }
Ok(()) Ok(())
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "license.ts")] #[ts(export, export_to = "license.ts")]
pub enum LicenseCheckStatus { pub enum LicenseCheckStatus {
PersonalUse, PersonalUse { trial_ended: NaiveDateTime },
CommercialUse, CommercialUse,
InvalidLicense, InvalidLicense,
Trialing { end: NaiveDateTime }, Trialing { end: NaiveDateTime },
TrialEnded { end: NaiveDateTime },
} }
pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> { pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<LicenseCheckStatus> {
@@ -114,7 +113,7 @@ pub async fn check_license<R: Runtime>(app_handle: &AppHandle<R>) -> Result<Lice
match (has_activation_id, trial_period_active) { match (has_activation_id, trial_period_active) {
(false, true) => Ok(LicenseCheckStatus::Trialing { end: trial_end }), (false, true) => Ok(LicenseCheckStatus::Trialing { end: trial_end }),
(false, false) => Ok(LicenseCheckStatus::TrialEnded { end: trial_end }), (false, false) => Ok(LicenseCheckStatus::PersonalUse { trial_ended: trial_end }),
(true, _) => { (true, _) => {
info!("Checking license activation"); info!("Checking license activation");
// A license has been activated, so let's check the license server // A license has been activated, so let's check the license server

View File

@@ -1,14 +1,18 @@
import type { LicenseCheckStatus } from '@yaakapp-internal/license'; import type { LicenseCheckStatus } from '@yaakapp-internal/license';
import { useLicense } from '@yaakapp-internal/license'; import { useLicense } from '@yaakapp-internal/license';
import { useOpenSettings } from '../hooks/useOpenSettings'; import { useOpenSettings } from '../hooks/useOpenSettings';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { SettingsTab } from './Settings/Settings'; import { SettingsTab } from './Settings/Settings';
const labels: Record<LicenseCheckStatus['type'], string | null> = { const details: Record<
LicenseCheckStatus['type'],
{ label: string; color: ButtonProps['color'] } | null
> = {
commercial_use: null, commercial_use: null,
personal_use: 'Personal Use', invalid_license: { label: 'Invalid License', color: 'danger' },
trial_ended: 'Personal Use', personal_use: { label: 'Personal Use', color: 'success' },
trialing: 'Active Trial', trialing: { label: 'Personal Use', color: 'success' },
}; };
export function LicenseBadge() { export function LicenseBadge() {
@@ -19,8 +23,8 @@ export function LicenseBadge() {
return null; return null;
} }
const label = labels[check.data.type]; const detail = details[check.data.type];
if (label == null) { if (detail == null) {
return null; return null;
} }
@@ -30,13 +34,9 @@ export function LicenseBadge() {
variant="border" variant="border"
className="!rounded-full mx-1" className="!rounded-full mx-1"
onClick={() => openSettings.mutate()} onClick={() => openSettings.mutate()}
color={ color={detail.color}
check.data.type == 'trial_ended' || check.data.type === 'personal_use'
? 'primary'
: 'success'
}
> >
{label} {detail.label}
</Button> </Button>
); );
} }

View File

@@ -1,16 +1,11 @@
import { open } from '@tauri-apps/plugin-shell'; import { open } from '@tauri-apps/plugin-shell';
import { useLicense } from '@yaakapp-internal/license'; import { useLicense } from '@yaakapp-internal/license';
import classNames from 'classnames'; import { formatDistanceToNow } from 'date-fns';
import { format, formatDistanceToNow } from 'date-fns';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useCopy } from '../../hooks/useCopy';
import { useSettings } from '../../hooks/useSettings';
import { useTimedBoolean } from '../../hooks/useTimedBoolean';
import { useToggle } from '../../hooks/useToggle'; import { useToggle } from '../../hooks/useToggle';
import { Banner } from '../core/Banner'; import { Banner } from '../core/Banner';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
import { Link } from '../core/Link'; import { Link } from '../core/Link';
import { PlainInput } from '../core/PlainInput'; import { PlainInput } from '../core/PlainInput';
import { HStack, VStack } from '../core/Stacks'; import { HStack, VStack } from '../core/Stacks';
@@ -19,15 +14,13 @@ export function SettingsLicense() {
const { check, activate } = useLicense(); const { check, activate } = useLicense();
const [key, setKey] = useState<string>(''); const [key, setKey] = useState<string>('');
const [activateFormVisible, toggleActivateFormVisible] = useToggle(false); const [activateFormVisible, toggleActivateFormVisible] = useToggle(false);
const settings = useSettings();
const specialAnnouncement = if (check.isPending) {
settings.createdAt < '2024-12-03' && check.data?.type !== 'commercial_use'; return null;
const [copied, setCopied] = useTimedBoolean(); }
const copy = useCopy({ disableToast: true });
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{check.data?.type === 'personal_use' && <Banner color="info">You&apos;re</Banner>}
{check.data?.type === 'commercial_use' && ( {check.data?.type === 'commercial_use' && (
<Banner color="success"> <Banner color="success">
<strong>License active!</strong> Enjoy using Yaak for commercial use. <strong>License active!</strong> Enjoy using Yaak for commercial use.
@@ -39,56 +32,24 @@ export function SettingsLicense() {
using Yaak for commercial use, please purchase a commercial use license. using Yaak for commercial use, please purchase a commercial use license.
</Banner> </Banner>
)} )}
{check.data?.type === 'trial_ended' && !specialAnnouncement && ( {check.data?.type === 'personal_use' && (
<Banner color="primary"> <Banner color="primary" className="flex flex-col gap-2">
<strong>Your trial ended on {format(check.data.end, 'MMMM dd, yyyy')}</strong>. A <h2 className="text-lg font-semibold">Commercial License</h2>
commercial-use license is required if you use Yaak within a for-profit organization of two <p>
or more people. A commercial license is required if you use Yaak within a for-profit organization of two
or more people.
</p>
<p>
<Link href="https://yaak.app/pricing" className="text-sm">
Learn More
</Link>
</p>
</Banner> </Banner>
)} )}
{check.error && <Banner color="danger">{check.error}</Banner>} {check.error && <Banner color="danger">{check.error}</Banner>}
{activate.error && <Banner color="danger">{activate.error}</Banner>} {activate.error && <Banner color="danger">{activate.error}</Banner>}
{specialAnnouncement && (
<VStack className="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{' '}
<button
title="Copy coupon code"
className="hover:text-notice"
onClick={() => {
setCopied();
copy('EARLYAAK');
}}
>
<InlineCode className="inline-flex items-center gap-1">
EARLYAAK{' '}
<Icon
icon={copied ? 'check' : 'copy'}
size="xs"
className={classNames(copied && 'text-success')}
/>
</InlineCode>
</button>{' '}
at checkout for 50% off your first year of the individual plan.
</p>
<p>~ Greg</p>
</VStack>
)}
{check.data?.type === 'commercial_use' ? ( {check.data?.type === 'commercial_use' ? (
<HStack space={2}> <HStack space={2}>
<Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}> <Button variant="border" color="secondary" size="sm" onClick={toggleActivateFormVisible}>
@@ -105,6 +66,9 @@ export function SettingsLicense() {
</HStack> </HStack>
) : ( ) : (
<HStack space={2}> <HStack space={2}>
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
Activate License
</Button>
<Button <Button
color="secondary" color="secondary"
size="sm" size="sm"
@@ -113,9 +77,6 @@ export function SettingsLicense() {
> >
Purchase Purchase
</Button> </Button>
<Button color="primary" size="sm" onClick={toggleActivateFormVisible}>
Activate License
</Button>
</HStack> </HStack>
)} )}

View File

@@ -15,8 +15,8 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
className, className,
`x-theme-banner--${color}`, `x-theme-banner--${color}`,
'whitespace-pre-wrap', 'whitespace-pre-wrap',
'border border-dashed border-border-subtle bg-surface', 'border border-dashed border-border bg-surface',
'italic px-3 py-2 rounded select-auto cursor-text', 'px-3 py-2 rounded select-auto cursor-text',
'overflow-x-auto text-text', 'overflow-x-auto text-text',
)} )}
> >