'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 (
{ let v = e.target.value.trim() if (v && !v.startsWith('#')) { v = `#${v}` } v = v.slice(0, 1) + v.slice(1).replace(/[^0-9a-fA-F]/g, '') onChange(v.slice(0, 7)) }} onFocus={(e) => e.target.select()} placeholder={placeholder} maxLength={7} className={cn(CHIP_FIELD_INPUT, 'font-mono')} />
{!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} />
{logoUpload.previewUrl && ( )}
{wordmarkUpload.previewUrl && ( )}
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' />
) }