import { useCallback, useEffect, useState } from 'react'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { ApiErrorPanel } from '../components/shared/ApiErrorPanel'; import { ApiError, getErrorSummary, getSikshyaApi, SIKSHYA_ENDPOINTS } from '../api'; import { ButtonPrimary } from '../components/shared/buttons'; import { Spinner as SharedSpinner } from '../components/shared/Spinner'; import { NavIcon } from '../components/NavIcon'; import { useSikshyaDialog } from '../components/shared/SikshyaDialogContext'; import { useShellState } from '../context/ShellStateContext'; import type { SikshyaReactConfig } from '../types'; import { TopRightToast, useTopRightToast } from '../components/shared/TopRightToast'; import { __, sprintf } from '../lib/i18n'; type LicenseInfo = { key: string; status: string; last_checked: number; server_response: Record; }; type LicenseBootstrap = { is_license_active: boolean; pro_plugin_active: boolean; upgrade_url: string; site_tier: string; site_tier_label?: string; /** Paid plan line from server tier + EDD price_id (when license active). */ commercial_plan_summary?: string; license_info: LicenseInfo | null; }; type LicenseMutationResponse = LicenseBootstrap & { status?: string; notice?: string; server_response?: unknown; license_info?: LicenseInfo | null; edd_api_request?: unknown; edd_api_response?: unknown; }; type DebugPayload = { type: string; at: string; response?: unknown; error?: unknown; }; function noticeFromBody(body: unknown): string | undefined { if (!body || typeof body !== 'object') return undefined; const n = (body as { notice?: unknown }).notice; return typeof n === 'string' && n.length ? n : undefined; } function maskKey(key: string): string { if (!key || key.length < 8) return key; return `${key.slice(0, 4)}••••••••••••${key.slice(-4)}`; } function statusBadge(status: string): { label: string; className: string } { const s = status.toLowerCase(); if (s === 'active' || s === 'valid') { return { label: 'Active', className: 'bg-emerald-50 text-emerald-800 ring-1 ring-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-200 dark:ring-emerald-900/40', }; } if (s === 'expired') { return { label: 'Expired', className: 'bg-rose-50 text-rose-800 ring-1 ring-rose-200 dark:bg-rose-950/40 dark:text-rose-200 dark:ring-rose-900/40', }; } if (s === 'disabled' || s === 'invalid') { return { label: s === 'invalid' ? __('Invalid', 'sikshya') : __('Disabled', 'sikshya'), className: 'bg-amber-50 text-amber-900 ring-1 ring-amber-200 dark:bg-amber-950/40 dark:text-amber-100 dark:ring-amber-900/40', }; } return { label: 'Inactive', className: 'bg-slate-100 text-slate-700 ring-1 ring-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:ring-slate-700', }; } function readStr(obj: Record, key: string): string | undefined { const v = obj[key]; return typeof v === 'string' ? v : undefined; } function readNum(obj: Record, key: string): number | undefined { const v = obj[key]; return typeof v === 'number' && Number.isFinite(v) ? v : undefined; } export function LicensePage(props: { embedded?: boolean; config: SikshyaReactConfig; title: string }) { const { config, title } = props; const { confirm } = useSikshyaDialog(); const { refreshShell } = useShellState(); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [data, setData] = useState(null); const [licenseKey, setLicenseKey] = useState(''); const [showKey, setShowKey] = useState(false); const [busy, setBusy] = useState<'activate' | 'save' | 'check' | 'deactivate' | null>(null); const toast = useTopRightToast(4000); const [debugOpen, setDebugOpen] = useState(false); const [debugData, setDebugData] = useState(null); const upgradeUrl = config.licensing?.upgradeUrl || 'https://mantrabrain.com/plugins/sikshya-lms/pricing/'; const load = useCallback(async () => { setLoading(true); setLoadError(null); try { const res = await getSikshyaApi().get(SIKSHYA_ENDPOINTS.admin.license); setData(res); if (res.license_info?.key) { setLicenseKey(res.license_info.key); } } catch (e) { setLoadError(e); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const pushDebug = (type: string, response: unknown, error?: unknown) => { setDebugData({ type, at: new Date().toISOString(), response, error }); }; const showToast = (kind: 'success' | 'error', text: string) => { if (kind === 'success') { toast.success(__('Success', 'sikshya'), text); return; } toast.error(__('Error', 'sikshya'), text); }; const proInstalled = Boolean(data?.pro_plugin_active); const onActivate = async (e: React.FormEvent) => { e.preventDefault(); const key = licenseKey.trim(); if (!key) { showToast('error', 'Please enter a license key.'); return; } setBusy('activate'); try { const res = await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.licenseActivate, { license_key: key, }); pushDebug('activate', res); const activated = res.status === 'valid' || res.status === 'active' || res.is_license_active === true; if (activated) { setData((prev) => ({ ...(prev || ({} as LicenseBootstrap)), ...res })); if (res.license_info?.key) setLicenseKey(res.license_info.key); await load(); showToast('success', res.notice || 'License activated.'); } else { showToast('error', res.notice || 'Activation did not complete.'); } await refreshShell(); } catch (err) { const body = err instanceof ApiError ? err.body : null; pushDebug('activate', body, err); showToast('error', noticeFromBody(body) || getErrorSummary(err)); } finally { setBusy(null); } }; const onSave = async () => { const key = licenseKey.trim(); if (!key) { showToast('error', 'Please enter a license key.'); return; } setBusy('save'); try { const res = await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.licenseSave, { license_key: key, }); pushDebug('save', res); await load(); await refreshShell(); showToast('success', res.notice || 'License key saved.'); } catch (err) { const body = err instanceof ApiError ? err.body : null; pushDebug('save', body, err); showToast('error', noticeFromBody(body) || getErrorSummary(err)); } finally { setBusy(null); } }; const onCheck = async () => { setBusy('check'); try { const res = await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.licenseCheck, {}); pushDebug('check', res); await load(); await refreshShell(); showToast('success', res.notice || 'Status refreshed.'); } catch (err) { const body = err instanceof ApiError ? err.body : null; pushDebug('check', body, err); showToast('error', noticeFromBody(body) || getErrorSummary(err)); } finally { setBusy(null); } }; const onDeactivate = async () => { const ok = await confirm({ title: __('Deactivate license', 'sikshya'), message: __('This frees an activation on your account and turns off Pro features on this site until you activate again. Continue?', 'sikshya'), confirmLabel: __('Deactivate', 'sikshya'), variant: 'danger', }); if (!ok) return; setBusy('deactivate'); try { const res = await getSikshyaApi().post(SIKSHYA_ENDPOINTS.admin.licenseDeactivate, {}); pushDebug('deactivate', res); if (res.status === 'deactivated') { setLicenseKey(''); await load(); await refreshShell(); showToast('success', res.notice || 'License deactivated.'); } else { showToast('error', res.notice || 'Deactivation did not complete.'); } } catch (err) { const body = err instanceof ApiError ? err.body : null; pushDebug('deactivate', body, err); showToast('error', noticeFromBody(body) || getErrorSummary(err)); } finally { setBusy(null); } }; const shell = (inner: React.ReactNode) => ( {inner} ); if (loading && !data) { return shell(
); } if (!loading && loadError && !data) { return shell(
void load()} />
); } if (data && !proInstalled) { return shell(

{__('License', 'sikshya')}

{sprintf( __('You are running %1$s without the Pro add-on. Install and activate %2$s to enter a license key and unlock premium features.', 'sikshya'), 'Sikshya', 'Sikshya Pro' )}

{__('Upgrade to Sikshya Pro', 'sikshya')}

  • Content drip, prerequisites, and gradebook
  • Subscriptions, multi-instructor revenue, and advanced certificates
  • Priority updates and commercial support channels
{ window.open(upgradeUrl, '_blank', 'noopener,noreferrer'); }} > View plans
); } if (!data) { return shell(
Unable to load license state.
); } const lic = data.license_info; const keyActive = lic?.status === 'active' || lic?.status === 'valid'; const planLine = (data.commercial_plan_summary && data.commercial_plan_summary.trim()) || (data.site_tier && data.site_tier !== 'free' ? String(data.site_tier_label || data.site_tier).trim() : ''); const sr = (lic?.server_response || {}) as Record; const customerName = readStr(sr, 'customer_name'); const customerEmail = readStr(sr, 'customer_email'); const expires = readStr(sr, 'expires'); const siteCount = readNum(sr, 'site_count'); const licenseLimit = readNum(sr, 'license_limit'); return shell(

{__('License management', 'sikshya')}

Activate your Sikshya Pro license for updates and premium modules.

{lic?.status ? ( {statusBadge(lic.status).label} ) : null}
{keyActive && planLine ? (
Your plan

{planLine}

) : null} {!keyActive ? (

{__('Activate your license', 'sikshya')}

setLicenseKey(ev.target.value)} placeholder={__('Paste your license key', 'sikshya')} disabled={busy === 'activate'} autoComplete="off" />
{busy === 'activate' ? : null} Save & activate

Purchase or retrieve your key from your Sikshya account, then paste it here. This site must be allowed on your license.

) : null} {keyActive && lic ? (

{__('License key', 'sikshya')}

{showKey ? lic.key : maskKey(lic.key)}
{customerName ? (
{__('Licensed to', 'sikshya')}
{customerName}
) : null} {customerEmail ? (
{__('Email', 'sikshya')}
{customerEmail}
) : null} {expires && expires !== 'lifetime' ? (
{__('Expires', 'sikshya')}
{Number.isNaN(Date.parse(expires)) ? expires : new Date(expires).toLocaleDateString()}
) : null} {expires === 'lifetime' ? (
{__('Expires', 'sikshya')}
{__('Lifetime', 'sikshya')}
) : null} {siteCount !== undefined ? (
{__('Activations', 'sikshya')}
{siteCount} {licenseLimit !== undefined ? ` / ${licenseLimit}` : ''}
) : null}
) : null}
Need help?

If activation fails, confirm this site URL matches your license and that you still have activations available.

{debugOpen ? (
{__('Debug', 'sikshya')}
{debugData ? ( ) : null}
{debugData ? (
                  {JSON.stringify(debugData, null, 2)}
                
) : (

Run save, activate, check, or deactivate to capture the last response here.

)}
) : null}
); } const Oe = 'block text-sm font-medium text-slate-800 dark:text-slate-200'; const Inp = 'mt-1.5 w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-slate-600 dark:bg-slate-800 dark:text-white'; // Spinner variants delegate to the shared `` component. Kept as // thin wrappers so the dozens of existing call sites on this page keep // working without one-by-one renames. New code should import Spinner // directly from `components/shared/Spinner`. function Spinner() { return ; } function SpinnerLight() { return ; } function SpinnerNeutral() { return ; }