'use client'
import { useEffect, useRef, useState } from 'react'
import { Button, ChipInput, cn, Label, Loader, toast } from '@sim/emcn'
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/predicates'
import { toError } from '@sim/utils/errors'
import { Image as ImageIcon, X } from 'lucide-react'
import Image from 'next/image'
import { useParams } from 'next/navigation'
import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
import { HEX_COLOR_REGEX } from '@/lib/branding'
import { isBillingEnabled } from '@/lib/core/config/env-flags'
import { getUserRole } from '@/lib/workspaces/organization/utils'
import {
CHIP_FIELD_INPUT,
CHIP_FIELD_SHELL,
} from '@/app/workspace/[workspaceId]/components/credential-detail'
import { saveDiscardActions } from '@/app/workspace/[workspaceId]/settings/components/save-discard-actions/save-discard-actions'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload'
import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard'
import { SettingRow } from '@/ee/components/setting-row'
import {
useUpdateWhitelabelSettings,
useWhitelabelSettings,
type WhitelabelSettingsPayload,
} from '@/ee/whitelabeling/hooks/whitelabel'
import { useOrganizations } from '@/hooks/queries/organization'
import { useSubscriptionData } from '@/hooks/queries/subscription'
const logger = createLogger('WhitelabelingSettings')
interface DropZoneProps {
onDrop: (e: React.DragEvent) => void
children: React.ReactNode
className?: string
}
function DropZone({ onDrop, children, className }: DropZoneProps) {
const [isDragging, setIsDragging] = useState(false)
return (
{
if (e.dataTransfer.types.includes('Files')) {
e.preventDefault()
setIsDragging(true)
}
}}
onDragLeave={(e) => {
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragging(false)
}
}}
onDrop={(e) => {
setIsDragging(false)
onDrop(e)
}}
>
{children}
{isDragging && (
)}
)
}
interface ColorInputProps {
label: string
value: string
onChange: (value: string) => void
placeholder?: string
}
function ColorInput({ label, value, onChange, placeholder = '#000000' }: ColorInputProps) {
const isValidHex = !value || HEX_COLOR_REGEX.test(value)
const showColor = Boolean(value) && isValidHex
return (
{!isValidHex && (
Must be a valid hex color (e.g. #33c482)
)}
)
}
export function WhitelabelingSettings() {
const params = useParams<{ workspaceId: string }>()
const { data: session } = useSession()
const { data: orgsData } = useOrganizations()
const { data: subscriptionData } = useSubscriptionData()
const activeOrganization = orgsData?.activeOrganization
const orgId = activeOrganization?.id
const { data: savedSettings, isLoading } = useWhitelabelSettings(orgId)
const updateSettings = useUpdateWhitelabelSettings()
const userEmail = session?.user?.email
const userRole = getUserRole(activeOrganization, userEmail)
const canManage = isOrgAdminRole(userRole)
const subscriptionAccess = getSubscriptionAccessState(subscriptionData?.data)
const hasEnterprisePlan = subscriptionAccess.hasUsableEnterpriseAccess
const [brandName, setBrandName] = useState('')
const [primaryColor, setPrimaryColor] = useState('')
const [primaryHoverColor, setPrimaryHoverColor] = useState('')
const [accentColor, setAccentColor] = useState('')
const [accentHoverColor, setAccentHoverColor] = useState('')
const [supportEmail, setSupportEmail] = useState('')
const [documentationUrl, setDocumentationUrl] = useState('')
const [termsUrl, setTermsUrl] = useState('')
const [privacyUrl, setPrivacyUrl] = useState('')
const [logoUrl, setLogoUrl] = useState(null)
const [wordmarkUrl, setWordmarkUrl] = useState(null)
const formInitializedRef = useRef(false)
const [savedBrandName, setSavedBrandName] = useState('')
const [savedPrimaryColor, setSavedPrimaryColor] = useState('')
const [savedPrimaryHoverColor, setSavedPrimaryHoverColor] = useState('')
const [savedAccentColor, setSavedAccentColor] = useState('')
const [savedAccentHoverColor, setSavedAccentHoverColor] = useState('')
const [savedSupportEmail, setSavedSupportEmail] = useState('')
const [savedDocumentationUrl, setSavedDocumentationUrl] = useState('')
const [savedTermsUrl, setSavedTermsUrl] = useState('')
const [savedPrivacyUrl, setSavedPrivacyUrl] = useState('')
const [savedLogoUrl, setSavedLogoUrl] = useState(null)
const [savedWordmarkUrl, setSavedWordmarkUrl] = useState(null)
useEffect(() => {
if (!savedSettings || formInitializedRef.current) return
const brand = savedSettings.brandName ?? ''
const primary = savedSettings.primaryColor ?? ''
const primaryHover = savedSettings.primaryHoverColor ?? ''
const accent = savedSettings.accentColor ?? ''
const accentHover = savedSettings.accentHoverColor ?? ''
const support = savedSettings.supportEmail ?? ''
const docs = savedSettings.documentationUrl ?? ''
const terms = savedSettings.termsUrl ?? ''
const privacy = savedSettings.privacyUrl ?? ''
const logo = savedSettings.logoUrl ?? null
const wordmark = savedSettings.wordmarkUrl ?? null
setBrandName(brand)
setPrimaryColor(primary)
setPrimaryHoverColor(primaryHover)
setAccentColor(accent)
setAccentHoverColor(accentHover)
setSupportEmail(support)
setDocumentationUrl(docs)
setTermsUrl(terms)
setPrivacyUrl(privacy)
setLogoUrl(logo)
setWordmarkUrl(wordmark)
setSavedBrandName(brand)
setSavedPrimaryColor(primary)
setSavedPrimaryHoverColor(primaryHover)
setSavedAccentColor(accent)
setSavedAccentHoverColor(accentHover)
setSavedSupportEmail(support)
setSavedDocumentationUrl(docs)
setSavedTermsUrl(terms)
setSavedPrivacyUrl(privacy)
setSavedLogoUrl(logo)
setSavedWordmarkUrl(wordmark)
formInitializedRef.current = true
}, [savedSettings])
const logoUpload = useProfilePictureUpload({
currentImage: logoUrl,
onUpload: (url) => setLogoUrl(url),
onError: (error) => toast.error(error),
context: 'workspace-logos',
workspaceId: params.workspaceId,
})
const wordmarkUpload = useProfilePictureUpload({
currentImage: wordmarkUrl,
onUpload: (url) => setWordmarkUrl(url),
onError: (error) => toast.error(error),
context: 'workspace-logos',
workspaceId: params.workspaceId,
})
const hasChanges =
formInitializedRef.current &&
(brandName !== savedBrandName ||
primaryColor !== savedPrimaryColor ||
primaryHoverColor !== savedPrimaryHoverColor ||
accentColor !== savedAccentColor ||
accentHoverColor !== savedAccentHoverColor ||
supportEmail !== savedSupportEmail ||
documentationUrl !== savedDocumentationUrl ||
termsUrl !== savedTermsUrl ||
privacyUrl !== savedPrivacyUrl ||
(logoUpload.previewUrl || null) !== savedLogoUrl ||
(wordmarkUpload.previewUrl || null) !== savedWordmarkUrl)
useSettingsUnsavedGuard({ isDirty: hasChanges })
async function handleSave() {
if (!orgId) return
const colorFields: Array<[string, string]> = [
['Primary color', primaryColor],
['Primary hover color', primaryHoverColor],
['Accent color', accentColor],
['Accent hover color', accentHoverColor],
]
for (const [fieldName, value] of colorFields) {
if (value && !HEX_COLOR_REGEX.test(value)) {
toast.error(`${fieldName} must be a valid hex color (e.g. #33c482)`)
return
}
}
const settings: WhitelabelSettingsPayload = {
brandName: brandName || null,
logoUrl: logoUpload.previewUrl || null,
wordmarkUrl: wordmarkUpload.previewUrl || null,
primaryColor: primaryColor || null,
primaryHoverColor: primaryHoverColor || null,
accentColor: accentColor || null,
accentHoverColor: accentHoverColor || null,
supportEmail: supportEmail || null,
documentationUrl: documentationUrl || null,
termsUrl: termsUrl || null,
privacyUrl: privacyUrl || null,
}
try {
await updateSettings.mutateAsync({ orgId, settings })
setSavedBrandName(brandName)
setSavedPrimaryColor(primaryColor)
setSavedPrimaryHoverColor(primaryHoverColor)
setSavedAccentColor(accentColor)
setSavedAccentHoverColor(accentHoverColor)
setSavedSupportEmail(supportEmail)
setSavedDocumentationUrl(documentationUrl)
setSavedTermsUrl(termsUrl)
setSavedPrivacyUrl(privacyUrl)
setSavedLogoUrl(logoUpload.previewUrl || null)
setSavedWordmarkUrl(wordmarkUpload.previewUrl || null)
toast.success('Whitelabeling settings saved.')
} catch (error) {
logger.error('Failed to save whitelabel settings', { error })
toast.error(toError(error).message)
}
}
function handleDiscard() {
setBrandName(savedBrandName)
setPrimaryColor(savedPrimaryColor)
setPrimaryHoverColor(savedPrimaryHoverColor)
setAccentColor(savedAccentColor)
setAccentHoverColor(savedAccentHoverColor)
setSupportEmail(savedSupportEmail)
setDocumentationUrl(savedDocumentationUrl)
setTermsUrl(savedTermsUrl)
setPrivacyUrl(savedPrivacyUrl)
setLogoUrl(savedLogoUrl)
setWordmarkUrl(savedWordmarkUrl)
}
if (isBillingEnabled) {
if (!activeOrganization) {
return (
You must be part of an organization to configure whitelabeling.
)
}
if (!hasEnterprisePlan) {
return (
Whitelabeling is available on Enterprise plans only.
)
}
if (!canManage) {
return (
Only organization owners and admins can configure whitelabeling settings.
)
}
}
if (isLoading) {
return null
}
const isUploading = logoUpload.isUploading || wordmarkUpload.isUploading
return (
setBrandName(e.target.value)}
placeholder='Your Company'
className='max-w-[320px]'
maxLength={64}
/>
setSupportEmail(e.target.value)}
placeholder='support@yourcompany.com'
/>
setDocumentationUrl(e.target.value)}
placeholder='https://docs.yourcompany.com'
/>
setTermsUrl(e.target.value)}
placeholder='https://yourcompany.com/terms'
/>
setPrivacyUrl(e.target.value)}
placeholder='https://yourcompany.com/privacy'
/>
)
}