feat(frontend): show active app domain with click-to-copy

Surface the domain the app is actually reachable on right now (verified
custom domain, else the platform subdomain) as a single "Application
Domain" field. Clicking it copies the address to the clipboard and shows
a "Copied" toast. Centralizes the active-domain logic so the page header
and the verified-custom-domain block stay in sync, and always loads
domain-info so the platform host is correct even without a custom domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-18 14:31:45 +03:30
parent a3a7e17cca
commit 04b2f040e0
3 changed files with 44 additions and 14 deletions
@@ -303,7 +303,9 @@ export default function AppDetailPage() {
}>({
queryKey: ['domain-info', appId],
queryFn: () => api.get(`/applications/${appId}/domain`).then((r) => r.data),
enabled: showDomainSetup || (!!app && (app.customDomainStatus === 'pending_dns' || app.customDomainStatus === 'verified')),
// Always load: we surface the active app domain (platform host included) even
// when no custom domain is configured, so the platform host must be available.
enabled: !!app,
});
const { data: domainPriceData } = useQuery<{ monthlyPrice: number }>({
@@ -1045,6 +1047,18 @@ export default function AppDetailPage() {
const pageLocked = isAnyDeleting;
// The host the app is actually reachable on right now: the verified custom
// domain when present, otherwise the platform-assigned subdomain. Single source
// of truth so every place that shows "the app's domain" stays in sync.
const platformHost = `${app.subdomain}.${domainInfo?.platformDomain || 'apps.cloudhost.ir'}`;
const currentDomain =
app.customDomain && app.customDomainStatus === 'verified' ? app.customDomain : platformHost;
const copyDomain = (domain: string) => {
navigator.clipboard.writeText(domain);
notify.success(ad.copied);
};
return (
<>
<DeletingModal open={pageLocked} resourceName={app.name} resourceKind="application" />
@@ -1067,10 +1081,7 @@ export default function AppDetailPage() {
</div>
<p className="text-sm text-gray-500 truncate">
{app.runtime}{app.runtimeVersion ? ` v${app.runtimeVersion}` : ''}{app.phpVersion ? ` — PHP ${app.phpVersion}` : ''} ·{' '}
{app.customDomain && app.customDomainStatus === 'verified'
? <a href={`https://${app.customDomain}`} target="_blank" rel="noopener noreferrer" className="text-emerald-600 hover:underline">{app.customDomain}</a>
: <span>{app.subdomain}.{domainInfo?.platformDomain || 'apps.cloudhost.ir'}</span>
}
<a href={`https://${currentDomain}`} target="_blank" rel="noopener noreferrer" dir="ltr" className="text-primary-600 hover:underline">{currentDomain}</a>
</p>
</div>
</div>
@@ -1604,16 +1615,24 @@ export default function AppDetailPage() {
)}
</div>
{/* Platform domain (always shown) */}
{/* Application domain — the host the app is reachable on right now
(verified custom domain, else the platform subdomain). Click to copy. */}
<div className="bg-gray-50 rounded-xl p-4 mb-4">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-500 mb-1">{ad.platformDomain}</p>
<p className="text-sm font-mono font-medium text-gray-800">
{app.subdomain}.{domainInfo?.platformDomain || 'apps.cloudhost.ir'}
</p>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-xs text-gray-500 mb-1">{ad.appDomain}</p>
<button
type="button"
onClick={() => copyDomain(currentDomain)}
title={ad.copyDomainHint}
dir="ltr"
className="group inline-flex items-center gap-1.5 max-w-full text-sm font-mono font-medium text-gray-800 hover:text-primary-600 transition-colors"
>
<span className="truncate">{currentDomain}</span>
<Copy className="w-3.5 h-3.5 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
</button>
</div>
<span className="badge badge-green text-xs">{ad.active}</span>
<span className="badge badge-green text-xs shrink-0">{ad.active}</span>
</div>
</div>
@@ -1623,7 +1642,16 @@ export default function AppDetailPage() {
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-emerald-600 mb-1">{ad.customDomain}</p>
<p className="text-sm font-mono font-medium text-emerald-800">{app.customDomain}</p>
<button
type="button"
onClick={() => copyDomain(app.customDomain!)}
title={ad.copyDomainHint}
dir="ltr"
className="group inline-flex items-center gap-1.5 max-w-full text-sm font-mono font-medium text-emerald-800 hover:text-emerald-600 transition-colors"
>
<span className="truncate">{app.customDomain}</span>
<Copy className="w-3.5 h-3.5 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
</button>
<p className="text-xs text-emerald-500 mt-1">
<CheckCircle className="w-3 h-3 inline" /> SSL Active Verified on{' '}
{app.customDomainVerifiedAt ? new Date(app.customDomainVerifiedAt).toLocaleString(locale) : ''}