/** * — el primitivo del blindaje. Un host lo monta en UNA línea * envolviendo su app autenticada: * * * * * * Comportamiento según el estado (resuelto por el backend del host): * - Sin enforcement, o estado operable (valid/stale/grace) → renderiza children. * stale/grace además montan el degradado por encima. * - enforcement && missing/invalid/expired → modal BLOQUEANTE full-screen, * no descartable, con el formulario de activación. Al activar con éxito el * host refresca el estado y el gate se abre sin recargar. * * Es branding-aware si el host pasa `branding` (logo/nombre desde el * PlatformConfig del SDK); si no, cae a un encabezado neutro. * * INDEPENDIENTE del kernel: no importa su cliente ni fija su versión. Solo * consume `LicenseState` y una promesa `onActivate`. */ import { useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { ShieldAlert, Clock, Loader2 } from 'lucide-react' import { Button, Input } from '@asteby/metacore-ui' import { isLicenseBlocking, isTrialExpired, type LicenseState, type LicenseBranding, } from './types' import { LicenseStatusBadge } from './license-status-badge' import { LicenseExpiryBanner } from './license-expiry-banner' function cx(...parts: Array): string { return parts.filter(Boolean).join(' ') } export interface LicenseGateProps { /** Estado actual, resuelto por el backend del host. `undefined` mientras * carga → fail-open (renderiza children, nunca destella el candado). */ state: LicenseState | undefined /** Activa una licencia con el código/token pegado. Resuelve → el host * refresca `state` y el gate se abre. Rechaza con un Error cuyo `message` * se muestra al usuario. */ onActivate: (code: string) => Promise children: ReactNode /** Branding opcional (logo/nombre) para el encabezado del modal. */ branding?: LicenseBranding /** Acción "Gestionar licencia" del banner degradado (opcional). */ onManage?: () => void /** Si el usuario actual puede activar la licencia (p. ej. Platform Root / * superadmin). Default true — backward-compatible. Cuando es false, el modal * bloqueante se muestra SIN el formulario de activación y en su lugar * aparece `readOnlyMessage`. */ canActivate?: boolean /** Mensaje mostrado en el modal bloqueante cuando `canActivate` es false. */ readOnlyMessage?: string className?: string } export function LicenseGate({ state, onActivate, children, branding, onManage, canActivate = true, readOnlyMessage, className, }: LicenseGateProps) { const blocking = isLicenseBlocking(state) return ( <> {/* stale/grace/upcoming degradan con banner; el propio banner decide si mostrarse. Bloqueado → no hace falta, el modal manda. */} {!blocking && ( )} {children} {blocking && state && ( )} ) } interface LicenseGateModalProps { state: LicenseState onActivate: (code: string) => Promise branding?: LicenseBranding canActivate: boolean readOnlyMessage?: string className?: string } function LicenseGateModal({ state, onActivate, branding, canActivate, readOnlyMessage, className, }: LicenseGateModalProps) { const { t } = useTranslation() const [code, setCode] = useState('') const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) const trialExpired = isTrialExpired(state) const title = trialExpired ? t('license.gate.trial_title', { defaultValue: 'Tu prueba gratuita terminó', }) : t('license.gate.title', { defaultValue: 'Activa tu licencia' }) const description = trialExpired ? t('license.gate.trial_description', { defaultValue: 'Tu prueba gratuita terminó. Activa una licencia para continuar. Pega tu clave (lic_…) o el token firmado que te entregaron.', }) : state.status === 'expired' ? t('license.gate.expired_description', { defaultValue: 'Tu licencia venció y el periodo de gracia terminó. Activa una licencia vigente para seguir operando. Pega tu clave (lic_…) o el token firmado.', }) : state.status === 'invalid' ? t('license.gate.invalid_description', { defaultValue: 'La licencia de esta instancia no es válida. Pega una clave (lic_…) o el token firmado que te entregó tu proveedor para reactivarla.', }) : t('license.gate.missing_description', { defaultValue: 'Esta instancia necesita una licencia activa para operar. Pega tu clave (lic_…) o el token firmado para activarla.', }) const canSubmit = code.trim().length > 0 && !submitting const submit = async () => { if (!canSubmit) return setSubmitting(true) setError(null) try { await onActivate(code.trim()) // Éxito → el host refresca `state`; el gate se re-renderiza y este // modal se desmonta solo. No recargamos. } catch (e) { const message = e instanceof Error && e.message ? e.message : t('license.gate.activate_error', { defaultValue: 'No se pudo activar la licencia.', }) setError(message) setSubmitting(false) } } return (
{branding?.logo ? ( {branding.name ) : (
{trialExpired ? ( ) : ( )}
)}

{title}

{branding?.name ? branding.name : t('license.gate.instance', { defaultValue: 'Esta instancia', })}

{description}

{state.reason && (

{state.reason}

)} {!canActivate ? (

{readOnlyMessage ?? t('license.gate.read_only', { defaultValue: 'Contacta al administrador de la plataforma para activar la licencia.', })}

) : (
{ e.preventDefault() void submit() }} > { setCode(e.target.value) if (error) setError(null) }} /> {error && (

{error}

)}
)}
) }