- Replaced original JSON with minimized binary format (gzip + base64). - Adjusted rendering and QR code generation for compatibility. - Reduced payload size for improved efficiency.
2853 lines
111 KiB
JavaScript
2853 lines
111 KiB
JavaScript
// src/app.jsx
|
|
var EnhancedCopyButton = ({ text, className = "", children }) => {
|
|
const [copied, setCopied] = React.useState(false);
|
|
const handleCopy = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2e3);
|
|
} catch (error) {
|
|
console.error("Copy failed:", error);
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand("copy");
|
|
document.body.removeChild(textArea);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2e3);
|
|
}
|
|
};
|
|
return React.createElement("button", {
|
|
onClick: handleCopy,
|
|
className: `${className} transition-all duration-200`
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "icon",
|
|
className: `${copied ? "fas fa-check accent-green" : "fas fa-copy text-secondary"} mr-2`
|
|
}),
|
|
copied ? "Copied!" : children
|
|
]);
|
|
};
|
|
var VerificationStep = ({ verificationCode, onConfirm, onReject, localConfirmed, remoteConfirmed, bothConfirmed }) => {
|
|
return React.createElement("div", {
|
|
className: "card-minimal rounded-xl p-6 border-purple-500/20"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "header",
|
|
className: "flex items-center mb-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "icon",
|
|
className: "w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-shield-alt accent-purple"
|
|
})
|
|
]),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-medium text-primary"
|
|
}, "Security verification")
|
|
]),
|
|
React.createElement("div", {
|
|
key: "content",
|
|
className: "space-y-4"
|
|
}, [
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-sm"
|
|
}, "Verify the security code with your contact via another communication channel (voice, SMS, etc.):"),
|
|
React.createElement("div", {
|
|
key: "code-display",
|
|
className: "text-center"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "code",
|
|
className: "verification-code text-2xl py-4"
|
|
}, verificationCode)
|
|
]),
|
|
// Verification status indicators
|
|
React.createElement("div", {
|
|
key: "verification-status",
|
|
className: "space-y-2"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "local-status",
|
|
className: `flex items-center justify-between p-2 rounded-lg ${localConfirmed ? "bg-green-500/10 border border-green-500/20" : "bg-gray-500/10 border border-gray-500/20"}`
|
|
}, [
|
|
React.createElement("span", {
|
|
key: "local-label",
|
|
className: "text-sm text-secondary"
|
|
}, "Your confirmation:"),
|
|
React.createElement("div", {
|
|
key: "local-indicator",
|
|
className: "flex items-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "local-icon",
|
|
className: `fas ${localConfirmed ? "fa-check-circle text-green-400" : "fa-clock text-gray-400"} mr-2`
|
|
}),
|
|
React.createElement("span", {
|
|
key: "local-text",
|
|
className: `text-sm ${localConfirmed ? "text-green-400" : "text-gray-400"}`
|
|
}, localConfirmed ? "Confirmed" : "Pending")
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "remote-status",
|
|
className: `flex items-center justify-between p-2 rounded-lg ${remoteConfirmed ? "bg-green-500/10 border border-green-500/20" : "bg-gray-500/10 border border-gray-500/20"}`
|
|
}, [
|
|
React.createElement("span", {
|
|
key: "remote-label",
|
|
className: "text-sm text-secondary"
|
|
}, "Peer confirmation:"),
|
|
React.createElement("div", {
|
|
key: "remote-indicator",
|
|
className: "flex items-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "remote-icon",
|
|
className: `fas ${remoteConfirmed ? "fa-check-circle text-green-400" : "fa-clock text-gray-400"} mr-2`
|
|
}),
|
|
React.createElement("span", {
|
|
key: "remote-text",
|
|
className: `text-sm ${remoteConfirmed ? "text-green-400" : "text-gray-400"}`
|
|
}, remoteConfirmed ? "Confirmed" : "Pending")
|
|
])
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "warning",
|
|
className: "p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
|
}, [
|
|
React.createElement("p", {
|
|
className: "text-yellow-400 text-sm flex items-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-exclamation-triangle mr-2"
|
|
}),
|
|
"Make sure the codes match exactly.!"
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "buttons",
|
|
className: "flex space-x-3"
|
|
}, [
|
|
React.createElement("button", {
|
|
key: "confirm",
|
|
onClick: onConfirm,
|
|
disabled: localConfirmed,
|
|
className: `flex-1 py-3 px-4 rounded-lg font-medium transition-all duration-200 ${localConfirmed ? "bg-gray-500/20 text-gray-400 cursor-not-allowed" : "btn-verify text-white"}`
|
|
}, [
|
|
React.createElement("i", {
|
|
className: `fas ${localConfirmed ? "fa-check-circle" : "fa-check"} mr-2`
|
|
}),
|
|
localConfirmed ? "Confirmed" : "The codes match"
|
|
]),
|
|
React.createElement("button", {
|
|
key: "reject",
|
|
onClick: onReject,
|
|
className: "flex-1 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 py-3 px-4 rounded-lg font-medium transition-all duration-200"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-times mr-2"
|
|
}),
|
|
"The codes do not match"
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
};
|
|
var EnhancedChatMessage = ({ message, type, timestamp }) => {
|
|
const formatTime = (ts) => {
|
|
return new Date(ts).toLocaleTimeString("ru-RU", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
});
|
|
};
|
|
const getMessageStyle = () => {
|
|
switch (type) {
|
|
case "sent":
|
|
return {
|
|
container: "ml-auto bg-orange-500/15 border-orange-500/20 text-primary",
|
|
icon: "fas fa-lock accent-orange",
|
|
label: "Encrypted"
|
|
};
|
|
case "received":
|
|
return {
|
|
container: "mr-auto card-minimal text-primary",
|
|
icon: "fas fa-unlock-alt accent-green",
|
|
label: "Decrypted"
|
|
};
|
|
case "system":
|
|
return {
|
|
container: "mx-auto bg-yellow-500/10 border border-yellow-500/20 text-yellow-400",
|
|
icon: "fas fa-info-circle accent-yellow",
|
|
label: "System"
|
|
};
|
|
default:
|
|
return {
|
|
container: "mx-auto card-minimal text-secondary",
|
|
icon: "fas fa-circle text-muted",
|
|
label: "Unknown"
|
|
};
|
|
}
|
|
};
|
|
const style = getMessageStyle();
|
|
return React.createElement("div", {
|
|
className: `message-slide mb-3 p-3 rounded-lg max-w-md break-words ${style.container} border`
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "content",
|
|
className: "flex items-start space-x-2"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "icon",
|
|
className: `${style.icon} text-sm mt-0.5 opacity-70`
|
|
}),
|
|
React.createElement("div", {
|
|
key: "text",
|
|
className: "flex-1"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "message",
|
|
className: "text-sm"
|
|
}, message),
|
|
timestamp && React.createElement("div", {
|
|
key: "meta",
|
|
className: "flex items-center justify-between mt-1 text-xs opacity-50"
|
|
}, [
|
|
React.createElement("span", {
|
|
key: "time"
|
|
}, formatTime(timestamp)),
|
|
React.createElement("span", {
|
|
key: "status",
|
|
className: "text-xs"
|
|
}, style.label)
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
};
|
|
var EnhancedConnectionSetup = ({
|
|
messages,
|
|
onCreateOffer,
|
|
onCreateAnswer,
|
|
onConnect,
|
|
onClearData,
|
|
onVerifyConnection,
|
|
connectionStatus,
|
|
offerData,
|
|
answerData,
|
|
offerInput,
|
|
setOfferInput,
|
|
answerInput,
|
|
setAnswerInput,
|
|
showOfferStep,
|
|
showAnswerStep,
|
|
verificationCode,
|
|
showVerification,
|
|
showQRCode,
|
|
qrCodeUrl,
|
|
showQRScanner,
|
|
setShowQRCode,
|
|
setShowQRScanner,
|
|
setShowQRScannerModal,
|
|
offerPassword,
|
|
answerPassword,
|
|
localVerificationConfirmed,
|
|
remoteVerificationConfirmed,
|
|
bothVerificationsConfirmed,
|
|
// QR control props
|
|
qrFramesTotal,
|
|
qrFrameIndex,
|
|
qrManualMode,
|
|
toggleQrManualMode,
|
|
nextQrFrame,
|
|
prevQrFrame,
|
|
markAnswerCreated
|
|
}) => {
|
|
const [mode, setMode] = React.useState("select");
|
|
const resetToSelect = () => {
|
|
setMode("select");
|
|
onClearData();
|
|
};
|
|
const handleVerificationConfirm = () => {
|
|
onVerifyConnection(true);
|
|
};
|
|
const handleVerificationReject = () => {
|
|
onVerifyConnection(false);
|
|
};
|
|
if (showVerification) {
|
|
return React.createElement("div", {
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "verification",
|
|
className: "w-full max-w-md"
|
|
}, [
|
|
React.createElement(VerificationStep, {
|
|
verificationCode,
|
|
onConfirm: handleVerificationConfirm,
|
|
onReject: handleVerificationReject,
|
|
localConfirmed: localVerificationConfirmed,
|
|
remoteConfirmed: remoteVerificationConfirmed,
|
|
bothConfirmed: bothVerificationsConfirmed
|
|
})
|
|
])
|
|
]);
|
|
}
|
|
if (mode === "select") {
|
|
return React.createElement("div", {
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "selector",
|
|
className: "w-full max-w-4xl"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "header",
|
|
className: "text-center mb-8"
|
|
}, [
|
|
React.createElement("h2", {
|
|
key: "title",
|
|
className: "text-2xl font-semibold text-primary mb-3"
|
|
}, "Start secure communication"),
|
|
React.createElement("p", {
|
|
key: "subtitle",
|
|
className: "text-secondary max-w-2xl mx-auto"
|
|
}, "Choose a connection method for a secure channel with ECDH encryption and Perfect Forward Secrecy.")
|
|
]),
|
|
React.createElement("div", {
|
|
key: "options",
|
|
className: "flex flex-col md:flex-row items-center justify-center gap-6 max-w-3xl mx-auto"
|
|
}, [
|
|
// Create Connection
|
|
React.createElement("div", {
|
|
key: "create",
|
|
onClick: () => setMode("create"),
|
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 create"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "icon",
|
|
className: "w-12 h-12 bg-blue-500/10 border border-blue-500/20 rounded-lg flex items-center justify-center mx-auto mb-4"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-plus text-xl text-blue-400"
|
|
})
|
|
]),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-semibold text-primary text-center mb-3"
|
|
}, "Create channel"),
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-center text-sm mb-4"
|
|
}, "Initiate a new secure connection"),
|
|
React.createElement("div", {
|
|
key: "features",
|
|
className: "space-y-2"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "f1",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-key accent-orange mr-2 text-xs"
|
|
}),
|
|
"Generating ECDH keys"
|
|
]),
|
|
React.createElement("div", {
|
|
key: "f2",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-shield-alt accent-orange mr-2 text-xs"
|
|
}),
|
|
"Verification code"
|
|
]),
|
|
React.createElement("div", {
|
|
key: "f3",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-sync-alt accent-purple mr-2 text-xs"
|
|
}),
|
|
"PFS key rotation"
|
|
])
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "divider",
|
|
className: "flex flex-row md:flex-col items-center gap-4 px-4 w-full md:w-auto"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "line-a",
|
|
className: "h-px flex-1 bg-gradient-to-r from-transparent via-zinc-700 to-transparent md:h-32 md:w-px md:flex-none md:bg-gradient-to-b"
|
|
}),
|
|
React.createElement("div", {
|
|
key: "or-text",
|
|
className: "text-zinc-600 text-sm font-medium px-3"
|
|
}, "OR"),
|
|
React.createElement("div", {
|
|
key: "line-b",
|
|
className: "h-px flex-1 bg-gradient-to-r from-transparent via-zinc-700 to-transparent md:h-32 md:w-px md:flex-none md:bg-gradient-to-b"
|
|
})
|
|
]),
|
|
// Join Connection
|
|
React.createElement("div", {
|
|
key: "join",
|
|
onClick: () => setMode("join"),
|
|
className: "card-minimal rounded-xl p-6 cursor-pointer group flex-1 join"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "icon",
|
|
className: "w-12 h-12 bg-green-500/10 border border-green-500/20 rounded-lg flex items-center justify-center mx-auto mb-4"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-link text-xl accent-green"
|
|
})
|
|
]),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-semibold text-primary text-center mb-3"
|
|
}, "Join"),
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-center text-sm mb-4"
|
|
}, "Connect to an existing secure channel"),
|
|
React.createElement("div", {
|
|
key: "features",
|
|
className: "space-y-2"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "f1",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-paste accent-green mr-2 text-xs"
|
|
}),
|
|
"Paste Offer invitation"
|
|
]),
|
|
React.createElement("div", {
|
|
key: "f2",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-check-circle accent-green mr-2 text-xs"
|
|
}),
|
|
"Automatic verification"
|
|
]),
|
|
React.createElement("div", {
|
|
key: "f3",
|
|
className: "flex items-center text-sm text-muted"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-sync-alt accent-purple mr-2 text-xs"
|
|
}),
|
|
"PFS protection"
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
React.createElement(SecurityFeatures, { key: "security-features" }),
|
|
React.createElement(Testimonials, { key: "testimonials" }),
|
|
React.createElement(UniqueFeatureSlider, { key: "unique-features-slider" }),
|
|
React.createElement(DownloadApps, { key: "download-apps" }),
|
|
React.createElement(ComparisonTable, { key: "comparison-table" }),
|
|
React.createElement(Roadmap, { key: "roadmap" })
|
|
])
|
|
]);
|
|
}
|
|
if (mode === "create") {
|
|
return React.createElement("div", {
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "create-flow",
|
|
className: "w-full max-w-3xl space-y-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "header",
|
|
className: "text-center"
|
|
}, [
|
|
React.createElement("button", {
|
|
key: "back",
|
|
onClick: resetToSelect,
|
|
className: "mb-4 text-secondary hover:text-primary transition-colors flex items-center mx-auto text-sm"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-arrow-left mr-2"
|
|
}),
|
|
"Back to selection"
|
|
]),
|
|
React.createElement("h2", {
|
|
key: "title",
|
|
className: "text-xl font-semibold text-primary mb-2"
|
|
}, "Creating a secure channel")
|
|
]),
|
|
// Step 1
|
|
!showAnswerStep && React.createElement("div", {
|
|
key: "step1",
|
|
className: "card-minimal rounded-xl p-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "step-header",
|
|
className: "flex items-center mb-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "number",
|
|
className: "step-number mr-3"
|
|
}, "1"),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-medium text-primary"
|
|
}, "Generating ECDH keys and verification code")
|
|
]),
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-sm mb-4"
|
|
}, "Creating cryptographically strong keys and codes to protect against attacks"),
|
|
!showOfferStep && React.createElement("button", {
|
|
key: "create-btn",
|
|
onClick: onCreateOffer,
|
|
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`
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-shield-alt mr-2"
|
|
}),
|
|
"Create secure keys"
|
|
]),
|
|
showOfferStep && React.createElement("div", {
|
|
key: "offer-result",
|
|
className: "mt-6 space-y-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "success",
|
|
className: "p-3 bg-green-500/10 border border-green-500/20 rounded-lg"
|
|
}, [
|
|
React.createElement("p", {
|
|
className: "text-green-400 text-sm font-medium flex items-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-check-circle mr-2"
|
|
}),
|
|
"Secure invitation created! Send the code to your contact"
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "offer-data",
|
|
className: "space-y-3"
|
|
}, [
|
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
|
React.createElement("div", {
|
|
key: "buttons",
|
|
className: "flex gap-2"
|
|
}, [
|
|
React.createElement(EnhancedCopyButton, {
|
|
key: "copy",
|
|
text: (() => {
|
|
try {
|
|
const min = typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
|
|
if (typeof window.encodeBinaryToPrefixed === "function") {
|
|
return window.encodeBinaryToPrefixed(min);
|
|
}
|
|
if (typeof window.compressToPrefixedGzip === "function") {
|
|
return window.compressToPrefixedGzip(min);
|
|
}
|
|
return min;
|
|
} catch {
|
|
return typeof offerData === "object" ? JSON.stringify(offerData) : offerData || "";
|
|
}
|
|
})(),
|
|
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")
|
|
]),
|
|
showQRCode && 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 connect"),
|
|
React.createElement("div", {
|
|
key: "qr-wrapper",
|
|
className: "flex justify-center"
|
|
}, [
|
|
React.createElement("img", {
|
|
key: "qr-image",
|
|
src: qrCodeUrl,
|
|
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]"
|
|
})
|
|
]),
|
|
(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"
|
|
}, [
|
|
(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"),
|
|
(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"
|
|
}, "Your contact can scan this QR code to quickly join the secure session")
|
|
])
|
|
])
|
|
])
|
|
]),
|
|
// Step 2 - Session Type Selection
|
|
// showOfferStep && React.createElement('div', {
|
|
// key: 'step2',
|
|
// className: "card-minimal rounded-xl p-6"
|
|
// }, [
|
|
// React.createElement('div', {
|
|
// key: 'step-header',
|
|
// className: "flex items-center mb-4"
|
|
// }, [
|
|
// React.createElement('div', {
|
|
// key: 'number',
|
|
// className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
|
// }, '2'),
|
|
// React.createElement('h3', {
|
|
// key: 'title',
|
|
// className: "text-lg font-medium text-primary"
|
|
// }, "Select session type")
|
|
// ]),
|
|
// React.createElement('p', {
|
|
// key: 'description',
|
|
// className: "text-secondary text-sm mb-4"
|
|
// }, "Choose a session plan or use limited demo mode for testing."),
|
|
// React.createElement(SessionTypeSelector, {
|
|
// key: 'session-selector',
|
|
// onSelectType: (sessionType) => {
|
|
// // Save the selected session type
|
|
// setSelectedSessionType(sessionType);
|
|
// console.log('🎯 Session type selected:', sessionType);
|
|
// // FIX: For demo sessions, we immediately call automatic activation
|
|
// if (sessionType === 'demo') {
|
|
// console.log('🎮 Demo session selected, scheduling automatic activation...');
|
|
// // Delay activation for 2 seconds to stabilize
|
|
// setTimeout(() => {
|
|
// if (sessionManager) {
|
|
// console.log('🚀 Triggering demo session activation from selection...');
|
|
// handleDemoVerification();
|
|
// }
|
|
// }, 2000);
|
|
// }
|
|
// // Open a modal payment window
|
|
// if (typeof window.showPaymentModal === 'function') {
|
|
// window.showPaymentModal(sessionType);
|
|
// } else {
|
|
// // Fallback - show session information
|
|
// console.log('Selected session type:', sessionType);
|
|
// }
|
|
// },
|
|
// onCancel: resetToSelect,
|
|
// sessionManager: window.sessionManager
|
|
// })
|
|
// ]),
|
|
// Step 3 - Waiting for response
|
|
showOfferStep && React.createElement("div", {
|
|
key: "step2",
|
|
className: "card-minimal rounded-xl p-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "step-header",
|
|
className: "flex items-center mb-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "number",
|
|
className: "w-8 h-8 bg-blue-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
|
}, "2"),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-medium text-primary"
|
|
}, "Waiting for the peer's response")
|
|
]),
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-sm mb-4"
|
|
}, "Paste the encrypted invitation code from your contact."),
|
|
React.createElement("div", {
|
|
key: "buttons",
|
|
className: "flex gap-2 mb-4"
|
|
}, [
|
|
React.createElement("button", {
|
|
key: "scan-btn",
|
|
onClick: () => setShowQRScannerModal(true),
|
|
className: "px-4 py-2 bg-purple-500/10 hover:bg-purple-500/20 text-purple-400 border border-purple-500/20 rounded text-sm font-medium transition-all duration-200"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "icon",
|
|
className: "fas fa-qrcode mr-2"
|
|
}),
|
|
"Scan QR Code"
|
|
])
|
|
]),
|
|
React.createElement("textarea", {
|
|
key: "input",
|
|
value: answerInput,
|
|
onChange: (e2) => {
|
|
setAnswerInput(e2.target.value);
|
|
if (e2.target.value.trim().length > 0) {
|
|
if (typeof markAnswerCreated === "function") {
|
|
markAnswerCreated();
|
|
}
|
|
}
|
|
},
|
|
rows: 6,
|
|
placeholder: "Paste the encrypted response code from your contact or scan QR code...",
|
|
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg resize-none mb-4 text-secondary placeholder-gray-500 focus:border-orange-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|
}),
|
|
React.createElement("button", {
|
|
key: "connect-btn",
|
|
onClick: onConnect,
|
|
disabled: !answerInput.trim(),
|
|
className: "w-full btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-rocket mr-2"
|
|
}),
|
|
"Establish connection"
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
if (mode === "join") {
|
|
return React.createElement("div", {
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "join-flow",
|
|
className: "w-full max-w-3xl space-y-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "header",
|
|
className: "text-center"
|
|
}, [
|
|
React.createElement("button", {
|
|
key: "back",
|
|
onClick: resetToSelect,
|
|
className: "mb-4 text-secondary hover:text-primary transition-colors flex items-center mx-auto text-sm"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-arrow-left mr-2"
|
|
}),
|
|
"Back to selection"
|
|
]),
|
|
React.createElement("h2", {
|
|
key: "title",
|
|
className: "text-xl font-semibold text-primary mb-2"
|
|
}, "Joining the secure channel")
|
|
]),
|
|
showAnswerStep ? null : React.createElement("div", {
|
|
key: "step1",
|
|
className: "card-minimal rounded-xl p-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "step-header",
|
|
className: "flex items-center mb-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "number",
|
|
className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
|
}, "1"),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-medium text-primary"
|
|
}, "Paste secure invitation")
|
|
]),
|
|
React.createElement("p", {
|
|
key: "description",
|
|
className: "text-secondary text-sm mb-4"
|
|
}, "Copy and paste the encrypted invitation code from the initiator."),
|
|
React.createElement("textarea", {
|
|
key: "input",
|
|
value: offerInput,
|
|
onChange: (e2) => {
|
|
setOfferInput(e2.target.value);
|
|
if (e2.target.value.trim().length > 0) {
|
|
if (typeof markAnswerCreated === "function") {
|
|
markAnswerCreated();
|
|
}
|
|
}
|
|
},
|
|
rows: 8,
|
|
placeholder: "Paste the encrypted invitation code or scan QR code...",
|
|
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg resize-none mb-4 text-secondary placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|
}),
|
|
React.createElement("div", {
|
|
key: "buttons",
|
|
className: "flex gap-2 mb-4"
|
|
}, [
|
|
React.createElement("button", {
|
|
key: "scan-btn",
|
|
onClick: () => setShowQRScannerModal(true),
|
|
className: "px-4 py-2 bg-purple-500/10 hover:bg-purple-500/20 text-purple-400 border border-purple-500/20 rounded text-sm font-medium transition-all duration-200"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "icon",
|
|
className: "fas fa-qrcode mr-2"
|
|
}),
|
|
"Scan QR Code"
|
|
]),
|
|
React.createElement("button", {
|
|
key: "process-btn",
|
|
onClick: onCreateAnswer,
|
|
disabled: !offerInput.trim() || connectionStatus === "connecting",
|
|
className: "flex-1 btn-secondary text-white py-2 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-cogs mr-2"
|
|
}),
|
|
"Process invitation"
|
|
])
|
|
]),
|
|
showQRScanner && React.createElement("div", {
|
|
key: "qr-scanner",
|
|
className: "p-4 bg-gray-800/50 border border-gray-600/30 rounded-lg text-center"
|
|
}, [
|
|
React.createElement("h4", {
|
|
key: "scanner-title",
|
|
className: "text-sm font-medium text-primary mb-3"
|
|
}, "QR Code Scanner"),
|
|
React.createElement("p", {
|
|
key: "scanner-description",
|
|
className: "text-xs text-gray-400 mb-3"
|
|
}, "Use your device camera to scan the QR code from the invitation"),
|
|
React.createElement("button", {
|
|
key: "open-scanner",
|
|
onClick: () => {
|
|
if (typeof setShowQRScannerModal === "function") {
|
|
setShowQRScannerModal(true);
|
|
} else {
|
|
console.error("setShowQRScannerModal is not a function:", setShowQRScannerModal);
|
|
}
|
|
},
|
|
className: "w-full px-4 py-3 bg-purple-600 hover:bg-purple-500 text-white rounded-lg font-medium transition-all duration-200 mb-3"
|
|
}, [
|
|
React.createElement("i", {
|
|
key: "camera-icon",
|
|
className: "fas fa-camera mr-2"
|
|
}),
|
|
"Open Camera Scanner"
|
|
]),
|
|
React.createElement("button", {
|
|
key: "test-qr",
|
|
onClick: async () => {
|
|
console.log("Creating test QR code...");
|
|
if (window.generateQRCode) {
|
|
const testData = '{"type":"test","message":"Hello QR Scanner!"}';
|
|
const qrUrl = await window.generateQRCode(testData);
|
|
console.log("Test QR code generated:", qrUrl);
|
|
const newWindow = window.open();
|
|
newWindow.document.write(`<img src="${qrUrl}" style="width: 300px; height: 300px;">`);
|
|
}
|
|
},
|
|
className: "px-3 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-300 border border-green-500/20 rounded text-xs font-medium transition-all duration-200 mr-2"
|
|
}, "Test QR"),
|
|
React.createElement("button", {
|
|
key: "close-scanner",
|
|
onClick: () => setShowQRScanner(false),
|
|
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")
|
|
])
|
|
]),
|
|
// Step 2
|
|
showAnswerStep && React.createElement("div", {
|
|
key: "step2",
|
|
className: "card-minimal rounded-xl p-6"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "step-header",
|
|
className: "flex items-center mb-4"
|
|
}, [
|
|
React.createElement("div", {
|
|
key: "number",
|
|
className: "step-number mr-3"
|
|
}, "2"),
|
|
React.createElement("h3", {
|
|
key: "title",
|
|
className: "text-lg font-medium text-primary"
|
|
}, "Sending a secure response")
|
|
]),
|
|
React.createElement("div", {
|
|
key: "success",
|
|
className: "p-3 bg-green-500/10 border border-green-500/20 rounded-lg mb-4"
|
|
}, [
|
|
React.createElement("p", {
|
|
className: "text-green-400 text-sm font-medium flex items-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-check-circle mr-2"
|
|
}),
|
|
"Secure response created! Send this code to the initiator:"
|
|
])
|
|
]),
|
|
React.createElement("div", {
|
|
key: "answer-data",
|
|
className: "space-y-3 mb-4"
|
|
}, [
|
|
// Raw JSON hidden intentionally; users copy compressed string or use QR
|
|
React.createElement(EnhancedCopyButton, {
|
|
key: "copy",
|
|
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"
|
|
}, "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]"
|
|
})
|
|
]),
|
|
(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"
|
|
}, [
|
|
(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"),
|
|
(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", {
|
|
key: "info",
|
|
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
|
}, [
|
|
React.createElement("p", {
|
|
className: "text-purple-400 text-sm flex items-center justify-center"
|
|
}, [
|
|
React.createElement("i", {
|
|
className: "fas fa-shield-alt mr-2"
|
|
}),
|
|
"The connection will be established with verification"
|
|
])
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
}
|
|
};
|
|
var createScrollToBottomFunction = (chatMessagesRef) => {
|
|
return () => {
|
|
if (chatMessagesRef && chatMessagesRef.current) {
|
|
const scrollAttempt = () => {
|
|
if (chatMessagesRef.current) {
|
|
chatMessagesRef.current.scrollTo({
|
|
top: chatMessagesRef.current.scrollHeight,
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
};
|
|
scrollAttempt();
|
|
setTimeout(scrollAttempt, 50);
|
|
setTimeout(scrollAttempt, 150);
|
|
setTimeout(scrollAttempt, 300);
|
|
requestAnimationFrame(() => {
|
|
setTimeout(scrollAttempt, 100);
|
|
});
|
|
}
|
|
};
|
|
};
|
|
var EnhancedChatInterface = ({
|
|
messages,
|
|
messageInput,
|
|
setMessageInput,
|
|
onSendMessage,
|
|
onDisconnect,
|
|
keyFingerprint,
|
|
isVerified,
|
|
chatMessagesRef,
|
|
scrollToBottom,
|
|
webrtcManager
|
|
}) => {
|
|
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|
const [showFileTransfer, setShowFileTransfer] = React.useState(false);
|
|
React.useEffect(() => {
|
|
if (chatMessagesRef.current && messages.length > 0) {
|
|
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
|
|
if (isNearBottom) {
|
|
const smoothScroll = () => {
|
|
if (chatMessagesRef.current) {
|
|
chatMessagesRef.current.scrollTo({
|
|
top: chatMessagesRef.current.scrollHeight,
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
};
|
|
smoothScroll();
|
|
setTimeout(smoothScroll, 50);
|
|
setTimeout(smoothScroll, 150);
|
|
}
|
|
}
|
|
}, [messages, chatMessagesRef]);
|
|
const handleScroll = () => {
|
|
if (chatMessagesRef.current) {
|
|
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
|
|
setShowScrollButton(!isNearBottom);
|
|
}
|
|
};
|
|
const handleScrollToBottom = () => {
|
|
console.log("\u{1F50D} handleScrollToBottom called, scrollToBottom type:", typeof scrollToBottom);
|
|
if (typeof scrollToBottom === "function") {
|
|
scrollToBottom();
|
|
setShowScrollButton(false);
|
|
} else {
|
|
console.error("scrollToBottom is not a function:", scrollToBottom);
|
|
if (chatMessagesRef.current) {
|
|
chatMessagesRef.current.scrollTo({
|
|
top: chatMessagesRef.current.scrollHeight,
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
setShowScrollButton(false);
|
|
}
|
|
};
|
|
const handleKeyPress = (e2) => {
|
|
if (e2.key === "Enter" && !e2.shiftKey) {
|
|
e2.preventDefault();
|
|
onSendMessage();
|
|
}
|
|
};
|
|
const isFileTransferReady = () => {
|
|
if (!webrtcManager) return false;
|
|
const connected = webrtcManager.isConnected ? webrtcManager.isConnected() : false;
|
|
const verified = webrtcManager.isVerified || false;
|
|
const hasDataChannel = webrtcManager.dataChannel && webrtcManager.dataChannel.readyState === "open";
|
|
return connected && verified && hasDataChannel;
|
|
};
|
|
return React.createElement(
|
|
"div",
|
|
{
|
|
className: "chat-container flex flex-col",
|
|
style: { backgroundColor: "#272827", height: "calc(100vh - 64px)" }
|
|
},
|
|
[
|
|
// Область сообщений
|
|
React.createElement(
|
|
"div",
|
|
{ className: "flex-1 flex flex-col overflow-hidden" },
|
|
React.createElement(
|
|
"div",
|
|
{ className: "flex-1 max-w-4xl mx-auto w-full p-4 overflow-hidden" },
|
|
React.createElement(
|
|
"div",
|
|
{
|
|
ref: chatMessagesRef,
|
|
onScroll: handleScroll,
|
|
className: "h-full overflow-y-auto space-y-3 hide-scrollbar pr-2 scroll-smooth"
|
|
},
|
|
messages.length === 0 ? React.createElement(
|
|
"div",
|
|
{ className: "flex items-center justify-center h-full" },
|
|
React.createElement(
|
|
"div",
|
|
{ className: "text-center max-w-md" },
|
|
[
|
|
React.createElement(
|
|
"div",
|
|
{ className: "w-16 h-16 bg-green-500/10 border border-green-500/20 rounded-xl flex items-center justify-center mx-auto mb-4" },
|
|
React.createElement(
|
|
"svg",
|
|
{ className: "w-8 h-8 text-green-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
|
})
|
|
)
|
|
),
|
|
React.createElement("h3", { className: "text-lg font-medium text-gray-300 mb-2" }, "Secure channel is ready!"),
|
|
React.createElement("p", { className: "text-gray-400 text-sm mb-4" }, "All messages are protected by modern cryptographic algorithms"),
|
|
React.createElement(
|
|
"div",
|
|
{ className: "text-left space-y-2" },
|
|
[
|
|
["End-to-end encryption", "M5 13l4 4L19 7"],
|
|
["Protection against replay attacks", "M5 13l4 4L19 7"],
|
|
["Integrity verification", "M5 13l4 4L19 7"],
|
|
["Perfect Forward Secrecy", "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"]
|
|
].map(
|
|
([text, d], i) => React.createElement(
|
|
"div",
|
|
{ key: `f${i}`, className: "flex items-center text-sm text-gray-400" },
|
|
[
|
|
React.createElement(
|
|
"svg",
|
|
{
|
|
className: `w-4 h-4 mr-3 ${i === 3 ? "text-purple-500" : "text-green-500"}`,
|
|
fill: "none",
|
|
stroke: "currentColor",
|
|
viewBox: "0 0 24 24"
|
|
},
|
|
React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d
|
|
})
|
|
),
|
|
text
|
|
]
|
|
)
|
|
)
|
|
)
|
|
]
|
|
)
|
|
) : messages.map(
|
|
(msg) => React.createElement(EnhancedChatMessage, {
|
|
key: msg.id,
|
|
message: msg.message,
|
|
type: msg.type,
|
|
timestamp: msg.timestamp
|
|
})
|
|
)
|
|
)
|
|
)
|
|
),
|
|
// Кнопка прокрутки вниз
|
|
showScrollButton && React.createElement(
|
|
"button",
|
|
{
|
|
onClick: handleScrollToBottom,
|
|
className: "fixed right-6 w-12 h-12 bg-green-500/20 hover:bg-green-500/30 border border-green-500/30 text-green-400 rounded-full flex items-center justify-center transition-all duration-200 shadow-lg z-50",
|
|
style: { bottom: "160px" }
|
|
},
|
|
React.createElement(
|
|
"svg",
|
|
{ className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d: "M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
})
|
|
)
|
|
),
|
|
React.createElement(
|
|
"div",
|
|
{
|
|
className: "flex-shrink-0 border-t border-gray-500/10",
|
|
style: { backgroundColor: "#272827" }
|
|
},
|
|
React.createElement(
|
|
"div",
|
|
{ className: "max-w-4xl mx-auto px-4" },
|
|
[
|
|
React.createElement(
|
|
"button",
|
|
{
|
|
onClick: () => setShowFileTransfer(!showFileTransfer),
|
|
className: `flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4 ${showFileTransfer ? "mb-4" : ""}`
|
|
},
|
|
[
|
|
React.createElement(
|
|
"svg",
|
|
{
|
|
className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? "rotate-180" : ""}`,
|
|
fill: "none",
|
|
stroke: "currentColor",
|
|
viewBox: "0 0 24 24"
|
|
},
|
|
showFileTransfer ? React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d: "M5 15l7-7 7 7"
|
|
}) : React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"
|
|
})
|
|
),
|
|
showFileTransfer ? "Hide file transfer" : "Send files"
|
|
]
|
|
),
|
|
showFileTransfer && React.createElement(window.FileTransferComponent || (() => React.createElement("div", {
|
|
className: "p-4 text-center text-red-400"
|
|
}, "FileTransferComponent not loaded")), {
|
|
webrtcManager,
|
|
isConnected: isFileTransferReady()
|
|
})
|
|
]
|
|
)
|
|
),
|
|
React.createElement(
|
|
"div",
|
|
{ className: "border-t border-gray-500/10" },
|
|
React.createElement(
|
|
"div",
|
|
{ className: "max-w-4xl mx-auto p-4" },
|
|
React.createElement(
|
|
"div",
|
|
{ className: "flex items-stretch space-x-3" },
|
|
[
|
|
React.createElement(
|
|
"div",
|
|
{ className: "flex-1 relative" },
|
|
[
|
|
React.createElement("textarea", {
|
|
value: messageInput,
|
|
onChange: (e2) => setMessageInput(e2.target.value),
|
|
onKeyDown: handleKeyPress,
|
|
placeholder: "Enter message to encrypt...",
|
|
rows: 2,
|
|
maxLength: 2e3,
|
|
style: { backgroundColor: "#272827" },
|
|
className: "w-full p-3 border border-gray-600 rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|
}),
|
|
React.createElement(
|
|
"div",
|
|
{ className: "absolute bottom-2 right-3 flex items-center space-x-2 text-xs text-gray-400" },
|
|
[
|
|
React.createElement("span", null, `${messageInput.length}/2000`),
|
|
React.createElement("span", null, "\u2022 Enter to send")
|
|
]
|
|
)
|
|
]
|
|
),
|
|
React.createElement(
|
|
"button",
|
|
{
|
|
onClick: onSendMessage,
|
|
disabled: !messageInput.trim(),
|
|
className: "bg-green-400/20 text-green-400 p-3 rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-h-[72px]"
|
|
},
|
|
React.createElement(
|
|
"svg",
|
|
{ className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
|
|
React.createElement("path", {
|
|
strokeLinecap: "round",
|
|
strokeLinejoin: "round",
|
|
strokeWidth: 2,
|
|
d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
|
|
})
|
|
)
|
|
)
|
|
]
|
|
)
|
|
)
|
|
)
|
|
]
|
|
);
|
|
};
|
|
var EnhancedSecureP2PChat = () => {
|
|
const [messages, setMessages] = React.useState([]);
|
|
const [connectionStatus, setConnectionStatus] = React.useState("disconnected");
|
|
const [messageInput, setMessageInput] = React.useState("");
|
|
const [offerData, setOfferData] = React.useState("");
|
|
const [answerData, setAnswerData] = React.useState("");
|
|
const [offerInput, setOfferInput] = React.useState("");
|
|
const [answerInput, setAnswerInput] = React.useState("");
|
|
const [keyFingerprint, setKeyFingerprint] = React.useState("");
|
|
const [verificationCode, setVerificationCode] = React.useState("");
|
|
const [showOfferStep, setShowOfferStep] = React.useState(false);
|
|
const [showAnswerStep, setShowAnswerStep] = React.useState(false);
|
|
const [showVerification, setShowVerification] = React.useState(false);
|
|
const [showQRCode, setShowQRCode] = React.useState(false);
|
|
const [qrCodeUrl, setQrCodeUrl] = React.useState("");
|
|
const [showQRScanner, setShowQRScanner] = React.useState(false);
|
|
const [showQRScannerModal, setShowQRScannerModal] = React.useState(false);
|
|
const [isVerified, setIsVerified] = React.useState(false);
|
|
const [securityLevel, setSecurityLevel] = React.useState(null);
|
|
const [localVerificationConfirmed, setLocalVerificationConfirmed] = React.useState(false);
|
|
const [remoteVerificationConfirmed, setRemoteVerificationConfirmed] = React.useState(false);
|
|
const [bothVerificationsConfirmed, setBothVerificationsConfirmed] = React.useState(false);
|
|
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
|
const [pendingSession, setPendingSession] = React.useState(null);
|
|
const [connectionState, setConnectionState] = React.useState({
|
|
status: "disconnected",
|
|
hasActiveAnswer: false,
|
|
answerCreatedAt: null,
|
|
isUserInitiatedDisconnect: false
|
|
});
|
|
const updateConnectionState = (newState, options = {}) => {
|
|
const { preserveAnswer = false, isUserAction = false } = options;
|
|
setConnectionState((prev) => ({
|
|
...prev,
|
|
...newState,
|
|
isUserInitiatedDisconnect: isUserAction,
|
|
hasActiveAnswer: preserveAnswer ? prev.hasActiveAnswer : false,
|
|
answerCreatedAt: preserveAnswer ? prev.answerCreatedAt : null
|
|
}));
|
|
};
|
|
const shouldPreserveAnswerData = () => {
|
|
const now = Date.now();
|
|
const answerAge = now - (connectionState.answerCreatedAt || 0);
|
|
const maxPreserveTime = 3e5;
|
|
const hasAnswerData = answerData && answerData.trim().length > 0 || answerInput && answerInput.trim().length > 0;
|
|
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;
|
|
return shouldPreserve;
|
|
};
|
|
const markAnswerCreated = () => {
|
|
updateConnectionState({
|
|
hasActiveAnswer: true,
|
|
answerCreatedAt: Date.now()
|
|
});
|
|
};
|
|
React.useEffect(() => {
|
|
window.forceCleanup = () => {
|
|
handleClearData();
|
|
if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.disconnect();
|
|
}
|
|
};
|
|
window.clearLogs = () => {
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
};
|
|
return () => {
|
|
delete window.forceCleanup;
|
|
delete window.clearLogs;
|
|
};
|
|
}, []);
|
|
const webrtcManagerRef = React.useRef(null);
|
|
window.webrtcManagerRef = webrtcManagerRef;
|
|
const addMessageWithAutoScroll = React.useCallback((message, type) => {
|
|
const newMessage = {
|
|
message,
|
|
type,
|
|
id: Date.now() + Math.random(),
|
|
timestamp: Date.now()
|
|
};
|
|
setMessages((prev) => {
|
|
const updated = [...prev, newMessage];
|
|
setTimeout(() => {
|
|
if (chatMessagesRef?.current) {
|
|
const container = chatMessagesRef.current;
|
|
try {
|
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
|
|
if (isNearBottom || prev.length === 0) {
|
|
requestAnimationFrame(() => {
|
|
if (container && container.scrollTo) {
|
|
container.scrollTo({
|
|
top: container.scrollHeight,
|
|
behavior: "smooth"
|
|
});
|
|
}
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.warn("Scroll error:", error);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
}, 50);
|
|
return updated;
|
|
});
|
|
}, []);
|
|
const updateSecurityLevel = React.useCallback(async () => {
|
|
if (window.isUpdatingSecurity) {
|
|
return;
|
|
}
|
|
window.isUpdatingSecurity = true;
|
|
try {
|
|
if (webrtcManagerRef.current) {
|
|
setSecurityLevel({
|
|
level: "MAXIMUM",
|
|
score: 100,
|
|
color: "green",
|
|
details: "All security features enabled by default",
|
|
passedChecks: 10,
|
|
totalChecks: 10,
|
|
isRealData: true
|
|
});
|
|
if (window.DEBUG_MODE) {
|
|
const currentLevel = webrtcManagerRef.current.ecdhKeyPair && webrtcManagerRef.current.ecdsaKeyPair ? await webrtcManagerRef.current.calculateSecurityLevel() : {
|
|
level: "MAXIMUM",
|
|
score: 100,
|
|
sessionType: "premium",
|
|
passedChecks: 10,
|
|
totalChecks: 10
|
|
};
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to update security level:", error);
|
|
setSecurityLevel({
|
|
level: "ERROR",
|
|
score: 0,
|
|
color: "red",
|
|
details: "Verification failed"
|
|
});
|
|
} finally {
|
|
setTimeout(() => {
|
|
window.isUpdatingSecurity = false;
|
|
}, 2e3);
|
|
}
|
|
}, []);
|
|
React.useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setSessionTimeLeft(0);
|
|
}, 1e3);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
const chatMessagesRef = React.useRef(null);
|
|
const scrollToBottom = createScrollToBottomFunction(chatMessagesRef);
|
|
React.useEffect(() => {
|
|
if (messages.length > 0 && chatMessagesRef.current) {
|
|
scrollToBottom();
|
|
setTimeout(scrollToBottom, 50);
|
|
setTimeout(scrollToBottom, 150);
|
|
}
|
|
}, [messages]);
|
|
React.useEffect(() => {
|
|
if (webrtcManagerRef.current) {
|
|
console.log("\u26A0\uFE0F WebRTC Manager already initialized, skipping...");
|
|
return;
|
|
}
|
|
const handleMessage = (message, type) => {
|
|
if (typeof message === "string" && message.trim().startsWith("{")) {
|
|
try {
|
|
const parsedMessage = JSON.parse(message);
|
|
const blockedTypes = [
|
|
"file_transfer_start",
|
|
"file_transfer_response",
|
|
"file_chunk",
|
|
"chunk_confirmation",
|
|
"file_transfer_complete",
|
|
"file_transfer_error",
|
|
"heartbeat",
|
|
"verification",
|
|
"verification_response",
|
|
"verification_confirmed",
|
|
"verification_both_confirmed",
|
|
"peer_disconnect",
|
|
"key_rotation_signal",
|
|
"key_rotation_ready",
|
|
"security_upgrade"
|
|
];
|
|
if (parsedMessage.type && blockedTypes.includes(parsedMessage.type)) {
|
|
console.log(`Blocked system/file message from chat: ${parsedMessage.type}`);
|
|
return;
|
|
}
|
|
} catch (parseError) {
|
|
}
|
|
}
|
|
addMessageWithAutoScroll(message, type);
|
|
};
|
|
const handleStatusChange = (status) => {
|
|
setConnectionStatus(status);
|
|
if (status === "connected") {
|
|
document.dispatchEvent(new CustomEvent("new-connection"));
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} else if (status === "verifying") {
|
|
setShowVerification(true);
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} else if (status === "verified") {
|
|
setIsVerified(true);
|
|
setShowVerification(false);
|
|
setBothVerificationsConfirmed(true);
|
|
setConnectionStatus("connected");
|
|
setTimeout(() => {
|
|
setIsVerified(true);
|
|
}, 0);
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} else if (status === "connecting") {
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} else if (status === "disconnected") {
|
|
updateConnectionState({ status: "disconnected" });
|
|
setConnectionStatus("disconnected");
|
|
if (shouldPreserveAnswerData()) {
|
|
setIsVerified(false);
|
|
setShowVerification(false);
|
|
return;
|
|
}
|
|
setIsVerified(false);
|
|
setShowVerification(false);
|
|
document.dispatchEvent(new CustomEvent("disconnected"));
|
|
setLocalVerificationConfirmed(false);
|
|
setRemoteVerificationConfirmed(false);
|
|
setBothVerificationsConfirmed(false);
|
|
setOfferData(null);
|
|
setAnswerData(null);
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
setShowAnswerStep(false);
|
|
setKeyFingerprint("");
|
|
setVerificationCode("");
|
|
setSecurityLevel(null);
|
|
setSessionTimeLeft(0);
|
|
setTimeout(() => {
|
|
setConnectionStatus("disconnected");
|
|
setShowVerification(false);
|
|
setOfferData(null);
|
|
setAnswerData(null);
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
setShowAnswerStep(false);
|
|
setMessages([]);
|
|
}, 1e3);
|
|
} else if (status === "peer_disconnected") {
|
|
setSessionTimeLeft(0);
|
|
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
|
setTimeout(() => {
|
|
setKeyFingerprint("");
|
|
setVerificationCode("");
|
|
setSecurityLevel(null);
|
|
setIsVerified(false);
|
|
setShowVerification(false);
|
|
setConnectionStatus("disconnected");
|
|
setLocalVerificationConfirmed(false);
|
|
setRemoteVerificationConfirmed(false);
|
|
setBothVerificationsConfirmed(false);
|
|
setOfferData(null);
|
|
setAnswerData(null);
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
setShowAnswerStep(false);
|
|
setMessages([]);
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
}, 2e3);
|
|
}
|
|
};
|
|
const handleKeyExchange = (fingerprint) => {
|
|
if (fingerprint === "") {
|
|
setKeyFingerprint("");
|
|
} else {
|
|
setKeyFingerprint(fingerprint);
|
|
}
|
|
};
|
|
const handleVerificationRequired = (code) => {
|
|
if (code === "") {
|
|
setVerificationCode("");
|
|
setShowVerification(false);
|
|
} else {
|
|
setVerificationCode(code);
|
|
setShowVerification(true);
|
|
}
|
|
};
|
|
const handleVerificationStateChange = (state) => {
|
|
setLocalVerificationConfirmed(state.localConfirmed);
|
|
setRemoteVerificationConfirmed(state.remoteConfirmed);
|
|
setBothVerificationsConfirmed(state.bothConfirmed);
|
|
};
|
|
const handleAnswerError = (errorType, errorMessage) => {
|
|
if (errorType === "replay_attack") {
|
|
setSessionTimeLeft(0);
|
|
setPendingSession(null);
|
|
addMessageWithAutoScroll("\u{1F4A1} Data is outdated. Please create a new invitation or use a current response code.", "system");
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
} else if (errorType === "security_violation") {
|
|
setSessionTimeLeft(0);
|
|
setPendingSession(null);
|
|
addMessageWithAutoScroll(` Security breach: ${errorMessage}`, "system");
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
}
|
|
};
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
|
handleMessage,
|
|
handleStatusChange,
|
|
handleKeyExchange,
|
|
handleVerificationRequired,
|
|
handleAnswerError,
|
|
handleVerificationStateChange
|
|
);
|
|
handleMessage(" SecureBit.chat Enhanced Security Edition v4.2.12 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.", "system");
|
|
const handleBeforeUnload = (event) => {
|
|
if (event.type === "beforeunload" && !isTabSwitching) {
|
|
if (webrtcManagerRef.current && webrtcManagerRef.current.isConnected()) {
|
|
try {
|
|
webrtcManagerRef.current.sendSystemMessage({
|
|
type: "peer_disconnect",
|
|
reason: "user_disconnect",
|
|
timestamp: Date.now()
|
|
});
|
|
} catch (error) {
|
|
}
|
|
setTimeout(() => {
|
|
if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.disconnect();
|
|
}
|
|
}, 100);
|
|
} else if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.disconnect();
|
|
}
|
|
} else if (isTabSwitching) {
|
|
event.preventDefault();
|
|
event.returnValue = "";
|
|
}
|
|
};
|
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
let isTabSwitching = false;
|
|
let tabSwitchTimeout = null;
|
|
const handleVisibilityChange = () => {
|
|
if (document.visibilityState === "hidden") {
|
|
isTabSwitching = true;
|
|
if (tabSwitchTimeout) {
|
|
clearTimeout(tabSwitchTimeout);
|
|
}
|
|
tabSwitchTimeout = setTimeout(() => {
|
|
isTabSwitching = false;
|
|
}, 5e3);
|
|
} else if (document.visibilityState === "visible") {
|
|
isTabSwitching = false;
|
|
if (tabSwitchTimeout) {
|
|
clearTimeout(tabSwitchTimeout);
|
|
tabSwitchTimeout = null;
|
|
}
|
|
}
|
|
};
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.setFileTransferCallbacks(
|
|
// Progress callback
|
|
(progress) => {
|
|
console.log("File progress:", progress);
|
|
},
|
|
// File received callback
|
|
(fileData) => {
|
|
const sizeMb = Math.max(1, Math.round((fileData.fileSize || 0) / (1024 * 1024)));
|
|
const downloadMessage = React.createElement("div", {
|
|
className: "flex items-center space-x-2"
|
|
}, [
|
|
React.createElement("span", { key: "label" }, ` File received: ${fileData.fileName} (${sizeMb} MB)`),
|
|
React.createElement("button", {
|
|
key: "btn",
|
|
className: "px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-xs",
|
|
onClick: async () => {
|
|
try {
|
|
const url = await fileData.getObjectURL();
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = fileData.fileName;
|
|
a.click();
|
|
setTimeout(() => fileData.revokeObjectURL(url), 15e3);
|
|
} catch (e2) {
|
|
console.error("Download failed:", e2);
|
|
addMessageWithAutoScroll(` File upload error: ${String(e2?.message || e2)}`, "system");
|
|
}
|
|
}
|
|
}, "Download")
|
|
]);
|
|
addMessageWithAutoScroll(downloadMessage, "system");
|
|
},
|
|
// Error callback
|
|
(error) => {
|
|
console.error("File transfer error:", error);
|
|
if (error.includes("Connection not ready")) {
|
|
addMessageWithAutoScroll(` File transfer error: connection not ready. Try again later.`, "system");
|
|
} else if (error.includes("File too large")) {
|
|
addMessageWithAutoScroll(` File is too big. Maximum size: 100 MB`, "system");
|
|
} else {
|
|
addMessageWithAutoScroll(` File transfer error: ${error}`, "system");
|
|
}
|
|
}
|
|
);
|
|
}
|
|
return () => {
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
if (tabSwitchTimeout) {
|
|
clearTimeout(tabSwitchTimeout);
|
|
tabSwitchTimeout = null;
|
|
}
|
|
if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.disconnect();
|
|
webrtcManagerRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
const compressOfferData = (offerData2) => {
|
|
try {
|
|
const offer = typeof offerData2 === "string" ? JSON.parse(offerData2) : offerData2;
|
|
const minimalOffer = {
|
|
type: offer.type,
|
|
version: offer.version,
|
|
timestamp: offer.timestamp,
|
|
sessionId: offer.sessionId,
|
|
connectionId: offer.connectionId,
|
|
verificationCode: offer.verificationCode,
|
|
salt: offer.salt,
|
|
// Use only key fingerprints instead of full keys
|
|
keyFingerprints: offer.keyFingerprints,
|
|
// Add a reference to get full data
|
|
fullDataAvailable: true,
|
|
compressionLevel: "minimal"
|
|
};
|
|
return JSON.stringify(minimalOffer);
|
|
} catch (error) {
|
|
console.error("Error compressing offer data:", error);
|
|
return offerData2;
|
|
}
|
|
};
|
|
const createQRReference = (offerData2) => {
|
|
try {
|
|
const referenceId = `offer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
localStorage.setItem(`qr_offer_${referenceId}`, JSON.stringify(offerData2));
|
|
const qrReference = {
|
|
type: "secure_offer_reference",
|
|
referenceId,
|
|
timestamp: Date.now(),
|
|
message: "Scan this QR code and use the reference ID to get full offer data"
|
|
};
|
|
return JSON.stringify(qrReference);
|
|
} catch (error) {
|
|
console.error("Error creating QR reference:", error);
|
|
return null;
|
|
}
|
|
};
|
|
const createTemplateOffer = (offer) => {
|
|
const templateOffer = {
|
|
type: "enhanced_secure_offer_template",
|
|
version: "4.0",
|
|
sessionId: offer.sessionId,
|
|
connectionId: offer.connectionId,
|
|
verificationCode: offer.verificationCode,
|
|
timestamp: offer.timestamp,
|
|
// Avoid bulky fields (SDP, raw keys); keep only fingerprints and essentials
|
|
keyFingerprints: offer.keyFingerprints,
|
|
// Keep concise auth hints (omit large nonces)
|
|
authChallenge: offer?.authChallenge?.challenge,
|
|
// Optionally include a compact capability hint if small
|
|
capabilities: Array.isArray(offer.capabilities) && offer.capabilities.length <= 5 ? offer.capabilities : void 0
|
|
};
|
|
return templateOffer;
|
|
};
|
|
const MAX_QR_LEN = 800;
|
|
const BIN_MAX_QR_LEN = 400;
|
|
const [qrFramesTotal, setQrFramesTotal] = 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 stopQrAnimation = () => {
|
|
try {
|
|
if (qrAnimationRef.current.timer) {
|
|
clearInterval(qrAnimationRef.current.timer);
|
|
}
|
|
} catch {
|
|
}
|
|
qrAnimationRef.current = { timer: null, chunks: [], idx: 0, active: false };
|
|
setQrFrameIndex(0);
|
|
setQrFramesTotal(0);
|
|
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 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) {
|
|
const intervalMs = 3e3;
|
|
qrAnimationRef.current.active = true;
|
|
clearInterval(qrAnimationRef.current.timer);
|
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
|
}
|
|
console.log("QR Manual mode disabled - auto-scroll resumed");
|
|
}
|
|
};
|
|
const nextQrFrame = async () => {
|
|
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);
|
|
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 {
|
|
console.log("\u{1F3AE} No multiple frames to navigate");
|
|
}
|
|
};
|
|
const prevQrFrame = async () => {
|
|
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);
|
|
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 {
|
|
console.log("\u{1F3AE} No multiple frames to navigate");
|
|
}
|
|
};
|
|
const qrChunksBufferRef = React.useRef({ id: null, total: 0, seen: /* @__PURE__ */ new Set(), items: [] });
|
|
const generateQRCode = async (data) => {
|
|
try {
|
|
const originalSize = typeof data === "string" ? data.length : JSON.stringify(data).length;
|
|
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
const isDesktop = typeof window !== "undefined" && (window.innerWidth || 0) >= 1024;
|
|
const QR_SIZE = isDesktop ? 720 : 512;
|
|
if (payload.length <= MAX_QR_LEN) {
|
|
if (!window.generateQRCode) throw new Error("QR code generator unavailable");
|
|
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 });
|
|
setQrCodeUrl(qrDataUrl);
|
|
setQrFramesTotal(1);
|
|
setQrFrameIndex(1);
|
|
return;
|
|
}
|
|
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 TARGET_CHUNKS = 10;
|
|
const FRAME_MAX = Math.max(200, Math.floor(payload.length / TARGET_CHUNKS));
|
|
const total = Math.ceil(payload.length / FRAME_MAX);
|
|
const rawChunks = [];
|
|
for (let i = 0; i < total; i++) {
|
|
const seq = i + 1;
|
|
const part = payload.slice(i * FRAME_MAX, (i + 1) * FRAME_MAX);
|
|
rawChunks.push(JSON.stringify({ hdr: { v: 1, id, seq, total, rt: "raw" }, body: part }));
|
|
}
|
|
if (!window.generateQRCode) throw new Error("QR code generator unavailable");
|
|
if (rawChunks.length === 1) {
|
|
const url = await window.generateQRCode(rawChunks[0], { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE });
|
|
setQrCodeUrl(url);
|
|
setQrFramesTotal(1);
|
|
setQrFrameIndex(1);
|
|
return;
|
|
}
|
|
qrAnimationRef.current.chunks = rawChunks;
|
|
qrAnimationRef.current.idx = 0;
|
|
qrAnimationRef.current.active = true;
|
|
setQrFramesTotal(rawChunks.length);
|
|
setQrFrameIndex(1);
|
|
const EC_OPTS = { errorCorrectionLevel: "M", margin: 2, size: QR_SIZE };
|
|
await renderNext();
|
|
if (!qrManualMode) {
|
|
const intervalMs = 4e3;
|
|
qrAnimationRef.current.active = true;
|
|
qrAnimationRef.current.timer = setInterval(renderAndAdvance, intervalMs);
|
|
}
|
|
return;
|
|
} catch (error) {
|
|
console.error("QR code generation failed:", error);
|
|
setMessages((prev) => [...prev, {
|
|
message: ` QR code generation failed: ${error.message}`,
|
|
type: "error"
|
|
}]);
|
|
}
|
|
};
|
|
const reconstructFromTemplate = (templateData) => {
|
|
const fullOffer = {
|
|
type: "enhanced_secure_offer",
|
|
version: templateData.version,
|
|
timestamp: templateData.timestamp,
|
|
sessionId: templateData.sessionId,
|
|
connectionId: templateData.connectionId,
|
|
verificationCode: templateData.verificationCode,
|
|
salt: templateData.salt,
|
|
sdp: templateData.sdp,
|
|
keyFingerprints: templateData.keyFingerprints,
|
|
capabilities: templateData.capabilities,
|
|
// Reconstruct ECDH key object
|
|
ecdhPublicKey: {
|
|
keyType: "ECDH",
|
|
keyData: templateData.ecdhKeyData,
|
|
timestamp: templateData.timestamp - 1e3,
|
|
// Approximate
|
|
version: templateData.version,
|
|
signature: templateData.ecdhSignature
|
|
},
|
|
// Reconstruct ECDSA key object
|
|
ecdsaPublicKey: {
|
|
keyType: "ECDSA",
|
|
keyData: templateData.ecdsaKeyData,
|
|
timestamp: templateData.timestamp - 999,
|
|
// Approximate
|
|
version: templateData.version,
|
|
signature: templateData.ecdsaSignature
|
|
},
|
|
// Reconstruct auth challenge
|
|
authChallenge: {
|
|
challenge: templateData.authChallenge,
|
|
timestamp: templateData.timestamp,
|
|
nonce: templateData.authNonce,
|
|
version: templateData.version
|
|
},
|
|
// Generate security level (can be recalculated)
|
|
securityLevel: {
|
|
level: "CRITICAL",
|
|
score: 20,
|
|
color: "red",
|
|
verificationResults: {
|
|
encryption: { passed: false, details: "Encryption not working", points: 0 },
|
|
keyExchange: { passed: true, details: "Simple key exchange verified", points: 15 },
|
|
messageIntegrity: { passed: false, details: "Message integrity failed", points: 0 },
|
|
rateLimiting: { passed: true, details: "Rate limiting active", points: 5 },
|
|
ecdsa: { passed: false, details: "Enhanced session required - feature not available", points: 0 },
|
|
metadataProtection: { passed: false, details: "Enhanced session required - feature not available", points: 0 },
|
|
pfs: { passed: false, details: "Enhanced session required - feature not available", points: 0 },
|
|
nestedEncryption: { passed: false, details: "Enhanced session required - feature not available", points: 0 },
|
|
packetPadding: { passed: false, details: "Enhanced session required - feature not available", points: 0 },
|
|
advancedFeatures: { passed: false, details: "Premium session required - feature not available", points: 0 }
|
|
},
|
|
timestamp: templateData.timestamp,
|
|
details: "Real verification: 20/100 security checks passed (2/4 available)",
|
|
isRealData: true,
|
|
passedChecks: 2,
|
|
totalChecks: 4,
|
|
sessionType: "demo",
|
|
maxPossibleScore: 50
|
|
}
|
|
};
|
|
return fullOffer;
|
|
};
|
|
const handleQRScan = async (scannedData) => {
|
|
try {
|
|
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) {
|
|
const { hdr } = parsedData;
|
|
if (!qrChunksBufferRef.current.id || qrChunksBufferRef.current.id !== hdr.id) {
|
|
qrChunksBufferRef.current = { id: hdr.id, total: hdr.total || 1, seen: /* @__PURE__ */ new Set(), items: [], lastUpdateMs: Date.now() };
|
|
try {
|
|
document.dispatchEvent(new CustomEvent("qr-scan-progress", { detail: { id: hdr.id, seq: 0, total: hdr.total || 1 } }));
|
|
} catch {
|
|
}
|
|
}
|
|
if (!qrChunksBufferRef.current.seen.has(hdr.seq)) {
|
|
qrChunksBufferRef.current.seen.add(hdr.seq);
|
|
qrChunksBufferRef.current.items.push(scannedData);
|
|
qrChunksBufferRef.current.lastUpdateMs = Date.now();
|
|
}
|
|
try {
|
|
const uniqueCount = qrChunksBufferRef.current.seen.size;
|
|
document.dispatchEvent(new CustomEvent("qr-scan-progress", { detail: { id: hdr.id, seq: uniqueCount, total: qrChunksBufferRef.current.total || hdr.total || 0 } }));
|
|
} catch {
|
|
}
|
|
const isComplete = qrChunksBufferRef.current.seen.size >= (qrChunksBufferRef.current.total || 1);
|
|
if (!isComplete) {
|
|
return Promise.resolve(false);
|
|
}
|
|
if (hdr.rt === "raw") {
|
|
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("");
|
|
const 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. RAW 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("RAW multi-frame reconstruction failed:", e2);
|
|
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) {
|
|
try {
|
|
const results = await window.receiveAndProcess(qrChunksBufferRef.current.items);
|
|
if (results.length > 0) {
|
|
const { payloadObj } = results[0];
|
|
if (showOfferStep) {
|
|
setAnswerInput(JSON.stringify(payloadObj, null, 2));
|
|
} else {
|
|
setOfferInput(JSON.stringify(payloadObj, null, 2));
|
|
}
|
|
setMessages((prev) => [...prev, { message: "All frames captured. COSE 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("COSE multi-chunk processing failed:", e2);
|
|
}
|
|
return Promise.resolve(false);
|
|
} else {
|
|
return Promise.resolve(false);
|
|
}
|
|
}
|
|
if (parsedData.type === "enhanced_secure_offer_template") {
|
|
console.log("QR scan: Template-based offer detected, reconstructing...");
|
|
const fullOffer = reconstructFromTemplate(parsedData);
|
|
if (showOfferStep) {
|
|
setAnswerInput(JSON.stringify(fullOffer, null, 2));
|
|
console.log("\u{1F4F1} Template data populated to answerInput (waiting for response mode)");
|
|
} else {
|
|
setOfferInput(JSON.stringify(fullOffer, null, 2));
|
|
console.log("\u{1F4F1} Template data populated to offerInput (paste invitation mode)");
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: "\u{1F4F1} QR code scanned successfully! Full offer reconstructed from template.",
|
|
type: "success"
|
|
}]);
|
|
setShowQRScannerModal(false);
|
|
return true;
|
|
} else if (parsedData.type === "secure_offer_reference" && parsedData.referenceId) {
|
|
const fullOfferData = localStorage.getItem(`qr_offer_${parsedData.referenceId}`);
|
|
if (fullOfferData) {
|
|
const fullOffer = JSON.parse(fullOfferData);
|
|
if (showOfferStep) {
|
|
setAnswerInput(JSON.stringify(fullOffer, null, 2));
|
|
} else {
|
|
setOfferInput(JSON.stringify(fullOffer, null, 2));
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: "\u{1F4F1} QR code scanned successfully! Full offer data retrieved.",
|
|
type: "success"
|
|
}]);
|
|
setShowQRScannerModal(false);
|
|
return true;
|
|
} else {
|
|
setMessages((prev) => [...prev, {
|
|
message: "QR code reference found but full data not available. Please use copy/paste.",
|
|
type: "error"
|
|
}]);
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!parsedData.sdp && parsedData.type === "enhanced_secure_offer") {
|
|
setMessages((prev) => [...prev, {
|
|
message: "Compressed QR may omit SDP for brevity. Use copy/paste if connection fails.",
|
|
type: "warning"
|
|
}]);
|
|
}
|
|
if (showOfferStep) {
|
|
console.log("QR scan: Populating answerInput with:", parsedData);
|
|
setAnswerInput(JSON.stringify(parsedData, null, 2));
|
|
} else {
|
|
console.log("QR scan: Populating offerInput with:", parsedData);
|
|
setOfferInput(JSON.stringify(parsedData, null, 2));
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: "\u{1F4F1} QR code scanned successfully!",
|
|
type: "success"
|
|
}]);
|
|
setShowQRScannerModal(false);
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
if (showOfferStep) {
|
|
setAnswerInput(scannedData);
|
|
} else {
|
|
setOfferInput(scannedData);
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: "\u{1F4F1} QR code scanned successfully!",
|
|
type: "success"
|
|
}]);
|
|
setShowQRScannerModal(false);
|
|
return true;
|
|
}
|
|
};
|
|
const handleCreateOffer = async () => {
|
|
try {
|
|
setOfferData("");
|
|
setShowOfferStep(false);
|
|
setShowQRCode(false);
|
|
setQrCodeUrl("");
|
|
const offer = await webrtcManagerRef.current.createSecureOffer();
|
|
setOfferData(offer);
|
|
setShowOfferStep(true);
|
|
const offerString = typeof offer === "object" ? JSON.stringify(offer) : offer;
|
|
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(
|
|
(m) => m.type === "system" && (m.message.includes("Secure invitation created") || m.message.includes("Send the encrypted code"))
|
|
);
|
|
if (existingMessages.length === 0) {
|
|
setMessages((prev) => [...prev, {
|
|
message: "Secure invitation created and encrypted!",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
setMessages((prev) => [...prev, {
|
|
message: "\u{1F4E4} Send the invitation code to your interlocutor via a secure channel (voice call, SMS, etc.)..",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} catch (error) {
|
|
setMessages((prev) => [...prev, {
|
|
message: `Error creating invitation: ${error.message}`,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
};
|
|
const handleCreateAnswer = async () => {
|
|
try {
|
|
if (!offerInput.trim()) {
|
|
setMessages((prev) => [...prev, {
|
|
message: "You need to insert the invitation code from your interlocutor.",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
return;
|
|
}
|
|
try {
|
|
setMessages((prev) => [...prev, {
|
|
message: "Processing the secure invitation...",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
let offer;
|
|
try {
|
|
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) {
|
|
throw new Error(`Invalid invitation format: ${parseError.message}`);
|
|
}
|
|
if (!offer || typeof offer !== "object") {
|
|
throw new Error("The invitation must be an object");
|
|
}
|
|
const isValidOfferType = offer.t === "offer" || offer.type === "enhanced_secure_offer";
|
|
if (!isValidOfferType) {
|
|
throw new Error("Invalid invitation type. Expected offer or enhanced_secure_offer");
|
|
}
|
|
const answer = await webrtcManagerRef.current.createSecureAnswer(offer);
|
|
setAnswerData(answer);
|
|
setShowAnswerStep(true);
|
|
const answerString = typeof answer === "object" ? JSON.stringify(answer) : answer;
|
|
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 (typeof markAnswerCreated === "function") {
|
|
markAnswerCreated();
|
|
}
|
|
}
|
|
const existingResponseMessages = messages.filter(
|
|
(m) => m.type === "system" && (m.message.includes("Secure response created") || m.message.includes("Send the response"))
|
|
);
|
|
if (existingResponseMessages.length === 0) {
|
|
setMessages((prev) => [...prev, {
|
|
message: "Secure response created!",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
setMessages((prev) => [...prev, {
|
|
message: "Send the response code to the initiator via a secure channel or let them scan the QR code below.",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in handleCreateAnswer:", error);
|
|
setMessages((prev) => [...prev, {
|
|
message: `Error processing the invitation: ${error.message}`,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in handleCreateAnswer:", error);
|
|
setMessages((prev) => [...prev, {
|
|
message: `Invitation processing error: ${error.message}`,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
};
|
|
const handleConnect = async () => {
|
|
try {
|
|
if (!answerInput.trim()) {
|
|
setMessages((prev) => [...prev, {
|
|
message: "You need to insert the response code from your interlocutor.",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
return;
|
|
}
|
|
try {
|
|
setMessages((prev) => [...prev, {
|
|
message: "Processing the secure response...",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
let answer;
|
|
try {
|
|
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) {
|
|
throw new Error(`Invalid response format: ${parseError.message}`);
|
|
}
|
|
if (!answer || typeof answer !== "object") {
|
|
throw new Error("The response must be an object");
|
|
}
|
|
const answerType = answer.t || answer.type;
|
|
if (!answerType || answerType !== "answer" && answerType !== "enhanced_secure_answer") {
|
|
throw new Error("Invalid response type. Expected answer or enhanced_secure_answer");
|
|
}
|
|
await webrtcManagerRef.current.handleSecureAnswer(answer);
|
|
if (pendingSession) {
|
|
setPendingSession(null);
|
|
setMessages((prev) => [...prev, {
|
|
message: `All security features enabled by default`,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: "Finalizing the secure connection...",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
if (!window.isUpdatingSecurity) {
|
|
updateSecurityLevel().catch(console.error);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in handleConnect inner try:", error);
|
|
let errorMessage = "Connection setup error";
|
|
if (error.message.includes("CRITICAL SECURITY FAILURE")) {
|
|
if (error.message.includes("ECDH public key structure")) {
|
|
errorMessage = "Invalid response code - missing or corrupted cryptographic key. Please check the code and try again.";
|
|
} else if (error.message.includes("ECDSA public key structure")) {
|
|
errorMessage = "Invalid response code - missing signature verification key. Please check the code and try again.";
|
|
} else {
|
|
errorMessage = "Security validation failed - possible attack detected";
|
|
}
|
|
} else if (error.message.includes("too old") || error.message.includes("replay")) {
|
|
errorMessage = "Response data is outdated - please use a fresh invitation";
|
|
} else if (error.message.includes("MITM") || error.message.includes("signature")) {
|
|
errorMessage = "Security breach detected - connection rejected";
|
|
} else if (error.message.includes("Invalid") || error.message.includes("format")) {
|
|
errorMessage = "Invalid response format - please check the code";
|
|
} else {
|
|
errorMessage = ` ${error.message}`;
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: errorMessage,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now(),
|
|
showRetryButton: true
|
|
}]);
|
|
if (!error.message.includes("too old") && !error.message.includes("replay")) {
|
|
setPendingSession(null);
|
|
setSessionTimeLeft(0);
|
|
}
|
|
setConnectionStatus("failed");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in handleConnect outer try:", error);
|
|
let errorMessage = "Connection setup error";
|
|
if (error.message.includes("CRITICAL SECURITY FAILURE")) {
|
|
if (error.message.includes("ECDH public key structure")) {
|
|
errorMessage = "Invalid response code - missing or corrupted cryptographic key. Please check the code and try again.";
|
|
} else if (error.message.includes("ECDSA public key structure")) {
|
|
errorMessage = "Invalid response code - missing signature verification key. Please check the code and try again.";
|
|
} else {
|
|
errorMessage = "Security validation failed - possible attack detected";
|
|
}
|
|
} else if (error.message.includes("too old") || error.message.includes("replay")) {
|
|
errorMessage = "Response data is outdated - please use a fresh invitation";
|
|
} else if (error.message.includes("MITM") || error.message.includes("signature")) {
|
|
errorMessage = "Security breach detected - connection rejected";
|
|
} else if (error.message.includes("Invalid") || error.message.includes("format")) {
|
|
errorMessage = "Invalid response format - please check the code";
|
|
} else {
|
|
errorMessage = `${error.message}`;
|
|
}
|
|
setMessages((prev) => [...prev, {
|
|
message: errorMessage,
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now(),
|
|
showRetryButton: true
|
|
}]);
|
|
if (!error.message.includes("too old") && !error.message.includes("replay")) {
|
|
setPendingSession(null);
|
|
setSessionTimeLeft(0);
|
|
}
|
|
setConnectionStatus("failed");
|
|
}
|
|
};
|
|
const handleVerifyConnection = (isValid) => {
|
|
if (isValid) {
|
|
webrtcManagerRef.current.confirmVerification();
|
|
setLocalVerificationConfirmed(true);
|
|
} else {
|
|
setMessages((prev) => [...prev, {
|
|
message: " Verification rejected. The connection is unsafe! Session reset..",
|
|
type: "system",
|
|
id: Date.now(),
|
|
timestamp: Date.now()
|
|
}]);
|
|
setLocalVerificationConfirmed(false);
|
|
setRemoteVerificationConfirmed(false);
|
|
setBothVerificationsConfirmed(false);
|
|
setShowVerification(false);
|
|
setVerificationCode("");
|
|
setConnectionStatus("disconnected");
|
|
setOfferData(null);
|
|
setAnswerData(null);
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
setShowAnswerStep(false);
|
|
setKeyFingerprint("");
|
|
setSecurityLevel(null);
|
|
setIsVerified(false);
|
|
setMessages([]);
|
|
setSessionTimeLeft(0);
|
|
setPendingSession(null);
|
|
document.dispatchEvent(new CustomEvent("disconnected"));
|
|
handleDisconnect();
|
|
}
|
|
};
|
|
const handleSendMessage = async () => {
|
|
if (!messageInput.trim()) {
|
|
return;
|
|
}
|
|
if (!webrtcManagerRef.current) {
|
|
return;
|
|
}
|
|
if (!webrtcManagerRef.current.isConnected()) {
|
|
return;
|
|
}
|
|
try {
|
|
addMessageWithAutoScroll(messageInput.trim(), "sent");
|
|
await webrtcManagerRef.current.sendMessage(messageInput);
|
|
setMessageInput("");
|
|
} catch (error) {
|
|
const msg = String(error?.message || error);
|
|
if (!/queued for sending|Data channel not ready/i.test(msg)) {
|
|
addMessageWithAutoScroll(`Sending error: ${msg}`, "system");
|
|
}
|
|
}
|
|
};
|
|
const handleClearData = () => {
|
|
setOfferData("");
|
|
setAnswerData("");
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
if (!shouldPreserveAnswerData()) {
|
|
setShowAnswerStep(false);
|
|
}
|
|
setShowVerification(false);
|
|
setShowQRCode(false);
|
|
setShowQRScanner(false);
|
|
setShowQRScannerModal(false);
|
|
if (!shouldPreserveAnswerData()) {
|
|
setQrCodeUrl("");
|
|
}
|
|
setVerificationCode("");
|
|
setIsVerified(false);
|
|
setKeyFingerprint("");
|
|
setSecurityLevel(null);
|
|
setConnectionStatus("disconnected");
|
|
setMessages([]);
|
|
setMessageInput("");
|
|
setLocalVerificationConfirmed(false);
|
|
setRemoteVerificationConfirmed(false);
|
|
setBothVerificationsConfirmed(false);
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
setSessionTimeLeft(0);
|
|
setPendingSession(null);
|
|
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
|
};
|
|
const handleDisconnect = () => {
|
|
setSessionTimeLeft(0);
|
|
updateConnectionState({
|
|
status: "disconnected",
|
|
isUserInitiatedDisconnect: true
|
|
});
|
|
if (webrtcManagerRef.current) {
|
|
webrtcManagerRef.current.disconnect();
|
|
}
|
|
setKeyFingerprint("");
|
|
setVerificationCode("");
|
|
setSecurityLevel(null);
|
|
setIsVerified(false);
|
|
setShowVerification(false);
|
|
setConnectionStatus("disconnected");
|
|
setLocalVerificationConfirmed(false);
|
|
setRemoteVerificationConfirmed(false);
|
|
setBothVerificationsConfirmed(false);
|
|
setConnectionStatus("disconnected");
|
|
setShowVerification(false);
|
|
setOfferData(null);
|
|
setAnswerData(null);
|
|
setOfferInput("");
|
|
setAnswerInput("");
|
|
setShowOfferStep(false);
|
|
setShowAnswerStep(false);
|
|
setKeyFingerprint("");
|
|
setVerificationCode("");
|
|
setSecurityLevel(null);
|
|
setIsVerified(false);
|
|
setMessages([]);
|
|
if (typeof console.clear === "function") {
|
|
console.clear();
|
|
}
|
|
document.dispatchEvent(new CustomEvent("peer-disconnect"));
|
|
document.dispatchEvent(new CustomEvent("disconnected"));
|
|
document.dispatchEvent(new CustomEvent("session-cleanup", {
|
|
detail: {
|
|
timestamp: Date.now(),
|
|
reason: "manual_disconnect"
|
|
}
|
|
}));
|
|
setTimeout(() => {
|
|
setSessionTimeLeft(0);
|
|
}, 500);
|
|
handleClearData();
|
|
setTimeout(() => {
|
|
}, 1e3);
|
|
};
|
|
const handleSessionActivated = (session) => {
|
|
let message;
|
|
if (session.type === "demo") {
|
|
message = ` Demo session activated for 6 minutes. You can create invitations!`;
|
|
} else {
|
|
message = ` All security features enabled by default. You can create invitations!`;
|
|
}
|
|
addMessageWithAutoScroll(message, "system");
|
|
};
|
|
React.useEffect(() => {
|
|
if (connectionStatus === "connected" && isVerified) {
|
|
addMessageWithAutoScroll(" Secure connection successfully established and verified! You can now communicate safely with full protection against MITM attacks and Perfect Forward Secrecy..", "system");
|
|
}
|
|
}, [connectionStatus, isVerified]);
|
|
const isConnectedAndVerified = (connectionStatus === "connected" || connectionStatus === "verified") && isVerified;
|
|
React.useEffect(() => {
|
|
if (isConnectedAndVerified && pendingSession && connectionStatus !== "failed") {
|
|
setPendingSession(null);
|
|
setSessionTimeLeft(0);
|
|
addMessageWithAutoScroll(" All security features enabled by default", "system");
|
|
}
|
|
}, [isConnectedAndVerified, pendingSession, connectionStatus]);
|
|
return React.createElement("div", {
|
|
className: "minimal-bg min-h-screen"
|
|
}, [
|
|
React.createElement(EnhancedMinimalHeader, {
|
|
key: "header",
|
|
status: connectionStatus,
|
|
fingerprint: keyFingerprint,
|
|
verificationCode,
|
|
onDisconnect: handleDisconnect,
|
|
isConnected: isConnectedAndVerified,
|
|
securityLevel,
|
|
// sessionManager removed - all features enabled by default
|
|
sessionTimeLeft,
|
|
webrtcManager: webrtcManagerRef.current
|
|
}),
|
|
React.createElement(
|
|
"main",
|
|
{
|
|
key: "main"
|
|
},
|
|
/* @__PURE__ */ (() => {
|
|
return isConnectedAndVerified;
|
|
})() ? (() => {
|
|
return React.createElement(EnhancedChatInterface, {
|
|
messages,
|
|
messageInput,
|
|
setMessageInput,
|
|
onSendMessage: handleSendMessage,
|
|
onDisconnect: handleDisconnect,
|
|
keyFingerprint,
|
|
isVerified,
|
|
chatMessagesRef,
|
|
scrollToBottom,
|
|
webrtcManager: webrtcManagerRef.current
|
|
});
|
|
})() : React.createElement(EnhancedConnectionSetup, {
|
|
onCreateOffer: handleCreateOffer,
|
|
onCreateAnswer: handleCreateAnswer,
|
|
onConnect: handleConnect,
|
|
onClearData: handleClearData,
|
|
onVerifyConnection: handleVerifyConnection,
|
|
connectionStatus,
|
|
offerData,
|
|
answerData,
|
|
offerInput,
|
|
setOfferInput,
|
|
answerInput,
|
|
setAnswerInput,
|
|
showOfferStep,
|
|
showAnswerStep,
|
|
verificationCode,
|
|
showVerification,
|
|
showQRCode,
|
|
qrCodeUrl,
|
|
showQRScanner,
|
|
setShowQRCode,
|
|
setShowQRScanner,
|
|
setShowQRScannerModal,
|
|
messages,
|
|
localVerificationConfirmed,
|
|
remoteVerificationConfirmed,
|
|
bothVerificationsConfirmed,
|
|
// QR control props
|
|
qrFramesTotal,
|
|
qrFrameIndex,
|
|
qrManualMode,
|
|
toggleQrManualMode,
|
|
nextQrFrame,
|
|
prevQrFrame,
|
|
// PAKE passwords removed - using SAS verification instead
|
|
markAnswerCreated
|
|
})
|
|
)
|
|
]);
|
|
};
|
|
function initializeApp() {
|
|
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
|
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById("root"));
|
|
} else {
|
|
console.error("\u041C\u043E\u0434\u0443\u043B\u0438 \u043D\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u044B:", {
|
|
hasCrypto: !!window.EnhancedSecureCryptoUtils,
|
|
hasWebRTC: !!window.EnhancedSecureWebRTCManager
|
|
});
|
|
}
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
console.error("Unhandled promise rejection:", event.reason);
|
|
event.preventDefault();
|
|
});
|
|
window.addEventListener("error", (event) => {
|
|
console.error("Global error:", event.error);
|
|
event.preventDefault();
|
|
});
|
|
if (!window.initializeApp) {
|
|
window.initializeApp = initializeApp;
|
|
}
|
|
}
|
|
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById("root"));
|
|
//# sourceMappingURL=app.js.map
|