Compare commits

..

No commits in common. "fix/swish-qr-scannability" and "master" have entirely different histories.

4 changed files with 7 additions and 86 deletions

View file

@ -112,16 +112,6 @@ describe('PaymentRedirect', () => {
expect(wrapper.find('.payment__qr-img').exists()).toBe(true) expect(wrapper.find('.payment__qr-img').exists()).toBe(true)
}) })
expect(mockToDataURL).toHaveBeenCalledTimes(1) expect(mockToDataURL).toHaveBeenCalledTimes(1)
// Regression guard: the QR must use a spec-compliant 4-module quiet zone
// and pure black-on-white so the Swish app can scan it off a screen.
expect(mockToDataURL).toHaveBeenCalledWith(
expect.stringContaining('app.swish.nu'),
expect.objectContaining({
margin: 4,
errorCorrectionLevel: 'M',
color: { dark: '#000000', light: '#ffffff' },
}),
)
}) })
it('renders a Swish payment link', async () => { it('renders a Swish payment link', async () => {

View file

@ -1,52 +0,0 @@
import { describe, it, expect } from 'vitest'
import { buildSwishPaymentUrl } from '@/api/payment'
describe('buildSwishPaymentUrl', () => {
it('normalises Swedish national format to international', () => {
expect(buildSwishPaymentUrl('0701234567', 49, 'test')).toContain(
'sw=46701234567',
)
})
it('strips a leading + from international format', () => {
const url = buildSwishPaymentUrl('+46701234567', 49, 'test')
expect(url).toContain('sw=46701234567')
expect(url).not.toContain('sw=%2B')
expect(url).not.toContain('sw=+')
})
it('leaves already-international numbers unchanged', () => {
expect(buildSwishPaymentUrl('46701234567', 49, 'test')).toContain(
'sw=46701234567',
)
})
it('leaves Swish Business numbers (123…) unchanged', () => {
expect(buildSwishPaymentUrl('1234567890', 49, 'test')).toContain(
'sw=1234567890',
)
})
it('strips whitespace from the number', () => {
expect(buildSwishPaymentUrl('070 123 45 67', 49, 'test')).toContain(
'sw=46701234567',
)
})
it('includes the amount with two decimal places in amt', () => {
expect(buildSwishPaymentUrl('0701234567', 49, 'test')).toContain(
'amt=49.00',
)
})
it('URL-encodes the message in the msg parameter', () => {
const url = buildSwishPaymentUrl('0701234567', 49, 'ABC 123')
expect(url).toContain('msg=ABC+123')
})
it('uses the correct Swish C2B base URL', () => {
expect(buildSwishPaymentUrl('0701234567', 49, 'test')).toContain(
'https://app.swish.nu/1/p/sw/?',
)
})
})

View file

@ -48,12 +48,9 @@ export function buildSwishPaymentUrl(
* - 123 (Swish Business number) unchanged * - 123 (Swish Business number) unchanged
* - 46 (already international) unchanged * - 46 (already international) unchanged
* - 0 (Swedish national format) 46 + rest without leading 0 * - 0 (Swedish national format) 46 + rest without leading 0
* - +46 (international with plus) 46 (the plus is stripped first)
*/ */
function normalizeSwishNumber(number: string): string { function normalizeSwishNumber(number: string): string {
// Strip whitespace and a leading "+": a number stored as "+46 70 …" would const trimmed = number.replace(/\s/g, '')
// otherwise miss every prefix check and leak a "+" into the `sw` param.
const trimmed = number.replace(/[\s+]/g, '')
if (trimmed.startsWith('123')) return trimmed if (trimmed.startsWith('123')) return trimmed
if (trimmed.startsWith('46')) return trimmed if (trimmed.startsWith('46')) return trimmed
if (trimmed.startsWith('0')) return '46' + trimmed.slice(1) if (trimmed.startsWith('0')) return '46' + trimmed.slice(1)

View file

@ -27,30 +27,16 @@ onMounted(async () => {
const info = await fetchSwishInfo() const info = await fetchSwishInfo()
swishNumber.value = info.number swishNumber.value = info.number
swishAmount.value = info.amount swishAmount.value = info.amount
} catch {
error.value = 'Kunde inte ladda betalningsinformation. Försök igen senare.'
return
}
// QR generation is best-effort and isolated from fetchSwishInfo: if the QR
// library throws, the Swish payment link and manual fallback still render
// instead of surfacing a misleading "could not load payment info" error.
try {
if (swishPaymentUrl.value) { if (swishPaymentUrl.value) {
qrDataUrl.value = await QRCode.toDataURL(swishPaymentUrl.value, { qrDataUrl.value = await QRCode.toDataURL(swishPaymentUrl.value, {
// Swish requires a reliably scannable black-on-white QR. The previous width: 224,
// settings (margin 2, #111827, 224px) produced a 2-module quiet zone margin: 2,
// half the QR spec minimum which the Swish app's scanner fails to color: { dark: '#111827', light: '#ffffff' },
// read when scanning off a screen. Use the spec-compliant 4-module
// quiet zone, pure black, and larger modules.
width: 288,
margin: 4,
errorCorrectionLevel: 'M',
color: { dark: '#000000', light: '#ffffff' },
}) })
} }
} catch { } catch {
// ignored: payment link + manual fallback remain usable error.value = 'Kunde inte ladda betalningsinformation. Försök igen senare.'
} }
}) })
@ -253,8 +239,8 @@ async function confirmPayment() {
} }
.payment__qr-img { .payment__qr-img {
width: 288px; width: 224px;
height: 288px; height: 224px;
border-radius: var(--radius-md); border-radius: var(--radius-md);
margin: 0 auto var(--space-sm); margin: 0 auto var(--space-sm);
} }