From a899d7d3a81cc7c4d56b6da6cc30e46090421d7d Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 2 Jul 2026 22:19:22 -0400 Subject: [PATCH] fix(privacy): generate invite QR locally instead of api.qrserver.com (H5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- LOTUS_TESTING.md | 2 + package-lock.json | 10 ++++ package.json | 1 + .../general/RoomShareInvite.tsx | 47 ++++++------------- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/LOTUS_TESTING.md b/LOTUS_TESTING.md index be069fd7c..741c76884 100644 --- a/LOTUS_TESTING.md +++ b/LOTUS_TESTING.md @@ -675,6 +675,8 @@ Run the axe DevTools extension (or Lighthouse → Accessibility) on a room view, ## 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):** - **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`.) diff --git a/package-lock.json b/package-lock.json index 06410259b..2edf42049 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "millify": "6.1.0", "pdfjs-dist": "5.7.284", "prismjs": "1.30.0", + "qrcode.react": "4.2.0", "react": "19.2.6", "react-aria": "3.48.0", "react-blurhash": "0.3.0", @@ -10758,6 +10759,15 @@ "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": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", diff --git a/package.json b/package.json index f46312fb7..e9b7a9cbf 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "millify": "6.1.0", "pdfjs-dist": "5.7.284", "prismjs": "1.30.0", + "qrcode.react": "4.2.0", "react": "19.2.6", "react-aria": "3.48.0", "react-blurhash": "0.3.0", diff --git a/src/app/features/common-settings/general/RoomShareInvite.tsx b/src/app/features/common-settings/general/RoomShareInvite.tsx index 4477af7e8..8377d1d77 100644 --- a/src/app/features/common-settings/general/RoomShareInvite.tsx +++ b/src/app/features/common-settings/general/RoomShareInvite.tsx @@ -1,5 +1,6 @@ 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 { SequenceCardStyle } from '../../room-settings/styles.css'; import { SettingTile } from '../../../components/setting-tile'; @@ -12,11 +13,9 @@ export function RoomShareInvite() { const mx = useMatrixClient(); const room = useRoom(); const [copied, setCopied] = useState(false); - const [qrError, setQrError] = useState(false); const domain = mx.getDomain() ?? 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(() => { navigator.clipboard.writeText(inviteUrl).then(() => { @@ -64,35 +63,19 @@ export function RoomShareInvite() { - {qrError ? ( - - - - QR code unavailable - - - ) : ( - QR code for room invite link setQrError(true)} - style={{ display: 'block', borderRadius: config.radii.R300 }} - /> - )} + {/* Generated locally (qrcode.react) — no third-party service, works + offline + under strict CSP. White padded quiet-zone so the + default black-on-white code scans on any theme. */} + + +