diff --git a/README.md b/README.md
index afc7ca2..ed5fdad 100644
--- a/README.md
+++ b/README.md
@@ -1,116 +1,66 @@
-# Threema UX rework + QR code
+# Threema UX v2 — QR available on demand
+
+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 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
+ `` (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
```
-src/lib/threema-gateway-config.ts # NEW — single source of truth for gateway ID + QR path
-src/components/channel-users/threema-setup.tsx # NEW — QR + 3-step instructions component
-src/components/channel-users/channel-users.tsx # MODIFIED — renders for the threema channel
-deploy/patch-i18n-threema.mjs # REPLACES earlier version — customer-friendly texts in 4 langs
-public/threema/qr_code_AIAGENT.png # NEW — the QR you uploaded
+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
```
-## What changed (UX, customer-facing)
-
-1. **Package description / instructions / disclaimer.** No mention of
- "Gateway account", "Gateway credentials", or anything about asterisks.
- The disclaimer now explicitly says messages are end-to-end encrypted
- only up to PieCed's messaging service (where they get decrypted to
- route to the assistant) — accurate, not overclaiming. Crucially it
- also says **Threema charges per message** so customers know that
- sending/receiving on this channel has a cost separate from their
- PieCed subscription.
-
-2. **Threema ID help text.** Now tells the customer to add **their
- own** ID, with explicit instructions for finding it in the Threema
- app (Settings → My Threema ID). Drops the asterisk / Gateway-prefix
- explanation entirely.
-
-3. **QR code component (NEW).** Shown above the help text in the
- channel-users panel when the threema channel is enabled. Three-step
- flow: open Threema, scan, add your own ID below.
-
-4. **All four languages** (en/de/fr/it) updated consistently.
-
-## What changed (technical)
-
-- Gateway constants centralised in `src/lib/threema-gateway-config.ts`.
- Today hardcoded to `*AIAGENT` + `/threema/qr_code_AIAGENT.png`. When
- you need multiple gateway accounts, edit that one file (and the
- inline TODO block tells the next person how).
-- The component uses Next.js `` — automatic optimisation,
- lazy-loaded by default (no `priority`).
-- The PNG goes in `public/threema/`, served as a static asset under
- `/threema/qr_code_AIAGENT.png` — no API route, no auth wrapper, the
- QR encodes a Threema invitation anyone can scan anyway.
-
## Apply
```bash
cd /path/to/pieced-portal
-# Drop in new files (overwrites prior channel-users.tsx and i18n script)
-cp -r /* .
+# Remove the old inline component if you applied the previous archive
+rm -f src/components/channel-users/threema-setup.tsx
-# Quick TS check (the new component uses next/image — should be fine)
+# Drop new + modified files
+unzip -o /path/to/threema-ux-v2.zip
+
+# Update messages
+node deploy/patch-i18n-threema.mjs
+
+# TS check
npx tsc --noEmit
-# Patch the message files
-node deploy/patch-i18n-threema.mjs
-# Should print 4 lines, one per language
-
-# Commit + push
git add -A
-git status # eyeball the changes
-git commit -m "Threema: customer-friendly texts + QR setup component"
+git commit -m "Threema QR: on-demand modal + auto-open on first add"
git push
```
-## Verify after redeploy
+After redeploy, the threema card under Authorized Users shows:
-1. Open the portal as a customer admin, pick the test tenant.
-2. Disable + re-enable Threema in the package list (or just look at
- the channel-users panel — the QR shows there regardless).
-3. Authorized Users → threema section should show:
- - QR code on the left
- - "AIAGENT" label under the QR (no asterisk)
- - 3-step instruction list
- - Help text below: "Enter your own Threema ID..."
- - Existing user pills + Add input
-
-Scan the QR with your phone — it should prompt to add `*AIAGENT` as a
-contact in Threema with trust level "verified by the operator" (the
-QR encodes the contact's public key).
-
-## Billing log query (re-run after sending a few messages)
-
-```bash
-POD=$(kubectl -n threema-gateway get pods -l 'cnpg.io/cluster=pieced-threema-gateway-db,role=primary' -o jsonpath='{.items[0].metadata.name}')
-DBPASS=$(kubectl -n threema-gateway get secret pieced-threema-gateway-db-app -o jsonpath='{.data.password}' | base64 -d)
-
-# Per-tenant in/out counts
-kubectl -n threema-gateway exec $POD -- env PGPASSWORD="$DBPASS" \
- psql -U relay -d relay -c \
- "SELECT tenant_name, direction, count(*), max(created_at) FROM messages GROUP BY tenant_name, direction ORDER BY tenant_name, direction;"
+```
+threema [Show QR] 0 users
+ ────────────────────
+
+ ────────────────────
+ [ input: A8K2P3X7 ] [ Add ]
+ ^^^ focusing this opens the QR modal the first time
```
-That's your billing source — count(*) × Threema's per-message rate ×
-direction-specific multiplier = customer charge.
-
-## Future: dynamic gateway accounts
-
-Today's hardcoded `*AIAGENT` works for one shared gateway. When you
-move to per-tenant gateway accounts (Threema's bigger plans, or to
-isolate billing per tenant):
-
-1. Update `src/lib/threema-gateway-config.ts`:
- - Make the constants a lookup keyed by tenant
- - Or fetch from `/api/tenants//threema` (new admin route)
-2. Update `threema-setup.tsx` to accept gateway info as props
-3. Update the parent `channel-users.tsx` to pass tenant-specific values
-4. Replace the static PNG with a server route that generates per-tenant
- QR codes from each gateway's `id` + `publicKey`
-
-The single config file means this refactor is contained — every
-consumer reads from one place, so once that one place returns the
-right values, everything else falls in line.
+Clicking "Show QR" or focusing the input → modal with QR + steps.
diff --git a/deploy/patch-i18n-threema.mjs b/deploy/patch-i18n-threema.mjs
index 87a7dcc..decdcc5 100644
--- a/deploy/patch-i18n-threema.mjs
+++ b/deploy/patch-i18n-threema.mjs
@@ -37,6 +37,7 @@ 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",
},
},
de: {
@@ -56,6 +57,7 @@ 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",
},
},
fr: {
@@ -75,6 +77,7 @@ 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",
},
},
it: {
@@ -94,6 +97,7 @@ 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",
},
},
};
diff --git a/src/components/channel-users/channel-users.tsx b/src/components/channel-users/channel-users.tsx
index 06b4c0a..02a2edf 100644
--- a/src/components/channel-users/channel-users.tsx
+++ b/src/components/channel-users/channel-users.tsx
@@ -3,6 +3,7 @@
import { useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
+import { ThreemaQrModal } from "./threema-qr-modal";
/** Maps channel IDs to the instructions for finding the user ID. */
const CHANNEL_ID_HELP: Record = {
@@ -51,6 +52,14 @@ export function ChannelUsers({
const [inputValues, setInputValues] = useState>({});
const [channelUsers, setChannelUsers] =
useState>(initialChannelUsers);
+ /** Which channel's QR helper modal is open, if any. */
+ const [showQrFor, setShowQrFor] = useState(null);
+ /**
+ * Tracks channels for which we've already auto-opened the helper
+ * modal on this page load. Prevents the modal from re-popping every
+ * time the user refocuses the input after dismissing it.
+ */
+ const [autoOpened, setAutoOpened] = useState>(() => new Set());
const updateChannelUsers = useCallback(
async (updated: Record) => {
@@ -216,9 +225,19 @@ export function ChannelUsers({
className="bg-surface-2 border border-border rounded-lg p-4"
>
-
- {channel}
-
+
+
+ {channel}
+
+ {channel === "threema" && (
+
+ )}
+
{users.length} {t("users")}
@@ -266,6 +285,17 @@ export function ChannelUsers({
[channel]: e.target.value,
}))
}
+ onFocus={() => {
+ // For threema specifically, open the QR helper the
+ // first time the user clicks into the input on this
+ // page load. We don't repeat after dismiss — the
+ // "Show QR" button next to the channel name covers
+ // re-opens on demand.
+ if (channel === "threema" && !autoOpened.has("threema")) {
+ setShowQrFor("threema");
+ setAutoOpened((prev) => new Set(prev).add("threema"));
+ }
+ }}
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd(channel);
}}
@@ -284,6 +314,11 @@ export function ChannelUsers({
);
})}
+
+ setShowQrFor(null)}
+ />
);
}
\ No newline at end of file
diff --git a/src/components/channel-users/threema-qr-modal.tsx b/src/components/channel-users/threema-qr-modal.tsx
new file mode 100644
index 0000000..e0d0072
--- /dev/null
+++ b/src/components/channel-users/threema-qr-modal.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useEffect } from "react";
+import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
+
+interface ThreemaQrModalProps {
+ open: boolean;
+ onClose: () => void;
+}
+
+/**
+ * On-demand modal showing the QR for adding the assistant on Threema.
+ * Triggered by the "Show QR" button in the threema channel card and
+ * closes on overlay click, ESC, or the close button.
+ *
+ * Uses a plain not next/image — image optimization adds nothing
+ * for a 57KB static PNG and removes a potential source of rendering
+ * bugs in the Next.js standalone build.
+ */
+export function ThreemaQrModal({ open, onClose }: ThreemaQrModalProps) {
+ const t = useTranslations("channelUsers.threemaSetup");
+
+ useEffect(() => {
+ if (!open) return;
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") onClose();
+ }
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [open, onClose]);
+
+ if (!open) return null;
+
+ return (
+
+ );
+}
diff --git a/src/lib/threema-gateway-config.ts b/src/lib/threema-gateway-config.ts
new file mode 100644
index 0000000..414d17f
--- /dev/null
+++ b/src/lib/threema-gateway-config.ts
@@ -0,0 +1,33 @@
+/**
+ * Threema central gateway info shown to customers.
+ *
+ * Today PieCed runs exactly one Threema Gateway account (*AIAGENT) and
+ * every tenant talks to that. The constants below are hardcoded for
+ * that account. The values are intentionally kept here (and not split
+ * across i18n / env / runtime config) so when we move to multiple
+ * gateway accounts there's a single file to refactor.
+ *
+ * To go dynamic (future):
+ * 1. Replace `THREEMA_GATEWAY` constant with a runtime lookup —
+ * either per-tenant from the relay's admin API, or from an
+ * 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).
+ */
+
+export const THREEMA_GATEWAY = {
+ /** Technical Threema Gateway ID, with leading asterisk. */
+ id: "*AIAGENT",
+ /** Display name shown to customers (no asterisk). */
+ displayName: "AIAGENT",
+ /** Public path to the QR code PNG served from `public/`. */
+ qrCodePath: "/threema/qr_code_AIAGENT.png",
+} as const;
diff --git a/src/messages/de.json b/src/messages/de.json
index a11a453..1b6988d 100644
--- a/src/messages/de.json
+++ b/src/messages/de.json
@@ -402,7 +402,8 @@
"step1": "Öffnen Sie Threema auf Ihrem Telefon.",
"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"
+ "qrAlt": "QR-Code, um {gateway} als Threema-Kontakt hinzuzufügen",
+ "showQr": "QR anzeigen"
}
},
"team": {
diff --git a/src/messages/en.json b/src/messages/en.json
index fecc918..63cd2a5 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -402,7 +402,8 @@
"step1": "Open Threema on your phone.",
"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"
+ "qrAlt": "QR code to add {gateway} as a Threema contact",
+ "showQr": "Show QR"
}
},
"team": {
diff --git a/src/messages/fr.json b/src/messages/fr.json
index ca37cc0..490e77b 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -402,7 +402,8 @@
"step1": "Ouvrez Threema sur votre téléphone.",
"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"
+ "qrAlt": "QR code pour ajouter {gateway} comme contact Threema",
+ "showQr": "Afficher le QR"
}
},
"team": {
diff --git a/src/messages/it.json b/src/messages/it.json
index bbd71f8..5050e06 100644
--- a/src/messages/it.json
+++ b/src/messages/it.json
@@ -402,7 +402,8 @@
"step1": "Apri Threema sul tuo telefono.",
"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"
+ "qrAlt": "QR code per aggiungere {gateway} come contatto Threema",
+ "showQr": "Mostra QR"
}
},
"team": {