mirror of
https://github.com/mountain-loop/yaak.git
synced 2025-12-23 22:48:55 -05:00
Add license handling for expired licenses
This commit is contained in:
@@ -85,13 +85,18 @@ impl YaakNotifier {
|
||||
let license_check = {
|
||||
use yaak_license::{LicenseCheckStatus, check_license};
|
||||
match check_license(window).await {
|
||||
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
||||
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
|
||||
Ok(LicenseCheckStatus::InvalidLicense) => "invalid_license".to_string(),
|
||||
Ok(LicenseCheckStatus::Trialing { .. }) => "trialing".to_string(),
|
||||
Err(_) => "unknown".to_string(),
|
||||
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal",
|
||||
Ok(LicenseCheckStatus::Active { .. }) => "commercial",
|
||||
Ok(LicenseCheckStatus::PastDue { .. }) => "past_due",
|
||||
Ok(LicenseCheckStatus::Inactive { .. }) => "invalid_license",
|
||||
Ok(LicenseCheckStatus::Trialing { .. }) => "trialing",
|
||||
Ok(LicenseCheckStatus::Expired { .. }) => "expired",
|
||||
Ok(LicenseCheckStatus::Error { .. }) => "error",
|
||||
Err(_) => "unknown",
|
||||
}
|
||||
.to_string()
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "license"))]
|
||||
let license_check = "disabled".to_string();
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ export type ActivateLicenseRequestPayload = { licenseKey: string, appVersion: st
|
||||
|
||||
export type ActivateLicenseResponsePayload = { activationId: string, };
|
||||
|
||||
export type CheckActivationResponsePayload = { active: boolean, };
|
||||
|
||||
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 chrono::{NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
@@ -24,13 +24,6 @@ pub struct CheckActivationRequestPayload {
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "license.ts")]
|
||||
@@ -63,6 +56,49 @@ pub struct APIErrorResponsePayload {
|
||||
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>(
|
||||
window: &WebviewWindow<R>,
|
||||
license_key: &str,
|
||||
@@ -141,16 +177,6 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
|
||||
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> {
|
||||
let payload = CheckActivationRequestPayload {
|
||||
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 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 trial_period_active = Utc::now().naive_utc() < trial_end;
|
||||
let trial_period_active = Utc::now() < trial_end;
|
||||
|
||||
match (has_activation_id, trial_period_active) {
|
||||
(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");
|
||||
// A license has been activated, so let's check the license server
|
||||
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?;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let body: CheckActivationResponsePayload = response.json().await?;
|
||||
if !body.active {
|
||||
info!("Inactive License {:?}", body);
|
||||
return Ok(LicenseCheckStatus::InvalidLicense);
|
||||
let body_text = response.text().await?;
|
||||
match serde_json::from_str::<LicenseCheckStatus>(&body_text) {
|
||||
Ok(b) => Ok(b),
|
||||
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>,
|
||||
model_id: &str,
|
||||
) -> 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 plugin = results
|
||||
.iter()
|
||||
|
||||
@@ -1,22 +1,86 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import type { LicenseCheckStatus } from '@yaakapp-internal/license';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import { differenceInCalendarDays } from 'date-fns';
|
||||
import { formatDate } from 'date-fns/format';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import { openSettings } from '../commands/openSettings';
|
||||
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { CargoFeature } from './CargoFeature';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { PillButton } from './core/PillButton';
|
||||
|
||||
const details: Record<
|
||||
LicenseCheckStatus['type'],
|
||||
{ label: ReactNode; color: ButtonProps['color'] } | null
|
||||
> = {
|
||||
commercial_use: null,
|
||||
invalid_license: { label: 'License Error', color: 'danger' },
|
||||
personal_use: { label: 'Personal Use', color: 'notice' },
|
||||
trialing: { label: 'Commercial Trial', color: 'secondary' },
|
||||
};
|
||||
const dismissedAtom = atomWithKVStorage<string | null>('dismissed_license_expired', null);
|
||||
|
||||
function getDetail(
|
||||
data: LicenseCheckStatus,
|
||||
dismissedExpired: string | null,
|
||||
): { label: ReactNode; color: ButtonProps['color']; options?: DropdownItem[] } | null | undefined {
|
||||
const dismissedAt = dismissedExpired ? new Date(dismissedExpired).getTime() : null;
|
||||
|
||||
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() {
|
||||
return (
|
||||
@@ -29,10 +93,15 @@ export function LicenseBadge() {
|
||||
function LicenseBadgeCmp() {
|
||||
const { check } = useLicense();
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const dismissed = useAtomValue(dismissedAtom);
|
||||
|
||||
// Dismissed license badge
|
||||
if (settings.hideLicenseBadge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (check.error) {
|
||||
// Failed to check for license. Probably a network or server error so just don't
|
||||
// show anything.
|
||||
// Failed to check for license. Probably a network or server error, so just don't show anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -41,19 +110,30 @@ function LicenseBadgeCmp() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dismissed license badge
|
||||
if (settings.hideLicenseBadge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const detail = details[check.data.type];
|
||||
const detail = getDetail(check.data, dismissed);
|
||||
if (detail == 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 (
|
||||
<PillButton color={detail.color} onClick={() => openSettings.mutate('license')}>
|
||||
<PillButton color={detail.color} onClick={openLicenseDialog}>
|
||||
{detail.label}
|
||||
</PillButton>
|
||||
);
|
||||
}
|
||||
|
||||
function openLicenseDialog() {
|
||||
openSettings.mutate('license');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import { useLicense } from '@yaakapp-internal/license';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { formatDate } from 'date-fns/format';
|
||||
import { useState } from 'react';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { pluralizeCount } from '../../lib/pluralize';
|
||||
@@ -31,71 +32,120 @@ function SettingsLicenseCmp() {
|
||||
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 (
|
||||
<div className="flex flex-col gap-6 max-w-xl">
|
||||
{check.data?.type === 'commercial_use' ? (
|
||||
<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}
|
||||
{renderBanner()}
|
||||
|
||||
{check.error && <Banner color="danger">{check.error}</Banner>}
|
||||
{activate.error && <Banner color="danger">{activate.error}</Banner>}
|
||||
|
||||
{check.data?.type === 'invalid_license' && (
|
||||
<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' ? (
|
||||
{check.data?.status === 'active' ? (
|
||||
<HStack space={2}>
|
||||
<Button variant="border" color="secondary" size="sm" onClick={() => deactivate.mutate()}>
|
||||
Deactivate License
|
||||
@@ -120,7 +170,7 @@ function SettingsLicenseCmp() {
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={() =>
|
||||
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 workspaceMeta = useAtomValue(activeWorkspaceMetaAtom);
|
||||
const showEncryptionSetup =
|
||||
workspace?.encryptionKeyChallenge != null && workspaceMeta?.encryptionKey == null;
|
||||
workspace != null &&
|
||||
workspaceMeta != null &&
|
||||
workspace.encryptionKeyChallenge != null &&
|
||||
workspaceMeta.encryptionKey == null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
AlarmClockIcon,
|
||||
AlertTriangleIcon,
|
||||
ArchiveIcon,
|
||||
ArrowBigDownDashIcon,
|
||||
@@ -67,6 +68,7 @@ import {
|
||||
FolderSymlinkIcon,
|
||||
FolderSyncIcon,
|
||||
FolderUpIcon,
|
||||
GiftIcon,
|
||||
GitBranchIcon,
|
||||
GitBranchPlusIcon,
|
||||
GitCommitIcon,
|
||||
@@ -129,6 +131,7 @@ import type { CSSProperties, HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
const icons = {
|
||||
alarm_clock: AlarmClockIcon,
|
||||
alert_triangle: AlertTriangleIcon,
|
||||
archive: ArchiveIcon,
|
||||
arrow_big_down_dash: ArrowBigDownDashIcon,
|
||||
@@ -194,6 +197,7 @@ const icons = {
|
||||
folder_symlink: FolderSymlinkIcon,
|
||||
folder_sync: FolderSyncIcon,
|
||||
folder_up: FolderUpIcon,
|
||||
gift: GiftIcon,
|
||||
git_branch: GitBranchIcon,
|
||||
git_branch_plus: GitBranchPlusIcon,
|
||||
git_commit: GitCommitIcon,
|
||||
|
||||
Reference in New Issue
Block a user