fix(privacy): generate invite QR locally instead of api.qrserver.com (H5)
The Share Room QR was fetched from the third-party api.qrserver.com, leaking which rooms a user shares (and failing offline / under strict CSP). Now rendered locally via qrcode.react (QRCodeSVG) — no network request, works offline. Added a white quiet-zone container so the code scans on any theme; dropped the qrError fallback (local generation can't fail the same way). Removed api.qrserver.com from the prod CSP img-src (matrix repo). Build verified (rolldown interop OK). Verification steps added to LOTUS_TESTING. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -675,6 +675,8 @@ Run the axe DevTools extension (or Lighthouse → Accessibility) on a room view,
|
|||||||
|
|
||||||
## Outstanding verification backlog
|
## Outstanding verification backlog
|
||||||
|
|
||||||
|
**Invite QR is now generated LOCALLY (2026-07):** Room settings → Share Room → the QR code renders (a black-on-white SVG in a white box) with **no network request** to `api.qrserver.com` (check DevTools Network — there should be no external QR fetch, and it should work offline / behind strict CSP). **Scan it** with a phone camera / Matrix app → it opens the correct `matrix.to` room-invite link. (`api.qrserver.com` was removed from the prod CSP img-src, so a regression would make the QR blank rather than silently phone home.)
|
||||||
|
|
||||||
**Unread dot on federated rooms + avatar-decoration console storm (2026-07):**
|
**Unread dot on federated rooms + avatar-decoration console storm (2026-07):**
|
||||||
|
|
||||||
- **Read receipts (regression guard — highest priority):** open several rooms and open the Home/Direct tabs (which mark all orphan rooms read on mount) → rooms **stay read**, unread dots clear and don't come back. (A prior attempt sent a receipt for the thread _root_ when a thread's replies weren't loaded, which the SDK treats as a main receipt at an old event and re-unread every room on every mark-read. Fixed + locked by `notifications.test.ts`.)
|
- **Read receipts (regression guard — highest priority):** open several rooms and open the Home/Direct tabs (which mark all orphan rooms read on mount) → rooms **stay read**, unread dots clear and don't come back. (A prior attempt sent a receipt for the thread _root_ when a thread's replies weren't loaded, which the SDK treats as a main receipt at an old event and re-unread every room on every mark-read. Fixed + locked by `notifications.test.ts`.)
|
||||||
|
|||||||
Generated
+10
@@ -57,6 +57,7 @@
|
|||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "5.7.284",
|
"pdfjs-dist": "5.7.284",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-aria": "3.48.0",
|
"react-aria": "3.48.0",
|
||||||
"react-blurhash": "0.3.0",
|
"react-blurhash": "0.3.0",
|
||||||
@@ -10758,6 +10759,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/raf-schd": {
|
"node_modules/raf-schd": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "5.7.284",
|
"pdfjs-dist": "5.7.284",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
"react-aria": "3.48.0",
|
"react-aria": "3.48.0",
|
||||||
"react-blurhash": "0.3.0",
|
"react-blurhash": "0.3.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Box, Button, color, config, Icon, Icons, Text } from 'folds';
|
import { Box, Button, config, Icon, Icons, Text } from 'folds';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
import { SequenceCard } from '../../../components/sequence-card';
|
import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
import { SequenceCardStyle } from '../../room-settings/styles.css';
|
||||||
import { SettingTile } from '../../../components/setting-tile';
|
import { SettingTile } from '../../../components/setting-tile';
|
||||||
@@ -12,11 +13,9 @@ export function RoomShareInvite() {
|
|||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const room = useRoom();
|
const room = useRoom();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [qrError, setQrError] = useState(false);
|
|
||||||
|
|
||||||
const domain = mx.getDomain() ?? undefined;
|
const domain = mx.getDomain() ?? undefined;
|
||||||
const inviteUrl = getMatrixToRoom(room.roomId, domain ? [domain] : undefined);
|
const inviteUrl = getMatrixToRoom(room.roomId, domain ? [domain] : undefined);
|
||||||
const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(inviteUrl)}`;
|
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(inviteUrl).then(() => {
|
navigator.clipboard.writeText(inviteUrl).then(() => {
|
||||||
@@ -64,35 +63,19 @@ export function RoomShareInvite() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box justifyContent="Center">
|
<Box justifyContent="Center">
|
||||||
{qrError ? (
|
{/* Generated locally (qrcode.react) — no third-party service, works
|
||||||
<Box
|
offline + under strict CSP. White padded quiet-zone so the
|
||||||
direction="Column"
|
default black-on-white code scans on any theme. */}
|
||||||
alignItems="Center"
|
<Box
|
||||||
justifyContent="Center"
|
style={{
|
||||||
gap="100"
|
padding: config.space.S200,
|
||||||
style={{
|
background: '#ffffff',
|
||||||
width: 160,
|
borderRadius: config.radii.R300,
|
||||||
height: 160,
|
lineHeight: 0,
|
||||||
borderRadius: config.radii.R300,
|
}}
|
||||||
background: color.SurfaceVariant.Container,
|
>
|
||||||
}}
|
<QRCodeSVG value={inviteUrl} size={160} level="M" title="Room invite QR code" />
|
||||||
>
|
</Box>
|
||||||
<Icon size="400" src={Icons.Warning} />
|
|
||||||
<Text size="T200" priority="300" align="Center">
|
|
||||||
QR code unavailable
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={qrSrc}
|
|
||||||
alt="QR code for room invite link"
|
|
||||||
width={160}
|
|
||||||
height={160}
|
|
||||||
loading="lazy"
|
|
||||||
onError={() => setQrError(true)}
|
|
||||||
style={{ display: 'block', borderRadius: config.radii.R300 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</CutoutCard>
|
</CutoutCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user