121 lines
3.6 KiB
TypeScript
121 lines
3.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState, type ReactNode } from "react";
|
||
import { checkFeatureAccess } from "../adapters";
|
||
import { UpgradePrompt } from "./UpgradePrompt";
|
||
|
||
/**
|
||
* Feature gate — entitlement API only when featureKey is provided.
|
||
* Never uses local capability allowlists. Never hides silently.
|
||
*/
|
||
export function FeatureLock({
|
||
locked: lockedProp,
|
||
reason: reasonProp,
|
||
featureKey,
|
||
tenantId,
|
||
requiredPlan: requiredPlanProp,
|
||
requiredCapability: requiredCapabilityProp,
|
||
requiredBundle: requiredBundleProp,
|
||
children,
|
||
fallback,
|
||
}: {
|
||
locked?: boolean;
|
||
reason?: string | null;
|
||
featureKey?: string;
|
||
tenantId?: string;
|
||
requiredPlan?: string | null;
|
||
requiredCapability?: string | null;
|
||
requiredBundle?: string | null;
|
||
children: ReactNode;
|
||
fallback?: ReactNode;
|
||
}) {
|
||
const [prompt, setPrompt] = useState(false);
|
||
const [entitlementLocked, setEntitlementLocked] = useState(featureKey ? true : Boolean(lockedProp));
|
||
const [reason, setReason] = useState<string | null | undefined>(reasonProp);
|
||
const [requiredPlan, setRequiredPlan] = useState(requiredPlanProp);
|
||
const [requiredCapability, setRequiredCapability] = useState(requiredCapabilityProp);
|
||
const [requiredBundle, setRequiredBundle] = useState(requiredBundleProp);
|
||
|
||
useEffect(() => {
|
||
if (!featureKey || !tenantId) {
|
||
setEntitlementLocked(Boolean(lockedProp));
|
||
setReason(reasonProp);
|
||
setRequiredPlan(requiredPlanProp);
|
||
setRequiredCapability(requiredCapabilityProp);
|
||
setRequiredBundle(requiredBundleProp);
|
||
return;
|
||
}
|
||
let cancelled = false;
|
||
void checkFeatureAccess(tenantId, featureKey).then((r) => {
|
||
if (cancelled) return;
|
||
setEntitlementLocked(!r.has_access);
|
||
setReason(r.reason || reasonProp);
|
||
setRequiredPlan(r.required_plan || requiredPlanProp);
|
||
setRequiredCapability(r.required_capability || requiredCapabilityProp);
|
||
setRequiredBundle(r.required_bundle || requiredBundleProp);
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [
|
||
featureKey,
|
||
tenantId,
|
||
lockedProp,
|
||
reasonProp,
|
||
requiredPlanProp,
|
||
requiredCapabilityProp,
|
||
requiredBundleProp,
|
||
]);
|
||
|
||
const locked = featureKey ? entitlementLocked : Boolean(lockedProp);
|
||
|
||
if (!locked) return <>{children}</>;
|
||
|
||
const promptEl = (
|
||
<UpgradePrompt
|
||
open={prompt}
|
||
message={reason || undefined}
|
||
requiredPlan={requiredPlan}
|
||
requiredCapability={requiredCapability}
|
||
requiredBundle={requiredBundle}
|
||
onClose={() => setPrompt(false)}
|
||
/>
|
||
);
|
||
|
||
if (fallback) {
|
||
return (
|
||
<>
|
||
<div
|
||
role="button"
|
||
tabIndex={0}
|
||
onClick={() => setPrompt(true)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" || e.key === " ") setPrompt(true);
|
||
}}
|
||
>
|
||
{fallback}
|
||
</div>
|
||
{promptEl}
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className="relative">
|
||
<div className="pointer-events-none select-none opacity-40 blur-[0.5px]">{children}</div>
|
||
<div className="absolute inset-0 flex items-center justify-center rounded-xl bg-white/70 p-4">
|
||
<button
|
||
type="button"
|
||
className="rounded-xl border border-primary/30 bg-white px-4 py-2 text-sm font-semibold text-primary shadow-sm"
|
||
onClick={() => setPrompt(true)}
|
||
>
|
||
قفل — برای باز کردن ارتقا دهید
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{promptEl}
|
||
</>
|
||
);
|
||
}
|