diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 79f4e965b7ab..e4c4c10c6f01 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -775,6 +775,8 @@ export interface ConversationsSettings { widget_identification_form_title?: string widget_identification_form_description?: string widget_placeholder_text?: string + widget_ticket_recovery_text?: string + widget_ticket_recovery_link_text?: string widget_position?: 'bottom_left' | 'bottom_right' | 'top_left' | 'top_right' slack_enabled?: boolean slack_team_id?: string | null diff --git a/posthog/models/remote_config.py b/posthog/models/remote_config.py index 658ecd00ef99..d13c03ad8cdc 100644 --- a/posthog/models/remote_config.py +++ b/posthog/models/remote_config.py @@ -301,6 +301,9 @@ def build_config(self, bypass_recordings_quota_cache: bool = False) -> dict[str, "identificationFormDescription": conv_settings.get("widget_identification_form_description") or "Please provide your details so we can help you better.", "placeholderText": conv_settings.get("widget_placeholder_text") or "Type your message...", + "ticketRecoveryText": conv_settings.get("widget_ticket_recovery_text") + or "Don't see your previous tickets?", + "ticketRecoveryLinkText": conv_settings.get("widget_ticket_recovery_link_text") or "Recover them here", "widgetPosition": conv_settings.get("widget_position") or "bottom_right", } else: diff --git a/posthog/models/test/test_remote_config.py b/posthog/models/test/test_remote_config.py index b5d63ee945a4..b729a509bd2f 100644 --- a/posthog/models/test/test_remote_config.py +++ b/posthog/models/test/test_remote_config.py @@ -194,6 +194,8 @@ def test_conversations_enabled_with_defaults(self): == "Please provide your details so we can help you better." ) assert self.remote_config.config["conversations"]["placeholderText"] == "Type your message..." + assert self.remote_config.config["conversations"]["ticketRecoveryText"] == "Don't see your previous tickets?" + assert self.remote_config.config["conversations"]["ticketRecoveryLinkText"] == "Recover them here" def test_conversations_enabled_with_custom_config(self): self.team.conversations_enabled = True @@ -209,6 +211,8 @@ def test_conversations_enabled_with_custom_config(self): "widget_identification_form_title": "Let's get started", "widget_identification_form_description": "Tell us about yourself", "widget_placeholder_text": "Ask away...", + "widget_ticket_recovery_text": "Missing your tickets?", + "widget_ticket_recovery_link_text": "Restore access", } self.team.save() self.sync_remote_config() @@ -223,6 +227,8 @@ def test_conversations_enabled_with_custom_config(self): assert self.remote_config.config["conversations"]["identificationFormTitle"] == "Let's get started" assert self.remote_config.config["conversations"]["identificationFormDescription"] == "Tell us about yourself" assert self.remote_config.config["conversations"]["placeholderText"] == "Ask away..." + assert self.remote_config.config["conversations"]["ticketRecoveryText"] == "Missing your tickets?" + assert self.remote_config.config["conversations"]["ticketRecoveryLinkText"] == "Restore access" def test_conversations_disabled_returns_false(self): self.team.conversations_enabled = False diff --git a/products/conversations/frontend/scenes/settings/WidgetSection.tsx b/products/conversations/frontend/scenes/settings/WidgetSection.tsx index 544fff940880..bcba8ce2e2d3 100644 --- a/products/conversations/frontend/scenes/settings/WidgetSection.tsx +++ b/products/conversations/frontend/scenes/settings/WidgetSection.tsx @@ -19,7 +19,7 @@ import { SceneSection } from '~/layout/scenes/components/SceneSection' import { supportSettingsLogic } from './supportSettingsLogic' export function WidgetSection(): JSX.Element { - const { currentTeam } = useValues(teamLogic) + const { currentTeam, currentTeamLoading } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) const { generateNewToken, @@ -32,6 +32,10 @@ export function WidgetSection(): JSX.Element { saveIdentificationFormDescription, setPlaceholderTextValue, savePlaceholderText, + setTicketRecoveryTextValue, + saveTicketRecoveryText, + setTicketRecoveryLinkTextValue, + saveTicketRecoveryLinkText, } = useActions(supportSettingsLogic) const { widgetEnabledLoading, @@ -39,6 +43,8 @@ export function WidgetSection(): JSX.Element { identificationFormTitleValue, identificationFormDescriptionValue, placeholderTextValue, + ticketRecoveryTextValue, + ticketRecoveryLinkTextValue, } = useValues(supportSettingsLogic) return ( @@ -190,6 +196,70 @@ export function WidgetSection(): JSX.Element { + +
+
+ +

+ Only shown to visitors who aren't using identity verification. +

+
+
+ + + Save + +
+
+ +
+
+ +

+ The clickable part of the ticket recovery message. +

+
+
+ + + Save + +
+
diff --git a/products/conversations/frontend/scenes/settings/supportSettingsLogic.ts b/products/conversations/frontend/scenes/settings/supportSettingsLogic.ts index 76acf4bbe81c..2b3a63ee8df6 100644 --- a/products/conversations/frontend/scenes/settings/supportSettingsLogic.ts +++ b/products/conversations/frontend/scenes/settings/supportSettingsLogic.ts @@ -138,6 +138,8 @@ export interface supportSettingsLogicValues { name: string }[] teamsTeamsLoading: boolean + ticketRecoveryLinkTextValue: string | null + ticketRecoveryTextValue: string | null widgetEnabledLoading: boolean } @@ -382,6 +384,12 @@ export interface supportSettingsLogicActions { saveSlackTicketEmoji: () => { value: true } + saveTicketRecoveryLinkText: () => { + value: true + } + saveTicketRecoveryText: () => { + value: true + } sendTestEmail: (configId: string) => { configId: string } @@ -494,6 +502,12 @@ export interface supportSettingsLogicActions { status: 'error' | 'idle' | 'installed' | 'installing' | 'needs_org_catalog' teamId: string | null } + setTicketRecoveryLinkTextValue: (value: string | null) => { + value: string | null + } + setTicketRecoveryTextValue: (value: string | null) => { + value: string | null + } setWidgetEnabledLoading: (loading: boolean) => { loading: boolean } @@ -600,6 +614,10 @@ export const supportSettingsLogic = kea([ saveIdentificationFormDescription: true, setPlaceholderTextValue: (value: string | null) => ({ value }), savePlaceholderText: true, + setTicketRecoveryTextValue: (value: string | null) => ({ value }), + saveTicketRecoveryText: true, + setTicketRecoveryLinkTextValue: (value: string | null) => ({ value }), + saveTicketRecoveryLinkText: true, // Notification recipients setNotificationRecipients: (users: UserBasicType[]) => ({ users }), // Slack channel settings (SupportHog) @@ -738,6 +756,18 @@ export const supportSettingsLogic = kea([ setPlaceholderTextValue: (_, { value }) => value, }, ], + ticketRecoveryTextValue: [ + null as string | null, + { + setTicketRecoveryTextValue: (_, { value }) => value, + }, + ], + ticketRecoveryLinkTextValue: [ + null as string | null, + { + setTicketRecoveryLinkTextValue: (_, { value }) => value, + }, + ], // Email multi-config state emailConfigs: [ [] as EmailConfigStatus[], @@ -1277,6 +1307,30 @@ export const supportSettingsLogic = kea([ }, }) }, + saveTicketRecoveryText: () => { + const trimmedValue = values.ticketRecoveryTextValue?.trim() + if (!trimmedValue) { + return + } + actions.updateCurrentTeam({ + conversations_settings: { + ...values.currentTeam?.conversations_settings, + widget_ticket_recovery_text: trimmedValue, + }, + }) + }, + saveTicketRecoveryLinkText: () => { + const trimmedValue = values.ticketRecoveryLinkTextValue?.trim() + if (!trimmedValue) { + return + } + actions.updateCurrentTeam({ + conversations_settings: { + ...values.currentTeam?.conversations_settings, + widget_ticket_recovery_link_text: trimmedValue, + }, + }) + }, setNotificationRecipients: ({ users }) => { actions.updateCurrentTeam({ conversations_settings: { @@ -1647,6 +1701,8 @@ export const supportSettingsLogic = kea([ actions.setIdentificationFormTitleValue(null) actions.setIdentificationFormDescriptionValue(null) actions.setPlaceholderTextValue(null) + actions.setTicketRecoveryTextValue(null) + actions.setTicketRecoveryLinkTextValue(null) actions.setSlackTicketEmojiValue(null) actions.setSlackBotIconUrlValue(null) actions.setSlackBotDisplayNameValue(null)