mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 17:58:27 +02:00
Add license handling for expired licenses
This commit is contained in:
@@ -85,13 +85,18 @@ impl YaakNotifier {
|
|||||||
let license_check = {
|
let license_check = {
|
||||||
use yaak_license::{LicenseCheckStatus, check_license};
|
use yaak_license::{LicenseCheckStatus, check_license};
|
||||||
match check_license(window).await {
|
match check_license(window).await {
|
||||||
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal",
|
||||||
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
|
Ok(LicenseCheckStatus::Active { .. }) => "commercial",
|
||||||
Ok(LicenseCheckStatus::InvalidLicense) => "invalid_license".to_string(),
|
Ok(LicenseCheckStatus::PastDue { .. }) => "past_due",
|
||||||
Ok(LicenseCheckStatus::Trialing { .. }) => "trialing".to_string(),
|
Ok(LicenseCheckStatus::Inactive { .. }) => "invalid_license",
|
||||||
Err(_) => "unknown".to_string(),
|
Ok(LicenseCheckStatus::Trialing { .. }) => "trialing",
|
||||||
|
Ok(LicenseCheckStatus::Expired { .. }) => "expired",
|
||||||
|
Ok(LicenseCheckStatus::Error { .. }) => "error",
|
||||||
|
Err(_) => "unknown",
|
||||||
}
|
}
|
||||||
|
.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "license"))]
|
#[cfg(not(feature = "license"))]
|
||||||
let license_check = "disabled".to_string();
|
let license_check = "disabled".to_string();
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ export type ActivateLicenseRequestPayload = { licenseKey: string, appVersion: st
|
|||||||
|
|
||||||
export type ActivateLicenseResponsePayload = { activationId: string, };
|
export type ActivateLicenseResponsePayload = { activationId: string, };
|
||||||
|
|
||||||
export type CheckActivationResponsePayload = { active: boolean, };
|
|
||||||
|
|
||||||
export type DeactivateLicenseRequestPayload = { appVersion: string, appPlatform: string, };
|
export type DeactivateLicenseRequestPayload = { appVersion: string, appPlatform: string, };
|
||||||
|
|
||||||
export type LicenseCheckStatus = { "type": "personal_use", trial_ended: string, } | { "type": "commercial_use" } | { "type": "invalid_license" } | { "type": "trialing", end: string, };
|
export type LicenseCheckStatus = { "status": "personal_use", "data": { trial_ended: string, } } | { "status": "trialing", "data": { end: string, } } | { "status": "error", "data": { message: string, code: string, } } | { "status": "active", "data": { periodEnd: string, cancelAt: string | null, } } | { "status": "inactive", "data": { status: string, } } | { "status": "expired", "data": { changes: number, changesUrl: string | null, billingUrl: string, periodEnd: string, } } | { "status": "past_due", "data": { billingUrl: string, periodEnd: string, } };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::error::Error::{ClientError, ServerError};
|
use crate::error::Error::{ClientError, JsonError, ServerError};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ops::Add;
|
use std::ops::Add;
|
||||||
@@ -24,13 +24,6 @@ pub struct CheckActivationRequestPayload {
|
|||||||
pub app_platform: String,
|
pub app_platform: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
#[ts(export, export_to = "license.ts")]
|
|
||||||
pub struct CheckActivationResponsePayload {
|
|
||||||
pub active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "license.ts")]
|
#[ts(export, export_to = "license.ts")]
|
||||||
@@ -63,6 +56,49 @@ pub struct APIErrorResponsePayload {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "snake_case", tag = "status", content = "data")]
|
||||||
|
#[ts(export, export_to = "license.ts")]
|
||||||
|
pub enum LicenseCheckStatus {
|
||||||
|
// Local Types
|
||||||
|
PersonalUse {
|
||||||
|
trial_ended: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
Trialing {
|
||||||
|
end: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
Error {
|
||||||
|
message: String,
|
||||||
|
code: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Server Types
|
||||||
|
Active {
|
||||||
|
#[serde(rename = "periodEnd")]
|
||||||
|
period_end: DateTime<Utc>,
|
||||||
|
#[serde(rename = "cancelAt")]
|
||||||
|
cancel_at: Option<DateTime<Utc>>,
|
||||||
|
},
|
||||||
|
Inactive {
|
||||||
|
status: String,
|
||||||
|
},
|
||||||
|
Expired {
|
||||||
|
changes: i32,
|
||||||
|
#[serde(rename = "changesUrl")]
|
||||||
|
changes_url: Option<String>,
|
||||||
|
#[serde(rename = "billingUrl")]
|
||||||
|
billing_url: String,
|
||||||
|
#[serde(rename = "periodEnd")]
|
||||||
|
period_end: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
PastDue {
|
||||||
|
#[serde(rename = "billingUrl")]
|
||||||
|
billing_url: String,
|
||||||
|
#[serde(rename = "periodEnd")]
|
||||||
|
period_end: DateTime<Utc>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn activate_license<R: Runtime>(
|
pub async fn activate_license<R: Runtime>(
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
license_key: &str,
|
license_key: &str,
|
||||||
@@ -141,16 +177,6 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "snake_case", tag = "type")]
|
|
||||||
#[ts(export, export_to = "license.ts")]
|
|
||||||
pub enum LicenseCheckStatus {
|
|
||||||
PersonalUse { trial_ended: NaiveDateTime },
|
|
||||||
CommercialUse,
|
|
||||||
InvalidLicense,
|
|
||||||
Trialing { end: NaiveDateTime },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<LicenseCheckStatus> {
|
||||||
let payload = CheckActivationRequestPayload {
|
let payload = CheckActivationRequestPayload {
|
||||||
app_platform: get_os_str().to_string(),
|
app_platform: get_os_str().to_string(),
|
||||||
@@ -159,10 +185,10 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
|
|||||||
let activation_id = get_activation_id(window.app_handle()).await;
|
let activation_id = get_activation_id(window.app_handle()).await;
|
||||||
|
|
||||||
let settings = window.db().get_settings();
|
let settings = window.db().get_settings();
|
||||||
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS));
|
let trial_end = settings.created_at.add(Duration::from_secs(TRIAL_SECONDS)).and_utc();
|
||||||
|
|
||||||
let has_activation_id = !activation_id.is_empty();
|
let has_activation_id = !activation_id.is_empty();
|
||||||
let trial_period_active = Utc::now().naive_utc() < trial_end;
|
let trial_period_active = Utc::now() < trial_end;
|
||||||
|
|
||||||
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 }),
|
||||||
@@ -173,7 +199,7 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
|
|||||||
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
|
||||||
let client = yaak_api_client(window.app_handle())?;
|
let client = yaak_api_client(window.app_handle())?;
|
||||||
let path = format!("/licenses/activations/{activation_id}/check");
|
let path = format!("/licenses/activations/{activation_id}/check-v2");
|
||||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||||
|
|
||||||
if response.status().is_client_error() {
|
if response.status().is_client_error() {
|
||||||
@@ -189,13 +215,14 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
|
|||||||
return Err(ServerError);
|
return Err(ServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: CheckActivationResponsePayload = response.json().await?;
|
let body_text = response.text().await?;
|
||||||
if !body.active {
|
match serde_json::from_str::<LicenseCheckStatus>(&body_text) {
|
||||||
info!("Inactive License {:?}", body);
|
Ok(b) => Ok(b),
|
||||||
return Ok(LicenseCheckStatus::InvalidLicense);
|
Err(e) => {
|
||||||
|
warn!("Failed to decode server response: {} {:?}", body_text, e);
|
||||||
|
Err(JsonError(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(LicenseCheckStatus::CommercialUse)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -622,6 +622,14 @@ impl PluginManager {
|
|||||||
values: HashMap<String, JsonPrimitive>,
|
values: HashMap<String, JsonPrimitive>,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<GetHttpAuthenticationConfigResponse> {
|
) -> Result<GetHttpAuthenticationConfigResponse> {
|
||||||
|
if auth_name == "none" {
|
||||||
|
return Ok(GetHttpAuthenticationConfigResponse {
|
||||||
|
args: Vec::new(),
|
||||||
|
plugin_ref_id: "auth-none".to_string(),
|
||||||
|
actions: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let results = self.get_http_authentication_summaries(window).await?;
|
let results = self.get_http_authentication_summaries(window).await?;
|
||||||
let plugin = results
|
let plugin = results
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,22 +1,86 @@
|
|||||||
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
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 { settingsAtom } from '@yaakapp-internal/models';
|
import { settingsAtom } from '@yaakapp-internal/models';
|
||||||
|
import { differenceInCalendarDays } from 'date-fns';
|
||||||
|
import { formatDate } from 'date-fns/format';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { openSettings } from '../commands/openSettings';
|
import { openSettings } from '../commands/openSettings';
|
||||||
|
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { CargoFeature } from './CargoFeature';
|
import { CargoFeature } from './CargoFeature';
|
||||||
import type { ButtonProps } from './core/Button';
|
import type { ButtonProps } from './core/Button';
|
||||||
|
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
||||||
|
import { Icon } from './core/Icon';
|
||||||
import { PillButton } from './core/PillButton';
|
import { PillButton } from './core/PillButton';
|
||||||
|
|
||||||
const details: Record<
|
const dismissedAtom = atomWithKVStorage<string | null>('dismissed_license_expired', null);
|
||||||
LicenseCheckStatus['type'],
|
|
||||||
{ label: ReactNode; color: ButtonProps['color'] } | null
|
function getDetail(
|
||||||
> = {
|
data: LicenseCheckStatus,
|
||||||
commercial_use: null,
|
dismissedExpired: string | null,
|
||||||
invalid_license: { label: 'License Error', color: 'danger' },
|
): { label: ReactNode; color: ButtonProps['color']; options?: DropdownItem[] } | null | undefined {
|
||||||
personal_use: { label: 'Personal Use', color: 'notice' },
|
const dismissedAt = dismissedExpired ? new Date(dismissedExpired).getTime() : null;
|
||||||
trialing: { label: 'Commercial Trial', color: 'secondary' },
|
|
||||||
};
|
switch (data.status) {
|
||||||
|
case 'active':
|
||||||
|
return null;
|
||||||
|
case 'personal_use':
|
||||||
|
return { label: 'Personal Use', color: 'notice' };
|
||||||
|
case 'trialing':
|
||||||
|
return { label: 'Commercial Trial', color: 'secondary' };
|
||||||
|
case 'error':
|
||||||
|
return { label: 'Error', color: 'danger' };
|
||||||
|
case 'inactive':
|
||||||
|
return { label: 'Personal Use', color: 'notice' };
|
||||||
|
case 'past_due':
|
||||||
|
return { label: 'Past Due', color: 'danger' };
|
||||||
|
case 'expired':
|
||||||
|
// Don't show the expired message if it's been less than 14 days since the last dismissal
|
||||||
|
if (dismissedAt && differenceInCalendarDays(new Date(), dismissedAt) < 14) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: 'notice',
|
||||||
|
label: data.data.changes > 0 ? 'Updates Paused' : 'License Expired',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: `${data.data.changes} New Updates`,
|
||||||
|
color: 'success',
|
||||||
|
leftSlot: <Icon icon="gift" />,
|
||||||
|
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
||||||
|
hidden: data.data.changes === 0 || data.data.changesUrl == null,
|
||||||
|
onSelect: () => openUrl(data.data.changesUrl ?? ''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator',
|
||||||
|
label: `License expired ${formatDate(data.data.periodEnd, 'MMM dd, yyyy')}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <div className="min-w-[12rem]">Renew License</div>,
|
||||||
|
leftSlot: <Icon icon="refresh" />,
|
||||||
|
rightSlot: <Icon icon="external_link" size="sm" className="opacity-disabled" />,
|
||||||
|
hidden: data.data.changesUrl == null,
|
||||||
|
onSelect: () => openUrl(data.data.billingUrl),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Enter License Key',
|
||||||
|
leftSlot: <Icon icon="key_round" />,
|
||||||
|
hidden: data.data.changesUrl == null,
|
||||||
|
onSelect: openLicenseDialog,
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: <span className="text-text-subtle">Remind me Later</span>,
|
||||||
|
leftSlot: <Icon icon="alarm_clock" className="text-text-subtle" />,
|
||||||
|
onSelect: () => jotaiStore.set(dismissedAtom, new Date().toISOString()),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function LicenseBadge() {
|
export function LicenseBadge() {
|
||||||
return (
|
return (
|
||||||
@@ -29,10 +93,15 @@ export function LicenseBadge() {
|
|||||||
function LicenseBadgeCmp() {
|
function LicenseBadgeCmp() {
|
||||||
const { check } = useLicense();
|
const { check } = useLicense();
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
const dismissed = useAtomValue(dismissedAtom);
|
||||||
|
|
||||||
|
// Dismissed license badge
|
||||||
|
if (settings.hideLicenseBadge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (check.error) {
|
if (check.error) {
|
||||||
// Failed to check for license. Probably a network or server error so just don't
|
// Failed to check for license. Probably a network or server error, so just don't show anything.
|
||||||
// show anything.
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +110,30 @@ function LicenseBadgeCmp() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dismissed license badge
|
const detail = getDetail(check.data, dismissed);
|
||||||
if (settings.hideLicenseBadge) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail = details[check.data.type];
|
|
||||||
if (detail == null) {
|
if (detail == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (detail.options && detail.options.length > 0) {
|
||||||
|
return (
|
||||||
|
<Dropdown items={detail.options}>
|
||||||
|
<PillButton color={detail.color}>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{detail.label} <Icon icon="chevron_down" className="opacity-60" />
|
||||||
|
</div>
|
||||||
|
</PillButton>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PillButton color={detail.color} onClick={() => openSettings.mutate('license')}>
|
<PillButton color={detail.color} onClick={openLicenseDialog}>
|
||||||
{detail.label}
|
{detail.label}
|
||||||
</PillButton>
|
</PillButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLicenseDialog() {
|
||||||
|
openSettings.mutate('license');
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
import { useLicense } from '@yaakapp-internal/license';
|
import { useLicense } from '@yaakapp-internal/license';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
import { formatDate } from 'date-fns/format';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useToggle } from '../../hooks/useToggle';
|
import { useToggle } from '../../hooks/useToggle';
|
||||||
import { pluralizeCount } from '../../lib/pluralize';
|
import { pluralizeCount } from '../../lib/pluralize';
|
||||||
@@ -31,71 +32,120 @@ function SettingsLicenseCmp() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderBanner = () => {
|
||||||
|
if (!check.data) return null;
|
||||||
|
|
||||||
|
switch (check.data.status) {
|
||||||
|
case 'active':
|
||||||
|
return <Banner color="success">Your license is active 🥳</Banner>;
|
||||||
|
|
||||||
|
case 'trialing':
|
||||||
|
return (
|
||||||
|
<Banner color="info" className="@container flex items-center gap-x-5 max-w-xl">
|
||||||
|
<LocalImage src="static/greg.jpeg" className="hidden @sm:block rounded-full h-14 w-14" />
|
||||||
|
<p className="w-full">
|
||||||
|
<strong>
|
||||||
|
{pluralizeCount('day', differenceInDays(check.data.data.end, new Date()))}
|
||||||
|
</strong>{' '}
|
||||||
|
left to evaluate Yaak for commercial use.
|
||||||
|
<br />
|
||||||
|
<span className="opacity-50">Personal use is always free, forever.</span>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||||
|
<Link noUnderline href="mailto:support@yaak.app">
|
||||||
|
Contact Support
|
||||||
|
</Link>
|
||||||
|
<Icon icon="dot" size="sm" color="secondary" />
|
||||||
|
<Link
|
||||||
|
noUnderline
|
||||||
|
href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'personal_use':
|
||||||
|
return (
|
||||||
|
<Banner color="notice" className="@container flex items-center gap-x-5 max-w-xl">
|
||||||
|
<LocalImage src="static/greg.jpeg" className="hidden @sm:block rounded-full h-14 w-14" />
|
||||||
|
<p className="w-full">
|
||||||
|
Your commercial-use trial has ended.
|
||||||
|
<br />
|
||||||
|
<span className="opacity-50">
|
||||||
|
You may continue using Yaak for personal use free, forever.
|
||||||
|
<br />A license is required for commercial use.
|
||||||
|
</span>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||||
|
<Link noUnderline href="mailto:support@yaak.app">
|
||||||
|
Contact Support
|
||||||
|
</Link>
|
||||||
|
<Icon icon="dot" size="sm" color="secondary" />
|
||||||
|
<Link
|
||||||
|
noUnderline
|
||||||
|
href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'inactive':
|
||||||
|
return (
|
||||||
|
<Banner color="danger">
|
||||||
|
Your license is invalid. Please <Link href="https://yaak.app/dashboard">Sign In</Link>{' '}
|
||||||
|
for more details
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'expired':
|
||||||
|
return (
|
||||||
|
<Banner color="notice">
|
||||||
|
Your license expired{' '}
|
||||||
|
<strong>{formatDate(check.data.data.periodEnd, 'MMMM dd, yyyy')}</strong>. Please{' '}
|
||||||
|
<Link href="https://yaak.app/dashboard">Resubscribe</Link> to continue receiving
|
||||||
|
updates.
|
||||||
|
{check.data.data.changesUrl && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<Link href={check.data.data.changesUrl}>What's new in latest builds</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'past_due':
|
||||||
|
return (
|
||||||
|
<Banner color="danger">
|
||||||
|
<strong>Your payment method needs attention.</strong>
|
||||||
|
<br />
|
||||||
|
To re-activate your license, please{' '}
|
||||||
|
<Link href={check.data.data.billingUrl}>update your billing info</Link>.
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
return (
|
||||||
|
<Banner color="danger">
|
||||||
|
License check failed: {check.data.data.message} (Code: {check.data.data.code})
|
||||||
|
</Banner>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 max-w-xl">
|
<div className="flex flex-col gap-6 max-w-xl">
|
||||||
{check.data?.type === 'commercial_use' ? (
|
{renderBanner()}
|
||||||
<Banner color="success">Your license is active 🥳</Banner>
|
|
||||||
) : check.data?.type === 'trialing' ? (
|
|
||||||
<Banner color="info" className="@container flex items-center gap-x-5 max-w-xl">
|
|
||||||
<LocalImage src="static/greg.jpeg" className="hidden @sm:block rounded-full h-14 w-14" />
|
|
||||||
<p className="w-full">
|
|
||||||
<strong>{pluralizeCount('day', differenceInDays(check.data.end, new Date()))}</strong>{' '}
|
|
||||||
left to evaluate Yaak for commercial use.
|
|
||||||
<br />
|
|
||||||
<span className="opacity-50">Personal use is always free, forever.</span>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
|
||||||
<Link noUnderline href="mailto:support@yaak.app">
|
|
||||||
Contact Support
|
|
||||||
</Link>
|
|
||||||
<Icon icon="dot" size="sm" color="secondary" />
|
|
||||||
<Link
|
|
||||||
noUnderline
|
|
||||||
href={`https://yaak.app/pricing?s=learn&t=${check.data?.type ?? ''}`}
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</Banner>
|
|
||||||
) : check.data?.type === 'personal_use' ? (
|
|
||||||
<Banner color="notice" className="@container flex items-center gap-x-5 max-w-xl">
|
|
||||||
<LocalImage src="static/greg.jpeg" className="hidden @sm:block rounded-full h-14 w-14" />
|
|
||||||
<p className="w-full">
|
|
||||||
Your commercial-use trial has ended.
|
|
||||||
<br />
|
|
||||||
<span className="opacity-50">
|
|
||||||
You may continue using Yaak for personal use free, forever.
|
|
||||||
<br />A license is required for commercial use.
|
|
||||||
</span>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
|
||||||
<Link noUnderline href="mailto:support@yaak.app">
|
|
||||||
Contact Support
|
|
||||||
</Link>
|
|
||||||
<Icon icon="dot" size="sm" color="secondary" />
|
|
||||||
<Link
|
|
||||||
noUnderline
|
|
||||||
href={`https://yaak.app/pricing?s=learn&t=${check.data?.type ?? ''}`}
|
|
||||||
>
|
|
||||||
Learn More
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</p>
|
|
||||||
</Banner>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{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>}
|
||||||
|
|
||||||
{check.data?.type === 'invalid_license' && (
|
{check.data?.status === 'active' ? (
|
||||||
<Banner color="danger">
|
|
||||||
Your license is invalid. Please <Link href="https://yaak.app/dashboard">Sign In</Link> for
|
|
||||||
more details
|
|
||||||
</Banner>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{check.data?.type === 'commercial_use' ? (
|
|
||||||
<HStack space={2}>
|
<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
|
Deactivate License
|
||||||
@@ -120,7 +170,7 @@ function SettingsLicenseCmp() {
|
|||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openUrl(
|
openUrl(
|
||||||
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.type ?? ''}`,
|
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ''}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
const workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||||
const showEncryptionSetup =
|
const showEncryptionSetup =
|
||||||
workspace?.encryptionKeyChallenge != null && workspaceMeta?.encryptionKey == null;
|
workspace != null &&
|
||||||
|
workspaceMeta != null &&
|
||||||
|
workspace.encryptionKeyChallenge != null &&
|
||||||
|
workspaceMeta.encryptionKey == null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Color } from '@yaakapp-internal/plugins';
|
import type { Color } from '@yaakapp-internal/plugins';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
|
AlarmClockIcon,
|
||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
ArchiveIcon,
|
ArchiveIcon,
|
||||||
ArrowBigDownDashIcon,
|
ArrowBigDownDashIcon,
|
||||||
@@ -67,6 +68,7 @@ import {
|
|||||||
FolderSymlinkIcon,
|
FolderSymlinkIcon,
|
||||||
FolderSyncIcon,
|
FolderSyncIcon,
|
||||||
FolderUpIcon,
|
FolderUpIcon,
|
||||||
|
GiftIcon,
|
||||||
GitBranchIcon,
|
GitBranchIcon,
|
||||||
GitBranchPlusIcon,
|
GitBranchPlusIcon,
|
||||||
GitCommitIcon,
|
GitCommitIcon,
|
||||||
@@ -129,6 +131,7 @@ import type { CSSProperties, HTMLAttributes } from 'react';
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
alarm_clock: AlarmClockIcon,
|
||||||
alert_triangle: AlertTriangleIcon,
|
alert_triangle: AlertTriangleIcon,
|
||||||
archive: ArchiveIcon,
|
archive: ArchiveIcon,
|
||||||
arrow_big_down_dash: ArrowBigDownDashIcon,
|
arrow_big_down_dash: ArrowBigDownDashIcon,
|
||||||
@@ -194,6 +197,7 @@ const icons = {
|
|||||||
folder_symlink: FolderSymlinkIcon,
|
folder_symlink: FolderSymlinkIcon,
|
||||||
folder_sync: FolderSyncIcon,
|
folder_sync: FolderSyncIcon,
|
||||||
folder_up: FolderUpIcon,
|
folder_up: FolderUpIcon,
|
||||||
|
gift: GiftIcon,
|
||||||
git_branch: GitBranchIcon,
|
git_branch: GitBranchIcon,
|
||||||
git_branch_plus: GitBranchPlusIcon,
|
git_branch_plus: GitBranchPlusIcon,
|
||||||
git_commit: GitCommitIcon,
|
git_commit: GitCommitIcon,
|
||||||
|
|||||||
Reference in New Issue
Block a user