Skip to content

Feat/add qr code support for sharing and joining groups - #500

Closed
theo-engels wants to merge 2 commits into
spliit-app:mainfrom
theo-engels:feat/add-qr-code-support-for-sharing-and-joining-groups
Closed

Feat/add qr code support for sharing and joining groups#500
theo-engels wants to merge 2 commits into
spliit-app:mainfrom
theo-engels:feat/add-qr-code-support-for-sharing-and-joining-groups

Conversation

@theo-engels

Copy link
Copy Markdown
Contributor
  • Add QR code functionality to improve group sharing and joining experience:

    • Display QR codes in share dialog with Spliit logo overlay
    • Support downloading QR codes as PNG images
    • Use high error correction level for reliable scanning
  • Add camera-based QR code scanner for joining groups

    • Enable mobile devices to scan QR codes directly
    • Toggle between URL input and QR scanning on mobile
    • Handle camera permissions and error states gracefully
  • Add new dependencies:

    • qrcode.react (^4.2.0) for QR code generation
    • html5-qrcode (^2.3.8) for camera-based scanning
  • Add translation keys for QR code UI elements

    • Share QR code dialog (title, description, download button)
    • Scanner mode toggle (URL mode vs QR mode)

Generating a QR code can be done on desktop and mobile clients, using the same workflow as for sharing a URL, but with an additional QR code icon. Selecting this will generate the QR Code.

Generate QR Code icon
QR Code generate 1

Show QR Code
QR Code generate 2

For a new group member that is all that would be needed to take the user to the site and access the group. For existing Spliit users, there is also an option to aquire the URL via the QR Code and add it to their list. To go this go to the top level groups section , and select the current Add by URL button. If on a mobile device it will now offer the user to add by URL as per the current option, or via QR code, at which point it would prompt the user to

Aquire Group via QR Code
QR Code aquire 1

- Add internationalization (i18n) to QrCodeScanner component using next-intl
  - Extract hardcoded UI strings: "Start Camera", "Stop Scanning", camera error messages
  - Follow existing translation conventions used in other components

- Fix ShareQrCodeDialog to support multiple instances on same page
  - Replace hardcoded id="qr-code-svg" with per-instance unique id via useId()
  - Update handleDownload to reference dynamic id instead of document.getElementById()
  - Ensures no DOM id conflicts when multiple dialogs are rendered
@theo-engels
theo-engels marked this pull request as ready for review January 31, 2026 16:04
BastiOfBerlin pushed a commit to BastiOfBerlin/spliit that referenced this pull request Jun 11, 2026
Port several improvements from upstream spliit-app/spliit:

- Exchange rates: migrate from api.frankfurter.app to api.frankfurter.dev/v1,
  fixing broken foreign-currency conversion (upstream spliit-app#515 / bugs spliit-app#513, spliit-app#514).
- Selectors: wrap category and currency lists in cmdk's CommandList so keyboard
  (arrow-key) navigation works again (upstream spliit-app#491).
- Group sharing: render a scannable QR code of the invite link in the share
  popover via qrcode.react (upstream spliit-app#500).
- Currencies: add COP, VND, MKD, and MYR to the supported list and regenerate
  the per-locale currency data (upstream spliit-app#486, spliit-app#507, spliit-app#516, spliit-app#521).
- React Compiler: enable Next.js 16's stable reactCompiler for automatic
  memoization, adding the babel-plugin-react-compiler dev dependency
  (upstream spliit-app#503).
- Docker build: parse package.json name/version with POSIX tools instead of
  `node -p`, so the image can be built on hosts without Node.js (upstream spliit-app#219).
- Docs: document the Docker Compose workflow and stack in the README
  (upstream spliit-app#505).
BastiOfBerlin pushed a commit to BastiOfBerlin/spliit that referenced this pull request Jun 11, 2026
Port several improvements from upstream spliit-app/spliit:

- Exchange rates: migrate from api.frankfurter.app to api.frankfurter.dev/v1,
  fixing broken foreign-currency conversion (upstream spliit-app#515 / bugs spliit-app#513, spliit-app#514).
- Selectors: wrap category and currency lists in cmdk's CommandList so keyboard
  (arrow-key) navigation works again (upstream spliit-app#491).
- Group sharing: render a scannable QR code of the invite link in the share
  popover via qrcode.react (upstream spliit-app#500).
- Currencies: add COP, VND, MKD, and MYR to the supported list and regenerate
  the per-locale currency data (upstream spliit-app#486, spliit-app#507, spliit-app#516, spliit-app#521).
- React Compiler: enable Next.js 16's stable reactCompiler for automatic
  memoization, adding the babel-plugin-react-compiler dev dependency
  (upstream spliit-app#503).
- Docker build: parse package.json name/version with POSIX tools instead of
  `node -p`, so the image can be built on hosts without Node.js (upstream spliit-app#219).
- Docs: document the Docker Compose workflow and stack in the README
  (upstream spliit-app#505).
Ecklebe pushed a commit to Ecklebe/spliit that referenced this pull request Jul 19, 2026
@t0ma5

t0ma5 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@theo-engels thanks for this! using it now

BastiOfBerlin pushed a commit to BastiOfBerlin/spliit that referenced this pull request Aug 13, 2026
Layered on top of spliit-app#500, from a fork that had arrived at a QR code
independently.

- Parse the pasted or scanned URL with the URL API instead of building a
  RegExp from window.location.origin, which interpolates it unescaped.
  Resolving against the current origin keeps spliit-app#500's support for relative
  /groups/<id> links from a scanned code, while an absolute link retains
  its own origin and so still fails the same-origin check — the previous
  relative-URL fallback matched any host.

- Let the scanner be retried after a permission denial. The start button
  rendered only while hasPermission was null, so a dismissed browser
  prompt left the error message as the only remaining UI until the
  popover was closed and reopened.

- Use marginSize instead of includeMargin, deprecated in qrcode.react 4,
  and give the QR an explicit title and colours so it stays legible when
  the dialog is rendered against a dark theme.

- Add titles to the copy and share buttons, which had none.
BastiOfBerlin added a commit that referenced this pull request Aug 13, 2026
# feat: QR codes for sharing and joining groups

Picks up **#500** by @theo-engels, rebased onto current `main` and with
a
few fixes layered on top. Their commit is unchanged and still under
their
name — everything I added is in a separate commit at the end.

Closes #500.

## What it does

Two things, both from the original PR:

- **Share a group as a QR code.** A `QrCode` button in the share popover
opens
a dialog with the group's URL as a QR, the Spliit logo embedded in the
middle,
  and a button to download it as a PNG.
- **Join a group by scanning one.** On narrow viewports, "Add by URL"
gains a
  second mode that opens the camera and accepts a scanned group link.

## Why it needed a rebase

The PR conflicted, but only on `package-lock.json` — its lockfile commit
(`3fabe84`) was cut from `d3b151e` back in January and no longer
applies.

I regenerated the lockfile against current `main` on npm 11 rather than
replaying that diff. It produces the same 17-line/11-line shape, and
after #556
I'd rather not replay a six-month-old lockfile diff onto `main` again.

`package.json` and the source files rebased cleanly.

## Commits

| | |
|---|---|
| `ee4f467` | @theo-engels' original commit, cherry-picked with `-x`,
byte-identical |
| `a849a7f` | regenerate `package-lock.json` for the two new
dependencies |
| `3195ad4` | apply the repo's Prettier config to the three touched
files |
| `622e0e0` | the fixes below |

**Please merge this with a merge commit or a rebase rather than a
squash** —
squashing collapses their commit and mine into one authorship.

## What I changed on top

### The relative-URL fallback accepted any host

This is the one worth a close look. #500 extends the "add by URL" parser
so a
scanned code can carry a relative link:

```js
const [, groupId] =
  urlToProcess.match(new RegExp(`${window.location.origin}/groups/([^/]+)`)) ??
  urlToProcess.match(/\/groups\/([^/?]+)/) ?? // Also match relative URLs from QR
  []
```

The second pattern is unanchored and has no origin check, so it also
matches
absolute URLs on **any** host — `https://evil.example/groups/abc` scans
as a
valid group link. The first pattern has a separate problem that predates
this
PR: it interpolates `window.location.origin` into a `RegExp` unescaped.

Both go away with the URL API:

```js
const parsed = new URL(urlToProcess, window.location.origin)
if (parsed.origin === window.location.origin) {
  groupId = parsed.pathname.match(/^\/groups\/([^/]+)/)?.[1]
}
```

Resolving against the current origin is what keeps the relative-link
case
working — a relative path inherits our origin and passes, while an
absolute URL
keeps its own and fails the check. `pathname` also drops the query
string, so
the `?ref=share` on shared links no longer needs its own character
class.

### The scanner couldn't be retried after a denied permission

The start button rendered only while `hasPermission === null`:

```jsx
{!isScanning && hasPermission === null && ( <Button …> )}
```

So dismissing the browser's camera prompt — easy to do by accident —
flipped it
to `false` and left the error message as the only remaining UI. The only
way
back was to close and reopen the popover. It now renders whenever the
scanner
isn't running, and `startScanning` resets the flag so a stale error
doesn't sit
underneath a fresh attempt.

### Smaller things

- `includeMargin` is deprecated in qrcode.react 4; switched to
`marginSize`.
- Gave the QR explicit `bgColor`/`fgColor` and a `title`. It renders
over a
themed surface, so it shouldn't inherit one, and the title gives screen
  readers something to announce.
- Added `title` attributes to the copy and share buttons in the share
popover,
which had none. New `en-US` strings only, so Weblate picks them up
normally.

## New dependencies

| | | |
|---|---|---|
| `qrcode.react` | ISC | 45 kB unminified, rendering only |
| `html5-qrcode` | Apache-2.0 | 375 kB minified, unpacked 3.4 MB |

`html5-qrcode` is not small, and it's worth deciding deliberately. It's
imported
by `qr-code-scanner.tsx`, a client component reachable from the groups
list, so
Next will split it into that route's chunk rather than the shared bundle
— but
anyone landing on the groups page pays for it once the chunk loads. If
that's
not a trade you want, a `next/dynamic` import behind the scan-mode
toggle would
defer it to the moment someone actually taps "Scan QR", and I'm happy to
add
that here.

## Verification

`npm ci --ignore-scripts`, `npx prisma generate`, `npm run check-types`,
`npm run lint`, `npm run check-formatting` — all clean against current
`main`.
Lint reports 16 warnings, all of them pre-existing.

Not yet exercised on a physical device: the camera path needs a real
phone, and
the PNG download path needs a browser check on Safari in particular
(`canvas.toDataURL` on an SVG-sourced image is the fiddly part). Worth
someone
confirming both before this goes out.

---------

Co-authored-by: theo-engels <theoengels@proton.me>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants