Phase A: acht weitere QR-Typen

E-Mail, SMS, WhatsApp, WLAN, Standort, vCard, Zahlung und App-Link sind
umgesetzt. Damit sind 9 der 15 Typen nutzbar.

Erweiterungen am Typ-Vertrag, die dafuer noetig waren:

- supportsStatic: nicht jeder Typ kann statisch. Beim App-Link steht das
  Ziel erst beim Scannen fest, also gibt es ihn nur dynamisch.
- RedirectContext: die Weiterleitung reicht den User-Agent durch, damit der
  App-Link iOS, Android und alles Uebrige unterscheiden kann.
- assertDynamicPossible(): supportsDynamic ist eine Eigenschaft des Typs,
  aber nicht jede Eingabe taugt dafuer. Ein GiroCode ist kein Web-Ziel,
  obwohl der Typ 'Zahlung' als Zahlungslink sehr wohl dynamisch sein kann.
  Ohne diese Probe liess sich ein Code speichern, der erst beim Scannen
  aufgefallen waere - also nach dem Drucken. Greift beim Anlegen und beim
  Bearbeiten.

Sorgfalt bei den Formaten, gesammelt in src/lib/qr/encoding.ts:

- WLAN maskiert ; , : \\ und setzt rein hexadezimale Werte in
  Anfuehrungszeichen, sonst deuten Geraete sie als Hexzahl.
- vCard 3.0 mit CRLF, Maskierung und Faltung nach 75 Oktetten, oktettweise
  gezaehlt, damit kein mehrbytiges Zeichen zerrissen wird.
- GiroCode nach EPC069-12 mit fester Feldreihenfolge, IBAN-Pruefsumme nach
  Modulo 97 und der 331-Byte-Grenze.
- Rufnummern werden nach E.164 normalisiert, '(0)' faellt weg.
- Standort nimmt Koordinaten auch aus einem Google-Maps- oder Apple-Link an.

Formular-Bausteine (Select, Textarea, Checkbox, FieldRow, FieldSet) fuer die
mehrfeldrigen Typen ergaenzt.

Geprueft: 225 Pruefungen, davon 80 neu - Dekodierung je Typ aus dem fertigen
PNG, Validierung, Betriebsarten, geraeteabhaengige Weiterleitung und die
Formulare im Browser.
This commit is contained in:
Florian Wilde
2026-08-14 11:30:09 +02:00
parent c6da20f419
commit 7f7cb6fa20
24 changed files with 1693 additions and 81 deletions
+10 -4
View File
@@ -48,7 +48,8 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
const [data, setData] = useState<QrFormData>(qrType.defaultData);
const [design, setDesign] = useState<QrDesign>(defaultDesign);
const [isDynamic, setIsDynamic] = useState(false);
// Typen ohne statische Variante — etwa der App-Link — starten dynamisch.
const [isDynamic, setIsDynamic] = useState(!qrType.supportsStatic);
const [label, setLabel] = useState("");
const [saved, setSaved] = useState<QrCodeDto | null>(null);
@@ -283,9 +284,10 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
<button
type="button"
aria-pressed={!isDynamic}
disabled={!qrType.supportsStatic}
onClick={() => setIsDynamic(false)}
className={cx(
"rounded-xl border p-4 text-left transition-colors",
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
!isDynamic
? "border-indigo-600 bg-indigo-50/60"
: "border-slate-300 bg-white hover:bg-slate-50",
@@ -296,7 +298,9 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
Statisch
</span>
<span className="mt-1 block text-sm text-slate-600">
Funktioniert ohne diesen Server, dafür unveränderlich.
{qrType.supportsStatic
? "Funktioniert ohne diesen Server, dafür unveränderlich."
: `Für „${qrType.title}“ nicht möglich.`}
</span>
</button>
@@ -318,7 +322,9 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
<Badge tone="neutral">mit Statistik</Badge>
</span>
<span className="mt-1 block text-sm text-slate-600">
Ziel jederzeit änderbar, Scans werden gezählt.
{qrType.supportsDynamic
? "Ziel jederzeit änderbar, Scans werden gezählt."
: `Für „${qrType.title}“ nicht möglich — die Daten stecken im Code selbst.`}
</span>
</button>
</div>