feat(support): attach images and PDFs to the in-app contact form

Add optional image and PDF attachments to the existing in-app support contact form, with client-side limits, server-side validation, and email delivery. Preserve the existing subject, rate-limit, analytics, and storage behavior.
This commit is contained in:
Mattsson
2026-08-26 12:32:29 +02:00
committed by GitHub
parent 151fb1384c
commit 00e7ac92ae
10 changed files with 797 additions and 21 deletions
@@ -271,5 +271,63 @@ describe('submitFeedback', () => {
await submitFeedback({ subject: 'Moms', message: 'hemlig fritext om bolaget' })
expect(JSON.stringify(captureMock.mock.calls)).not.toContain('hemlig fritext')
})
it('does not add attachment metadata to analytics', async () => {
stubFetchOk()
await submitFeedback({
message: 'Se bilagan',
files: [new File(['x'], 'kontoutdrag-privat.png', { type: 'image/png' })],
})
const props = captureMock.mock.calls[0][1] as Record<string, unknown>
expect(props).not.toHaveProperty('attachment_count')
expect(JSON.stringify(captureMock.mock.calls)).not.toContain('kontoutdrag-privat')
})
})
describe('attachments', () => {
it('sends multipart with the files when there are any', async () => {
const fetchSpy = stubFetchOk()
const file = new File(['x'], 'skarmbild.png', { type: 'image/png' })
await submitFeedback({ subject: 'Trasig vy', message: 'Ser ut så här', files: [file] })
const init = fetchSpy.mock.calls[0][1]
expect(init.body).toBeInstanceOf(FormData)
// The browser has to set the multipart boundary itself.
expect(init.headers).toBeUndefined()
const form = init.body as FormData
expect(form.get('subject')).toBe('Trasig vy')
expect(form.get('message')).toBe('Ser ut så här')
expect(form.getAll('files')).toHaveLength(1)
expect((form.get('files') as File).name).toBe('skarmbild.png')
})
it('keeps the JSON body when the file list is empty', async () => {
const fetchSpy = stubFetchOk()
await submitFeedback({ subject: 'Moms', message: 'Jag fastnar', files: [] })
expect(fetchSpy.mock.calls[0][1].body).toBe(
JSON.stringify({ subject: 'Moms', message: 'Jag fastnar' })
)
})
it('surfaces the route error when an attachment is rejected', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ error: 'Du kan bifoga max 5 filer' }),
})
)
const result = await submitFeedback({
message: 'För många bilder',
files: [new File(['x'], 'a.png', { type: 'image/png' })],
})
expect(result.ok).toBe(false)
expect(result.error).toBe('Du kan bifoga max 5 filer')
})
})
})