174 lines
7.1 KiB
TypeScript
174 lines
7.1 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { AuthGuard } from "@/components/AuthGuard";
|
||
import { Button, Container } from "@/components/ui";
|
||
import { api, ApiError } from "@/lib/api";
|
||
import { useMe } from "@/hooks/useMe";
|
||
import { evaluateDomainPolicy, loadWorkspaceSnapshot } from "../adapters";
|
||
import type { AdapterResult, CommercialWorkspaceSnapshot, DomainPolicyResult } from "../types";
|
||
import { DomainPolicyPanel, FeatureLock, PlatformShell } from "../components";
|
||
import { CommercialEmptyState } from "../components/CommercialEmptyState";
|
||
|
||
function DomainsInner() {
|
||
const { me, loading: meLoading } = useMe();
|
||
const [snap, setSnap] = useState<AdapterResult<CommercialWorkspaceSnapshot | null> | null>(null);
|
||
const [policy, setPolicy] = useState<DomainPolicyResult | null>(null);
|
||
const [customDomain, setCustomDomain] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [msg, setMsg] = useState("");
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
const s = await loadWorkspaceSnapshot();
|
||
setSnap(s);
|
||
if (s.data) {
|
||
setPolicy(s.data.domain_policy || null);
|
||
} else if (me?.current_tenant_id) {
|
||
const pol = await evaluateDomainPolicy({ tenant_id: me.current_tenant_id });
|
||
setPolicy(pol.data);
|
||
}
|
||
}, [me?.current_tenant_id]);
|
||
|
||
useEffect(() => {
|
||
if (meLoading || !me) return;
|
||
void load();
|
||
}, [me, meLoading, load]);
|
||
|
||
const unlocked = Boolean(snap?.data?.custom_domain_unlocked ?? policy?.custom_domain_allowed);
|
||
|
||
const submit = async () => {
|
||
if (!snap?.data?.tenant_id || !customDomain.trim()) return;
|
||
setBusy(true);
|
||
setError("");
|
||
setMsg("");
|
||
try {
|
||
if (!unlocked) {
|
||
setError(policy?.reason || "دامنه اختصاصی بر اساس سیاست قفل است.");
|
||
setBusy(false);
|
||
return;
|
||
}
|
||
await api.onboarding.updateDomain(snap.data.tenant_id, {
|
||
custom_domain: customDomain.trim(),
|
||
});
|
||
setMsg("دامنه ثبت شد — تأیید DNS را از وضعیت زیرگیری کنید.");
|
||
await load();
|
||
} catch (err) {
|
||
setError(err instanceof ApiError ? err.message : "ثبت دامنه ناموفق بود");
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
if (meLoading) {
|
||
return (
|
||
<Container className="py-16 text-center">
|
||
<p className="text-sm text-gray-500">در حال بارگذاری…</p>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
const ws = snap?.data;
|
||
|
||
return (
|
||
<PlatformShell title="دامنه و SSL" subtitle="قبل از اشتراک فقط زیردامنه؛ بعد از entitlement دامنه اختصاصی">
|
||
{!ws ? (
|
||
<CommercialEmptyState title="Workspace نیست" description={snap?.message} actionHref="/onboarding" actionLabel="راهاندازی" />
|
||
) : (
|
||
<div className="space-y-6">
|
||
<div className="rounded-2xl border border-gray-100 bg-white p-5 dark:border-gray-800 dark:bg-gray-900">
|
||
<p className="text-sm text-gray-500">زیردامنه پلتفرم</p>
|
||
<p className="mt-1 font-mono text-lg text-secondary dark:text-gray-100" dir="ltr">
|
||
{ws.slug}.torbatyar.ir
|
||
</p>
|
||
{ws.primary_domain ? (
|
||
<p className="mt-2 text-sm">
|
||
دامنه اصلی:{" "}
|
||
<span className="font-mono" dir="ltr">
|
||
{ws.primary_domain}
|
||
</span>
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
|
||
<DomainPolicyPanel unlocked={unlocked} policy={policy || ws.domain_policy} tenantId={ws.tenant_id} />
|
||
|
||
<FeatureLock
|
||
locked={!unlocked}
|
||
reason={policy?.reason}
|
||
tenantId={ws.tenant_id}
|
||
featureKey="custom_domain"
|
||
fallback={
|
||
<div className="rounded-2xl border border-dashed border-gray-200 bg-gray-50 p-5 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-900">
|
||
اتصال دامنه اختصاصی قفل است.{" "}
|
||
<Link href="/billing" className="font-semibold text-primary">
|
||
ارتقا در Billing
|
||
</Link>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-5 dark:border-gray-800 dark:bg-gray-900">
|
||
<label className="block text-sm font-medium text-secondary dark:text-gray-100">
|
||
اتصال دامنه اختصاصی
|
||
<input
|
||
dir="ltr"
|
||
className="mt-1 w-full rounded-xl border border-gray-200 px-4 py-2.5 text-sm dark:border-gray-700 dark:bg-gray-950"
|
||
placeholder="shop.example.com"
|
||
value={customDomain}
|
||
onChange={(e) => setCustomDomain(e.target.value)}
|
||
/>
|
||
</label>
|
||
<Button loading={busy} onClick={() => void submit()}>
|
||
ذخیره و شروع تأیید
|
||
</Button>
|
||
</div>
|
||
</FeatureLock>
|
||
|
||
<section>
|
||
<h3 className="text-sm font-semibold text-secondary dark:text-gray-100">وضعیت دامنهها</h3>
|
||
{ws.domains.length ? (
|
||
<ul className="mt-3 space-y-2">
|
||
{ws.domains.map((d) => (
|
||
<li
|
||
key={d.id}
|
||
className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-gray-100 bg-white px-3 py-2 text-sm dark:border-gray-800 dark:bg-gray-900"
|
||
>
|
||
<span className="font-mono" dir="ltr">
|
||
{d.domain}
|
||
</span>
|
||
<span className="text-xs text-gray-500">
|
||
{d.domain_type} · {d.is_verified ? "verified" : "pending"}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<CommercialEmptyState
|
||
title="دامنهای ثبت نشده"
|
||
description="زیردامنه workspace بهصورت پیشفرض فعال است. دامنه اختصاصی پس از entitlement اضافه میشود."
|
||
hint="وضعیت تأیید DNS و SSL پس از ثبت دامنه اینجا نمایش داده میشود."
|
||
actionHref="/billing"
|
||
actionLabel="بررسی entitlement"
|
||
secondaryHref="/help"
|
||
secondaryLabel="راهنمای دامنه"
|
||
/>
|
||
)}
|
||
</section>
|
||
|
||
{error ? <p className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p> : null}
|
||
{msg ? <p className="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{msg}</p> : null}
|
||
</div>
|
||
)}
|
||
</PlatformShell>
|
||
);
|
||
}
|
||
|
||
export function CommercialDomainsPage() {
|
||
return (
|
||
<AuthGuard>
|
||
<DomainsInner />
|
||
</AuthGuard>
|
||
);
|
||
}
|