diff --git a/web/app/components/header/account-dropdown/index.spec.tsx b/web/app/components/header/account-dropdown/index.spec.tsx index a92f8503ee..c234d350d8 100644 --- a/web/app/components/header/account-dropdown/index.spec.tsx +++ b/web/app/components/header/account-dropdown/index.spec.tsx @@ -70,6 +70,7 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({ mockConfig: { IS_CLOUD_EDITION: false, ZENDESK_WIDGET_KEY: '', + SUPPORT_EMAIL_ADDRESS: '', }, mockEnv: { env: { @@ -80,6 +81,7 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({ vi.mock('@/config', () => ({ get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION }, get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY }, + get SUPPORT_EMAIL_ADDRESS() { return mockConfig.SUPPORT_EMAIL_ADDRESS }, IS_DEV: false, IS_CE_EDITION: false, })) diff --git a/web/app/components/header/account-dropdown/support.spec.tsx b/web/app/components/header/account-dropdown/support.spec.tsx index 90bcb9f3ec..de48b744c2 100644 --- a/web/app/components/header/account-dropdown/support.spec.tsx +++ b/web/app/components/header/account-dropdown/support.spec.tsx @@ -11,6 +11,10 @@ const { mockZendeskKey } = vi.hoisted(() => ({ mockZendeskKey: { value: 'test-key' }, })) +const { mockSupportEmailKey } = vi.hoisted(() => ({ + mockSupportEmailKey: { value: '' }, +})) + vi.mock('@/context/app-context', async (importOriginal) => { const actual = await importOriginal() return { @@ -33,6 +37,7 @@ vi.mock('@/config', async (importOriginal) => { ...actual, IS_CE_EDITION: false, get ZENDESK_WIDGET_KEY() { return mockZendeskKey.value }, + get SUPPORT_EMAIL_ADDRESS() { return mockSupportEmailKey.value }, } }) @@ -84,6 +89,7 @@ describe('Support', () => { vi.clearAllMocks() window.zE = vi.fn() mockZendeskKey.value = 'test-key' + mockSupportEmailKey.value = '' vi.mocked(useAppContext).mockReturnValue(baseAppContextValue) vi.mocked(useProviderContext).mockReturnValue({ ...baseProviderContextValue, @@ -96,7 +102,7 @@ describe('Support', () => { const renderSupport = () => { return render( - {}}> + { }}> open @@ -125,7 +131,7 @@ describe('Support', () => { }) }) - describe('Plan-based Channels', () => { + describe('Dedicated Channels', () => { it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => { // Act renderSupport() @@ -166,6 +172,27 @@ describe('Support', () => { expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument() expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument() }) + + it('should show email support if specified in the config', () => { + // Arrange + mockZendeskKey.value = '' + mockSupportEmailKey.value = 'support@example.com' + vi.mocked(useProviderContext).mockReturnValue({ + ...baseProviderContextValue, + plan: { + ...baseProviderContextValue.plan, + type: Plan.sandbox, + }, + }) + + // Act + renderSupport() + fireEvent.click(screen.getByText('common.userProfile.support')) + + // Assert + expect(screen.queryByText('common.userProfile.emailSupport')).toBeInTheDocument() + expect(screen.getByText('common.userProfile.emailSupport')?.closest('a')?.getAttribute('href')).toMatch(new RegExp(`^mailto:${mockSupportEmailKey.value}`)) + }) }) describe('Interactions and Links', () => { diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx index 7ec2766977..ead4509cce 100644 --- a/web/app/components/header/account-dropdown/support.tsx +++ b/web/app/components/header/account-dropdown/support.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu' import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils' import { Plan } from '@/app/components/billing/type' -import { ZENDESK_WIDGET_KEY } from '@/config' +import { SUPPORT_EMAIL_ADDRESS, ZENDESK_WIDGET_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { mailToSupport } from '../utils/util' @@ -17,8 +17,8 @@ export default function Support({ closeAccountDropdown }: SupportProps) { const { t } = useTranslation() const { plan } = useProviderContext() const { userProfile, langGeniusVersionInfo } = useAppContext() - const hasDedicatedChannel = plan.type !== Plan.sandbox - const hasZendeskWidget = !!ZENDESK_WIDGET_KEY?.trim() + const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim()) + const hasZendeskWidget = Boolean(ZENDESK_WIDGET_KEY.trim()) return ( @@ -49,7 +49,7 @@ export default function Support({ closeAccountDropdown }: SupportProps) { {hasDedicatedChannel && !hasZendeskWidget && ( } + render={} > { +export const mailToSupport = (account: string, plan: string, version: string, supportEmailAddress?: string) => { const subject = `Technical Support Request ${plan} ${account}` const body = ` Please do not remove the following information: @@ -21,5 +21,5 @@ export const mailToSupport = (account: string, plan: string, version: string) => Platform: Problem Description: ` - return generateMailToLink('support@dify.ai', subject, body) + return generateMailToLink(supportEmailAddress || 'support@dify.ai', subject, body) } diff --git a/web/config/index.ts b/web/config/index.ts index 35ea3780a8..e8526479a1 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -342,6 +342,12 @@ export const ZENDESK_FIELD_IDS = { '', ), } + +export const SUPPORT_EMAIL_ADDRESS = getStringConfig( + env.NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS, + '', +) + export const APP_VERSION = pkg.version export const IS_MARKETPLACE = env.NEXT_PUBLIC_IS_MARKETPLACE diff --git a/web/env.ts b/web/env.ts index f931c48677..8ecde76143 100644 --- a/web/env.ts +++ b/web/env.ts @@ -115,6 +115,7 @@ const clientSchema = { */ NEXT_PUBLIC_SENTRY_DSN: z.string().optional(), NEXT_PUBLIC_SITE_ABOUT: z.string().optional(), + NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS: z.email().optional(), NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: coercedBoolean.default(false), /** * The timeout for the text generation in millisecond @@ -184,6 +185,7 @@ export const env = createEnv({ NEXT_PUBLIC_PUBLIC_API_PREFIX: isServer ? process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX : getRuntimeEnvFromBody('publicApiPrefix'), NEXT_PUBLIC_SENTRY_DSN: isServer ? process.env.NEXT_PUBLIC_SENTRY_DSN : getRuntimeEnvFromBody('sentryDsn'), NEXT_PUBLIC_SITE_ABOUT: isServer ? process.env.NEXT_PUBLIC_SITE_ABOUT : getRuntimeEnvFromBody('siteAbout'), + NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS: isServer ? process.env.NEXT_PUBLIC_SUPPORT_EMAIL_ADDRESS : getRuntimeEnvFromBody('supportEmailAddress'), NEXT_PUBLIC_SUPPORT_MAIL_LOGIN: isServer ? process.env.NEXT_PUBLIC_SUPPORT_MAIL_LOGIN : getRuntimeEnvFromBody('supportMailLogin'), NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS: isServer ? process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS : getRuntimeEnvFromBody('textGenerationTimeoutMs'), NEXT_PUBLIC_TOP_K_MAX_VALUE: isServer ? process.env.NEXT_PUBLIC_TOP_K_MAX_VALUE : getRuntimeEnvFromBody('topKMaxValue'),