mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
797623e56f
Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral', aligning code with user-facing tier names. Variant type is now resolved once at activation/validation and stored in DB via license_variant_type, instead of string-matching the Lemon Squeezy variant_name on every read. Also captures variant_id for future lookups. Pre-existing installs auto-migrate on first getVariant() call.
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
import { apiFetch } from '@/lib/api';
|
|
|
|
export type LicenseTier = 'community' | 'paid';
|
|
export type LicenseStatus = 'community' | 'trial' | 'active' | 'expired' | 'disabled';
|
|
|
|
export type LicenseVariant = 'skipper' | 'admiral' | null;
|
|
|
|
export interface LicenseInfo {
|
|
tier: LicenseTier;
|
|
status: LicenseStatus;
|
|
variant: LicenseVariant;
|
|
customerName: string | null;
|
|
productName: string | null;
|
|
maskedKey: string | null;
|
|
validUntil: string | null;
|
|
trialDaysRemaining: number | null;
|
|
instanceId: string;
|
|
portalUrl: string | null;
|
|
isLifetime: boolean;
|
|
}
|
|
|
|
interface LicenseContextType {
|
|
license: LicenseInfo | null;
|
|
isPaid: boolean;
|
|
loading: boolean;
|
|
refresh: () => Promise<void>;
|
|
activate: (licenseKey: string) => Promise<{ success: boolean; error?: string }>;
|
|
deactivate: () => Promise<{ success: boolean; error?: string }>;
|
|
}
|
|
|
|
const LicenseContext = createContext<LicenseContextType | undefined>(undefined);
|
|
|
|
export function LicenseProvider({ children }: { children: ReactNode }) {
|
|
const [license, setLicense] = useState<LicenseInfo | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const res = await apiFetch('/license', { localOnly: true });
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setLicense(data);
|
|
}
|
|
} catch {
|
|
// Silently fail - license info is non-critical
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
}, [refresh]);
|
|
|
|
const activate = useCallback(async (licenseKey: string): Promise<{ success: boolean; error?: string }> => {
|
|
try {
|
|
const res = await apiFetch('/license/activate', {
|
|
method: 'POST',
|
|
localOnly: true,
|
|
body: JSON.stringify({ license_key: licenseKey }),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
setLicense(data.license);
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: data.error || 'Activation failed' };
|
|
} catch {
|
|
return { success: false, error: 'Network error. Please try again.' };
|
|
}
|
|
}, []);
|
|
|
|
const deactivate = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
|
|
try {
|
|
const res = await apiFetch('/license/deactivate', {
|
|
method: 'POST',
|
|
localOnly: true,
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
setLicense(data.license);
|
|
return { success: true };
|
|
}
|
|
return { success: false, error: data.error || 'Deactivation failed' };
|
|
} catch {
|
|
return { success: false, error: 'Network error. Please try again.' };
|
|
}
|
|
}, []);
|
|
|
|
const isPaid = license?.tier === 'paid';
|
|
|
|
return (
|
|
<LicenseContext.Provider value={{ license, isPaid, loading, refresh, activate, deactivate }}>
|
|
{children}
|
|
</LicenseContext.Provider>
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export function useLicense(): LicenseContextType {
|
|
const context = useContext(LicenseContext);
|
|
if (context === undefined) {
|
|
throw new Error('useLicense must be used within a LicenseProvider');
|
|
}
|
|
return context;
|
|
}
|