Skip to content

Commit ba35a02

Browse files
feat(frontend): add optimistic UI loading state to ReceiptQrCode
1 parent cf32b6f commit ba35a02

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

Dechat/dex_with_fiat_frontend/src/components/ReceiptQrCode.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React, { useEffect, useState } from 'react';
44
import QRCode from 'qrcode';
5+
import { useMediaQuery } from '@/hooks/useMediaQuery';
56

67
interface ReceiptQrCodeProps {
78
value: string;
@@ -10,9 +11,12 @@ interface ReceiptQrCodeProps {
1011

1112
export default function ReceiptQrCode({ value, label }: ReceiptQrCodeProps) {
1213
const [dataUrl, setDataUrl] = useState('');
14+
const [isGenerating, setIsGenerating] = useState(true);
15+
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
1316

1417
useEffect(() => {
1518
let cancelled = false;
19+
setIsGenerating(true);
1620

1721
QRCode.toDataURL(value, {
1822
width: 128,
@@ -21,28 +25,42 @@ export default function ReceiptQrCode({ value, label }: ReceiptQrCodeProps) {
2125
errorCorrectionLevel: 'M',
2226
})
2327
.then((url) => {
24-
if (!cancelled) setDataUrl(url);
28+
if (!cancelled) {
29+
setDataUrl(url);
30+
setIsGenerating(false);
31+
}
2532
})
2633
.catch(() => {
27-
if (!cancelled) setDataUrl('');
34+
if (!cancelled) {
35+
setDataUrl('');
36+
setIsGenerating(false);
37+
}
2838
});
2939

3040
return () => {
3141
cancelled = true;
3242
};
3343
}, [value]);
3444

35-
if (!dataUrl) return null;
36-
3745
return (
3846
<div className="receipt-qr-wrapper flex flex-col items-center gap-1 pt-2 border-t dark:border-gray-700">
39-
<img
40-
src={dataUrl}
41-
alt={label ?? 'Transaction verification QR code'}
42-
className="receipt-qr-code w-32 h-32"
43-
width={128}
44-
height={128}
45-
/>
47+
{isGenerating ? (
48+
<div
49+
role="status"
50+
aria-live="polite"
51+
className="receipt-qr-loading flex h-32 w-32 items-center justify-center rounded border border-dashed border-gray-300 bg-gray-50 text-[10px] uppercase tracking-wide text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400"
52+
>
53+
{prefersReducedMotion ? 'Generating QR code…' : 'Generating QR code…'}
54+
</div>
55+
) : dataUrl ? (
56+
<img
57+
src={dataUrl}
58+
alt={label ?? 'Transaction verification QR code'}
59+
className="receipt-qr-code h-32 w-32"
60+
width={128}
61+
height={128}
62+
/>
63+
) : null}
4664
<span className="receipt-qr-label text-[9px] text-gray-500 uppercase tracking-wide">
4765
Scan to verify
4866
</span>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import { render, screen, waitFor } from '@testing-library/react';
4+
import ReceiptQrCode from '../ReceiptQrCode';
5+
6+
let mockToDataURL: ReturnType<typeof vi.fn>;
7+
8+
vi.mock('qrcode', () => ({
9+
default: {
10+
toDataURL: vi.fn(),
11+
},
12+
}));
13+
14+
vi.mock('@/hooks/useMediaQuery', () => ({
15+
useMediaQuery: () => false,
16+
}));
17+
18+
describe('ReceiptQrCode', () => {
19+
beforeEach(async () => {
20+
vi.clearAllMocks();
21+
const qrcode = await import('qrcode');
22+
mockToDataURL = qrcode.default.toDataURL;
23+
mockToDataURL.mockResolvedValue('data:image/png;base64,qr');
24+
});
25+
26+
afterEach(() => {
27+
vi.restoreAllMocks();
28+
});
29+
30+
it('renders an accessible QR placeholder while the code is generating', async () => {
31+
mockToDataURL.mockImplementation(() => new Promise(() => {}));
32+
33+
render(<ReceiptQrCode value="abc" label="Verify transaction" />);
34+
35+
expect(screen.getByRole('status')).toHaveTextContent('Generating QR code…');
36+
});
37+
38+
it('renders the generated QR code image once the data URL is ready', async () => {
39+
render(<ReceiptQrCode value="abc" label="Verify transaction" />);
40+
41+
await waitFor(() => {
42+
expect(screen.getByRole('img', { name: 'Verify transaction' })).toBeInTheDocument();
43+
});
44+
});
45+
});

0 commit comments

Comments
 (0)