f462e65581
Co-authored-by: Cursor <cursoragent@cursor.com>
272 lines
8.4 KiB
TypeScript
272 lines
8.4 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
import { ipcChannels } from '../../../shared/ipc/contracts';
|
||
import { EULA_CURRENT_VERSION } from '../../../shared/license/eulaVersion';
|
||
import type { LicenseSnapshot } from '../../../shared/license/licenseSnapshot';
|
||
import { EULA_RU_MARKDOWN } from '../../legal/eulaRu';
|
||
import { getDndApi } from '../../shared/dndApi';
|
||
import { Button } from '../../shared/ui/controls';
|
||
import styles from '../EditorApp.module.css';
|
||
import { useEditorI18n } from '../i18n/EditorI18nContext';
|
||
|
||
type LicenseTokenModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onSaved: () => void;
|
||
};
|
||
|
||
export function LicenseTokenModal({ open, onClose, onSaved }: LicenseTokenModalProps) {
|
||
const { t } = useEditorI18n();
|
||
const [token, setToken] = useState('');
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
setToken('');
|
||
setSaving(false);
|
||
setError(null);
|
||
}, [open]);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose, open]);
|
||
|
||
if (!open) return null;
|
||
|
||
return createPortal(
|
||
<>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalBackdrop}
|
||
/>
|
||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||
<div className={styles.modalHeader}>
|
||
<div className={styles.modalTitle}>{t('license.tokenTitle')}</div>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalClose}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.tokenKey')}</div>
|
||
<textarea
|
||
className={styles.licenseTextarea}
|
||
value={token}
|
||
onChange={(e) => setToken(e.target.value)}
|
||
placeholder={t('license.tokenPlaceholder')}
|
||
spellCheck={false}
|
||
/>
|
||
</div>
|
||
{error ? <div className={styles.fieldError}>{error}</div> : null}
|
||
<div className={styles.modalFooter}>
|
||
<Button onClick={onClose} disabled={saving}>
|
||
{t('common.cancel')}
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
disabled={saving || !token.trim()}
|
||
onClick={() => {
|
||
void (async () => {
|
||
setSaving(true);
|
||
setError(null);
|
||
try {
|
||
await getDndApi().invoke(ipcChannels.license.setToken, { token: token.trim() });
|
||
onSaved();
|
||
onClose();
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : String(e));
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
})();
|
||
}}
|
||
>
|
||
{t('common.save')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</>,
|
||
document.body,
|
||
);
|
||
}
|
||
|
||
type EulaModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onAccepted: () => void;
|
||
};
|
||
|
||
export function EulaModal({ open, onClose, onAccepted }: EulaModalProps) {
|
||
const { t, locale } = useEditorI18n();
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose, open]);
|
||
|
||
if (!open) return null;
|
||
|
||
return createPortal(
|
||
<>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalBackdrop}
|
||
/>
|
||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||
<div className={styles.modalHeader}>
|
||
<div className={styles.modalTitle}>{t('license.eulaTitle')}</div>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalClose}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
{locale === 'en' ? <div className={styles.muted}>{t('license.eulaNoteEn')}</div> : null}
|
||
<div className={styles.eulaScroll}>{EULA_RU_MARKDOWN}</div>
|
||
<div className={styles.modalFooter}>
|
||
<Button onClick={onClose} disabled={saving}>
|
||
{t('license.eulaReject')}
|
||
</Button>
|
||
<Button
|
||
variant="primary"
|
||
disabled={saving}
|
||
onClick={() => {
|
||
void (async () => {
|
||
setSaving(true);
|
||
try {
|
||
await getDndApi().invoke(ipcChannels.license.acceptEula, {
|
||
version: EULA_CURRENT_VERSION,
|
||
});
|
||
onAccepted();
|
||
onClose();
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
})();
|
||
}}
|
||
>
|
||
{t('license.eulaAccept')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</>,
|
||
document.body,
|
||
);
|
||
}
|
||
|
||
type LicenseAboutModalProps = {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
snapshot: LicenseSnapshot | null;
|
||
};
|
||
|
||
function licenseReasonLabel(t: (key: string) => string, reason: LicenseSnapshot['reason']): string {
|
||
const key = `license.reason.${reason}`;
|
||
const label = t(key);
|
||
return label === key ? reason : label;
|
||
}
|
||
|
||
export function LicenseAboutModal({ open, onClose, snapshot }: LicenseAboutModalProps) {
|
||
const { t, locale } = useEditorI18n();
|
||
const dateLocale = locale === 'en' ? 'en-US' : 'ru-RU';
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose, open]);
|
||
|
||
if (!open) return null;
|
||
|
||
const expText =
|
||
snapshot?.summary?.exp != null
|
||
? new Date(snapshot.summary.exp * 1000).toLocaleString(dateLocale, {
|
||
dateStyle: 'long',
|
||
timeStyle: 'short',
|
||
})
|
||
: '—';
|
||
|
||
return createPortal(
|
||
<>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalBackdrop}
|
||
/>
|
||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||
<div className={styles.modalHeader}>
|
||
<div className={styles.modalTitle}>{t('license.aboutTitle')}</div>
|
||
<button
|
||
type="button"
|
||
aria-label={t('common.close')}
|
||
onClick={onClose}
|
||
className={styles.modalClose}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
{snapshot?.devSkip ? <div className={styles.fieldError}>{t('license.aboutDevSkip')}</div> : null}
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.aboutStatus')}</div>
|
||
<div>{snapshot ? licenseReasonLabel(t, snapshot.reason) : '—'}</div>
|
||
</div>
|
||
{snapshot?.summary ? (
|
||
<>
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.aboutProduct')}</div>
|
||
<div>{snapshot.summary.pid}</div>
|
||
</div>
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.aboutLicenseId')}</div>
|
||
<div style={{ wordBreak: 'break-all' }}>{snapshot.summary.sub}</div>
|
||
</div>
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.aboutExpiry')}</div>
|
||
<div>{expText}</div>
|
||
</div>
|
||
<div className={styles.fieldGrid}>
|
||
<div className={styles.fieldLabel}>{t('license.aboutDevice')}</div>
|
||
<div style={{ wordBreak: 'break-all' }}>{snapshot.deviceId}</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.muted}>{t('license.aboutNoData')}</div>
|
||
)}
|
||
<div className={styles.modalFooter}>
|
||
<Button variant="primary" onClick={onClose}>
|
||
{t('common.close')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</>,
|
||
document.body,
|
||
);
|
||
}
|