export type PayloadKind = "text" | "url" | "email" | "sms" | "wifi" | "vcard" | "geo"; export interface PayloadFields { primary: string; secondary?: string; tertiary?: string; hidden?: boolean; } function escapeWifi(value: string): string { return value.replace(/[\\;,:"]/gu, (character) => `\\${character}`); } function escapeVcard(value: string): string { return value .replaceAll("\\", "\\\\") .replaceAll("\n", "\\n") .replaceAll(";", "\\;") .replaceAll(",", "\\,"); } export function buildStructuredPayload( kind: PayloadKind, fields: PayloadFields, ): string { const primary = fields.primary.trim(); if (!primary) throw new Error("The primary value is required."); switch (kind) { case "text": return fields.primary; case "url": { const url = new URL(primary); if (!["http:", "https:"].includes(url.protocol)) throw new Error("The URL builder accepts HTTP and HTTPS URLs only."); return url.href; } case "email": return `mailto:${encodeURIComponent(primary)}${fields.secondary ? `?subject=${encodeURIComponent(fields.secondary)}` : ""}`; case "sms": return `SMSTO:${primary}:${fields.secondary ?? ""}`; case "wifi": { const supplied = (fields.tertiary || "WPA").trim(); if (!/^(?:WPA|WEP|nopass)$/iu.test(supplied)) throw new Error("Wi-Fi security must be WPA, WEP, or nopass."); const security = supplied.toLowerCase() === "nopass" ? "nopass" : supplied.toUpperCase(); return `WIFI:T:${security};S:${escapeWifi(primary)};P:${escapeWifi(fields.secondary ?? "")};H:${fields.hidden ? "true" : "false"};;`; } case "vcard": return [ "BEGIN:VCARD", "VERSION:3.0", `FN:${escapeVcard(primary)}`, fields.secondary ? `TEL:${escapeVcard(fields.secondary)}` : "", fields.tertiary ? `EMAIL:${escapeVcard(fields.tertiary)}` : "", "END:VCARD", ] .filter(Boolean) .join("\r\n"); case "geo": { const latitude = Number(primary); const longitude = Number(fields.secondary); if ( !Number.isFinite(latitude) || latitude < -90 || latitude > 90 || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 ) throw new Error("Latitude must be −90…90 and longitude −180…180."); return `geo:${latitude},${longitude}`; } } } export function gtinCheckDigit(body: string): number { if (!/^\d{1,17}$/u.test(body)) throw new Error("A GTIN body must contain 1–17 decimal digits."); let sum = 0; for ( let index = body.length - 1, position = 0; index >= 0; index -= 1, position += 1 ) sum += Number(body[index]) * (position % 2 === 0 ? 3 : 1); return (10 - (sum % 10)) % 10; } export function validateGtin(value: string): { valid: boolean; expected: string; } { if (![8, 12, 13, 14].includes(value.length) || !/^\d+$/u.test(value)) return { valid: false, expected: "GTIN length must be 8, 12, 13, or 14 digits.", }; const expected = String(gtinCheckDigit(value.slice(0, -1))); return { valid: value.endsWith(expected), expected }; }