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
+37 -5
View File
@@ -7,6 +7,7 @@ import { designSchema } from "@/lib/qr/design";
import {
QR_TYPE_IDS,
QrRequestError,
QrTypeError,
type QrTypeModule,
} from "@/lib/qr/type-module";
import { shortUrl } from "@/lib/env";
@@ -40,6 +41,35 @@ export type QrRequest = z.infer<typeof qrRequestSchema>;
// dass ein Ringschluss zwischen den beiden Modulen entsteht.
export { QrRequestError };
/**
* Prüft, ob sich aus diesen Daten überhaupt ein Weiterleitungsziel bilden
* lässt — und zwar, indem es einmal gebildet wird.
*
* `supportsDynamic` ist eine Eigenschaft des Typs, aber nicht jede Eingabe
* eines dynamikfähigen Typs taugt dafür: ein GiroCode etwa ist kein Web-Ziel,
* obwohl der Typ „Zahlung“ in seiner Link-Variante sehr wohl dynamisch sein
* kann. Ohne diese Probe entstünde ein gespeicherter Code, der erst beim
* Scannen auffällt — also genau dann, wenn er schon gedruckt ist.
*/
export function assertDynamicPossible(
qrType: QrTypeModule,
data: Record<string, unknown>,
): void {
if (!qrType.supportsDynamic) {
throw new QrRequestError(
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
);
}
try {
qrType.redirectTarget(data, {});
} catch (error) {
if (error instanceof QrTypeError) {
throw new QrRequestError(error.message);
}
throw error;
}
}
export type ResolvedQrRequest = {
qrType: QrTypeModule;
data: Record<string, unknown>;
@@ -62,11 +92,7 @@ export async function resolveQrRequest(
}
if (request.mode === "dynamic") {
if (!qrType.supportsDynamic) {
throw new QrRequestError(
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
);
}
assertDynamicPossible(qrType, validated.data);
const slug = request.slug;
if (slug !== undefined && !isValidSlug(slug)) {
throw new QrRequestError("Ungültige Kurz-URL.");
@@ -78,6 +104,12 @@ export async function resolveQrRequest(
};
}
if (!qrType.supportsStatic) {
throw new QrRequestError(
`Der QR-Typ „${qrType.title}“ funktioniert nur als dynamischer Code.`,
);
}
return {
qrType,
data: validated.data,