diff --git a/web/apps/auth/src/pages/share.tsx b/web/apps/auth/src/pages/share.tsx new file mode 100644 index 0000000000..7b3c42a97b --- /dev/null +++ b/web/apps/auth/src/pages/share.tsx @@ -0,0 +1,241 @@ +import { decryptMetadataJSON_New } from "@/base/crypto"; +import React, { useEffect, useMemo, useState } from "react"; + +interface SharedCode { + startTime: number; + step: number; + codes: string; +} + +interface CodeDisplay { + currentCode: string; + nextCode: string; + progress: number; +} + +const Share: React.FC = () => { + const [sharedCode, setSharedCode] = useState(null); + const [error, setError] = useState(null); + const [timeStatus, setTimeStatus] = useState(-10); + const [codeDisplay, setCodeDisplay] = useState({ + currentCode: "", + nextCode: "", + progress: 0, + }); + + const base64UrlToByteArray = (base64Url: string): Uint8Array => { + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); + }; + + const formatCode = (code: string): string => + code.replace(/(.{3})/g, "$1 ").trim(); + + const getCodeDisplay = ( + codes: string[], + startTime: number, + stepDuration: number, + ): CodeDisplay => { + const currentTime = Date.now(); + const elapsedTime = (currentTime - startTime) / 1000; + const index = Math.floor(elapsedTime / stepDuration); + const progress = ((elapsedTime % stepDuration) / stepDuration) * 100; + + return { + currentCode: formatCode(codes[index] || ""), + nextCode: formatCode(codes[index + 1] || ""), + progress, + }; + }; + + const getTimeStatus = ( + currentTime: number, + startTime: number, + codesLength: number, + stepDuration: number, + ): number => { + if (currentTime < startTime) return -1; + const totalDuration = codesLength * stepDuration * 1000; + if (currentTime > startTime + totalDuration) return 1; + return 0; + }; + + useEffect(() => { + const decryptCode = async () => { + const urlParams = new URLSearchParams(window.location.search); + const data = urlParams.get("data"); + const header = urlParams.get("header"); + const key = window.location.hash.substring(1); + + if (!(data && header && key)) { + setError("Invalid URL. Please check the URL."); + return; + } + + try { + const decryptedCode = (await decryptMetadataJSON_New( + { + encryptedData: base64UrlToByteArray(data), + decryptionHeader: base64UrlToByteArray(header), + }, + base64UrlToByteArray(key), + )) as SharedCode; + setSharedCode(decryptedCode); + } catch (error) { + console.error("Failed to decrypt data:", error); + setError( + "Failed to get the data. Please check the URL and try again.", + ); + } + }; + decryptCode(); + }, []); + + useEffect(() => { + if (!sharedCode) return; + + const updateCode = () => { + const currentTime = Date.now(); + const codes = sharedCode.codes.split(","); + const status = getTimeStatus( + currentTime, + sharedCode.startTime, + codes.length, + sharedCode.step, + ); + setTimeStatus(status); + + if (status === 0) { + setCodeDisplay( + getCodeDisplay( + codes, + sharedCode.startTime, + sharedCode.step, + ), + ); + } + }; + + const interval = setInterval(updateCode, 100); + return () => clearInterval(interval); + }, [sharedCode]); + + const progressBarColor = useMemo( + () => (100 - codeDisplay.progress > 40 ? "#8E2DE2" : "#FFC107"), + [codeDisplay.progress], + ); + + const Message: React.FC<{ text: string }> = ({ text }) => ( +

{text}

+ ); + + return ( +
+
ente
+ +
+ {error &&

{error}

} + {timeStatus === -10 && !error && ( + + )} + {timeStatus === -1 && ( + + )} + {timeStatus === 1 && } + {timeStatus === 0 && ( +
+
+
+
+
+ {codeDisplay.currentCode} +
+
+

+ {codeDisplay.nextCode === "" + ? "Last code" + : "next"} +

+ {codeDisplay.nextCode !== "" && ( +

+ {codeDisplay.nextCode} +

+ )} +
+
+ )} +
+ + + + +
+ ); +}; + +export default Share;