Threema UX: static file middleware fix, *AIAGENT display, info banner
All checks were successful
Build and Push / build (push) Successful in 1m26s

This commit is contained in:
2026-05-17 17:29:25 +02:00
parent c0ff22394c
commit 55571b1e59
9 changed files with 128 additions and 75 deletions

View File

@@ -1,33 +1,22 @@
# Threema UX v2QR available on demand
# Threema UX v3three real fixes
Replaces the previous attempt that had the QR rendering inline above
the channel help text. That placement didn't work for you in practice
because it wasn't visible when you actually wanted it.
## What this fixes vs v2
## What this does instead
1. **"Show QR" link** next to the channel title in the threema card
visible at all times, clickable any time you want the QR.
2. **Auto-opens the modal** the first time you focus the add-ID input
on the page (so a new user adding their first ID sees it without
needing to click "Show QR" themselves). Doesn't re-pop after
dismissal — the link covers re-opens.
3. **Modal** with QR + 3-step instructions + "AIAGENT" label. Plain
`<img>` (no next/image), closes on ESC / overlay click / × button.
The earlier `threema-setup.tsx` component file is removed — replaced by
`threema-qr-modal.tsx`.
| # | Issue | Fix |
|---|-------|-----|
| 1 | QR image 404'd as `GET /en/threema/qr_code_AIAGENT.png` | Middleware matcher now excludes paths with file extensions, so static files in `public/` are not locale-prefixed |
| 2 | Displayed gateway name as `AIAGENT` (without asterisk) | `displayName` is now `*AIAGENT` (with asterisk) — what users actually see in their Threema contacts |
| 3 | "Show QR" hyperlink — too small, unclear what it does | Replaced with a proper accent-bordered info banner: icon + title + body explaining what to do + prominent "Show QR code" button |
## Files
```
src/lib/threema-gateway-config.ts # unchanged from before — central gateway constants
src/components/channel-users/threema-qr-modal.tsx # NEW — the modal
src/components/channel-users/channel-users.tsx # MODIFIED — Show QR button + focus auto-open + modal mount
deploy/patch-i18n-threema.mjs # adds threemaSetup.showQr label in 4 langs
public/threema/qr_code_AIAGENT.png # unchanged
src/middleware.ts # MODIFIED — matcher excludes dot-paths (static files)
src/lib/threema-gateway-config.ts # MODIFIED — displayName: "*AIAGENT"
src/components/channel-users/channel-users.tsx # MODIFIED — banner replaces inline hyperlink
src/components/channel-users/threema-qr-modal.tsx # UNCHANGED from v2 (label now reads "*AIAGENT" automatically via config)
deploy/patch-i18n-threema.mjs # MODIFIED — new bannerTitle/bannerBody/bannerButton keys, showQr dropped
public/threema/qr_code_AIAGENT.png # UNCHANGED
```
## Apply
@@ -35,32 +24,54 @@ public/threema/qr_code_AIAGENT.png # unchanged
```bash
cd /path/to/pieced-portal
# Remove the old inline component if you applied the previous archive
rm -f src/components/channel-users/threema-setup.tsx
# Drop new + modified files
unzip -o /path/to/threema-ux-v2.zip
# Update messages
unzip -o /path/to/threema-ux-v3.zip
node deploy/patch-i18n-threema.mjs
# TS check
npx tsc --noEmit
git add -A
git commit -m "Threema QR: on-demand modal + auto-open on first add"
git status # eyeball
git commit -m "Threema UX: middleware static fix, *AIAGENT display, info banner"
git push
```
After redeploy, the threema card under Authorized Users shows:
## Layout after redeploy
```
threema [Show QR] 0 users
────────────────────
<help text: 'Enter your own Threema ID...'>
────────────────────
[ input: A8K2P3X7 ] [ Add ]
^^^ focusing this opens the QR modal the first time
┌─────────────────────────────────────────────────────────┐
│ threema 2 users │
├─────────────────────────────────────────────────────────┤
│ ╔═══════════════════════════════════════════════════════╗│
│ ║ [icon] Set up Threema [ Show QR code ] ║│ ← prominent banner, accent border
│ ║ Open Threema on your phone and scan our ║│
│ ║ QR code to add the assistant as a contact. ║│
│ ║ Then add your own Threema ID below. ║│
│ ╚═══════════════════════════════════════════════════════╝│
├─────────────────────────────────────────────────────────┤
│ <help: "Enter your own Threema ID..."> │
│ ┌───────┐ ┌───────┐ │
│ │USER01 ✕│ │USER02 ✕│ │
│ └───────┘ └───────┘ │
│ [ A8K2P3X7 ] [ Add ] │
└─────────────────────────────────────────────────────────┘
```
Clicking "Show QR" or focusing the input → modal with QR + steps.
The banner is always visible whenever the threema channel is enabled.
Clicking "Show QR code" opens the modal with the QR and 3-step
instructions. ESC, overlay click, or × button closes.
Auto-open on first focus of the add-ID input is preserved from v2 —
the modal pops once when a customer clicks into the input to add their
first ID, so a brand-new customer who skipped the banner still gets
the QR right when they need it.
## Verification
After redeploy:
1. Open `https://app.pieced.ch/en/tenants/acme-gmbh-2acf4612` in the browser.
2. Scroll to the **Authorized Users → threema** card.
3. Visible banner with icon, title "Set up Threema", body text, and a
clearly clickable "Show QR code" button on the right.
4. Click the button → modal with the QR shows.
5. The label under the QR reads `*AIAGENT` (with asterisk).
6. Browser DevTools → Network → `GET /threema/qr_code_AIAGENT.png` is
`200`, not `404`, and not redirected to `/en/threema/...`.

View File

@@ -37,7 +37,9 @@ const i18n = {
step2: "Tap the scan icon and scan this QR code to add the assistant as a contact.",
step3: "Then add your own Threema ID below.",
qrAlt: "QR code to add {gateway} as a Threema contact",
showQr: "Show QR",
bannerTitle: "Set up Threema",
bannerBody: "Open Threema on your phone and scan our QR code to add the assistant as a contact. Then add your own Threema ID below.",
bannerButton: "Show QR code",
},
},
de: {
@@ -57,7 +59,9 @@ const i18n = {
step2: "Tippen Sie auf das Scan-Symbol und scannen Sie diesen QR-Code, um den Assistenten als Kontakt hinzuzufügen.",
step3: "Fügen Sie anschliessend unten Ihre eigene Threema-ID hinzu.",
qrAlt: "QR-Code, um {gateway} als Threema-Kontakt hinzuzufügen",
showQr: "QR anzeigen",
bannerTitle: "Threema einrichten",
bannerBody: "Öffnen Sie Threema auf Ihrem Telefon und scannen Sie unseren QR-Code, um den Assistenten als Kontakt hinzuzufügen. Geben Sie anschliessend unten Ihre eigene Threema-ID ein.",
bannerButton: "QR-Code anzeigen",
},
},
fr: {
@@ -77,7 +81,9 @@ const i18n = {
step2: "Appuyez sur l'icône de scan et scannez ce QR code pour ajouter l'assistant comme contact.",
step3: "Puis ajoutez votre propre identifiant Threema ci-dessous.",
qrAlt: "QR code pour ajouter {gateway} comme contact Threema",
showQr: "Afficher le QR",
bannerTitle: "Configurer Threema",
bannerBody: "Ouvrez Threema sur votre téléphone et scannez notre QR code pour ajouter l'assistant comme contact. Saisissez ensuite votre propre identifiant Threema ci-dessous.",
bannerButton: "Afficher le QR code",
},
},
it: {
@@ -97,7 +103,9 @@ const i18n = {
step2: "Tocca l'icona di scansione e scansiona questo QR code per aggiungere l'assistente ai contatti.",
step3: "Quindi aggiungi il tuo ID Threema qui sotto.",
qrAlt: "QR code per aggiungere {gateway} come contatto Threema",
showQr: "Mostra QR",
bannerTitle: "Configura Threema",
bannerBody: "Apri Threema sul tuo telefono e scansiona il nostro QR code per aggiungere l'assistente ai contatti. Inserisci poi il tuo ID Threema qui sotto.",
bannerButton: "Mostra QR code",
},
},
};

View File

@@ -225,24 +225,47 @@ export function ChannelUsers({
className="bg-surface-2 border border-border rounded-lg p-4"
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-text-primary capitalize">
{channel}
</h4>
{channel === "threema" && (
<button
onClick={() => setShowQrFor("threema")}
className="text-xs font-medium text-accent hover:text-accent-dim cursor-pointer underline underline-offset-2"
>
{t("threemaSetup.showQr")}
</button>
)}
</div>
<h4 className="text-sm font-medium text-text-primary capitalize">
{channel}
</h4>
<span className="text-xs text-text-muted tabular-nums">
{users.length} {t("users")}
</span>
</div>
{channel === "threema" && (
<div className="mb-3 flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between bg-accent/5 border border-accent/30 rounded-lg p-3">
<div className="flex items-start gap-2 flex-1">
<svg
className="w-4 h-4 mt-0.5 text-accent flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 4a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM15 4a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V4zM3 16a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H4a1 1 0 01-1-1v-4zM13 13h3v3h-3zM18 13h3v3h-3zM13 18h3v3h-3zM18 18h3v3h-3z"
/>
</svg>
<div className="text-xs text-text-secondary leading-relaxed">
<p className="font-medium text-text-primary mb-0.5">
{t("threemaSetup.bannerTitle")}
</p>
<p>{t("threemaSetup.bannerBody")}</p>
</div>
</div>
<button
onClick={() => setShowQrFor("threema")}
className="self-stretch sm:self-auto px-3 py-2 text-xs font-medium bg-accent text-surface-0 rounded-lg hover:bg-accent-dim transition-colors whitespace-nowrap cursor-pointer shadow-lg shadow-accent/20"
>
{t("threemaSetup.bannerButton")}
</button>
</div>
)}
{helpKey && (
<p className="text-xs text-text-secondary bg-surface-1 border border-border rounded-lg p-3 mb-3 whitespace-pre-line">
{t(helpKey)}

View File

@@ -13,21 +13,19 @@
* env var that lists the active account.
* 2. Move the QR PNG into a server-rendered route that takes a
* gateway ID query param.
* 3. Update consumers (today only ThreemaSetup) to accept the
* gateway info as a prop and pass it from a server component.
*
* In display contexts we strip the leading asterisk from the Threema
* ID — customers don't understand the `*X` prefix convention used for
* Gateway accounts, and the QR code carries the real value anyway. We
* keep the asterisk only for places where the technical value matters
* (server-side message routing, debug logs).
* 3. Update consumers to accept the gateway info as a prop and pass
* it from a server component.
*/
export const THREEMA_GATEWAY = {
/** Technical Threema Gateway ID, with leading asterisk. */
id: "*AIAGENT",
/** Display name shown to customers (no asterisk). */
displayName: "AIAGENT",
/**
* Display name shown to customers. INCLUDES the leading asterisk —
* customers need to recognise this exact string in their Threema
* contacts after scanning the QR, so we don't strip it.
*/
displayName: "*AIAGENT",
/** Public path to the QR code PNG served from `public/`. */
qrCodePath: "/threema/qr_code_AIAGENT.png",
} as const;

View File

@@ -403,7 +403,9 @@
"step2": "Tippen Sie auf das Scan-Symbol und scannen Sie diesen QR-Code, um den Assistenten als Kontakt hinzuzufügen.",
"step3": "Fügen Sie anschliessend unten Ihre eigene Threema-ID hinzu.",
"qrAlt": "QR-Code, um {gateway} als Threema-Kontakt hinzuzufügen",
"showQr": "QR anzeigen"
"bannerTitle": "Threema einrichten",
"bannerBody": "Öffnen Sie Threema auf Ihrem Telefon und scannen Sie unseren QR-Code, um den Assistenten als Kontakt hinzuzufügen. Geben Sie anschliessend unten Ihre eigene Threema-ID ein.",
"bannerButton": "QR-Code anzeigen"
}
},
"team": {

View File

@@ -403,7 +403,9 @@
"step2": "Tap the scan icon and scan this QR code to add the assistant as a contact.",
"step3": "Then add your own Threema ID below.",
"qrAlt": "QR code to add {gateway} as a Threema contact",
"showQr": "Show QR"
"bannerTitle": "Set up Threema",
"bannerBody": "Open Threema on your phone and scan our QR code to add the assistant as a contact. Then add your own Threema ID below.",
"bannerButton": "Show QR code"
}
},
"team": {

View File

@@ -403,7 +403,9 @@
"step2": "Appuyez sur l'icône de scan et scannez ce QR code pour ajouter l'assistant comme contact.",
"step3": "Puis ajoutez votre propre identifiant Threema ci-dessous.",
"qrAlt": "QR code pour ajouter {gateway} comme contact Threema",
"showQr": "Afficher le QR"
"bannerTitle": "Configurer Threema",
"bannerBody": "Ouvrez Threema sur votre téléphone et scannez notre QR code pour ajouter l'assistant comme contact. Saisissez ensuite votre propre identifiant Threema ci-dessous.",
"bannerButton": "Afficher le QR code"
}
},
"team": {

View File

@@ -403,7 +403,9 @@
"step2": "Tocca l'icona di scansione e scansiona questo QR code per aggiungere l'assistente ai contatti.",
"step3": "Quindi aggiungi il tuo ID Threema qui sotto.",
"qrAlt": "QR code per aggiungere {gateway} come contatto Threema",
"showQr": "Mostra QR"
"bannerTitle": "Configura Threema",
"bannerBody": "Apri Threema sul tuo telefono e scansiona il nostro QR code per aggiungere l'assistente ai contatti. Inserisci poi il tuo ID Threema qui sotto.",
"bannerButton": "Mostra QR code"
}
},
"team": {

View File

@@ -40,5 +40,10 @@ export default async function middleware(request: NextRequest) {
}
export const config = {
matcher: ["/((?!_next|favicon.ico|api).*)"],
// Excludes _next/* internal routes, the favicon, api routes, AND any
// path containing a dot (covers all static files served from public/,
// e.g. /threema/qr_code_AIAGENT.png). Without the dot exclusion, the
// i18n middleware prepends the locale ("/en/threema/qr_code_AIAGENT.png")
// and the file is not found.
matcher: ["/((?!_next|favicon.ico|api|.*\\..*).*)"],
};