Optimize JSON and QR codes
- Replaced original JSON with minimized binary format (gzip + base64). - Adjusted rendering and QR code generation for compatibility. - Reduced payload size for improved efficiency.
This commit is contained in:
File diff suppressed because one or more lines are too long
2
dist/app-boot.js.map
vendored
2
dist/app-boot.js.map
vendored
File diff suppressed because one or more lines are too long
349
dist/app.js
vendored
349
dist/app.js
vendored
@@ -489,7 +489,7 @@ var EnhancedConnectionSetup = ({
|
|||||||
}, "Creating a secure channel")
|
}, "Creating a secure channel")
|
||||||
]),
|
]),
|
||||||
// Step 1
|
// Step 1
|
||||||
React.createElement("div", {
|
!showAnswerStep && React.createElement("div", {
|
||||||
key: "step1",
|
key: "step1",
|
||||||
className: "card-minimal rounded-xl p-6"
|
className: "card-minimal rounded-xl p-6"
|
||||||
}, [
|
}, [
|
||||||
@@ -510,16 +510,16 @@ var EnhancedConnectionSetup = ({
|
|||||||
key: "description",
|
key: "description",
|
||||||
className: "text-secondary text-sm mb-4"
|
className: "text-secondary text-sm mb-4"
|
||||||
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
||||||
React.createElement("button", {
|
!showOfferStep && React.createElement("button", {
|
||||||
key: "create-btn",
|
key: "create-btn",
|
||||||
onClick: onCreateOffer,
|
onClick: onCreateOffer,
|
||||||
disabled: connectionStatus === "connecting" || showOfferStep,
|
disabled: connectionStatus === "connecting",
|
||||||
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
||||||
}, [
|
}, [
|
||||||
React.createElement("i", {
|
React.createElement("i", {
|
||||||
className: "fas fa-shield-alt mr-2"
|
className: "fas fa-shield-alt mr-2"
|
||||||
}),
|
}),
|
||||||
showOfferStep ? "Keys created \u2713" : "Create secure keys"
|
"Create secure keys"
|
||||||
]),
|
]),
|
||||||
showOfferStep && React.createElement("div", {
|
showOfferStep && React.createElement("div", {
|
||||||
key: "offer-result",
|
key: "offer-result",
|
||||||
@@ -542,46 +542,29 @@ var EnhancedConnectionSetup = ({
|
|||||||
key: "offer-data",
|
key: "offer-data",
|
||||||
className: "space-y-3"
|
className: "space-y-3"
|
||||||
}, [
|
}, [
|
||||||
React.createElement("textarea", {
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
||||||
key: "textarea",
|
|
||||||
value: typeof offerData === "object" ? JSON.stringify(offerData, null, 2) : offerData,
|
|
||||||
readOnly: true,
|
|
||||||
rows: 8,
|
|
||||||
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|
||||||
}),
|
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
key: "buttons",
|
key: "buttons",
|
||||||
className: "flex gap-2"
|
className: "flex gap-2"
|
||||||
}, [
|
}, [
|
||||||
React.createElement(EnhancedCopyButton, {
|
React.createElement(EnhancedCopyButton, {
|
||||||
key: "copy",
|
key: "copy",
|
||||||
text: typeof offerData === "object" ? JSON.stringify(offerData, null, 2) : offerData,
|
text: (() => {
|
||||||
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
|
||||||
}, "Copy invitation code"),
|
|
||||||
React.createElement("button", {
|
|
||||||
key: "qr-toggle",
|
|
||||||
onClick: async () => {
|
|
||||||
const next = !showQRCode;
|
|
||||||
setShowQRCode(next);
|
|
||||||
if (next) {
|
|
||||||
try {
|
try {
|
||||||
const payload = typeof offerData === "object" ? JSON.stringify(offerData) : offerData;
|
const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
|
||||||
if (payload && payload.length) {
|
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||||
await generateQRCode(payload);
|
return window.encodeBinaryToPrefixed(min);
|
||||||
}
|
}
|
||||||
} catch (e2) {
|
if (typeof window.compressToPrefixedGzip === "function") {
|
||||||
console.warn("QR regenerate on toggle failed:", e2);
|
return window.compressToPrefixedGzip(min);
|
||||||
}
|
}
|
||||||
|
return min;
|
||||||
|
} catch {
|
||||||
|
return typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
|
||||||
}
|
}
|
||||||
},
|
})(),
|
||||||
className: "px-3 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200"
|
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
||||||
}, [
|
}, "Copy invitation code")
|
||||||
React.createElement("i", {
|
|
||||||
key: "icon",
|
|
||||||
className: showQRCode ? "fas fa-eye-slash mr-1" : "fas fa-qrcode mr-1"
|
|
||||||
}),
|
|
||||||
showQRCode ? "Hide QR" : "Show QR"
|
|
||||||
])
|
|
||||||
]),
|
]),
|
||||||
showQRCode && qrCodeUrl && React.createElement("div", {
|
showQRCode && qrCodeUrl && React.createElement("div", {
|
||||||
key: "qr-container",
|
key: "qr-container",
|
||||||
@@ -785,8 +768,7 @@ var EnhancedConnectionSetup = ({
|
|||||||
className: "text-xl font-semibold text-primary mb-2"
|
className: "text-xl font-semibold text-primary mb-2"
|
||||||
}, "Joining the secure channel")
|
}, "Joining the secure channel")
|
||||||
]),
|
]),
|
||||||
// Step 1
|
showAnswerStep ? null : React.createElement("div", {
|
||||||
React.createElement("div", {
|
|
||||||
key: "step1",
|
key: "step1",
|
||||||
className: "card-minimal rounded-xl p-6"
|
className: "card-minimal rounded-xl p-6"
|
||||||
}, [
|
}, [
|
||||||
@@ -934,16 +916,23 @@ var EnhancedConnectionSetup = ({
|
|||||||
key: "answer-data",
|
key: "answer-data",
|
||||||
className: "space-y-3 mb-4"
|
className: "space-y-3 mb-4"
|
||||||
}, [
|
}, [
|
||||||
React.createElement("textarea", {
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
||||||
key: "textarea",
|
|
||||||
value: typeof answerData === "object" ? JSON.stringify(answerData, null, 2) : answerData,
|
|
||||||
readOnly: true,
|
|
||||||
rows: 6,
|
|
||||||
className: "w-full p-3 bg-custom-bg border border-green-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|
||||||
}),
|
|
||||||
React.createElement(EnhancedCopyButton, {
|
React.createElement(EnhancedCopyButton, {
|
||||||
key: "copy",
|
key: "copy",
|
||||||
text: typeof answerData === "object" ? JSON.stringify(answerData, null, 2) : answerData,
|
text: (() => {
|
||||||
|
try {
|
||||||
|
const min = typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
|
||||||
|
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||||
|
return window.encodeBinaryToPrefixed(min);
|
||||||
|
}
|
||||||
|
if (typeof window.compressToPrefixedGzip === "function") {
|
||||||
|
return window.compressToPrefixedGzip(min);
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
} catch {
|
||||||
|
return typeof answerData === "object" ? JSON.stringify(answerData) : answerData || "";
|
||||||
|
}
|
||||||
|
})(),
|
||||||
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
|
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
|
||||||
}, "Copy response code")
|
}, "Copy response code")
|
||||||
]),
|
]),
|
||||||
@@ -1823,6 +1812,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
return templateOffer;
|
return templateOffer;
|
||||||
};
|
};
|
||||||
const MAX_QR_LEN = 800;
|
const MAX_QR_LEN = 800;
|
||||||
|
const BIN_MAX_QR_LEN = 400;
|
||||||
const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
|
const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
|
||||||
const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
|
const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
|
||||||
const [qrManualMode, setQrManualMode] = React.useState(false);
|
const [qrManualMode, setQrManualMode] = React.useState(false);
|
||||||
@@ -1839,6 +1829,29 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setQrFramesTotal(0);
|
setQrFramesTotal(0);
|
||||||
setQrManualMode(false);
|
setQrManualMode(false);
|
||||||
};
|
};
|
||||||
|
const renderCurrent = async () => {
|
||||||
|
const { chunks, idx } = qrAnimationRef.current || {};
|
||||||
|
if (!chunks || !chunks.length) return;
|
||||||
|
const current = chunks[idx % chunks.length];
|
||||||
|
try {
|
||||||
|
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
const url = await (window.generateQRCode ? window.generateQRCode(current, { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE }) : Promise.resolve(""));
|
||||||
|
if (url) setQrCodeUrl(url);
|
||||||
|
} catch (e2) {
|
||||||
|
console.warn("Animated QR render error (current):", e2);
|
||||||
|
}
|
||||||
|
setQrFrameIndex((qrAnimationRef.current?.idx || 0) % (qrAnimationRef.current?.chunks?.length || 1) + 1);
|
||||||
|
};
|
||||||
|
const renderAndAdvance = async () => {
|
||||||
|
await renderCurrent();
|
||||||
|
const len = qrAnimationRef.current?.chunks?.length || 0;
|
||||||
|
if (len > 0) {
|
||||||
|
const nextIdx = ((qrAnimationRef.current?.idx || 0) + 1) % len;
|
||||||
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
|
setQrFrameIndex(nextIdx + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
const toggleQrManualMode = () => {
|
const toggleQrManualMode = () => {
|
||||||
const newManualMode = !qrManualMode;
|
const newManualMode = !qrManualMode;
|
||||||
setQrManualMode(newManualMode);
|
setQrManualMode(newManualMode);
|
||||||
@@ -1849,39 +1862,65 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}
|
}
|
||||||
console.log("QR Manual mode enabled - auto-scroll stopped");
|
console.log("QR Manual mode enabled - auto-scroll stopped");
|
||||||
} else {
|
} else {
|
||||||
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const intervalMs = 4e3;
|
const intervalMs = 3e3;
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
qrAnimationRef.current.active = true;
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
}
|
}
|
||||||
console.log("QR Manual mode disabled - auto-scroll resumed");
|
console.log("QR Manual mode disabled - auto-scroll resumed");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const nextQrFrame = () => {
|
const nextQrFrame = async () => {
|
||||||
console.log("\u{1F3AE} nextQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
console.log("\u{1F3AE} nextQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
||||||
if (qrAnimationRef.current.chunks.length > 1) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
||||||
qrAnimationRef.current.idx = nextIdx;
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
setQrFrameIndex(nextIdx + 1);
|
setQrFrameIndex(nextIdx + 1);
|
||||||
console.log("\u{1F3AE} Next frame index:", nextIdx + 1);
|
console.log("\u{1F3AE} Next frame index:", nextIdx + 1);
|
||||||
renderNext();
|
try {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
await renderCurrent();
|
||||||
|
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const intervalMs = 3e3;
|
||||||
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
} else {
|
||||||
|
qrAnimationRef.current.active = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("\u{1F3AE} No multiple frames to navigate");
|
console.log("\u{1F3AE} No multiple frames to navigate");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const prevQrFrame = () => {
|
const prevQrFrame = async () => {
|
||||||
console.log("\u{1F3AE} prevQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
console.log("\u{1F3AE} prevQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
||||||
if (qrAnimationRef.current.chunks.length > 1) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
||||||
qrAnimationRef.current.idx = prevIdx;
|
qrAnimationRef.current.idx = prevIdx;
|
||||||
setQrFrameIndex(prevIdx + 1);
|
setQrFrameIndex(prevIdx + 1);
|
||||||
console.log("\u{1F3AE} Previous frame index:", prevIdx + 1);
|
console.log("\u{1F3AE} Previous frame index:", prevIdx + 1);
|
||||||
renderNext();
|
try {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
await renderCurrent();
|
||||||
|
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const intervalMs = 3e3;
|
||||||
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
} else {
|
||||||
|
qrAnimationRef.current.active = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("\u{1F3AE} No multiple frames to navigate");
|
console.log("\u{1F3AE} No multiple frames to navigate");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const qrChunksBufferRef = React.useRef({ id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] });
|
const qrChunksBufferRef = React.useRef({ id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] });
|
||||||
const generateQRCode2 = async (data) => {
|
const generateQRCode = async (data) => {
|
||||||
try {
|
try {
|
||||||
const originalSize = typeof data === "string" ? data.length : JSON.stringify(data).length;
|
const originalSize = typeof data === "string" ? data.length : JSON.stringify(data).length;
|
||||||
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
||||||
@@ -1889,14 +1928,32 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
const QR_SIZE = isDesktop ? 720 : 512;
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
if (payload.length <= MAX_QR_LEN) {
|
if (payload.length <= MAX_QR_LEN) {
|
||||||
if (!window.generateQRCode) throw new Error("QR code generator unavailable");
|
if (!window.generateQRCode) throw new Error("QR code generator unavailable");
|
||||||
stopQrAnimation();
|
try {
|
||||||
|
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
|
setQrFrameIndex(0);
|
||||||
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
|
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
|
||||||
setQrCodeUrl(qrDataUrl);
|
setQrCodeUrl(qrDataUrl);
|
||||||
setQrFramesTotal(1);
|
setQrFramesTotal(1);
|
||||||
setQrFrameIndex(1);
|
setQrFrameIndex(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopQrAnimation();
|
try {
|
||||||
|
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
|
setQrFrameIndex(0);
|
||||||
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
const TARGET_CHUNKS = 10;
|
const TARGET_CHUNKS = 10;
|
||||||
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
|
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
|
||||||
@@ -1921,26 +1978,11 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setQrFramesTotal(rawChunks.length);
|
setQrFramesTotal(rawChunks.length);
|
||||||
setQrFrameIndex(1);
|
setQrFrameIndex(1);
|
||||||
const EC_OPTS = { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE };
|
const EC_OPTS = { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE };
|
||||||
const renderNext2 = async () => {
|
await renderNext();
|
||||||
const { chunks, idx, active } = qrAnimationRef.current;
|
|
||||||
if (!active || !chunks.length) return;
|
|
||||||
const current = chunks[idx % chunks.length];
|
|
||||||
try {
|
|
||||||
const url = await window.generateQRCode(current, EC_OPTS);
|
|
||||||
setQrCodeUrl(url);
|
|
||||||
} catch (e2) {
|
|
||||||
console.warn("Animated QR render error (raw):", e2);
|
|
||||||
}
|
|
||||||
const nextIdx = (idx + 1) % chunks.length;
|
|
||||||
qrAnimationRef.current.idx = nextIdx;
|
|
||||||
setQrFrameIndex(nextIdx + 1);
|
|
||||||
};
|
|
||||||
await renderNext2();
|
|
||||||
if (!qrManualMode) {
|
if (!qrManualMode) {
|
||||||
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
|
|
||||||
const isIOS = /iPhone|iPad|iPod/i.test(ua);
|
|
||||||
const intervalMs = 4e3;
|
const intervalMs = 4e3;
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext2, intervalMs);
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2018,7 +2060,18 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
};
|
};
|
||||||
const handleQRScan = async (scannedData) => {
|
const handleQRScan = async (scannedData) => {
|
||||||
try {
|
try {
|
||||||
const parsedData = JSON.parse(scannedData);
|
let parsedData;
|
||||||
|
if (typeof window.decodeAnyPayload === "function") {
|
||||||
|
const any = window.decodeAnyPayload(scannedData);
|
||||||
|
if (typeof any === "string") {
|
||||||
|
parsedData = JSON.parse(any);
|
||||||
|
} else {
|
||||||
|
parsedData = any;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const maybeDecompressed = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(scannedData) : scannedData;
|
||||||
|
parsedData = JSON.parse(maybeDecompressed);
|
||||||
|
}
|
||||||
if (parsedData.hdr && parsedData.body) {
|
if (parsedData.hdr && parsedData.body) {
|
||||||
const { hdr } = parsedData;
|
const { hdr } = parsedData;
|
||||||
if (!qrChunksBufferRef.current.id || qrChunksBufferRef.current.id !== hdr.id) {
|
if (!qrChunksBufferRef.current.id || qrChunksBufferRef.current.id !== hdr.id) {
|
||||||
@@ -2064,6 +2117,34 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
console.warn("RAW multi-frame reconstruction failed:", e2);
|
console.warn("RAW multi-frame reconstruction failed:", e2);
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
} else if (hdr.rt === "bin") {
|
||||||
|
try {
|
||||||
|
const parts = qrChunksBufferRef.current.items.map((s) => JSON.parse(s)).sort((a, b) => (a.hdr.seq || 0) - (b.hdr.seq || 0)).map((p) => p.body || "");
|
||||||
|
const fullText = parts.join("");
|
||||||
|
let payloadObj;
|
||||||
|
if (typeof window.decodeAnyPayload === "function") {
|
||||||
|
const any = window.decodeAnyPayload(fullText);
|
||||||
|
payloadObj = typeof any === "string" ? JSON.parse(any) : any;
|
||||||
|
} else {
|
||||||
|
payloadObj = JSON.parse(fullText);
|
||||||
|
}
|
||||||
|
if (showOfferStep) {
|
||||||
|
setAnswerInput(JSON.stringify(payloadObj, null, 2));
|
||||||
|
} else {
|
||||||
|
setOfferInput(JSON.stringify(payloadObj, null, 2));
|
||||||
|
}
|
||||||
|
setMessages((prev) => [...prev, { message: "All frames captured. BIN payload reconstructed.", type: "success" }]);
|
||||||
|
try {
|
||||||
|
document.dispatchEvent(new CustomEvent("qr-scan-complete", { detail: { id: hdr.id } }));
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrChunksBufferRef.current = { id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] };
|
||||||
|
setShowQRScannerModal(false);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (e2) {
|
||||||
|
console.warn("BIN multi-frame reconstruction failed:", e2);
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
} else if (window.receiveAndProcess) {
|
} else if (window.receiveAndProcess) {
|
||||||
try {
|
try {
|
||||||
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
|
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
|
||||||
@@ -2130,9 +2211,9 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!parsedData.sdp) {
|
if (!parsedData.sdp && parsedData.type === "enhanced_secure_offer") {
|
||||||
setMessages((prev) => [...prev, {
|
setMessages((prev) => [...prev, {
|
||||||
message: "QR code contains compressed data (SDP removed). Please use copy/paste for full data.",
|
message: "Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.",
|
||||||
type: "warning"
|
type: "warning"
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
@@ -2174,7 +2255,50 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setOfferData(offer);
|
setOfferData(offer);
|
||||||
setShowOfferStep(true);
|
setShowOfferStep(true);
|
||||||
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
||||||
await generateQRCode2(offerString);
|
try {
|
||||||
|
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||||
|
const bin = window.encodeBinaryToPrefixed(offerString);
|
||||||
|
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
const TARGET_CHUNKS = 10;
|
||||||
|
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||||
|
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||||
|
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||||
|
if (total < 2) {
|
||||||
|
total = 2;
|
||||||
|
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
|
||||||
|
}
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const seq = i + 1;
|
||||||
|
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
|
||||||
|
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: "bin" }, body: part }));
|
||||||
|
}
|
||||||
|
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
if (window.generateQRCode && chunks.length > 0) {
|
||||||
|
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
|
||||||
|
if (firstUrl) setQrCodeUrl(firstUrl);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
|
||||||
|
setQrFramesTotal(chunks.length);
|
||||||
|
setQrFrameIndex(1);
|
||||||
|
setQrManualMode(false);
|
||||||
|
const intervalMs = 3e3;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
try {
|
||||||
|
setShowQRCode(true);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
console.warn("Offer QR precompute failed:", e2);
|
||||||
|
}
|
||||||
const existingMessages = messages.filter(
|
const existingMessages = messages.filter(
|
||||||
(m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code"))
|
(m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code"))
|
||||||
);
|
);
|
||||||
@@ -2224,7 +2348,13 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}]);
|
}]);
|
||||||
let offer;
|
let offer;
|
||||||
try {
|
try {
|
||||||
offer = JSON.parse(offerInput.trim());
|
if (typeof window.decodeAnyPayload === "function") {
|
||||||
|
const any = window.decodeAnyPayload(offerInput.trim());
|
||||||
|
offer = typeof any === "string" ? JSON.parse(any) : any;
|
||||||
|
} else {
|
||||||
|
const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
|
||||||
|
offer = JSON.parse(rawText);
|
||||||
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
throw new Error(`Invalid invitation format: ${parseError.message}`);
|
throw new Error(`Invalid invitation format: ${parseError.message}`);
|
||||||
}
|
}
|
||||||
@@ -2239,7 +2369,62 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setAnswerData(answer);
|
setAnswerData(answer);
|
||||||
setShowAnswerStep(true);
|
setShowAnswerStep(true);
|
||||||
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||||
await generateQRCode2(answerString);
|
try {
|
||||||
|
if (typeof window.encodeBinaryToPrefixed === "function") {
|
||||||
|
const bin = window.encodeBinaryToPrefixed(answerString);
|
||||||
|
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
const TARGET_CHUNKS = 10;
|
||||||
|
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||||
|
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||||
|
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||||
|
if (total < 2) {
|
||||||
|
total = 2;
|
||||||
|
FRAME_MAX = Math.ceil(bin.length / 2) || 1;
|
||||||
|
}
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const seq = i + 1;
|
||||||
|
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
|
||||||
|
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: "bin" }, body: part }));
|
||||||
|
}
|
||||||
|
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
if (window.generateQRCode && chunks.length > 0) {
|
||||||
|
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: "M", size: QR_SIZE, margin: 2 });
|
||||||
|
if (firstUrl) setQrCodeUrl(firstUrl);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (qrAnimationRef.current && qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
|
||||||
|
setQrFramesTotal(chunks.length);
|
||||||
|
setQrFrameIndex(1);
|
||||||
|
setQrManualMode(false);
|
||||||
|
const intervalMs = 3e3;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
try {
|
||||||
|
setShowQRCode(true);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let url = "";
|
||||||
|
if (typeof window.generateCompressedQRCode === "function") {
|
||||||
|
url = await window.generateCompressedQRCode(answerString);
|
||||||
|
} else {
|
||||||
|
url = await generateQRCode(answerString);
|
||||||
|
}
|
||||||
|
if (url) setQrCodeUrl(url);
|
||||||
|
try {
|
||||||
|
setShowQRCode(true);
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e2) {
|
||||||
|
console.warn("Answer QR generation failed:", e2);
|
||||||
|
}
|
||||||
if (e.target.value.trim().length > 0) {
|
if (e.target.value.trim().length > 0) {
|
||||||
if (typeof markAnswerCreated === "function") {
|
if (typeof markAnswerCreated === "function") {
|
||||||
markAnswerCreated();
|
markAnswerCreated();
|
||||||
@@ -2304,7 +2489,13 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
}]);
|
}]);
|
||||||
let answer;
|
let answer;
|
||||||
try {
|
try {
|
||||||
answer = JSON.parse(answerInput.trim());
|
if (typeof window.decodeAnyPayload === "function") {
|
||||||
|
const anyAnswer = window.decodeAnyPayload(answerInput.trim());
|
||||||
|
answer = typeof anyAnswer === "string" ? JSON.parse(anyAnswer) : anyAnswer;
|
||||||
|
} else {
|
||||||
|
const rawText = typeof window.decompressIfNeeded === "function" ? window.decompressIfNeeded(answerInput.trim()) : answerInput.trim();
|
||||||
|
answer = JSON.parse(rawText);
|
||||||
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
throw new Error(`Invalid response format: ${parseError.message}`);
|
throw new Error(`Invalid response format: ${parseError.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
6
dist/app.js.map
vendored
6
dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
141
dist/qr-local.js
vendored
141
dist/qr-local.js
vendored
@@ -860,7 +860,7 @@ var require_reed_solomon_encoder = __commonJS({
|
|||||||
this.degree = degree;
|
this.degree = degree;
|
||||||
this.genPoly = Polynomial.generateECPolynomial(this.degree);
|
this.genPoly = Polynomial.generateECPolynomial(this.degree);
|
||||||
};
|
};
|
||||||
ReedSolomonEncoder.prototype.encode = function encode2(data) {
|
ReedSolomonEncoder.prototype.encode = function encode3(data) {
|
||||||
if (!this.genPoly) {
|
if (!this.genPoly) {
|
||||||
throw new Error("Encoder not initialized");
|
throw new Error("Encoder not initialized");
|
||||||
}
|
}
|
||||||
@@ -27551,7 +27551,7 @@ var require_cbor = __commonJS({
|
|||||||
(function(global2, undefined2) {
|
(function(global2, undefined2) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var POW_2_24 = Math.pow(2, -24), POW_2_32 = Math.pow(2, 32), POW_2_53 = Math.pow(2, 53);
|
var POW_2_24 = Math.pow(2, -24), POW_2_32 = Math.pow(2, 32), POW_2_53 = Math.pow(2, 53);
|
||||||
function encode2(value) {
|
function encode3(value) {
|
||||||
var data = new ArrayBuffer(256);
|
var data = new ArrayBuffer(256);
|
||||||
var dataView = new DataView(data);
|
var dataView = new DataView(data);
|
||||||
var lastLength;
|
var lastLength;
|
||||||
@@ -27694,7 +27694,7 @@ var require_cbor = __commonJS({
|
|||||||
retView.setUint8(i, dataView.getUint8(i));
|
retView.setUint8(i, dataView.getUint8(i));
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
function decode2(data, tagger, simpleValue) {
|
function decode3(data, tagger, simpleValue) {
|
||||||
var dataView = new DataView(data);
|
var dataView = new DataView(data);
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
if (typeof tagger !== "function")
|
if (typeof tagger !== "function")
|
||||||
@@ -27890,7 +27890,7 @@ var require_cbor = __commonJS({
|
|||||||
throw "Remaining bytes";
|
throw "Remaining bytes";
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
var obj = { encode: encode2, decode: decode2 };
|
var obj = { encode: encode3, decode: decode3 };
|
||||||
if (typeof define === "function" && define.amd)
|
if (typeof define === "function" && define.amd)
|
||||||
define("cbor/cbor", obj);
|
define("cbor/cbor", obj);
|
||||||
else if (typeof module !== "undefined" && module.exports)
|
else if (typeof module !== "undefined" && module.exports)
|
||||||
@@ -31900,9 +31900,6 @@ var Html5QrcodeScanner = (function() {
|
|||||||
return Html5QrcodeScanner2;
|
return Html5QrcodeScanner2;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// src/crypto/cose-qr.js
|
|
||||||
var cbor = __toESM(require_cbor());
|
|
||||||
|
|
||||||
// node_modules/pako/dist/pako.esm.mjs
|
// node_modules/pako/dist/pako.esm.mjs
|
||||||
var Z_FIXED$1 = 4;
|
var Z_FIXED$1 = 4;
|
||||||
var Z_BINARY = 0;
|
var Z_BINARY = 0;
|
||||||
@@ -36076,9 +36073,15 @@ var inflate_1$1 = {
|
|||||||
var { Deflate, deflate, deflateRaw, gzip } = deflate_1$1;
|
var { Deflate, deflate, deflateRaw, gzip } = deflate_1$1;
|
||||||
var { Inflate, inflate, inflateRaw, ungzip } = inflate_1$1;
|
var { Inflate, inflate, inflateRaw, ungzip } = inflate_1$1;
|
||||||
var deflate_1 = deflate;
|
var deflate_1 = deflate;
|
||||||
|
var gzip_1 = gzip;
|
||||||
var inflate_1 = inflate;
|
var inflate_1 = inflate;
|
||||||
|
var ungzip_1 = ungzip;
|
||||||
|
|
||||||
|
// src/scripts/qr-local.js
|
||||||
|
var cbor2 = __toESM(require_cbor());
|
||||||
|
|
||||||
// src/crypto/cose-qr.js
|
// src/crypto/cose-qr.js
|
||||||
|
var cbor = __toESM(require_cbor());
|
||||||
var base64 = __toESM(require_base64_js());
|
var base64 = __toESM(require_base64_js());
|
||||||
function toBase64Url(uint8) {
|
function toBase64Url(uint8) {
|
||||||
let b64 = base64.fromByteArray(uint8);
|
let b64 = base64.fromByteArray(uint8);
|
||||||
@@ -36421,12 +36424,83 @@ window.packSecurePayload = packSecurePayload;
|
|||||||
window.receiveAndProcess = receiveAndProcess;
|
window.receiveAndProcess = receiveAndProcess;
|
||||||
|
|
||||||
// src/scripts/qr-local.js
|
// src/scripts/qr-local.js
|
||||||
|
var COMPRESSION_PREFIX = "SB1:gz:";
|
||||||
|
var BINARY_PREFIX = "SB1:bin:";
|
||||||
|
function uint8ToBase64(bytes) {
|
||||||
|
let binary = "";
|
||||||
|
const chunkSize = 32768;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize);
|
||||||
|
binary += String.fromCharCode.apply(null, chunk);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
function base64ToUint8(b64) {
|
||||||
|
const binary = atob(b64);
|
||||||
|
const len = binary.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
function compressStringToBase64Gzip(text) {
|
||||||
|
const utf8 = new TextEncoder().encode(text);
|
||||||
|
const gz = gzip_1(utf8);
|
||||||
|
return uint8ToBase64(gz);
|
||||||
|
}
|
||||||
|
function decompressBase64GzipToString(b64) {
|
||||||
|
const gz = base64ToUint8(b64);
|
||||||
|
const out = ungzip_1(gz);
|
||||||
|
return new TextDecoder().decode(out);
|
||||||
|
}
|
||||||
async function generateQRCode(text, opts = {}) {
|
async function generateQRCode(text, opts = {}) {
|
||||||
const size = opts.size || 512;
|
const size = opts.size || 512;
|
||||||
const margin = opts.margin ?? 2;
|
const margin = opts.margin ?? 2;
|
||||||
const errorCorrectionLevel = opts.errorCorrectionLevel || "M";
|
const errorCorrectionLevel = opts.errorCorrectionLevel || "M";
|
||||||
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
|
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
|
||||||
}
|
}
|
||||||
|
async function generateCompressedQRCode(text, opts = {}) {
|
||||||
|
try {
|
||||||
|
const compressedB64 = compressStringToBase64Gzip(text);
|
||||||
|
const payload = COMPRESSION_PREFIX + compressedB64;
|
||||||
|
return await generateQRCode(payload, opts);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("generateCompressedQRCode failed, falling back to plain:", e?.message || e);
|
||||||
|
return await generateQRCode(text, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function base64ToBase64Url(b64) {
|
||||||
|
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||||
|
}
|
||||||
|
function base64UrlToBase64(b64url) {
|
||||||
|
let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const pad = b64.length % 4;
|
||||||
|
if (pad) b64 += "=".repeat(4 - pad);
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
function encodeObjectToBinaryBase64Url(obj) {
|
||||||
|
const cborBytes = cbor2.encode(obj);
|
||||||
|
const compressed = deflate_1(new Uint8Array(cborBytes));
|
||||||
|
const b64 = uint8ToBase64(compressed);
|
||||||
|
return base64ToBase64Url(b64);
|
||||||
|
}
|
||||||
|
function decodeBinaryBase64UrlToObject(b64url) {
|
||||||
|
const b64 = base64UrlToBase64(b64url);
|
||||||
|
const compressed = base64ToUint8(b64);
|
||||||
|
const decompressed = inflate_1(compressed);
|
||||||
|
const ab = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
|
||||||
|
return cbor2.decode(ab);
|
||||||
|
}
|
||||||
|
async function generateBinaryQRCodeFromObject(obj, opts = {}) {
|
||||||
|
try {
|
||||||
|
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||||
|
const payload = BINARY_PREFIX + b64url;
|
||||||
|
return await generateQRCode(payload, opts);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("generateBinaryQRCodeFromObject failed, falling back to JSON compressed:", e?.message || e);
|
||||||
|
const text = JSON.stringify(obj);
|
||||||
|
return await generateCompressedQRCode(text, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
||||||
try {
|
try {
|
||||||
console.log("\u{1F510} Generating COSE-based QR code...");
|
console.log("\u{1F510} Generating COSE-based QR code...");
|
||||||
@@ -36443,11 +36517,62 @@ async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.generateQRCode = generateQRCode;
|
window.generateQRCode = generateQRCode;
|
||||||
|
window.generateCompressedQRCode = generateCompressedQRCode;
|
||||||
|
window.generateBinaryQRCodeFromObject = generateBinaryQRCodeFromObject;
|
||||||
window.generateCOSEQRCode = generateCOSEQRCode;
|
window.generateCOSEQRCode = generateCOSEQRCode;
|
||||||
window.Html5Qrcode = Html5Qrcode;
|
window.Html5Qrcode = Html5Qrcode;
|
||||||
window.packSecurePayload = packSecurePayload;
|
window.packSecurePayload = packSecurePayload;
|
||||||
window.receiveAndProcess = receiveAndProcess;
|
window.receiveAndProcess = receiveAndProcess;
|
||||||
console.log("QR libraries loaded: generateQRCode, generateCOSEQRCode, Html5Qrcode, COSE functions");
|
window.decompressIfNeeded = function(scannedText) {
|
||||||
|
try {
|
||||||
|
if (typeof scannedText === "string" && scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||||
|
const b64 = scannedText.slice(COMPRESSION_PREFIX.length);
|
||||||
|
return decompressBase64GzipToString(b64);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("decompressIfNeeded failed:", e?.message || e);
|
||||||
|
}
|
||||||
|
return scannedText;
|
||||||
|
};
|
||||||
|
window.compressToPrefixedGzip = function(text) {
|
||||||
|
try {
|
||||||
|
const payload = String(text || "");
|
||||||
|
const compressedB64 = compressStringToBase64Gzip(payload);
|
||||||
|
return COMPRESSION_PREFIX + compressedB64;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("compressToPrefixedGzip failed:", e?.message || e);
|
||||||
|
return String(text || "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.encodeBinaryToPrefixed = function(objOrJson) {
|
||||||
|
try {
|
||||||
|
const obj = typeof objOrJson === "string" ? JSON.parse(objOrJson) : objOrJson;
|
||||||
|
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||||
|
return BINARY_PREFIX + b64url;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("encodeBinaryToPrefixed failed:", e?.message || e);
|
||||||
|
return typeof objOrJson === "string" ? objOrJson : JSON.stringify(objOrJson);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.decodeAnyPayload = function(scannedText) {
|
||||||
|
try {
|
||||||
|
if (typeof scannedText === "string") {
|
||||||
|
if (scannedText.startsWith(BINARY_PREFIX)) {
|
||||||
|
const b64url = scannedText.slice(BINARY_PREFIX.length);
|
||||||
|
return decodeBinaryBase64UrlToObject(b64url);
|
||||||
|
}
|
||||||
|
if (scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||||
|
const s = window.decompressIfNeeded(scannedText);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return scannedText;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("decodeAnyPayload failed:", e?.message || e);
|
||||||
|
}
|
||||||
|
return scannedText;
|
||||||
|
};
|
||||||
|
console.log("QR libraries loaded: generateQRCode, generateCompressedQRCode, generateBinaryQRCodeFromObject, Html5Qrcode, COSE functions");
|
||||||
/*! Bundled license information:
|
/*! Bundled license information:
|
||||||
|
|
||||||
pako/dist/pako.esm.mjs:
|
pako/dist/pako.esm.mjs:
|
||||||
|
|||||||
8
dist/qr-local.js.map
vendored
8
dist/qr-local.js.map
vendored
File diff suppressed because one or more lines are too long
343
src/app.jsx
343
src/app.jsx
@@ -517,7 +517,7 @@
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
// Step 1
|
// Step 1
|
||||||
React.createElement('div', {
|
!showAnswerStep && React.createElement('div', {
|
||||||
key: 'step1',
|
key: 'step1',
|
||||||
className: "card-minimal rounded-xl p-6"
|
className: "card-minimal rounded-xl p-6"
|
||||||
}, [
|
}, [
|
||||||
@@ -538,16 +538,16 @@
|
|||||||
key: 'description',
|
key: 'description',
|
||||||
className: "text-secondary text-sm mb-4"
|
className: "text-secondary text-sm mb-4"
|
||||||
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
||||||
React.createElement('button', {
|
!showOfferStep && React.createElement('button', {
|
||||||
key: 'create-btn',
|
key: 'create-btn',
|
||||||
onClick: onCreateOffer,
|
onClick: onCreateOffer,
|
||||||
disabled: connectionStatus === 'connecting' || showOfferStep,
|
disabled: connectionStatus === 'connecting',
|
||||||
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
||||||
}, [
|
}, [
|
||||||
React.createElement('i', {
|
React.createElement('i', {
|
||||||
className: 'fas fa-shield-alt mr-2'
|
className: 'fas fa-shield-alt mr-2'
|
||||||
}),
|
}),
|
||||||
showOfferStep ? 'Keys created ✓' : 'Create secure keys'
|
'Create secure keys'
|
||||||
]),
|
]),
|
||||||
|
|
||||||
showOfferStep && React.createElement('div', {
|
showOfferStep && React.createElement('div', {
|
||||||
@@ -571,46 +571,27 @@
|
|||||||
key: 'offer-data',
|
key: 'offer-data',
|
||||||
className: "space-y-3"
|
className: "space-y-3"
|
||||||
}, [
|
}, [
|
||||||
React.createElement('textarea', {
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
||||||
key: 'textarea',
|
|
||||||
value: typeof offerData === 'object' ? JSON.stringify(offerData, null, 2) : offerData,
|
|
||||||
readOnly: true,
|
|
||||||
rows: 8,
|
|
||||||
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|
||||||
}),
|
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
key: 'buttons',
|
key: 'buttons',
|
||||||
className: "flex gap-2"
|
className: "flex gap-2"
|
||||||
}, [
|
}, [
|
||||||
React.createElement(EnhancedCopyButton, {
|
React.createElement(EnhancedCopyButton, {
|
||||||
key: 'copy',
|
key: 'copy',
|
||||||
text: typeof offerData === 'object' ? JSON.stringify(offerData, null, 2) : offerData,
|
text: (() => {
|
||||||
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
|
||||||
}, 'Copy invitation code'),
|
|
||||||
React.createElement('button', {
|
|
||||||
key: 'qr-toggle',
|
|
||||||
onClick: async () => {
|
|
||||||
const next = !showQRCode;
|
|
||||||
setShowQRCode(next);
|
|
||||||
if (next) {
|
|
||||||
try {
|
try {
|
||||||
const payload = typeof offerData === 'object' ? JSON.stringify(offerData) : offerData;
|
const min = typeof offerData === 'object' ? JSON.stringify(offerData) : (offerData || '');
|
||||||
if (payload && payload.length) {
|
if (typeof window.encodeBinaryToPrefixed === 'function') {
|
||||||
await generateQRCode(payload);
|
return window.encodeBinaryToPrefixed(min);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
if (typeof window.compressToPrefixedGzip === 'function') {
|
||||||
console.warn('QR regenerate on toggle failed:', e);
|
return window.compressToPrefixedGzip(min);
|
||||||
}
|
}
|
||||||
}
|
return min;
|
||||||
},
|
} catch { return typeof offerData === 'object' ? JSON.stringify(offerData) : (offerData || ''); }
|
||||||
className: "px-3 py-2 bg-blue-500/10 hover:bg-blue-500/20 text-blue-400 border border-blue-500/20 rounded text-sm font-medium transition-all duration-200"
|
})(),
|
||||||
}, [
|
className: "flex-1 px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
||||||
React.createElement('i', {
|
}, 'Copy invitation code')
|
||||||
key: 'icon',
|
|
||||||
className: showQRCode ? 'fas fa-eye-slash mr-1' : 'fas fa-qrcode mr-1'
|
|
||||||
}),
|
|
||||||
showQRCode ? 'Hide QR' : 'Show QR'
|
|
||||||
])
|
|
||||||
]),
|
]),
|
||||||
showQRCode && qrCodeUrl && React.createElement('div', {
|
showQRCode && qrCodeUrl && React.createElement('div', {
|
||||||
key: 'qr-container',
|
key: 'qr-container',
|
||||||
@@ -827,8 +808,7 @@
|
|||||||
}, 'Joining the secure channel')
|
}, 'Joining the secure channel')
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Step 1
|
(showAnswerStep ? null : React.createElement('div', {
|
||||||
React.createElement('div', {
|
|
||||||
key: 'step1',
|
key: 'step1',
|
||||||
className: "card-minimal rounded-xl p-6"
|
className: "card-minimal rounded-xl p-6"
|
||||||
}, [
|
}, [
|
||||||
@@ -854,13 +834,11 @@
|
|||||||
value: offerInput,
|
value: offerInput,
|
||||||
onChange: (e) => {
|
onChange: (e) => {
|
||||||
setOfferInput(e.target.value);
|
setOfferInput(e.target.value);
|
||||||
// Mark answer as created when user manually enters data
|
|
||||||
if (e.target.value.trim().length > 0) {
|
if (e.target.value.trim().length > 0) {
|
||||||
if (typeof markAnswerCreated === 'function') {
|
if (typeof markAnswerCreated === 'function') {
|
||||||
markAnswerCreated();
|
markAnswerCreated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
rows: 8,
|
rows: 8,
|
||||||
placeholder: "Paste the encrypted invitation code or scan QR code...",
|
placeholder: "Paste the encrypted invitation code or scan QR code...",
|
||||||
@@ -908,7 +886,6 @@
|
|||||||
React.createElement('button', {
|
React.createElement('button', {
|
||||||
key: 'open-scanner',
|
key: 'open-scanner',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|
||||||
if (typeof setShowQRScannerModal === 'function') {
|
if (typeof setShowQRScannerModal === 'function') {
|
||||||
setShowQRScannerModal(true);
|
setShowQRScannerModal(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -931,7 +908,6 @@
|
|||||||
const testData = '{"type":"test","message":"Hello QR Scanner!"}';
|
const testData = '{"type":"test","message":"Hello QR Scanner!"}';
|
||||||
const qrUrl = await window.generateQRCode(testData);
|
const qrUrl = await window.generateQRCode(testData);
|
||||||
console.log('Test QR code generated:', qrUrl);
|
console.log('Test QR code generated:', qrUrl);
|
||||||
// Open QR code in new tab for testing
|
|
||||||
const newWindow = window.open();
|
const newWindow = window.open();
|
||||||
newWindow.document.write(`<img src="${qrUrl}" style="width: 300px; height: 300px;">`);
|
newWindow.document.write(`<img src="${qrUrl}" style="width: 300px; height: 300px;">`);
|
||||||
}
|
}
|
||||||
@@ -944,7 +920,7 @@
|
|||||||
className: "px-3 py-1 bg-gray-600/20 hover:bg-gray-600/30 text-gray-300 border border-gray-500/20 rounded text-xs font-medium transition-all duration-200"
|
className: "px-3 py-1 bg-gray-600/20 hover:bg-gray-600/30 text-gray-300 border border-gray-500/20 rounded text-xs font-medium transition-all duration-200"
|
||||||
}, 'Close Scanner')
|
}, 'Close Scanner')
|
||||||
])
|
])
|
||||||
]),
|
])),
|
||||||
|
|
||||||
// Step 2
|
// Step 2
|
||||||
showAnswerStep && React.createElement('div', {
|
showAnswerStep && React.createElement('div', {
|
||||||
@@ -981,16 +957,21 @@
|
|||||||
key: 'answer-data',
|
key: 'answer-data',
|
||||||
className: "space-y-3 mb-4"
|
className: "space-y-3 mb-4"
|
||||||
}, [
|
}, [
|
||||||
React.createElement('textarea', {
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
||||||
key: 'textarea',
|
|
||||||
value: typeof answerData === 'object' ? JSON.stringify(answerData, null, 2) : answerData,
|
|
||||||
readOnly: true,
|
|
||||||
rows: 6,
|
|
||||||
className: "w-full p-3 bg-custom-bg border border-green-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|
||||||
}),
|
|
||||||
React.createElement(EnhancedCopyButton, {
|
React.createElement(EnhancedCopyButton, {
|
||||||
key: 'copy',
|
key: 'copy',
|
||||||
text: typeof answerData === 'object' ? JSON.stringify(answerData, null, 2) : answerData,
|
text: (() => {
|
||||||
|
try {
|
||||||
|
const min = typeof answerData === 'object' ? JSON.stringify(answerData) : (answerData || '');
|
||||||
|
if (typeof window.encodeBinaryToPrefixed === 'function') {
|
||||||
|
return window.encodeBinaryToPrefixed(min);
|
||||||
|
}
|
||||||
|
if (typeof window.compressToPrefixedGzip === 'function') {
|
||||||
|
return window.compressToPrefixedGzip(min);
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
} catch { return typeof answerData === 'object' ? JSON.stringify(answerData) : (answerData || ''); }
|
||||||
|
})(),
|
||||||
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
|
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
|
||||||
}, 'Copy response code')
|
}, 'Copy response code')
|
||||||
]),
|
]),
|
||||||
@@ -2067,8 +2048,9 @@
|
|||||||
return templateOffer;
|
return templateOffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conservative QR payload limit (characters). Adjust per error correction level.
|
// Conservative QR payload limits (characters). Adjust per error correction level.
|
||||||
const MAX_QR_LEN = 800;
|
const MAX_QR_LEN = 800; // for JSON/plain/gzip
|
||||||
|
const BIN_MAX_QR_LEN = 400; // stricter for SB1:bin to improve scan reliability
|
||||||
const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
|
const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
|
||||||
const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
|
const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
|
||||||
const [qrManualMode, setQrManualMode] = React.useState(false);
|
const [qrManualMode, setQrManualMode] = React.useState(false);
|
||||||
@@ -2083,6 +2065,33 @@
|
|||||||
setQrManualMode(false);
|
setQrManualMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Render frame at current index (no index mutation)
|
||||||
|
const renderCurrent = async () => {
|
||||||
|
const { chunks, idx } = qrAnimationRef.current || {};
|
||||||
|
if (!chunks || !chunks.length) return;
|
||||||
|
const current = chunks[idx % chunks.length];
|
||||||
|
try {
|
||||||
|
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
const url = await (window.generateQRCode ? window.generateQRCode(current, { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE }) : Promise.resolve(''));
|
||||||
|
if (url) setQrCodeUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Animated QR render error (current):', e);
|
||||||
|
}
|
||||||
|
setQrFrameIndex(((qrAnimationRef.current?.idx || 0) % (qrAnimationRef.current?.chunks?.length || 1)) + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render current frame, then advance index by 1
|
||||||
|
const renderAndAdvance = async () => {
|
||||||
|
await renderCurrent();
|
||||||
|
const len = qrAnimationRef.current?.chunks?.length || 0;
|
||||||
|
if (len > 0) {
|
||||||
|
const nextIdx = ((qrAnimationRef.current?.idx || 0) + 1) % len;
|
||||||
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
|
setQrFrameIndex(nextIdx + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleQrManualMode = () => {
|
const toggleQrManualMode = () => {
|
||||||
const newManualMode = !qrManualMode;
|
const newManualMode = !qrManualMode;
|
||||||
setQrManualMode(newManualMode);
|
setQrManualMode(newManualMode);
|
||||||
@@ -2095,35 +2104,57 @@
|
|||||||
}
|
}
|
||||||
console.log('QR Manual mode enabled - auto-scroll stopped');
|
console.log('QR Manual mode enabled - auto-scroll stopped');
|
||||||
} else {
|
} else {
|
||||||
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const intervalMs = 4000;
|
const intervalMs = 3000;
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
qrAnimationRef.current.active = true;
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
}
|
}
|
||||||
console.log('QR Manual mode disabled - auto-scroll resumed');
|
console.log('QR Manual mode disabled - auto-scroll resumed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextQrFrame = () => {
|
const nextQrFrame = async () => {
|
||||||
console.log('🎮 nextQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
console.log('🎮 nextQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
||||||
if (qrAnimationRef.current.chunks.length > 1) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
||||||
qrAnimationRef.current.idx = nextIdx;
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
setQrFrameIndex(nextIdx + 1);
|
setQrFrameIndex(nextIdx + 1);
|
||||||
console.log('🎮 Next frame index:', nextIdx + 1);
|
console.log('🎮 Next frame index:', nextIdx + 1);
|
||||||
renderNext();
|
// Ensure auto-advance timer runs in manual mode too
|
||||||
|
try { clearInterval(qrAnimationRef.current.timer); } catch {}
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
await renderCurrent();
|
||||||
|
// If not in manual mode, restart auto timer
|
||||||
|
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const intervalMs = 3000;
|
||||||
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
} else {
|
||||||
|
qrAnimationRef.current.active = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('🎮 No multiple frames to navigate');
|
console.log('🎮 No multiple frames to navigate');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevQrFrame = () => {
|
const prevQrFrame = async () => {
|
||||||
console.log('🎮 prevQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
console.log('🎮 prevQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
||||||
if (qrAnimationRef.current.chunks.length > 1) {
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
||||||
qrAnimationRef.current.idx = prevIdx;
|
qrAnimationRef.current.idx = prevIdx;
|
||||||
setQrFrameIndex(prevIdx + 1);
|
setQrFrameIndex(prevIdx + 1);
|
||||||
console.log('🎮 Previous frame index:', prevIdx + 1);
|
console.log('🎮 Previous frame index:', prevIdx + 1);
|
||||||
renderNext();
|
try { clearInterval(qrAnimationRef.current.timer); } catch {}
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
await renderCurrent();
|
||||||
|
if (!qrManualMode && qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const intervalMs = 3000;
|
||||||
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
} else {
|
||||||
|
qrAnimationRef.current.active = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('🎮 No multiple frames to navigate');
|
console.log('🎮 No multiple frames to navigate');
|
||||||
}
|
}
|
||||||
@@ -2141,7 +2172,11 @@
|
|||||||
const QR_SIZE = isDesktop ? 720 : 512;
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
if (payload.length <= MAX_QR_LEN) {
|
if (payload.length <= MAX_QR_LEN) {
|
||||||
if (!window.generateQRCode) throw new Error('QR code generator unavailable');
|
if (!window.generateQRCode) throw new Error('QR code generator unavailable');
|
||||||
stopQrAnimation();
|
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
|
setQrFrameIndex(0);
|
||||||
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
|
const qrDataUrl = await window.generateQRCode(payload, { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
|
||||||
setQrCodeUrl(qrDataUrl);
|
setQrCodeUrl(qrDataUrl);
|
||||||
setQrFramesTotal(1);
|
setQrFramesTotal(1);
|
||||||
@@ -2149,7 +2184,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
stopQrAnimation();
|
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
|
setQrFrameIndex(0);
|
||||||
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
|
||||||
const TARGET_CHUNKS = 10;
|
const TARGET_CHUNKS = 10;
|
||||||
@@ -2175,27 +2214,12 @@
|
|||||||
setQrFramesTotal(rawChunks.length);
|
setQrFramesTotal(rawChunks.length);
|
||||||
setQrFrameIndex(1);
|
setQrFrameIndex(1);
|
||||||
const EC_OPTS = { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE };
|
const EC_OPTS = { errorCorrectionLevel: 'M', margin: 2, size: QR_SIZE };
|
||||||
const renderNext = async () => {
|
|
||||||
const { chunks, idx, active } = qrAnimationRef.current;
|
|
||||||
if (!active || !chunks.length) return;
|
|
||||||
const current = chunks[idx % chunks.length];
|
|
||||||
try {
|
|
||||||
const url = await window.generateQRCode(current, EC_OPTS);
|
|
||||||
setQrCodeUrl(url);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Animated QR render error (raw):', e);
|
|
||||||
}
|
|
||||||
const nextIdx = (idx + 1) % chunks.length;
|
|
||||||
qrAnimationRef.current.idx = nextIdx;
|
|
||||||
setQrFrameIndex(nextIdx + 1);
|
|
||||||
};
|
|
||||||
await renderNext();
|
await renderNext();
|
||||||
|
|
||||||
if (!qrManualMode) {
|
if (!qrManualMode) {
|
||||||
const ua = (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : '';
|
|
||||||
const isIOS = /iPhone|iPad|iPod/i.test(ua);
|
|
||||||
const intervalMs = 4000; // 4 seconds per frame for better readability
|
const intervalMs = 4000; // 4 seconds per frame for better readability
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
qrAnimationRef.current.active = true;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2279,11 +2303,21 @@
|
|||||||
|
|
||||||
const handleQRScan = async (scannedData) => {
|
const handleQRScan = async (scannedData) => {
|
||||||
try {
|
try {
|
||||||
|
// Prefer binary (CBOR) decode, else gzip JSON, else raw JSON
|
||||||
|
let parsedData;
|
||||||
|
if (typeof window.decodeAnyPayload === 'function') {
|
||||||
|
const any = window.decodeAnyPayload(scannedData);
|
||||||
|
if (typeof any === 'string') {
|
||||||
|
parsedData = JSON.parse(any);
|
||||||
|
} else {
|
||||||
|
parsedData = any; // object from binary
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const maybeDecompressed = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(scannedData) : scannedData;
|
||||||
|
parsedData = JSON.parse(maybeDecompressed);
|
||||||
|
}
|
||||||
|
|
||||||
// Try to parse as JSON first
|
// QR with hdr/body: COSE or RAW/BIN animated frames
|
||||||
const parsedData = JSON.parse(scannedData);
|
|
||||||
|
|
||||||
// QR with hdr/body: COSE or RAW animated frames
|
|
||||||
if (parsedData.hdr && parsedData.body) {
|
if (parsedData.hdr && parsedData.body) {
|
||||||
const { hdr } = parsedData;
|
const { hdr } = parsedData;
|
||||||
// Initialize/rotate buffer by id
|
// Initialize/rotate buffer by id
|
||||||
@@ -2309,7 +2343,7 @@
|
|||||||
// Explicitly keep scanner open
|
// Explicitly keep scanner open
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
// Completed: decide RAW vs COSE
|
// Completed: decide RAW vs BIN vs COSE
|
||||||
if (hdr.rt === 'raw') {
|
if (hdr.rt === 'raw') {
|
||||||
try {
|
try {
|
||||||
// Sort by seq and concatenate bodies
|
// Sort by seq and concatenate bodies
|
||||||
@@ -2334,6 +2368,34 @@
|
|||||||
console.warn('RAW multi-frame reconstruction failed:', e);
|
console.warn('RAW multi-frame reconstruction failed:', e);
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
} else if (hdr.rt === 'bin') {
|
||||||
|
try {
|
||||||
|
const parts = qrChunksBufferRef.current.items
|
||||||
|
.map(s => JSON.parse(s))
|
||||||
|
.sort((a, b) => (a.hdr.seq || 0) - (b.hdr.seq || 0))
|
||||||
|
.map(p => p.body || '');
|
||||||
|
const fullText = parts.join(''); // SB1:bin:...
|
||||||
|
let payloadObj;
|
||||||
|
if (typeof window.decodeAnyPayload === 'function') {
|
||||||
|
const any = window.decodeAnyPayload(fullText);
|
||||||
|
payloadObj = (typeof any === 'string') ? JSON.parse(any) : any;
|
||||||
|
} else {
|
||||||
|
payloadObj = JSON.parse(fullText);
|
||||||
|
}
|
||||||
|
if (showOfferStep) {
|
||||||
|
setAnswerInput(JSON.stringify(payloadObj, null, 2));
|
||||||
|
} else {
|
||||||
|
setOfferInput(JSON.stringify(payloadObj, null, 2));
|
||||||
|
}
|
||||||
|
setMessages(prev => [...prev, { message: 'All frames captured. BIN payload reconstructed.', type: 'success' }]);
|
||||||
|
try { document.dispatchEvent(new CustomEvent('qr-scan-complete', { detail: { id: hdr.id } })); } catch {}
|
||||||
|
qrChunksBufferRef.current = { id: null, total: 0, seen: new Set(), items: [] };
|
||||||
|
setShowQRScannerModal(false);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('BIN multi-frame reconstruction failed:', e);
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
} else if (window.receiveAndProcess) {
|
} else if (window.receiveAndProcess) {
|
||||||
try {
|
try {
|
||||||
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
|
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
|
||||||
@@ -2409,10 +2471,10 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if this is compressed data (missing SDP)
|
// If payload was compressed, it's already decompressed above; keep legacy warning only when clearly incomplete
|
||||||
if (!parsedData.sdp) {
|
if (!parsedData.sdp && parsedData.type === 'enhanced_secure_offer') {
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
message: 'QR code contains compressed data (SDP removed). Please use copy/paste for full data.',
|
message: 'Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
@@ -2467,10 +2529,46 @@
|
|||||||
setOfferData(offer);
|
setOfferData(offer);
|
||||||
setShowOfferStep(true);
|
setShowOfferStep(true);
|
||||||
|
|
||||||
// Generate QR code for the offer data
|
// Do not auto-generate single QR; prepare animated binary frames when user opens QR
|
||||||
// Use compact JSON (no pretty-printing) to reduce size
|
|
||||||
const offerString = typeof offer === 'object' ? JSON.stringify(offer) : offer;
|
const offerString = typeof offer === 'object' ? JSON.stringify(offer) : offer;
|
||||||
await generateQRCode(offerString);
|
try {
|
||||||
|
if (typeof window.encodeBinaryToPrefixed === 'function') {
|
||||||
|
const bin = window.encodeBinaryToPrefixed(offerString);
|
||||||
|
// Precompute frames to be ready instantly on show
|
||||||
|
const id = `bin_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
const TARGET_CHUNKS = 10;
|
||||||
|
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||||
|
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||||
|
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||||
|
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const seq = i + 1;
|
||||||
|
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
|
||||||
|
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: 'bin' }, body: part }));
|
||||||
|
}
|
||||||
|
// Seed first frame and start auto-advance immediately
|
||||||
|
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
if (window.generateQRCode && chunks.length > 0) {
|
||||||
|
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
|
||||||
|
if (firstUrl) setQrCodeUrl(firstUrl);
|
||||||
|
}
|
||||||
|
// Store precomputed chunks to ref, ready for animation
|
||||||
|
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
|
||||||
|
setQrFramesTotal(chunks.length);
|
||||||
|
setQrFrameIndex(1);
|
||||||
|
setQrManualMode(false);
|
||||||
|
// Start auto-advance loop for Offer immediately
|
||||||
|
const intervalMs = 3000;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
// Show QR immediately for Offer flow
|
||||||
|
try { setShowQRCode(true); } catch {}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Offer QR precompute failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
const existingMessages = messages.filter(m =>
|
const existingMessages = messages.filter(m =>
|
||||||
m.type === 'system' &&
|
m.type === 'system' &&
|
||||||
@@ -2530,8 +2628,14 @@
|
|||||||
|
|
||||||
let offer;
|
let offer;
|
||||||
try {
|
try {
|
||||||
// Parse the offer data directly (no decryption needed with SAS)
|
// Prefer binary decode first, then gzip JSON
|
||||||
offer = JSON.parse(offerInput.trim());
|
if (typeof window.decodeAnyPayload === 'function') {
|
||||||
|
const any = window.decodeAnyPayload(offerInput.trim());
|
||||||
|
offer = (typeof any === 'string') ? JSON.parse(any) : any;
|
||||||
|
} else {
|
||||||
|
const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(offerInput.trim()) : offerInput.trim();
|
||||||
|
offer = JSON.parse(rawText);
|
||||||
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
throw new Error(`Invalid invitation format: ${parseError.message}`);
|
throw new Error(`Invalid invitation format: ${parseError.message}`);
|
||||||
}
|
}
|
||||||
@@ -2552,10 +2656,51 @@
|
|||||||
setAnswerData(answer);
|
setAnswerData(answer);
|
||||||
setShowAnswerStep(true);
|
setShowAnswerStep(true);
|
||||||
|
|
||||||
// Generate QR code for the answer data
|
// Answer QR: precompute binary frames and start animation immediately
|
||||||
const answerString = typeof answer === 'object' ? JSON.stringify(answer) : answer;
|
const answerString = typeof answer === 'object' ? JSON.stringify(answer) : answer;
|
||||||
|
try {
|
||||||
await generateQRCode(answerString);
|
if (typeof window.encodeBinaryToPrefixed === 'function') {
|
||||||
|
const bin = window.encodeBinaryToPrefixed(answerString);
|
||||||
|
const id = `ans_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
const TARGET_CHUNKS = 10;
|
||||||
|
let FRAME_MAX = Math.max(200, Math.floor(bin.length / TARGET_CHUNKS));
|
||||||
|
if (FRAME_MAX <= 0) FRAME_MAX = 200;
|
||||||
|
let total = Math.ceil(bin.length / FRAME_MAX);
|
||||||
|
if (total < 2) { total = 2; FRAME_MAX = Math.ceil(bin.length / 2) || 1; }
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const seq = i + 1;
|
||||||
|
const part = bin.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
|
||||||
|
chunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: 'bin' }, body: part }));
|
||||||
|
}
|
||||||
|
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
|
||||||
|
const QR_SIZE = isDesktop ? 720 : 512;
|
||||||
|
if (window.generateQRCode && chunks.length > 0) {
|
||||||
|
const firstUrl = await window.generateQRCode(chunks[0], { errorCorrectionLevel: 'M', size: QR_SIZE, margin: 2 });
|
||||||
|
if (firstUrl) setQrCodeUrl(firstUrl);
|
||||||
|
}
|
||||||
|
try { if (qrAnimationRef.current && qrAnimationRef.current.timer) { clearInterval(qrAnimationRef.current.timer); } } catch {}
|
||||||
|
qrAnimationRef.current = { timer: null, chunks, idx: 0, active: true };
|
||||||
|
setQrFramesTotal(chunks.length);
|
||||||
|
setQrFrameIndex(1);
|
||||||
|
setQrManualMode(false);
|
||||||
|
const intervalMs = 3000;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
||||||
|
try { setShowQRCode(true); } catch {}
|
||||||
|
} else {
|
||||||
|
// Fallback: single QR compressed or plain
|
||||||
|
let url = '';
|
||||||
|
if (typeof window.generateCompressedQRCode === 'function') {
|
||||||
|
url = await window.generateCompressedQRCode(answerString);
|
||||||
|
} else {
|
||||||
|
url = await generateQRCode(answerString);
|
||||||
|
}
|
||||||
|
if (url) setQrCodeUrl(url);
|
||||||
|
try { setShowQRCode(true); } catch {}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Answer QR generation failed:', e);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark answer as created for state management
|
// Mark answer as created for state management
|
||||||
if (e.target.value.trim().length > 0) {
|
if (e.target.value.trim().length > 0) {
|
||||||
@@ -2633,8 +2778,14 @@
|
|||||||
|
|
||||||
let answer;
|
let answer;
|
||||||
try {
|
try {
|
||||||
// Parse the answer data directly (no decryption needed with SAS)
|
// Prefer binary decode first, then gzip JSON
|
||||||
answer = JSON.parse(answerInput.trim());
|
if (typeof window.decodeAnyPayload === 'function') {
|
||||||
|
const anyAnswer = window.decodeAnyPayload(answerInput.trim());
|
||||||
|
answer = (typeof anyAnswer === 'string') ? JSON.parse(anyAnswer) : anyAnswer;
|
||||||
|
} else {
|
||||||
|
const rawText = (typeof window.decompressIfNeeded === 'function') ? window.decompressIfNeeded(answerInput.trim()) : answerInput.trim();
|
||||||
|
answer = JSON.parse(rawText);
|
||||||
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
throw new Error(`Invalid response format: ${parseError.message}`);
|
throw new Error(`Invalid response format: ${parseError.message}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,44 @@
|
|||||||
|
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import { Html5Qrcode } from 'html5-qrcode';
|
import { Html5Qrcode } from 'html5-qrcode';
|
||||||
|
import { gzip, ungzip, deflate, inflate } from 'pako';
|
||||||
|
import * as cbor from 'cbor-js';
|
||||||
import { packSecurePayload, receiveAndProcess } from '../crypto/cose-qr.js';
|
import { packSecurePayload, receiveAndProcess } from '../crypto/cose-qr.js';
|
||||||
|
|
||||||
|
// Compact payload prefix to signal gzip+base64 content
|
||||||
|
const COMPRESSION_PREFIX = 'SB1:gz:';
|
||||||
|
const BINARY_PREFIX = 'SB1:bin:'; // CBOR + deflate + base64url
|
||||||
|
|
||||||
|
function uint8ToBase64(bytes) {
|
||||||
|
let binary = '';
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(i, i + chunkSize);
|
||||||
|
binary += String.fromCharCode.apply(null, chunk);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64ToUint8(b64) {
|
||||||
|
const binary = atob(b64);
|
||||||
|
const len = binary.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compressStringToBase64Gzip(text) {
|
||||||
|
const utf8 = new TextEncoder().encode(text);
|
||||||
|
const gz = gzip(utf8);
|
||||||
|
return uint8ToBase64(gz);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decompressBase64GzipToString(b64) {
|
||||||
|
const gz = base64ToUint8(b64);
|
||||||
|
const out = ungzip(gz);
|
||||||
|
return new TextDecoder().decode(out);
|
||||||
|
}
|
||||||
|
|
||||||
async function generateQRCode(text, opts = {}) {
|
async function generateQRCode(text, opts = {}) {
|
||||||
const size = opts.size || 512;
|
const size = opts.size || 512;
|
||||||
const margin = opts.margin ?? 2;
|
const margin = opts.margin ?? 2;
|
||||||
@@ -16,6 +52,56 @@ async function generateQRCode(text, opts = {}) {
|
|||||||
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
|
return await QRCode.toDataURL(text, { width: size, margin, errorCorrectionLevel });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate QR with gzip+base64 payload and recognizable prefix for scanners
|
||||||
|
async function generateCompressedQRCode(text, opts = {}) {
|
||||||
|
try {
|
||||||
|
const compressedB64 = compressStringToBase64Gzip(text);
|
||||||
|
const payload = COMPRESSION_PREFIX + compressedB64;
|
||||||
|
return await generateQRCode(payload, opts);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('generateCompressedQRCode failed, falling back to plain:', e?.message || e);
|
||||||
|
return await generateQRCode(text, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Binary (CBOR) encode/decode helpers ----
|
||||||
|
function base64ToBase64Url(b64) {
|
||||||
|
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
function base64UrlToBase64(b64url) {
|
||||||
|
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const pad = b64.length % 4;
|
||||||
|
if (pad) b64 += '='.repeat(4 - pad);
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeObjectToBinaryBase64Url(obj) {
|
||||||
|
const cborBytes = cbor.encode(obj);
|
||||||
|
const compressed = deflate(new Uint8Array(cborBytes));
|
||||||
|
const b64 = uint8ToBase64(compressed);
|
||||||
|
return base64ToBase64Url(b64);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeBinaryBase64UrlToObject(b64url) {
|
||||||
|
const b64 = base64UrlToBase64(b64url);
|
||||||
|
const compressed = base64ToUint8(b64);
|
||||||
|
const decompressed = inflate(compressed);
|
||||||
|
const ab = decompressed.buffer.slice(decompressed.byteOffset, decompressed.byteOffset + decompressed.byteLength);
|
||||||
|
return cbor.decode(ab);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateBinaryQRCodeFromObject(obj, opts = {}) {
|
||||||
|
try {
|
||||||
|
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||||
|
const payload = BINARY_PREFIX + b64url;
|
||||||
|
return await generateQRCode(payload, opts);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('generateBinaryQRCodeFromObject failed, falling back to JSON compressed:', e?.message || e);
|
||||||
|
const text = JSON.stringify(obj);
|
||||||
|
return await generateCompressedQRCode(text, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// COSE-based QR generation for large data
|
// COSE-based QR generation for large data
|
||||||
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
||||||
try {
|
try {
|
||||||
@@ -40,9 +126,68 @@ async function generateCOSEQRCode(data, senderKey = null, recipientKey = null) {
|
|||||||
|
|
||||||
// Expose functions to global scope
|
// Expose functions to global scope
|
||||||
window.generateQRCode = generateQRCode;
|
window.generateQRCode = generateQRCode;
|
||||||
|
window.generateCompressedQRCode = generateCompressedQRCode;
|
||||||
|
window.generateBinaryQRCodeFromObject = generateBinaryQRCodeFromObject;
|
||||||
window.generateCOSEQRCode = generateCOSEQRCode;
|
window.generateCOSEQRCode = generateCOSEQRCode;
|
||||||
window.Html5Qrcode = Html5Qrcode;
|
window.Html5Qrcode = Html5Qrcode;
|
||||||
window.packSecurePayload = packSecurePayload;
|
window.packSecurePayload = packSecurePayload;
|
||||||
window.receiveAndProcess = receiveAndProcess;
|
window.receiveAndProcess = receiveAndProcess;
|
||||||
|
|
||||||
console.log('QR libraries loaded: generateQRCode, generateCOSEQRCode, Html5Qrcode, COSE functions');
|
// Expose helper to transparently decompress scanner payloads
|
||||||
|
window.decompressIfNeeded = function (scannedText) {
|
||||||
|
try {
|
||||||
|
if (typeof scannedText === 'string' && scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||||
|
const b64 = scannedText.slice(COMPRESSION_PREFIX.length);
|
||||||
|
return decompressBase64GzipToString(b64);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('decompressIfNeeded failed:', e?.message || e);
|
||||||
|
}
|
||||||
|
return scannedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose helper to get compressed string with prefix for copy/paste flows
|
||||||
|
window.compressToPrefixedGzip = function (text) {
|
||||||
|
try {
|
||||||
|
const payload = String(text || '');
|
||||||
|
const compressedB64 = compressStringToBase64Gzip(payload);
|
||||||
|
return COMPRESSION_PREFIX + compressedB64;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('compressToPrefixedGzip failed:', e?.message || e);
|
||||||
|
return String(text || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose helpers for binary payloads in copy/paste
|
||||||
|
window.encodeBinaryToPrefixed = function (objOrJson) {
|
||||||
|
try {
|
||||||
|
const obj = typeof objOrJson === 'string' ? JSON.parse(objOrJson) : objOrJson;
|
||||||
|
const b64url = encodeObjectToBinaryBase64Url(obj);
|
||||||
|
return BINARY_PREFIX + b64url;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('encodeBinaryToPrefixed failed:', e?.message || e);
|
||||||
|
return typeof objOrJson === 'string' ? objOrJson : JSON.stringify(objOrJson);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.decodeAnyPayload = function (scannedText) {
|
||||||
|
try {
|
||||||
|
if (typeof scannedText === 'string') {
|
||||||
|
if (scannedText.startsWith(BINARY_PREFIX)) {
|
||||||
|
const b64url = scannedText.slice(BINARY_PREFIX.length);
|
||||||
|
return decodeBinaryBase64UrlToObject(b64url); // returns object
|
||||||
|
}
|
||||||
|
if (scannedText.startsWith(COMPRESSION_PREFIX)) {
|
||||||
|
const s = window.decompressIfNeeded(scannedText);
|
||||||
|
return s; // returns JSON string
|
||||||
|
}
|
||||||
|
// Not prefixed: return as-is
|
||||||
|
return scannedText;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('decodeAnyPayload failed:', e?.message || e);
|
||||||
|
}
|
||||||
|
return scannedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('QR libraries loaded: generateQRCode, generateCompressedQRCode, generateBinaryQRCodeFromObject, Html5Qrcode, COSE functions');
|
||||||
Reference in New Issue
Block a user