feat: upgrade Contact Support form with file attachments and Plain integration#240
feat: upgrade Contact Support form with file attachments and Plain integration#240
Conversation
…tadata Convert the issue report from a Popover to a Dialog with React Hook Form, add drag-and-drop file attachment support via Supabase Storage, and enrich the Plain thread payload with teamId, teamName, customerEmail, customerTier, and attachment links.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c5a5492ff5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| setAttachments((prev) => [ | ||
| ...prev, | ||
| { | ||
| url: data.url, | ||
| fileName: data.fileName, |
There was a problem hiding this comment.
Ignore late upload callbacks after dialog reset
Closing the dialog while files are still uploading resets local state, but in-flight upload callbacks still append attachments unconditionally; this repopulates a canceled form with stale files and can also drive uploadingCount below zero when callbacks decrement after reset. Users can then reopen the modal and accidentally submit attachments from a previous canceled attempt, so upload results need to be ignored/canceled once the form session is reset.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This issue doesn’t actually apply here. The files aren’t uploaded asynchronously in the background — they’re just added to local state as File[] objects (line 141). The actual upload to Plain happens inside the server action when the user clicks “Send” (line 152-156), as a single synchronous submission.
| const detectedMime = fileType?.mime ?? file.type | ||
|
|
||
| if (!ALLOWED_MIME_TYPES.includes(detectedMime)) { |
There was a problem hiding this comment.
Reject unknown file signatures in attachment validation
The MIME check falls back to the client-provided file.type whenever fileTypeFromBuffer cannot identify magic bytes, which lets a crafted upload bypass server-side type enforcement by labeling unsupported binary content as text/plain. This defeats the stated magic-byte validation and allows disallowed file formats into storage unless unknown signatures are rejected (or text files are validated independently).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
sent to plain directly, not stored anywhere, do not think we need to adress this
…x counter, match height
Update icon (Bug → LifeBuoy), dialog copy, form labels, toast messages, and PostHog event names to reflect general support rather than bug reporting.
Replace the two-step Supabase upload with Plain's native attachment system. Files are held in browser state and uploaded to Plain on submit via a single contactSupportAction server action. Remove the tRPC support router, revert storage parameterization, and drop the issue-attachments bucket dependency.
Move the customer metadata header (email, tier, team ID, Orbit link) from the thread body to an internal Plain note so it doesn't get quoted in email replies. Map tier slugs to readable names (base_v1 → Hobby, pro_v1 → Pro).
…eThread fields Switch from deprecated createThread components/attachmentIds to the recommended flow: createThread (bare) → sendCustomerChat (message + attachments). Use AttachmentType.Chat instead of CustomTimelineEntry.
…or handling - Use description field on createThread instead of deprecated components - Switch from Error to ActionError so error messages surface to the user instead of the generic "Unexpected Error" message
sendCustomerChat requires the thread to use the CHAT channel. Also include Plain error message in ActionError for easier debugging.
The API key lacks chat:create permission needed for sendCustomerChat. Revert to using createThread with components and attachmentIds which works with existing thread:create permission.
… logging - Put the ######## header block back into the thread body text - Add detailed logging for each stage of attachment upload (start, URL creation, upload, success/failure with response body) - Remove separate internal note in favor of inline header
beran-t
left a comment
There was a problem hiding this comment.
Code review completed. 1 issue found.
The action previously trusted client-supplied teamId, teamName, customerEmail, accountOwnerEmail, and customerTier without verification. An authenticated user could spoof these to impersonate another team. Now uses withTeamIdResolution middleware to verify team membership (matching every other team-scoped action) and fetches team name, email, and tier from the database server-side. Only description, teamIdOrSlug, and files are accepted from the client.
beran-t
left a comment
There was a problem hiding this comment.
Review Summary
Overall this is a well-structured PR. Authentication, authorization (via authActionClient + withTeamIdResolution), and error handling are solid. Team metadata is correctly fetched server-side to prevent spoofing. One issue found below.
- Set server MAX_FILE_SIZE to 10MB to match the UI's "max 10MB each" - Add client-side file size validation with toast error for oversized files - Move Contact Support button from header back to sidebar footer, next to the Feedback button
beran-t
left a comment
There was a problem hiding this comment.
Review by automated code review agent.
- Pass plain object to submitSupport() so withTeamIdResolution middleware can find teamIdOrSlug via the `in` operator (FormData stores entries internally, not as object properties) - Swap sidebar button order: Feedback left, Contact Support right - Add basis-1/2 for even 50/50 split
Allows linking directly to the support dialog by appending ?support=true to any dashboard URL. The param is cleaned from the URL after the dialog opens.
Add 'support' entry to TAB_URL_MAP so /dashboard?tab=support redirects to the user's default team sandboxes page with ?support=true, which auto-opens the Contact Support dialog.
Instead of a TAB_URL_MAP entry that hardcodes ?support=true (which breaks when the redirect URL already has query params), forward the support param via URL.searchParams.set() after building the redirect URL. Link is now /dashboard?support=true.
# Conflicts: # src/features/dashboard/navbar/report-issue-popover.tsx # src/features/dashboard/sidebar/footer.tsx # src/server/api/routers/support.ts
Apply biome import ordering, line wrapping, and replace div[role=button] with semantic <button> element.
Summary
createAttachmentUploadUrl— no Supabase Storage bucket neededwithTeamIdResolutionmiddleware to verify team membership; fetches team name, email, and tier from the database server-side instead of trusting client-supplied valuesActionErrorfor user-facing error messages instead of generic "Unexpected Error"?support=trueto automatically open the form. In src/app/dashboard/route.ts
Test plan