Threema UX: static file middleware fix, *AIAGENT display, info banner
All checks were successful
Build and Push / build (push) Successful in 1m26s
All checks were successful
Build and Push / build (push) Successful in 1m26s
This commit is contained in:
97
README.md
97
README.md
@@ -1,33 +1,22 @@
|
|||||||
# Threema UX v2 — QR available on demand
|
# Threema UX v3 — three real fixes
|
||||||
|
|
||||||
Replaces the previous attempt that had the QR rendering inline above
|
## What this fixes vs v2
|
||||||
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 does instead
|
| # | Issue | Fix |
|
||||||
|
|---|-------|-----|
|
||||||
1. **"Show QR" link** next to the channel title in the threema card —
|
| 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 |
|
||||||
visible at all times, clickable any time you want the QR.
|
| 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 |
|
||||||
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`.
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
```
|
```
|
||||||
src/lib/threema-gateway-config.ts # unchanged from before — central gateway constants
|
src/middleware.ts # MODIFIED — matcher excludes dot-paths (static files)
|
||||||
src/components/channel-users/threema-qr-modal.tsx # NEW — the modal
|
src/lib/threema-gateway-config.ts # MODIFIED — displayName: "*AIAGENT"
|
||||||
src/components/channel-users/channel-users.tsx # MODIFIED — Show QR button + focus auto-open + modal mount
|
src/components/channel-users/channel-users.tsx # MODIFIED — banner replaces inline hyperlink
|
||||||
deploy/patch-i18n-threema.mjs # adds threemaSetup.showQr label in 4 langs
|
src/components/channel-users/threema-qr-modal.tsx # UNCHANGED from v2 (label now reads "*AIAGENT" automatically via config)
|
||||||
public/threema/qr_code_AIAGENT.png # unchanged
|
deploy/patch-i18n-threema.mjs # MODIFIED — new bannerTitle/bannerBody/bannerButton keys, showQr dropped
|
||||||
|
public/threema/qr_code_AIAGENT.png # UNCHANGED
|
||||||
```
|
```
|
||||||
|
|
||||||
## Apply
|
## Apply
|
||||||
@@ -35,32 +24,54 @@ public/threema/qr_code_AIAGENT.png # unchanged
|
|||||||
```bash
|
```bash
|
||||||
cd /path/to/pieced-portal
|
cd /path/to/pieced-portal
|
||||||
|
|
||||||
# Remove the old inline component if you applied the previous archive
|
unzip -o /path/to/threema-ux-v3.zip
|
||||||
rm -f src/components/channel-users/threema-setup.tsx
|
|
||||||
|
|
||||||
# Drop new + modified files
|
|
||||||
unzip -o /path/to/threema-ux-v2.zip
|
|
||||||
|
|
||||||
# Update messages
|
|
||||||
node deploy/patch-i18n-threema.mjs
|
node deploy/patch-i18n-threema.mjs
|
||||||
|
|
||||||
# TS check
|
|
||||||
npx tsc --noEmit
|
npx tsc --noEmit
|
||||||
|
|
||||||
git add -A
|
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
|
git push
|
||||||
```
|
```
|
||||||
|
|
||||||
After redeploy, the threema card under Authorized Users shows:
|
## Layout after redeploy
|
||||||
|
|
||||||
```
|
```
|
||||||
threema [Show QR] 0 users
|
┌─────────────────────────────────────────────────────────┐
|
||||||
────────────────────
|
│ threema 2 users │
|
||||||
<help text: 'Enter your own Threema ID...'>
|
├─────────────────────────────────────────────────────────┤
|
||||||
────────────────────
|
│ ╔═══════════════════════════════════════════════════════╗│
|
||||||
[ input: A8K2P3X7 ] [ Add ]
|
│ ║ [icon] Set up Threema [ Show QR code ] ║│ ← prominent banner, accent border
|
||||||
^^^ focusing this opens the QR modal the first time
|
│ ║ 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/...`.
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ const i18n = {
|
|||||||
step2: "Tap the scan icon and scan this QR code to add the assistant as a contact.",
|
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.",
|
step3: "Then add your own Threema ID below.",
|
||||||
qrAlt: "QR code to add {gateway} as a Threema contact",
|
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: {
|
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.",
|
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.",
|
step3: "Fügen Sie anschliessend unten Ihre eigene Threema-ID hinzu.",
|
||||||
qrAlt: "QR-Code, um {gateway} als Threema-Kontakt hinzuzufügen",
|
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: {
|
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.",
|
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.",
|
step3: "Puis ajoutez votre propre identifiant Threema ci-dessous.",
|
||||||
qrAlt: "QR code pour ajouter {gateway} comme contact Threema",
|
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: {
|
it: {
|
||||||
@@ -97,7 +103,9 @@ const i18n = {
|
|||||||
step2: "Tocca l'icona di scansione e scansiona questo QR code per aggiungere l'assistente ai contatti.",
|
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.",
|
step3: "Quindi aggiungi il tuo ID Threema qui sotto.",
|
||||||
qrAlt: "QR code per aggiungere {gateway} come contatto Threema",
|
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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -225,24 +225,47 @@ export function ChannelUsers({
|
|||||||
className="bg-surface-2 border border-border rounded-lg p-4"
|
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 justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<h4 className="text-sm font-medium text-text-primary capitalize">
|
||||||
<h4 className="text-sm font-medium text-text-primary capitalize">
|
{channel}
|
||||||
{channel}
|
</h4>
|
||||||
</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>
|
|
||||||
<span className="text-xs text-text-muted tabular-nums">
|
<span className="text-xs text-text-muted tabular-nums">
|
||||||
{users.length} {t("users")}
|
{users.length} {t("users")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 && (
|
{helpKey && (
|
||||||
<p className="text-xs text-text-secondary bg-surface-1 border border-border rounded-lg p-3 mb-3 whitespace-pre-line">
|
<p className="text-xs text-text-secondary bg-surface-1 border border-border rounded-lg p-3 mb-3 whitespace-pre-line">
|
||||||
{t(helpKey)}
|
{t(helpKey)}
|
||||||
|
|||||||
@@ -13,21 +13,19 @@
|
|||||||
* env var that lists the active account.
|
* env var that lists the active account.
|
||||||
* 2. Move the QR PNG into a server-rendered route that takes a
|
* 2. Move the QR PNG into a server-rendered route that takes a
|
||||||
* gateway ID query param.
|
* gateway ID query param.
|
||||||
* 3. Update consumers (today only ThreemaSetup) to accept the
|
* 3. Update consumers to accept the gateway info as a prop and pass
|
||||||
* gateway info as a prop and pass it from a server component.
|
* 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).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const THREEMA_GATEWAY = {
|
export const THREEMA_GATEWAY = {
|
||||||
/** Technical Threema Gateway ID, with leading asterisk. */
|
/** Technical Threema Gateway ID, with leading asterisk. */
|
||||||
id: "*AIAGENT",
|
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/`. */
|
/** Public path to the QR code PNG served from `public/`. */
|
||||||
qrCodePath: "/threema/qr_code_AIAGENT.png",
|
qrCodePath: "/threema/qr_code_AIAGENT.png",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -403,7 +403,9 @@
|
|||||||
"step2": "Tippen Sie auf das Scan-Symbol und scannen Sie diesen QR-Code, um den Assistenten als Kontakt hinzuzufügen.",
|
"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.",
|
"step3": "Fügen Sie anschliessend unten Ihre eigene Threema-ID hinzu.",
|
||||||
"qrAlt": "QR-Code, um {gateway} als Threema-Kontakt hinzuzufügen",
|
"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": {
|
"team": {
|
||||||
|
|||||||
@@ -403,7 +403,9 @@
|
|||||||
"step2": "Tap the scan icon and scan this QR code to add the assistant as a contact.",
|
"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.",
|
"step3": "Then add your own Threema ID below.",
|
||||||
"qrAlt": "QR code to add {gateway} as a Threema contact",
|
"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": {
|
"team": {
|
||||||
|
|||||||
@@ -403,7 +403,9 @@
|
|||||||
"step2": "Appuyez sur l'icône de scan et scannez ce QR code pour ajouter l'assistant comme contact.",
|
"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.",
|
"step3": "Puis ajoutez votre propre identifiant Threema ci-dessous.",
|
||||||
"qrAlt": "QR code pour ajouter {gateway} comme contact Threema",
|
"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": {
|
"team": {
|
||||||
|
|||||||
@@ -403,7 +403,9 @@
|
|||||||
"step2": "Tocca l'icona di scansione e scansiona questo QR code per aggiungere l'assistente ai contatti.",
|
"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.",
|
"step3": "Quindi aggiungi il tuo ID Threema qui sotto.",
|
||||||
"qrAlt": "QR code per aggiungere {gateway} come contatto Threema",
|
"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": {
|
"team": {
|
||||||
|
|||||||
@@ -40,5 +40,10 @@ export default async function middleware(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
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|.*\\..*).*)"],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user