SupportFormV3.tsx316 lines · main
1// End of third-party imports
2import { SupportCategories } from '@supabase/shared-types/out/constants'
3import { useConstant, useFlag } from 'common'
4import { CLIENT_LIBRARIES } from 'common/constants'
5import { type Dispatch, type MouseEventHandler } from 'react'
6import type { SubmitHandler, UseFormReturn } from 'react-hook-form'
7import { Form, Separator } from 'ui'
8
9import {
10 AffectedServicesSelector,
11 CATEGORIES_WITHOUT_AFFECTED_SERVICES,
12} from './AffectedServicesSelector'
13import { AttachmentUploadDisplay, useAttachmentUpload } from './AttachmentUpload'
14import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
15import { ClientLibraryInfo } from './ClientLibraryInfo'
16import {
17 DASHBOARD_LOG_CATEGORIES,
18 getSanitizedBreadcrumbs,
19 uploadDashboardLog,
20} from './dashboard-logs'
21import { DashboardLogsToggle } from './DashboardLogsToggle'
22import { MessageField } from './MessageField'
23import { OrganizationSelector } from './OrganizationSelector'
24import { PlanExpectationInfoContent, ProjectAndPlanInfo } from './ProjectAndPlanInfo'
25import { SubjectAndSuggestionsInfo } from './SubjectAndSuggestionsInfo'
26import { SubmitButton } from './SubmitButton'
27import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
28import type { SupportFormValues } from './SupportForm.schema'
29import type { SupportFormActions, SupportFormState } from './SupportForm.state'
30import {
31 formatMessage,
32 formatStudioVersion,
33 getOrgSubscriptionPlan,
34 NO_ORG_MARKER,
35 NO_PROJECT_MARKER,
36} from './SupportForm.utils'
37import { SupportFormDirectEmailContent } from './SupportFormDirectEmailInfo'
38import { getProjectAuthConfig } from '@/data/auth/auth-config-query'
39import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send'
40import { type OrganizationPlanID } from '@/data/organizations/organization-query'
41import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
42import { useGenerateAttachmentURLsMutation } from '@/data/support/generate-attachment-urls-mutation'
43import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query'
44import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
45import { detectBrowser } from '@/lib/helpers'
46import { useProfile } from '@/lib/profile'
47
48const useIsSimplifiedForm = (slug: string, subscriptionPlanId?: OrganizationPlanID) => {
49 const simplifiedSupportForm = useFlag('simplifiedSupportForm')
50
51 if (subscriptionPlanId === 'platform') {
52 return true
53 }
54
55 if (typeof simplifiedSupportForm === 'string') {
56 const slugs = (simplifiedSupportForm as string).split(',').map((x) => x.trim())
57 return slugs.includes(slug)
58 }
59
60 return false
61}
62
63interface SupportFormV3Props {
64 form: UseFormReturn<SupportFormValues>
65 initialError: string | null
66 state: SupportFormState
67 dispatch: Dispatch<SupportFormActions>
68 selectedProjectRef?: string | null
69}
70
71export const SupportFormV3 = ({
72 form,
73 initialError,
74 state,
75 dispatch,
76 selectedProjectRef,
77}: SupportFormV3Props) => {
78 const { profile } = useProfile()
79 const respondToEmail = profile?.primary_email ?? 'your email'
80
81 const { organizationSlug, projectRef, category, severity, subject, library } = form.watch()
82
83 const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug
84 const currentProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef
85
86 const { data: organizations } = useOrganizationsQuery()
87 const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug)
88 const simplifiedSupportForm = useIsSimplifiedForm(organizationSlug, subscriptionPlanId)
89 const showClientLibraries = useIsFeatureEnabled('support:show_client_libraries')
90
91 const attachmentUpload = useAttachmentUpload()
92 const { mutateAsync: uploadDashboardLogFn } = useGenerateAttachmentURLsMutation()
93
94 const sanitizedLogSnapshot = useConstant(getSanitizedBreadcrumbs)
95
96 const { data: commit } = useDeploymentCommitQuery({
97 staleTime: 1000 * 60 * 10,
98 })
99
100 const { mutate: submitSupportTicket } = useSendSupportTicketMutation({
101 onSuccess: (_, variables) => {
102 dispatch({
103 type: 'SUCCESS',
104 sentProjectRef: variables.projectRef,
105 sentOrgSlug: variables.organizationSlug,
106 sentCategory: variables.category,
107 })
108 },
109 onError: (error) => {
110 dispatch({
111 type: 'ERROR',
112 message: error.message,
113 })
114 },
115 })
116
117 const onSubmit: SubmitHandler<SupportFormValues> = async (formValues) => {
118 if (
119 !simplifiedSupportForm &&
120 showClientLibraries &&
121 formValues.category === SupportCategories.PROBLEM &&
122 !formValues.library
123 ) {
124 form.setError('library', {
125 type: 'manual',
126 message: "Please select the library that you're facing issues with",
127 })
128 return
129 }
130
131 dispatch({ type: 'SUBMIT' })
132
133 const { attachDashboardLogs: formAttachDashboardLogs, ...values } = formValues
134 const attachDashboardLogs =
135 formAttachDashboardLogs && DASHBOARD_LOG_CATEGORIES.includes(values.category)
136
137 const [attachments, dashboardLogUrl] = await Promise.all([
138 attachmentUpload.createAttachments(),
139 attachDashboardLogs
140 ? uploadDashboardLog({
141 userId: profile?.gotrue_id,
142 sanitizedLogs: sanitizedLogSnapshot,
143 uploadDashboardLogFn,
144 })
145 : undefined,
146 ])
147
148 const selectedLibrary = values.library
149 ? CLIENT_LIBRARIES.find((library) => library.language === values.library)
150 : undefined
151
152 const payload = {
153 ...values,
154 organizationSlug: values.organizationSlug ?? NO_ORG_MARKER,
155 projectRef: values.projectRef ?? NO_PROJECT_MARKER,
156 allowSupportAccess:
157 values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
158 ? values.allowSupportAccess
159 : false,
160 library:
161 values.category === SupportCategories.PROBLEM && selectedLibrary !== undefined
162 ? selectedLibrary.key
163 : '',
164 message: formatMessage({
165 message: values.message,
166 attachments,
167 error: initialError,
168 }),
169 verified: true,
170 tags: ['dashboard-support-form'],
171 siteUrl: '',
172 additionalRedirectUrls: '',
173 affectedServices: CATEGORIES_WITHOUT_AFFECTED_SERVICES.includes(values.category)
174 ? ''
175 : values.affectedServices
176 .split(',')
177 .map((x) => x.trim().replace(/ /g, '_').toLowerCase())
178 .join(';'),
179 browserInformation: detectBrowser(),
180 dashboardLogs: dashboardLogUrl?.[0],
181 dashboardStudioVersion: commit ? formatStudioVersion(commit) : undefined,
182 }
183
184 if (values.projectRef !== NO_PROJECT_MARKER) {
185 try {
186 const authConfig = await getProjectAuthConfig({
187 projectRef: values.projectRef,
188 })
189 payload.siteUrl = authConfig.SITE_URL
190 payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST
191 } catch {
192 // Nice-to-have only
193 }
194 }
195
196 submitSupportTicket(payload)
197 }
198
199 const handleFormSubmit = form.handleSubmit(onSubmit)
200
201 const handleSubmitButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => {
202 handleFormSubmit(event)
203 }
204
205 const showPlanExpectationInfo =
206 !!selectedOrgSlug &&
207 subscriptionPlanId !== 'enterprise' &&
208 subscriptionPlanId !== 'platform' &&
209 category !== 'Login_issues'
210 const showDirectEmailInfo = state.type !== 'success' && selectedProjectRef !== undefined
211
212 return (
213 <Form {...form}>
214 <form id="support-form" className="flex min-h-full flex-col">
215 <div className="flex flex-col gap-y-6">
216 <OrganizationSelector form={form} orgSlug={organizationSlug} />
217 <ProjectAndPlanInfo
218 form={form}
219 orgSlug={selectedOrgSlug}
220 projectRef={currentProjectRef}
221 subscriptionPlanId={subscriptionPlanId}
222 category={category}
223 />
224 <CategoryAndSeverityInfo
225 form={form}
226 category={category}
227 severity={severity}
228 projectRef={projectRef}
229 />
230 </div>
231
232 <div className="flex flex-col gap-y-6 py-6">
233 <SubjectAndSuggestionsInfo form={form} subject={subject} category={category} />
234 {!simplifiedSupportForm && (
235 <>
236 <ClientLibraryInfo form={form} library={library} category={category} />
237 <AffectedServicesSelector form={form} category={category} />
238 </>
239 )}
240 <MessageField form={form} originalError={initialError} />
241 <AttachmentUploadDisplay {...attachmentUpload} />
242 </div>
243
244 {(DASHBOARD_LOG_CATEGORIES.includes(category) ||
245 (!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category)) ||
246 showPlanExpectationInfo ||
247 showDirectEmailInfo) && (
248 <div className="flex flex-col gap-y-6">
249 <Separator />
250
251 {DASHBOARD_LOG_CATEGORIES.includes(category) && (
252 <DashboardLogsToggle form={form} sanitizedLog={sanitizedLogSnapshot} align="right" />
253 )}
254
255 {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
256 <SupportAccessToggle form={form} align="right" />
257 )}
258
259 {(showPlanExpectationInfo || showDirectEmailInfo) && (
260 <SupportFormV3AdditionalInfoSection
261 orgSlug={selectedOrgSlug}
262 subscriptionPlanId={subscriptionPlanId}
263 projectRef={currentProjectRef}
264 showPlanExpectationInfo={showPlanExpectationInfo}
265 showDirectEmailInfo={showDirectEmailInfo}
266 />
267 )}
268 </div>
269 )}
270
271 <div className="sticky bottom-0 z-10 -mx-5 mt-6 border-t bg-panel-footer-light px-5 py-4">
272 <SubmitButton
273 isSubmitting={state.type === 'submitting'}
274 userEmail={respondToEmail}
275 onClick={handleSubmitButtonClick}
276 descriptionClassName="pr-0"
277 />
278 </div>
279 </form>
280 </Form>
281 )
282}
283
284interface SupportFormV3AdditionalInfoSectionProps {
285 orgSlug: string | null
286 subscriptionPlanId?: OrganizationPlanID
287 projectRef: string | null
288 showPlanExpectationInfo: boolean
289 showDirectEmailInfo: boolean
290}
291
292function SupportFormV3AdditionalInfoSection({
293 orgSlug,
294 subscriptionPlanId,
295 projectRef,
296 showPlanExpectationInfo,
297 showDirectEmailInfo,
298}: SupportFormV3AdditionalInfoSectionProps) {
299 return (
300 <div className="flex flex-col gap-y-5">
301 {showPlanExpectationInfo && orgSlug && (
302 <div className="flex flex-col gap-y-2">
303 <h5 className="text-foreground">Support varies by plan</h5>
304 <PlanExpectationInfoContent orgSlug={orgSlug} planId={subscriptionPlanId} />
305 </div>
306 )}
307
308 {showDirectEmailInfo && (
309 <div className="flex flex-col gap-y-2">
310 <h5 className="text-foreground">Having trouble submitting the form?</h5>
311 <SupportFormDirectEmailContent projectRef={projectRef} />
312 </div>
313 )}
314 </div>
315 )
316}