feat(qr-exchange): improved QR code exchange system
- Updated connection flow between users via QR codes - Added manual switching option in QR code generator - Increased number of QR codes for better readability
This commit is contained in:
File diff suppressed because one or more lines are too long
202
dist/app.js
vendored
202
dist/app.js
vendored
@@ -1004,7 +1004,14 @@ var EnhancedConnectionSetup = ({
|
|||||||
answerPassword,
|
answerPassword,
|
||||||
localVerificationConfirmed,
|
localVerificationConfirmed,
|
||||||
remoteVerificationConfirmed,
|
remoteVerificationConfirmed,
|
||||||
bothVerificationsConfirmed
|
bothVerificationsConfirmed,
|
||||||
|
// QR control props
|
||||||
|
qrFramesTotal,
|
||||||
|
qrFrameIndex,
|
||||||
|
qrManualMode,
|
||||||
|
toggleQrManualMode,
|
||||||
|
nextQrFrame,
|
||||||
|
prevQrFrame
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = React.useState("select");
|
const [mode, setMode] = React.useState("select");
|
||||||
const resetToSelect = () => {
|
const resetToSelect = () => {
|
||||||
@@ -1358,11 +1365,39 @@ var EnhancedConnectionSetup = ({
|
|||||||
src: qrCodeUrl,
|
src: qrCodeUrl,
|
||||||
alt: "QR Code for secure connection",
|
alt: "QR Code for secure connection",
|
||||||
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
||||||
}),
|
})
|
||||||
typeof qrFramesTotal !== "undefined" && typeof qrFrameIndex !== "undefined" && qrFramesTotal > 1 && React.createElement("div", {
|
]),
|
||||||
key: "qr-frame-indicator",
|
// Переключатель управления ниже QR кода
|
||||||
className: "ml-3 self-center text-xs text-gray-300"
|
(qrFramesTotal || 0) >= 1 && React.createElement("div", {
|
||||||
}, `Frame ${Math.max(1, qrFrameIndex || 1)}/${qrFramesTotal}`)
|
key: "qr-controls-below",
|
||||||
|
className: "mt-4 flex flex-col items-center gap-2"
|
||||||
|
}, [
|
||||||
|
React.createElement("div", {
|
||||||
|
key: "frame-indicator",
|
||||||
|
className: "text-xs text-gray-300"
|
||||||
|
}, `Frame ${Math.max(1, qrFrameIndex || 1)}/${qrFramesTotal || 1}`),
|
||||||
|
React.createElement("div", {
|
||||||
|
key: "control-buttons",
|
||||||
|
className: "flex gap-1"
|
||||||
|
}, [
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement("button", {
|
||||||
|
key: "prev-frame",
|
||||||
|
onClick: prevQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, "\u25C0"),
|
||||||
|
React.createElement("button", {
|
||||||
|
key: "toggle-manual",
|
||||||
|
onClick: toggleQrManualMode,
|
||||||
|
className: `px-2 py-1 rounded text-xs font-medium ${qrManualMode || false ? "bg-blue-500 text-white" : "bg-gray-600 text-gray-300 hover:bg-gray-500"}`
|
||||||
|
}, qrManualMode || false ? "Manual" : "Auto"),
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement("button", {
|
||||||
|
key: "next-frame",
|
||||||
|
onClick: nextQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, "\u25B6")
|
||||||
|
])
|
||||||
]),
|
]),
|
||||||
React.createElement("p", {
|
React.createElement("p", {
|
||||||
key: "qr-description",
|
key: "qr-description",
|
||||||
@@ -1679,6 +1714,63 @@ var EnhancedConnectionSetup = ({
|
|||||||
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")
|
||||||
]),
|
]),
|
||||||
|
// QR Code section for answer
|
||||||
|
qrCodeUrl && React.createElement("div", {
|
||||||
|
key: "qr-container",
|
||||||
|
className: "mt-4 p-4 bg-gray-800/50 border border-gray-600/30 rounded-lg text-center"
|
||||||
|
}, [
|
||||||
|
React.createElement("h4", {
|
||||||
|
key: "qr-title",
|
||||||
|
className: "text-sm font-medium text-primary mb-3"
|
||||||
|
}, "Scan QR code to complete connection"),
|
||||||
|
React.createElement("div", {
|
||||||
|
key: "qr-wrapper",
|
||||||
|
className: "flex justify-center"
|
||||||
|
}, [
|
||||||
|
React.createElement("img", {
|
||||||
|
key: "qr-image",
|
||||||
|
src: qrCodeUrl,
|
||||||
|
alt: "QR Code for secure response",
|
||||||
|
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
// Переключатель управления ниже QR кода
|
||||||
|
(qrFramesTotal || 0) >= 1 && React.createElement("div", {
|
||||||
|
key: "qr-controls-below",
|
||||||
|
className: "mt-4 flex flex-col items-center gap-2"
|
||||||
|
}, [
|
||||||
|
React.createElement("div", {
|
||||||
|
key: "frame-indicator",
|
||||||
|
className: "text-xs text-gray-300"
|
||||||
|
}, `Frame ${Math.max(1, qrFrameIndex || 1)}/${qrFramesTotal || 1}`),
|
||||||
|
React.createElement("div", {
|
||||||
|
key: "control-buttons",
|
||||||
|
className: "flex gap-1"
|
||||||
|
}, [
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement("button", {
|
||||||
|
key: "prev-frame",
|
||||||
|
onClick: prevQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, "\u25C0"),
|
||||||
|
React.createElement("button", {
|
||||||
|
key: "toggle-manual",
|
||||||
|
onClick: toggleQrManualMode,
|
||||||
|
className: `px-2 py-1 rounded text-xs font-medium ${qrManualMode ? "bg-blue-500 text-white" : "bg-gray-600 text-gray-300 hover:bg-gray-500"}`
|
||||||
|
}, qrManualMode ? "Manual" : "Auto"),
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement("button", {
|
||||||
|
key: "next-frame",
|
||||||
|
onClick: nextQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, "\u25B6")
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
React.createElement("p", {
|
||||||
|
key: "qr-description",
|
||||||
|
className: "text-xs text-gray-400 mt-2"
|
||||||
|
}, "The initiator can scan this QR code to complete the secure connection")
|
||||||
|
]),
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
key: "info",
|
key: "info",
|
||||||
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
||||||
@@ -2010,6 +2102,7 @@ var EnhancedChatInterface = ({
|
|||||||
};
|
};
|
||||||
var EnhancedSecureP2PChat = () => {
|
var EnhancedSecureP2PChat = () => {
|
||||||
console.log("\u{1F50D} EnhancedSecureP2PChat component initialized");
|
console.log("\u{1F50D} EnhancedSecureP2PChat component initialized");
|
||||||
|
console.log("\u{1F3AE} QR Manual Control Features Loaded!");
|
||||||
const [messages, setMessages] = React.useState([]);
|
const [messages, setMessages] = React.useState([]);
|
||||||
const [connectionStatus, setConnectionStatus] = React.useState("disconnected");
|
const [connectionStatus, setConnectionStatus] = React.useState("disconnected");
|
||||||
const [messageInput, setMessageInput] = React.useState("");
|
const [messageInput, setMessageInput] = React.useState("");
|
||||||
@@ -2052,18 +2145,21 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
const shouldPreserveAnswerData = () => {
|
const shouldPreserveAnswerData = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
||||||
const maxPreserveTime = 3e4;
|
const maxPreserveTime = 3e5;
|
||||||
const hasAnswerData = answerData && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
|
const hasAnswerData = answerData && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
|
||||||
const shouldPreserve = connectionState.hasActiveAnswer && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerData && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect;
|
const hasAnswerQR = qrCodeUrl && qrCodeUrl.trim().length > 0;
|
||||||
|
const shouldPreserve = connectionState.hasActiveAnswer && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerData && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect || hasAnswerQR && answerAge < maxPreserveTime && !connectionState.isUserInitiatedDisconnect;
|
||||||
console.log("\u{1F50D} shouldPreserveAnswerData check:", {
|
console.log("\u{1F50D} shouldPreserveAnswerData check:", {
|
||||||
hasActiveAnswer: connectionState.hasActiveAnswer,
|
hasActiveAnswer: connectionState.hasActiveAnswer,
|
||||||
hasAnswerData,
|
hasAnswerData,
|
||||||
|
hasAnswerQR,
|
||||||
answerAge,
|
answerAge,
|
||||||
maxPreserveTime,
|
maxPreserveTime,
|
||||||
isUserInitiatedDisconnect: connectionState.isUserInitiatedDisconnect,
|
isUserInitiatedDisconnect: connectionState.isUserInitiatedDisconnect,
|
||||||
shouldPreserve,
|
shouldPreserve,
|
||||||
answerData: answerData ? "exists" : "null",
|
answerData: answerData ? "exists" : "null",
|
||||||
answerInput: answerInput ? "exists" : "null"
|
answerInput: answerInput ? "exists" : "null",
|
||||||
|
qrCodeUrl: qrCodeUrl ? "exists" : "null"
|
||||||
});
|
});
|
||||||
return shouldPreserve;
|
return shouldPreserve;
|
||||||
};
|
};
|
||||||
@@ -2547,8 +2643,9 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
return templateOffer;
|
return templateOffer;
|
||||||
};
|
};
|
||||||
const MAX_QR_LEN = 800;
|
const MAX_QR_LEN = 800;
|
||||||
const [qrFramesTotal2, setQrFramesTotal] = React.useState(0);
|
const [qrFramesTotal, setQrFramesTotal] = React.useState(0);
|
||||||
const [qrFrameIndex2, setQrFrameIndex] = React.useState(0);
|
const [qrFrameIndex, setQrFrameIndex] = React.useState(0);
|
||||||
|
const [qrManualMode, setQrManualMode] = React.useState(false);
|
||||||
const qrAnimationRef = React.useRef({ timer: null, chunks: [], idx: 0, active: false });
|
const qrAnimationRef = React.useRef({ timer: null, chunks: [], idx: 0, active: false });
|
||||||
const stopQrAnimation = () => {
|
const stopQrAnimation = () => {
|
||||||
try {
|
try {
|
||||||
@@ -2560,6 +2657,48 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
setQrFrameIndex(0);
|
setQrFrameIndex(0);
|
||||||
setQrFramesTotal(0);
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
|
};
|
||||||
|
const toggleQrManualMode = () => {
|
||||||
|
const newManualMode = !qrManualMode;
|
||||||
|
setQrManualMode(newManualMode);
|
||||||
|
if (newManualMode) {
|
||||||
|
if (qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
}
|
||||||
|
console.log("QR Manual mode enabled - auto-scroll stopped");
|
||||||
|
} else {
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) {
|
||||||
|
const intervalMs = 4e3;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
||||||
|
}
|
||||||
|
console.log("QR Manual mode disabled - auto-scroll resumed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const nextQrFrame = () => {
|
||||||
|
console.log("\u{1F3AE} nextQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
||||||
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
|
setQrFrameIndex(nextIdx + 1);
|
||||||
|
console.log("\u{1F3AE} Next frame index:", nextIdx + 1);
|
||||||
|
renderNext();
|
||||||
|
} else {
|
||||||
|
console.log("\u{1F3AE} No multiple frames to navigate");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const prevQrFrame = () => {
|
||||||
|
console.log("\u{1F3AE} prevQrFrame called, qrFramesTotal:", qrFramesTotal, "qrAnimationRef.current:", qrAnimationRef.current);
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
||||||
|
qrAnimationRef.current.idx = prevIdx;
|
||||||
|
setQrFrameIndex(prevIdx + 1);
|
||||||
|
console.log("\u{1F3AE} Previous frame index:", prevIdx + 1);
|
||||||
|
renderNext();
|
||||||
|
} else {
|
||||||
|
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 generateQRCode2 = async (data) => {
|
||||||
@@ -2581,8 +2720,10 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
console.log("\u{1F39E}\uFE0F Using RAW animated QR frames (no compression)");
|
console.log("\u{1F39E}\uFE0F Using RAW animated QR frames (no compression)");
|
||||||
stopQrAnimation();
|
stopQrAnimation();
|
||||||
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
const FRAME_MAX = Math.max(300, Math.min(750, Math.floor(MAX_QR_LEN * 0.6)));
|
const TARGET_CHUNKS = 10;
|
||||||
|
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
|
||||||
const total = Math.ceil(payload.length / FRAME_MAX);
|
const total = Math.ceil(payload.length / FRAME_MAX);
|
||||||
|
console.log(`\u{1F4CA} Splitting ${payload.length} chars into ${total} chunks (max ${FRAME_MAX} chars per chunk)`);
|
||||||
const rawChunks = [];
|
const rawChunks = [];
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const seq = i + 1;
|
const seq = i + 1;
|
||||||
@@ -2603,7 +2744,7 @@ 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 renderNext = async () => {
|
const renderNext2 = async () => {
|
||||||
const { chunks, idx, active } = qrAnimationRef.current;
|
const { chunks, idx, active } = qrAnimationRef.current;
|
||||||
if (!active || !chunks.length) return;
|
if (!active || !chunks.length) return;
|
||||||
const current = chunks[idx % chunks.length];
|
const current = chunks[idx % chunks.length];
|
||||||
@@ -2617,11 +2758,13 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
qrAnimationRef.current.idx = nextIdx;
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
setQrFrameIndex(nextIdx + 1);
|
setQrFrameIndex(nextIdx + 1);
|
||||||
};
|
};
|
||||||
await renderNext();
|
await renderNext2();
|
||||||
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
|
if (!qrManualMode) {
|
||||||
const isIOS = /iPhone|iPad|iPod/i.test(ua);
|
const ua = typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : "";
|
||||||
const intervalMs = isIOS ? 2500 : 2e3;
|
const isIOS = /iPhone|iPad|iPod/i.test(ua);
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
const intervalMs = 4e3;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderNext2, intervalMs);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("QR code generation failed:", error);
|
console.error("QR code generation failed:", error);
|
||||||
@@ -2936,6 +3079,10 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
console.log("Secure answer created:", answer);
|
console.log("Secure answer created:", answer);
|
||||||
setAnswerData(answer);
|
setAnswerData(answer);
|
||||||
setShowAnswerStep(true);
|
setShowAnswerStep(true);
|
||||||
|
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
||||||
|
console.log("Generating QR code for answer data length:", answerString.length);
|
||||||
|
console.log("First 100 chars of answer data:", answerString.substring(0, 100));
|
||||||
|
await generateQRCode2(answerString);
|
||||||
markAnswerCreated2();
|
markAnswerCreated2();
|
||||||
const existingResponseMessages = messages.filter(
|
const existingResponseMessages = messages.filter(
|
||||||
(m) => m.type === "system" && (m.message.includes("Secure response created") || m.message.includes("Send the response"))
|
(m) => m.type === "system" && (m.message.includes("Secure response created") || m.message.includes("Send the response"))
|
||||||
@@ -2948,7 +3095,7 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}]);
|
}]);
|
||||||
setMessages((prev) => [...prev, {
|
setMessages((prev) => [...prev, {
|
||||||
message: "\u{1F4E4} Send the response code to the initiator via a secure channel..",
|
message: "\u{1F4E4} Send the response code to the initiator via a secure channel or let them scan the QR code below.",
|
||||||
type: "system",
|
type: "system",
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
@@ -3163,12 +3310,16 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
setOfferInput("");
|
setOfferInput("");
|
||||||
setAnswerInput("");
|
setAnswerInput("");
|
||||||
setShowOfferStep(false);
|
setShowOfferStep(false);
|
||||||
setShowAnswerStep(false);
|
if (!shouldPreserveAnswerData()) {
|
||||||
|
setShowAnswerStep(false);
|
||||||
|
}
|
||||||
setShowVerification(false);
|
setShowVerification(false);
|
||||||
setShowQRCode(false);
|
setShowQRCode(false);
|
||||||
setShowQRScanner(false);
|
setShowQRScanner(false);
|
||||||
setShowQRScannerModal(false);
|
setShowQRScannerModal(false);
|
||||||
setQrCodeUrl("");
|
if (!shouldPreserveAnswerData()) {
|
||||||
|
setQrCodeUrl("");
|
||||||
|
}
|
||||||
setVerificationCode("");
|
setVerificationCode("");
|
||||||
setIsVerified(false);
|
setIsVerified(false);
|
||||||
setKeyFingerprint("");
|
setKeyFingerprint("");
|
||||||
@@ -3327,7 +3478,14 @@ var EnhancedSecureP2PChat = () => {
|
|||||||
messages,
|
messages,
|
||||||
localVerificationConfirmed,
|
localVerificationConfirmed,
|
||||||
remoteVerificationConfirmed,
|
remoteVerificationConfirmed,
|
||||||
bothVerificationsConfirmed
|
bothVerificationsConfirmed,
|
||||||
|
// QR control props
|
||||||
|
qrFramesTotal,
|
||||||
|
qrFrameIndex,
|
||||||
|
qrManualMode,
|
||||||
|
toggleQrManualMode,
|
||||||
|
nextQrFrame,
|
||||||
|
prevQrFrame
|
||||||
// PAKE passwords removed - using SAS verification instead
|
// PAKE passwords removed - using SAS verification instead
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
6
dist/app.js.map
vendored
6
dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/qr-local.js
vendored
4
dist/qr-local.js
vendored
@@ -36171,7 +36171,8 @@ async function packSecurePayload(payloadObj, senderEcdsaPrivKey = null, recipien
|
|||||||
const compressed = deflate_1(cborFinal);
|
const compressed = deflate_1(cborFinal);
|
||||||
const encoded = toBase64Url(compressed);
|
const encoded = toBase64Url(compressed);
|
||||||
console.log(`\u{1F4CA} Compressed size: ${encoded.length} characters (${Math.round((1 - encoded.length / payloadJson.length) * 100)}% reduction)`);
|
console.log(`\u{1F4CA} Compressed size: ${encoded.length} characters (${Math.round((1 - encoded.length / payloadJson.length) * 100)}% reduction)`);
|
||||||
const QR_MAX = 900;
|
const TARGET_CHUNKS = 10;
|
||||||
|
const QR_MAX = Math.max(200, Math.floor(encoded.length / TARGET_CHUNKS));
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
if (encoded.length <= QR_MAX) {
|
if (encoded.length <= QR_MAX) {
|
||||||
chunks.push(JSON.stringify({
|
chunks.push(JSON.stringify({
|
||||||
@@ -36181,6 +36182,7 @@ async function packSecurePayload(payloadObj, senderEcdsaPrivKey = null, recipien
|
|||||||
} else {
|
} else {
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
const totalChunks = Math.ceil(encoded.length / QR_MAX);
|
const totalChunks = Math.ceil(encoded.length / QR_MAX);
|
||||||
|
console.log(`\u{1F4CA} COSE: Splitting ${encoded.length} chars into ${totalChunks} chunks (max ${QR_MAX} chars per chunk)`);
|
||||||
for (let i = 0, seq = 1; i < encoded.length; i += QR_MAX, seq++) {
|
for (let i = 0, seq = 1; i < encoded.length; i += QR_MAX, seq++) {
|
||||||
const part = encoded.slice(i, i + QR_MAX);
|
const part = encoded.slice(i, i + QR_MAX);
|
||||||
chunks.push(JSON.stringify({
|
chunks.push(JSON.stringify({
|
||||||
|
|||||||
4
dist/qr-local.js.map
vendored
4
dist/qr-local.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -103,13 +103,13 @@
|
|||||||
<link rel="stylesheet" href="src/styles/animations.css">
|
<link rel="stylesheet" href="src/styles/animations.css">
|
||||||
<link rel="stylesheet" href="src/styles/components.css">
|
<link rel="stylesheet" href="src/styles/components.css">
|
||||||
<script src="src/scripts/fa-check.js"></script>
|
<script src="src/scripts/fa-check.js"></script>
|
||||||
<script type="module" src="dist/qr-local.js"></script>
|
<script type="module" src="dist/qr-local.js?v=1757383302"></script>
|
||||||
<script type="module" src="src/components/QRScanner.js?v=2"></script>
|
<script type="module" src="src/components/QRScanner.js?v=3"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="dist/app.js?v=1757383301"></script>
|
<script type="module" src="dist/app.js?v=1757383304"></script>
|
||||||
<script type="module" src="dist/app-boot.js?v=1757383301"></script>
|
<script type="module" src="dist/app-boot.js?v=1757383304"></script>
|
||||||
|
|
||||||
<script src="src/scripts/pwa-register.js"></script>
|
<script src="src/scripts/pwa-register.js"></script>
|
||||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||||
|
|||||||
BIN
public.zip
BIN
public.zip
Binary file not shown.
225
src/app.jsx
225
src/app.jsx
@@ -1257,7 +1257,14 @@
|
|||||||
answerPassword,
|
answerPassword,
|
||||||
localVerificationConfirmed,
|
localVerificationConfirmed,
|
||||||
remoteVerificationConfirmed,
|
remoteVerificationConfirmed,
|
||||||
bothVerificationsConfirmed
|
bothVerificationsConfirmed,
|
||||||
|
// QR control props
|
||||||
|
qrFramesTotal,
|
||||||
|
qrFrameIndex,
|
||||||
|
qrManualMode,
|
||||||
|
toggleQrManualMode,
|
||||||
|
nextQrFrame,
|
||||||
|
prevQrFrame
|
||||||
}) => {
|
}) => {
|
||||||
const [mode, setMode] = React.useState('select');
|
const [mode, setMode] = React.useState('select');
|
||||||
|
|
||||||
@@ -1627,11 +1634,44 @@
|
|||||||
src: qrCodeUrl,
|
src: qrCodeUrl,
|
||||||
alt: "QR Code for secure connection",
|
alt: "QR Code for secure connection",
|
||||||
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
||||||
}),
|
})
|
||||||
(typeof qrFramesTotal !== 'undefined' && typeof qrFrameIndex !== 'undefined' && qrFramesTotal > 1) && React.createElement('div', {
|
]),
|
||||||
key: 'qr-frame-indicator',
|
|
||||||
className: "ml-3 self-center text-xs text-gray-300"
|
// Переключатель управления ниже QR кода
|
||||||
}, `Frame ${Math.max(1, qrFrameIndex || 1)}/${qrFramesTotal}`)
|
((qrFramesTotal || 0) >= 1) && React.createElement('div', {
|
||||||
|
key: 'qr-controls-below',
|
||||||
|
className: "mt-4 flex flex-col items-center gap-2"
|
||||||
|
}, [
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'frame-indicator',
|
||||||
|
className: "text-xs text-gray-300"
|
||||||
|
}, `Frame ${Math.max(1, (qrFrameIndex || 1))}/${qrFramesTotal || 1}`),
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'control-buttons',
|
||||||
|
className: "flex gap-1"
|
||||||
|
}, [
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement('button', {
|
||||||
|
key: 'prev-frame',
|
||||||
|
onClick: prevQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, '◀'),
|
||||||
|
React.createElement('button', {
|
||||||
|
key: 'toggle-manual',
|
||||||
|
onClick: toggleQrManualMode,
|
||||||
|
className: `px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
(qrManualMode || false)
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||||
|
}`
|
||||||
|
}, (qrManualMode || false) ? 'Manual' : 'Auto'),
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement('button', {
|
||||||
|
key: 'next-frame',
|
||||||
|
onClick: nextQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, '▶')
|
||||||
|
])
|
||||||
]),
|
]),
|
||||||
React.createElement('p', {
|
React.createElement('p', {
|
||||||
key: 'qr-description',
|
key: 'qr-description',
|
||||||
@@ -1958,6 +1998,68 @@
|
|||||||
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')
|
||||||
]),
|
]),
|
||||||
|
// QR Code section for answer
|
||||||
|
qrCodeUrl && React.createElement('div', {
|
||||||
|
key: 'qr-container',
|
||||||
|
className: "mt-4 p-4 bg-gray-800/50 border border-gray-600/30 rounded-lg text-center"
|
||||||
|
}, [
|
||||||
|
React.createElement('h4', {
|
||||||
|
key: 'qr-title',
|
||||||
|
className: "text-sm font-medium text-primary mb-3"
|
||||||
|
}, 'Scan QR code to complete connection'),
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'qr-wrapper',
|
||||||
|
className: "flex justify-center"
|
||||||
|
}, [
|
||||||
|
React.createElement('img', {
|
||||||
|
key: 'qr-image',
|
||||||
|
src: qrCodeUrl,
|
||||||
|
alt: "QR Code for secure response",
|
||||||
|
className: "max-w-none h-auto border border-gray-600/30 rounded w-[20rem] sm:w-[24rem] md:w-[28rem] lg:w-[32rem]"
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Переключатель управления ниже QR кода
|
||||||
|
((qrFramesTotal || 0) >= 1) && React.createElement('div', {
|
||||||
|
key: 'qr-controls-below',
|
||||||
|
className: "mt-4 flex flex-col items-center gap-2"
|
||||||
|
}, [
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'frame-indicator',
|
||||||
|
className: "text-xs text-gray-300"
|
||||||
|
}, `Frame ${Math.max(1, (qrFrameIndex || 1))}/${qrFramesTotal || 1}`),
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'control-buttons',
|
||||||
|
className: "flex gap-1"
|
||||||
|
}, [
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement('button', {
|
||||||
|
key: 'prev-frame',
|
||||||
|
onClick: prevQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, '◀'),
|
||||||
|
React.createElement('button', {
|
||||||
|
key: 'toggle-manual',
|
||||||
|
onClick: toggleQrManualMode,
|
||||||
|
className: `px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
qrManualMode
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||||
|
}`
|
||||||
|
}, qrManualMode ? 'Manual' : 'Auto'),
|
||||||
|
// Кнопки навигации показываем только если больше 1 части
|
||||||
|
(qrFramesTotal || 0) > 1 && React.createElement('button', {
|
||||||
|
key: 'next-frame',
|
||||||
|
onClick: nextQrFrame,
|
||||||
|
className: "w-6 h-6 bg-gray-600 hover:bg-gray-500 text-white rounded text-xs flex items-center justify-center"
|
||||||
|
}, '▶')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
React.createElement('p', {
|
||||||
|
key: 'qr-description',
|
||||||
|
className: "text-xs text-gray-400 mt-2"
|
||||||
|
}, 'The initiator can scan this QR code to complete the secure connection')
|
||||||
|
]),
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
key: 'info',
|
key: 'info',
|
||||||
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
||||||
@@ -2323,6 +2425,7 @@
|
|||||||
// Main Enhanced Application Component
|
// Main Enhanced Application Component
|
||||||
const EnhancedSecureP2PChat = () => {
|
const EnhancedSecureP2PChat = () => {
|
||||||
console.log('🔍 EnhancedSecureP2PChat component initialized');
|
console.log('🔍 EnhancedSecureP2PChat component initialized');
|
||||||
|
console.log('🎮 QR Manual Control Features Loaded!');
|
||||||
const [messages, setMessages] = React.useState([]);
|
const [messages, setMessages] = React.useState([]);
|
||||||
const [connectionStatus, setConnectionStatus] = React.useState('disconnected');
|
const [connectionStatus, setConnectionStatus] = React.useState('disconnected');
|
||||||
|
|
||||||
@@ -2387,27 +2490,34 @@
|
|||||||
const shouldPreserveAnswerData = () => {
|
const shouldPreserveAnswerData = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
||||||
const maxPreserveTime = 30000; // 30 seconds
|
const maxPreserveTime = 300000; // 5 minutes (увеличиваем время для QR кода)
|
||||||
|
|
||||||
// Дополнительная проверка на основе самих данных
|
// Дополнительная проверка на основе самих данных
|
||||||
const hasAnswerData = (answerData && answerData.trim().length > 0) ||
|
const hasAnswerData = (answerData && answerData.trim().length > 0) ||
|
||||||
(answerInput && answerInput.trim().length > 0);
|
(answerInput && answerInput.trim().length > 0);
|
||||||
|
|
||||||
|
// Проверяем наличие QR кода ответа
|
||||||
|
const hasAnswerQR = qrCodeUrl && qrCodeUrl.trim().length > 0;
|
||||||
|
|
||||||
const shouldPreserve = (connectionState.hasActiveAnswer &&
|
const shouldPreserve = (connectionState.hasActiveAnswer &&
|
||||||
answerAge < maxPreserveTime &&
|
answerAge < maxPreserveTime &&
|
||||||
!connectionState.isUserInitiatedDisconnect) ||
|
!connectionState.isUserInitiatedDisconnect) ||
|
||||||
(hasAnswerData && answerAge < maxPreserveTime &&
|
(hasAnswerData && answerAge < maxPreserveTime &&
|
||||||
|
!connectionState.isUserInitiatedDisconnect) ||
|
||||||
|
(hasAnswerQR && answerAge < maxPreserveTime &&
|
||||||
!connectionState.isUserInitiatedDisconnect);
|
!connectionState.isUserInitiatedDisconnect);
|
||||||
|
|
||||||
console.log('🔍 shouldPreserveAnswerData check:', {
|
console.log('🔍 shouldPreserveAnswerData check:', {
|
||||||
hasActiveAnswer: connectionState.hasActiveAnswer,
|
hasActiveAnswer: connectionState.hasActiveAnswer,
|
||||||
hasAnswerData: hasAnswerData,
|
hasAnswerData: hasAnswerData,
|
||||||
|
hasAnswerQR: hasAnswerQR,
|
||||||
answerAge: answerAge,
|
answerAge: answerAge,
|
||||||
maxPreserveTime: maxPreserveTime,
|
maxPreserveTime: maxPreserveTime,
|
||||||
isUserInitiatedDisconnect: connectionState.isUserInitiatedDisconnect,
|
isUserInitiatedDisconnect: connectionState.isUserInitiatedDisconnect,
|
||||||
shouldPreserve: shouldPreserve,
|
shouldPreserve: shouldPreserve,
|
||||||
answerData: answerData ? 'exists' : 'null',
|
answerData: answerData ? 'exists' : 'null',
|
||||||
answerInput: answerInput ? 'exists' : 'null'
|
answerInput: answerInput ? 'exists' : 'null',
|
||||||
|
qrCodeUrl: qrCodeUrl ? 'exists' : 'null'
|
||||||
});
|
});
|
||||||
|
|
||||||
return shouldPreserve;
|
return shouldPreserve;
|
||||||
@@ -3039,6 +3149,7 @@
|
|||||||
const MAX_QR_LEN = 800;
|
const MAX_QR_LEN = 800;
|
||||||
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);
|
||||||
|
|
||||||
// Animated QR state (for multi-chunk COSE)
|
// Animated QR state (for multi-chunk COSE)
|
||||||
const qrAnimationRef = React.useRef({ timer: null, chunks: [], idx: 0, active: false });
|
const qrAnimationRef = React.useRef({ timer: null, chunks: [], idx: 0, active: false });
|
||||||
@@ -3047,6 +3158,55 @@
|
|||||||
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
||||||
setQrFrameIndex(0);
|
setQrFrameIndex(0);
|
||||||
setQrFramesTotal(0);
|
setQrFramesTotal(0);
|
||||||
|
setQrManualMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функции для ручного управления QR анимацией
|
||||||
|
const toggleQrManualMode = () => {
|
||||||
|
const newManualMode = !qrManualMode;
|
||||||
|
setQrManualMode(newManualMode);
|
||||||
|
|
||||||
|
if (newManualMode) {
|
||||||
|
// Останавливаем автопрокрутку
|
||||||
|
if (qrAnimationRef.current.timer) {
|
||||||
|
clearInterval(qrAnimationRef.current.timer);
|
||||||
|
qrAnimationRef.current.timer = null;
|
||||||
|
}
|
||||||
|
console.log('QR Manual mode enabled - auto-scroll stopped');
|
||||||
|
} else {
|
||||||
|
// Возобновляем автопрокрутку
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1 && qrAnimationRef.current.active) {
|
||||||
|
const intervalMs = 4000;
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
||||||
|
}
|
||||||
|
console.log('QR Manual mode disabled - auto-scroll resumed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextQrFrame = () => {
|
||||||
|
console.log('🎮 nextQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const nextIdx = (qrAnimationRef.current.idx + 1) % qrAnimationRef.current.chunks.length;
|
||||||
|
qrAnimationRef.current.idx = nextIdx;
|
||||||
|
setQrFrameIndex(nextIdx + 1);
|
||||||
|
console.log('🎮 Next frame index:', nextIdx + 1);
|
||||||
|
renderNext();
|
||||||
|
} else {
|
||||||
|
console.log('🎮 No multiple frames to navigate');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevQrFrame = () => {
|
||||||
|
console.log('🎮 prevQrFrame called, qrFramesTotal:', qrFramesTotal, 'qrAnimationRef.current:', qrAnimationRef.current);
|
||||||
|
if (qrAnimationRef.current.chunks.length > 1) {
|
||||||
|
const prevIdx = (qrAnimationRef.current.idx - 1 + qrAnimationRef.current.chunks.length) % qrAnimationRef.current.chunks.length;
|
||||||
|
qrAnimationRef.current.idx = prevIdx;
|
||||||
|
setQrFrameIndex(prevIdx + 1);
|
||||||
|
console.log('🎮 Previous frame index:', prevIdx + 1);
|
||||||
|
renderNext();
|
||||||
|
} else {
|
||||||
|
console.log('🎮 No multiple frames to navigate');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Buffer for assembling scanned COSE chunks
|
// Buffer for assembling scanned COSE chunks
|
||||||
@@ -3074,8 +3234,13 @@
|
|||||||
console.log('🎞️ Using RAW animated QR frames (no compression)');
|
console.log('🎞️ Using RAW animated QR frames (no compression)');
|
||||||
stopQrAnimation();
|
stopQrAnimation();
|
||||||
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
const id = `raw_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||||
const FRAME_MAX = Math.max(300, Math.min(750, Math.floor(MAX_QR_LEN * 0.6)));
|
|
||||||
|
// Принудительно разбиваем на 10 частей для лучшего сканирования
|
||||||
|
const TARGET_CHUNKS = 10;
|
||||||
|
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
|
||||||
const total = Math.ceil(payload.length / FRAME_MAX);
|
const total = Math.ceil(payload.length / FRAME_MAX);
|
||||||
|
|
||||||
|
console.log(`📊 Splitting ${payload.length} chars into ${total} chunks (max ${FRAME_MAX} chars per chunk)`);
|
||||||
const rawChunks = [];
|
const rawChunks = [];
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const seq = i + 1;
|
const seq = i + 1;
|
||||||
@@ -3111,10 +3276,14 @@
|
|||||||
setQrFrameIndex(nextIdx + 1);
|
setQrFrameIndex(nextIdx + 1);
|
||||||
};
|
};
|
||||||
await renderNext();
|
await renderNext();
|
||||||
const ua = (typeof navigator !== 'undefined' && navigator.userAgent) ? navigator.userAgent : '';
|
|
||||||
const isIOS = /iPhone|iPad|iPod/i.test(ua);
|
// Запускаем автопрокрутку только если не в ручном режиме
|
||||||
const intervalMs = isIOS ? 2500 : 2000; // Slower animation for better readability
|
if (!qrManualMode) {
|
||||||
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
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
|
||||||
|
qrAnimationRef.current.timer = setInterval(renderNext, intervalMs);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('QR code generation failed:', error);
|
console.error('QR code generation failed:', error);
|
||||||
@@ -3488,6 +3657,12 @@
|
|||||||
setAnswerData(answer);
|
setAnswerData(answer);
|
||||||
setShowAnswerStep(true);
|
setShowAnswerStep(true);
|
||||||
|
|
||||||
|
// Generate QR code for the answer data
|
||||||
|
const answerString = typeof answer === 'object' ? JSON.stringify(answer) : answer;
|
||||||
|
console.log('Generating QR code for answer data length:', answerString.length);
|
||||||
|
console.log('First 100 chars of answer data:', answerString.substring(0, 100));
|
||||||
|
await generateQRCode(answerString);
|
||||||
|
|
||||||
// Mark answer as created for state management
|
// Mark answer as created for state management
|
||||||
markAnswerCreated();
|
markAnswerCreated();
|
||||||
|
|
||||||
@@ -3505,7 +3680,7 @@
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
message: '📤 Send the response code to the initiator via a secure channel..',
|
message: '📤 Send the response code to the initiator via a secure channel or let them scan the QR code below.',
|
||||||
type: 'system',
|
type: 'system',
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
@@ -3774,12 +3949,23 @@
|
|||||||
setOfferInput('');
|
setOfferInput('');
|
||||||
setAnswerInput('');
|
setAnswerInput('');
|
||||||
setShowOfferStep(false);
|
setShowOfferStep(false);
|
||||||
setShowAnswerStep(false);
|
|
||||||
|
// Сохраняем showAnswerStep если есть QR код ответа
|
||||||
|
if (!shouldPreserveAnswerData()) {
|
||||||
|
setShowAnswerStep(false);
|
||||||
|
}
|
||||||
|
|
||||||
setShowVerification(false);
|
setShowVerification(false);
|
||||||
setShowQRCode(false);
|
setShowQRCode(false);
|
||||||
setShowQRScanner(false);
|
setShowQRScanner(false);
|
||||||
setShowQRScannerModal(false);
|
setShowQRScannerModal(false);
|
||||||
setQrCodeUrl('');
|
|
||||||
|
// Сохраняем QR код ответа, если он был создан
|
||||||
|
// (не сбрасываем qrCodeUrl если есть активный ответ)
|
||||||
|
if (!shouldPreserveAnswerData()) {
|
||||||
|
setQrCodeUrl('');
|
||||||
|
}
|
||||||
|
|
||||||
setVerificationCode('');
|
setVerificationCode('');
|
||||||
setIsVerified(false);
|
setIsVerified(false);
|
||||||
setKeyFingerprint('');
|
setKeyFingerprint('');
|
||||||
@@ -3988,6 +4174,13 @@
|
|||||||
localVerificationConfirmed: localVerificationConfirmed,
|
localVerificationConfirmed: localVerificationConfirmed,
|
||||||
remoteVerificationConfirmed: remoteVerificationConfirmed,
|
remoteVerificationConfirmed: remoteVerificationConfirmed,
|
||||||
bothVerificationsConfirmed: bothVerificationsConfirmed,
|
bothVerificationsConfirmed: bothVerificationsConfirmed,
|
||||||
|
// QR control props
|
||||||
|
qrFramesTotal: qrFramesTotal,
|
||||||
|
qrFrameIndex: qrFrameIndex,
|
||||||
|
qrManualMode: qrManualMode,
|
||||||
|
toggleQrManualMode: toggleQrManualMode,
|
||||||
|
nextQrFrame: nextQrFrame,
|
||||||
|
prevQrFrame: prevQrFrame,
|
||||||
// PAKE passwords removed - using SAS verification instead
|
// PAKE passwords removed - using SAS verification instead
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
const [isScanning, setIsScanning] = React.useState(false);
|
const [isScanning, setIsScanning] = React.useState(false);
|
||||||
const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 });
|
const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 });
|
||||||
|
const [showFocusHint, setShowFocusHint] = React.useState(false);
|
||||||
|
const [manualMode, setManualMode] = React.useState(false);
|
||||||
|
const [scannedParts, setScannedParts] = React.useState(new Set());
|
||||||
|
const [currentQRId, setCurrentQRId] = React.useState(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
@@ -23,6 +27,15 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
const { id, seq, total } = e.detail || {};
|
const { id, seq, total } = e.detail || {};
|
||||||
if (!id || !total) return;
|
if (!id || !total) return;
|
||||||
setProgress({ id, seq, total });
|
setProgress({ id, seq, total });
|
||||||
|
|
||||||
|
// Обновляем ID текущего QR кода
|
||||||
|
if (id !== currentQRId) {
|
||||||
|
setCurrentQRId(id);
|
||||||
|
setScannedParts(new Set()); // Сбрасываем сканированные части для нового ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем отсканированную часть
|
||||||
|
setScannedParts(prev => new Set([...prev, seq]));
|
||||||
};
|
};
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
// Close scanner once app signals completion
|
// Close scanner once app signals completion
|
||||||
@@ -35,7 +48,54 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
document.removeEventListener('qr-scan-progress', onProgress, { passive: true });
|
document.removeEventListener('qr-scan-progress', onProgress, { passive: true });
|
||||||
document.removeEventListener('qr-scan-complete', onComplete, { passive: true });
|
document.removeEventListener('qr-scan-complete', onComplete, { passive: true });
|
||||||
};
|
};
|
||||||
}, []);
|
}, [currentQRId]);
|
||||||
|
|
||||||
|
// Функция для tap-to-focus
|
||||||
|
const handleTapToFocus = (event, html5Qrcode) => {
|
||||||
|
try {
|
||||||
|
// Показываем подсказку о фокусировке
|
||||||
|
setShowFocusHint(true);
|
||||||
|
setTimeout(() => setShowFocusHint(false), 2000);
|
||||||
|
|
||||||
|
// Получаем координаты клика относительно видео элемента
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
// Нормализуем координаты (0-1)
|
||||||
|
const normalizedX = x / rect.width;
|
||||||
|
const normalizedY = y / rect.height;
|
||||||
|
|
||||||
|
console.log('Tap to focus at:', { x, y, normalizedX, normalizedY });
|
||||||
|
|
||||||
|
// Попытка программной фокусировки (если поддерживается браузером)
|
||||||
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||||
|
// Это может не работать во всех браузерах, но попробуем
|
||||||
|
console.log('Attempting programmatic focus...');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Tap to focus error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функции для ручного управления
|
||||||
|
const toggleManualMode = () => {
|
||||||
|
setManualMode(!manualMode);
|
||||||
|
if (!manualMode) {
|
||||||
|
// При включении ручного режима останавливаем автопрокрутку
|
||||||
|
console.log('Manual mode enabled - auto-scroll disabled');
|
||||||
|
} else {
|
||||||
|
// При выключении ручного режима возобновляем автопрокрутку
|
||||||
|
console.log('Manual mode disabled - auto-scroll enabled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetProgress = () => {
|
||||||
|
setScannedParts(new Set());
|
||||||
|
setCurrentQRId(null);
|
||||||
|
setProgress({ id: null, seq: 0, total: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
const startScanner = async () => {
|
const startScanner = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -116,7 +176,16 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
cameraId, // Use specific camera ID
|
cameraId, // Use specific camera ID
|
||||||
{
|
{
|
||||||
fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3,
|
fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3,
|
||||||
qrbox: { width: qrboxSize, height: qrboxSize }
|
qrbox: { width: qrboxSize, height: qrboxSize },
|
||||||
|
// Улучшенные настройки для мобильных устройств
|
||||||
|
aspectRatio: 1.0,
|
||||||
|
videoConstraints: {
|
||||||
|
focusMode: "continuous", // Непрерывная автофокусировка
|
||||||
|
exposureMode: "continuous", // Непрерывная экспозиция
|
||||||
|
whiteBalanceMode: "continuous", // Непрерывный баланс белого
|
||||||
|
torch: false, // Вспышка выключена по умолчанию
|
||||||
|
facingMode: "environment" // Используем заднюю камеру
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(decodedText, decodedResult) => {
|
(decodedText, decodedResult) => {
|
||||||
console.log('QR Code detected:', decodedText);
|
console.log('QR Code detected:', decodedText);
|
||||||
@@ -153,6 +222,13 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
qrScannerRef.current = html5Qrcode;
|
qrScannerRef.current = html5Qrcode;
|
||||||
console.log('QR scanner started successfully');
|
console.log('QR scanner started successfully');
|
||||||
|
|
||||||
|
// Добавляем обработчик tap-to-focus для мобильных устройств
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.addEventListener('click', (event) => {
|
||||||
|
handleTapToFocus(event, html5Qrcode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error starting QR scanner:', err);
|
console.error('Error starting QR scanner:', err);
|
||||||
let errorMessage = 'Failed to start camera';
|
let errorMessage = 'Failed to start camera';
|
||||||
@@ -232,6 +308,66 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Индикатор прогресса сканирования
|
||||||
|
progress.total > 1 && React.createElement('div', {
|
||||||
|
key: 'progress-indicator',
|
||||||
|
className: "mb-4 p-3 bg-gray-800/50 border border-gray-600/30 rounded-lg"
|
||||||
|
}, [
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'progress-header',
|
||||||
|
className: "flex items-center justify-between mb-2"
|
||||||
|
}, [
|
||||||
|
React.createElement('span', {
|
||||||
|
key: 'progress-title',
|
||||||
|
className: "text-sm text-gray-300"
|
||||||
|
}, `QR ID: ${currentQRId ? currentQRId.substring(0, 8) + '...' : 'N/A'}`),
|
||||||
|
React.createElement('span', {
|
||||||
|
key: 'progress-count',
|
||||||
|
className: "text-sm text-blue-400"
|
||||||
|
}, `${scannedParts.size}/${progress.total} scanned`)
|
||||||
|
]),
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'progress-numbers',
|
||||||
|
className: "flex flex-wrap gap-1"
|
||||||
|
}, Array.from({ length: progress.total }, (_, i) => {
|
||||||
|
const partNumber = i + 1;
|
||||||
|
const isScanned = scannedParts.has(partNumber);
|
||||||
|
return React.createElement('div', {
|
||||||
|
key: `part-${partNumber}`,
|
||||||
|
className: `w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-colors ${
|
||||||
|
isScanned
|
||||||
|
? 'bg-green-500 text-white'
|
||||||
|
: 'bg-gray-600 text-gray-300'
|
||||||
|
}`
|
||||||
|
}, partNumber);
|
||||||
|
}))
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Панель управления
|
||||||
|
progress.total > 1 && React.createElement('div', {
|
||||||
|
key: 'control-panel',
|
||||||
|
className: "mb-4 flex gap-2"
|
||||||
|
}, [
|
||||||
|
React.createElement('button', {
|
||||||
|
key: 'manual-toggle',
|
||||||
|
onClick: toggleManualMode,
|
||||||
|
className: `px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||||
|
manualMode
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||||
|
}`
|
||||||
|
}, manualMode ? 'Manual Mode' : 'Auto Mode'),
|
||||||
|
React.createElement('button', {
|
||||||
|
key: 'reset-progress',
|
||||||
|
onClick: resetProgress,
|
||||||
|
className: "px-3 py-1 bg-red-500/20 text-red-400 border border-red-500/20 rounded text-xs font-medium hover:bg-red-500/30"
|
||||||
|
}, 'Reset'),
|
||||||
|
React.createElement('span', {
|
||||||
|
key: 'mode-hint',
|
||||||
|
className: "text-xs text-gray-400 self-center"
|
||||||
|
}, manualMode ? 'Tap to focus, scan manually' : 'Auto-scrolling enabled')
|
||||||
|
]),
|
||||||
|
|
||||||
React.createElement('div', {
|
React.createElement('div', {
|
||||||
key: 'scanner-content',
|
key: 'scanner-content',
|
||||||
@@ -297,12 +433,56 @@ const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
|||||||
React.createElement('p', {
|
React.createElement('p', {
|
||||||
key: 'scanning-text',
|
key: 'scanning-text',
|
||||||
className: "text-xs"
|
className: "text-xs"
|
||||||
}, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code')
|
}, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code'),
|
||||||
|
React.createElement('p', {
|
||||||
|
key: 'tap-hint',
|
||||||
|
className: "text-xs text-blue-300 mt-1"
|
||||||
|
}, 'Tap screen to focus')
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Подсказка о фокусировке
|
||||||
|
showFocusHint && React.createElement('div', {
|
||||||
|
key: 'focus-hint',
|
||||||
|
className: "absolute top-4 left-1/2 transform -translate-x-1/2 bg-green-500/90 text-white px-3 py-1 rounded-full text-xs font-medium z-10"
|
||||||
|
}, 'Focusing...'),
|
||||||
// Bottom overlay kept simple on mobile
|
// Bottom overlay kept simple on mobile
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Дополнительные подсказки для улучшения сканирования
|
||||||
|
React.createElement('div', {
|
||||||
|
key: 'scanning-tips',
|
||||||
|
className: "mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
||||||
|
}, [
|
||||||
|
React.createElement('h4', {
|
||||||
|
key: 'tips-title',
|
||||||
|
className: "text-blue-400 text-sm font-medium mb-2 flex items-center"
|
||||||
|
}, [
|
||||||
|
React.createElement('i', {
|
||||||
|
key: 'tips-icon',
|
||||||
|
className: 'fas fa-lightbulb mr-2'
|
||||||
|
}),
|
||||||
|
'Tips for better scanning:'
|
||||||
|
]),
|
||||||
|
React.createElement('ul', {
|
||||||
|
key: 'tips-list',
|
||||||
|
className: "text-xs text-blue-300 space-y-1"
|
||||||
|
}, [
|
||||||
|
React.createElement('li', {
|
||||||
|
key: 'tip-1'
|
||||||
|
}, '• Ensure good lighting'),
|
||||||
|
React.createElement('li', {
|
||||||
|
key: 'tip-2'
|
||||||
|
}, '• Hold phone steady'),
|
||||||
|
React.createElement('li', {
|
||||||
|
key: 'tip-3'
|
||||||
|
}, '• Tap screen to focus'),
|
||||||
|
React.createElement('li', {
|
||||||
|
key: 'tip-4'
|
||||||
|
}, '• Keep QR code in frame')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -139,8 +139,9 @@ export async function packSecurePayload(payloadObj, senderEcdsaPrivKey = null, r
|
|||||||
|
|
||||||
console.log(`📊 Compressed size: ${encoded.length} characters (${Math.round((1 - encoded.length/payloadJson.length) * 100)}% reduction)`);
|
console.log(`📊 Compressed size: ${encoded.length} characters (${Math.round((1 - encoded.length/payloadJson.length) * 100)}% reduction)`);
|
||||||
|
|
||||||
// 5. Chunking for QR codes
|
// 5. Chunking for QR codes - улучшенное разбиение для лучшего сканирования
|
||||||
const QR_MAX = 900; // Conservative per chunk length
|
const TARGET_CHUNKS = 10; // Целевое количество частей
|
||||||
|
const QR_MAX = Math.max(200, Math.floor(encoded.length / TARGET_CHUNKS)); // Динамический размер части
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
|
||||||
if (encoded.length <= QR_MAX) {
|
if (encoded.length <= QR_MAX) {
|
||||||
@@ -150,10 +151,12 @@ export async function packSecurePayload(payloadObj, senderEcdsaPrivKey = null, r
|
|||||||
body: encoded
|
body: encoded
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Multiple chunks
|
// Multiple chunks - разбиваем на больше частей для лучшего сканирования
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
const totalChunks = Math.ceil(encoded.length / QR_MAX);
|
const totalChunks = Math.ceil(encoded.length / QR_MAX);
|
||||||
|
|
||||||
|
console.log(`📊 COSE: Splitting ${encoded.length} chars into ${totalChunks} chunks (max ${QR_MAX} chars per chunk)`);
|
||||||
|
|
||||||
for (let i = 0, seq = 1; i < encoded.length; i += QR_MAX, seq++) {
|
for (let i = 0, seq = 1; i < encoded.length; i += QR_MAX, seq++) {
|
||||||
const part = encoded.slice(i, i + QR_MAX);
|
const part = encoded.slice(i, i + QR_MAX);
|
||||||
chunks.push(JSON.stringify({
|
chunks.push(JSON.stringify({
|
||||||
|
|||||||
Reference in New Issue
Block a user