From b45597f019e1bc4bdf078df188879e8b06842627 Mon Sep 17 00:00:00 2001 From: ashggan Date: Mon, 19 Jan 2026 17:15:30 +0200 Subject: [PATCH] feat(ui): Add 2026 account creation design components Implement new account creation flow based on 2026 design mockups: - CreateAccountNameAndImage: Name input and image password upload - SaveKeyImageRecovery: Key image save with expandable recovery options - ConfirmKeyImageWorks: Verification screen for key image - Shared UI components: StepIndicator, PageHeader, InfoBox, WarningItem, ImageUploadBox, PrimaryButton New route at /create-2026 with conditional header rendering. Co-Authored-By: Claude Opus 4.5 --- client/src/App.tsx | 31 +- .../components/2026/ConfirmKeyImageWorks.tsx | 188 +++++++++++ .../2026/CreateAccountNameAndImage.tsx | 153 +++++++++ client/src/components/2026/ImageUploadBox.tsx | 109 +++++++ client/src/components/2026/InfoBox.tsx | 38 +++ client/src/components/2026/PageHeader.tsx | 59 ++++ client/src/components/2026/PrimaryButton.tsx | 83 +++++ .../components/2026/SaveKeyImageRecovery.tsx | 308 ++++++++++++++++++ client/src/components/2026/StepIndicator.tsx | 56 ++++ client/src/components/2026/WarningItem.tsx | 49 +++ client/src/components/2026/index.ts | 12 + client/src/pages/CreateSafe2026.tsx | 265 +++++++++++++++ 12 files changed, 1344 insertions(+), 7 deletions(-) create mode 100644 client/src/components/2026/ConfirmKeyImageWorks.tsx create mode 100644 client/src/components/2026/CreateAccountNameAndImage.tsx create mode 100644 client/src/components/2026/ImageUploadBox.tsx create mode 100644 client/src/components/2026/InfoBox.tsx create mode 100644 client/src/components/2026/PageHeader.tsx create mode 100644 client/src/components/2026/PrimaryButton.tsx create mode 100644 client/src/components/2026/SaveKeyImageRecovery.tsx create mode 100644 client/src/components/2026/StepIndicator.tsx create mode 100644 client/src/components/2026/WarningItem.tsx create mode 100644 client/src/components/2026/index.ts create mode 100644 client/src/pages/CreateSafe2026.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 825e39b..4b5ea3e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,18 +1,22 @@ // ./src/App.tsx -import { Route, Routes } from "react-router"; +import { Route, Routes, useLocation } from "react-router"; import { BrowserRouter } from "react-router-dom"; import { ChakraProvider } from "@chakra-ui/react"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import CreateSafe from "./pages/CreateSafe"; +import CreateSafe2026 from "./pages/CreateSafe2026"; import theme from "./theme"; import TopNav from "./components/TopNav"; import { StoreModel } from "./types"; import { StoreProvider, createStore, action, computed } from "easy-peasy"; +// Routes that use the 2026 design (which has its own header) +const ROUTES_WITH_OWN_HEADER = ["/create-2026"]; + const store = createStore({ // State identity: null, @@ -50,17 +54,30 @@ const store = createStore({ }), }); +// Inner component that can use routing hooks (must be inside BrowserRouter) +function AppContent() { + const location = useLocation(); + const showTopNav = !ROUTES_WITH_OWN_HEADER.includes(location.pathname); + + return ( + <> + {showTopNav && } + + + } /> + } /> + + + + ); +} + function App() { return ( - - - - } /> - - + diff --git a/client/src/components/2026/ConfirmKeyImageWorks.tsx b/client/src/components/2026/ConfirmKeyImageWorks.tsx new file mode 100644 index 0000000..739230e --- /dev/null +++ b/client/src/components/2026/ConfirmKeyImageWorks.tsx @@ -0,0 +1,188 @@ +import { useState, useRef } from "react"; +import { Box, Flex, Text, Input, UnorderedList, ListItem } from "@chakra-ui/react"; +import { FiUpload } from "react-icons/fi"; +import PageHeader from "./PageHeader"; +import StepIndicator from "./StepIndicator"; + +interface ConfirmKeyImageWorksProps { + onBack: () => void; + onExit: () => void; + onComplete: (keyImageFile: File) => void; + onSignIn: () => void; + currentStep?: number; +} + +const ConfirmKeyImageWorks = ({ + onBack, + onExit, + onComplete, + onSignIn, + currentStep = 3, +}: ConfirmKeyImageWorksProps) => { + const [selectedFile, setSelectedFile] = useState(); + const [isDragging, setIsDragging] = useState(false); + const fileRef = useRef(null); + + const handleDragEnter = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(false); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(false); + const file = event.dataTransfer.files[0]; + if (file) { + setSelectedFile(file); + } + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setSelectedFile(file); + } + }; + + const browseFile = () => fileRef.current?.click(); + + const handleComplete = () => { + if (selectedFile) { + onComplete(selectedFile); + } + }; + + return ( + + + + + {/* Title Section */} + + Create account + + + Start keeping your records safe + + + {/* Step Indicator */} + + + {/* Confirm Key Image Section */} + + Confirm your key image works + + + Upload the key image you just saved to make sure it works correctly. + + + {/* Upload Area */} + + + + + + + + + {selectedFile ? selectedFile.name : "Upload your key image"} + + + The file you just downloaded + + + + + {/* Complete Setup Button */} + + + Complete setup + + + + {/* Tips Box */} + + + Use the exact file you just downloaded + Don't use a screenshot or re-saved version + If it doesn't work, go back and download again + + + + {/* Sign In Link */} + + + Already have an account? + + + Sign in + + + + + ); +}; + +export default ConfirmKeyImageWorks; diff --git a/client/src/components/2026/CreateAccountNameAndImage.tsx b/client/src/components/2026/CreateAccountNameAndImage.tsx new file mode 100644 index 0000000..f541963 --- /dev/null +++ b/client/src/components/2026/CreateAccountNameAndImage.tsx @@ -0,0 +1,153 @@ +import { useState } from "react"; +import { Box, Flex, Text, Input, OrderedList, ListItem } from "@chakra-ui/react"; +import PageHeader from "./PageHeader"; +import ImageUploadBox from "./ImageUploadBox"; +import InfoBox from "./InfoBox"; + +interface CreateAccountNameAndImageProps { + onBack: () => void; + onExit: () => void; + onContinue: (name: string, image: File) => void; + initialName?: string; +} + +const CreateAccountNameAndImage = ({ + onBack, + onExit, + onContinue, + initialName = "", +}: CreateAccountNameAndImageProps) => { + const [safeName, setSafeName] = useState(initialName); + const [selectedImage, setSelectedImage] = useState(); + + const isValid = safeName.trim().length >= 2 && selectedImage; + + const handleContinue = () => { + if (isValid && selectedImage) { + onContinue(safeName, selectedImage); + } + }; + + return ( + + + + + {/* Title Section */} + + Create account + + + Start keeping your records safe + + + {/* Name Input Section */} + + + Choose a Name for your Digital Safe + + + like a user name + + setSafeName(e.target.value)} + placeholder="e.g., My Safe, Personal Vault" + size="lg" + borderRadius="12px" + border="1px solid #E5E7EB" + bg="white" + fontSize="14px" + py="1.25rem" + px="1rem" + _placeholder={{ + color: "#9CA3AF", + }} + _focus={{ + borderColor: "#1B5086", + boxShadow: "0 0 0 1px #1B5086", + }} + /> + + + {/* Image Upload Section */} + + + Choose your Image Password + + + + + {/* Info Box */} + + + + You choose any PNG image you like + + + Sophia hides a digital key in that image and sends it back to you. The image looks identical but this new image will then open your Digital Safe. + + + Upload this key image to sign in - no password to remember + + + + + Why use this?{" "} + It's more secure than a password and nothing is stored that could reveal your account exists. + + + + + {/* Continue Button */} + + + Continue + + + + + ); +}; + +export default CreateAccountNameAndImage; diff --git a/client/src/components/2026/ImageUploadBox.tsx b/client/src/components/2026/ImageUploadBox.tsx new file mode 100644 index 0000000..a21e622 --- /dev/null +++ b/client/src/components/2026/ImageUploadBox.tsx @@ -0,0 +1,109 @@ +import { useRef, useState } from "react"; +import { Box, Flex, Text, Input } from "@chakra-ui/react"; +import { FiUpload } from "react-icons/fi"; + +interface ImageUploadBoxProps { + onImageSelect: (file: File) => void; + selectedImage?: File; + accept?: string; + title?: string; + subtitle?: string; +} + +const ImageUploadBox = ({ + onImageSelect, + selectedImage, + accept = "image/png", + title = "Upload PNG image", + subtitle = "PNG format keeps it unchanged", +}: ImageUploadBoxProps) => { + const fileRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const handleDragEnter = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(false); + }; + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault(); + }; + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + setIsDragging(false); + const file = event.dataTransfer.files[0]; + if (file) { + onImageSelect(file); + } + }; + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + onImageSelect(file); + } + }; + + const browseFile = () => fileRef.current?.click(); + + return ( + + + + + + + + + {selectedImage ? selectedImage.name : title} + + + {subtitle} + + + + ); +}; + +export default ImageUploadBox; diff --git a/client/src/components/2026/InfoBox.tsx b/client/src/components/2026/InfoBox.tsx new file mode 100644 index 0000000..2f56c48 --- /dev/null +++ b/client/src/components/2026/InfoBox.tsx @@ -0,0 +1,38 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import { FiHelpCircle } from "react-icons/fi"; +import { ReactNode } from "react"; + +interface InfoBoxProps { + title?: string; + children: ReactNode; + variant?: "info" | "tip"; +} + +const InfoBox = ({ title, children }: InfoBoxProps) => { + return ( + + {title && ( + + + + {title} + + + )} + + {children} + + + ); +}; + +export default InfoBox; diff --git a/client/src/components/2026/PageHeader.tsx b/client/src/components/2026/PageHeader.tsx new file mode 100644 index 0000000..82fc68f --- /dev/null +++ b/client/src/components/2026/PageHeader.tsx @@ -0,0 +1,59 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import { FiArrowLeft, FiX } from "react-icons/fi"; + +interface PageHeaderProps { + onBack?: () => void; + onExit?: () => void; +} + +const PageHeader = ({ onBack, onExit }: PageHeaderProps) => { + return ( + + {onBack ? ( + + + + Back + + + ) : ( + + )} + + {onExit && ( + + + + Exit + + + )} + + ); +}; + +export default PageHeader; diff --git a/client/src/components/2026/PrimaryButton.tsx b/client/src/components/2026/PrimaryButton.tsx new file mode 100644 index 0000000..825fabb --- /dev/null +++ b/client/src/components/2026/PrimaryButton.tsx @@ -0,0 +1,83 @@ +import { Button, ButtonProps } from "@chakra-ui/react"; +import { ReactNode } from "react"; + +interface PrimaryButtonProps extends Omit { + children: ReactNode; + variant?: "primary" | "secondary" | "text"; + leftIcon?: ReactNode; +} + +const PrimaryButton = ({ + children, + variant = "primary", + leftIcon, + isDisabled, + ...props +}: PrimaryButtonProps) => { + const getStyles = () => { + switch (variant) { + case "primary": + return { + bg: "#404040", + color: "white", + _hover: { + bg: isDisabled ? "#404040" : "#2D2D2D", + }, + _active: { + bg: "#1A1A1A", + }, + }; + case "secondary": + return { + bg: "transparent", + color: "#6B7C93", + border: "1px solid #E5E7EB", + _hover: { + bg: "#F9FAFB", + }, + _active: { + bg: "#F3F4F6", + }, + }; + case "text": + return { + bg: "transparent", + color: "#6B7C93", + _hover: { + color: "#1A2B4A", + textDecoration: "none", + }, + _active: { + color: "#1A2B4A", + }, + }; + default: + return {}; + } + }; + + return ( + + ); +}; + +export default PrimaryButton; diff --git a/client/src/components/2026/SaveKeyImageRecovery.tsx b/client/src/components/2026/SaveKeyImageRecovery.tsx new file mode 100644 index 0000000..cdac098 --- /dev/null +++ b/client/src/components/2026/SaveKeyImageRecovery.tsx @@ -0,0 +1,308 @@ +import { useState } from "react"; +import { + Box, + Flex, + Text, + Image, + Input, + Checkbox, + Collapse, +} from "@chakra-ui/react"; +import { + FiDownload, + FiCopy, + FiChevronDown, + FiChevronUp, + FiMail, + FiAlertTriangle, +} from "react-icons/fi"; +import PageHeader from "./PageHeader"; +import StepIndicator from "./StepIndicator"; +import WarningItem from "./WarningItem"; +import PrimaryButton from "./PrimaryButton"; + +interface SaveKeyImageRecoveryProps { + onBack: () => void; + onExit: () => void; + onDownload: () => void; + onContinue: (email?: string) => void; + keyImageUrl: string; + keyFingerprint: string; + currentStep?: number; +} + +const SaveKeyImageRecovery = ({ + onBack, + onExit, + onDownload, + onContinue, + keyImageUrl, + keyFingerprint, + currentStep = 2, +}: SaveKeyImageRecoveryProps) => { + const [isRecoveryExpanded, setIsRecoveryExpanded] = useState(false); + const [enableRecoveryEmail, setEnableRecoveryEmail] = useState(false); + const [recoveryEmail, setRecoveryEmail] = useState(""); + const [hasDownloaded, setHasDownloaded] = useState(false); + const [copiedFingerprint, setCopiedFingerprint] = useState(false); + + const handleCopyFingerprint = async () => { + try { + await navigator.clipboard.writeText(keyFingerprint); + setCopiedFingerprint(true); + setTimeout(() => setCopiedFingerprint(false), 2000); + } catch (err) { + console.error("Failed to copy fingerprint:", err); + } + }; + + const handleDownload = () => { + setHasDownloaded(true); + onDownload(); + }; + + const handleContinue = () => { + if (enableRecoveryEmail && recoveryEmail) { + onContinue(recoveryEmail); + } else { + onContinue(); + } + }; + + return ( + + + + + {/* Title Section */} + + Create account + + + Start keeping your records safe + + + {/* Step Indicator */} + + + {/* Save Key Image Section */} + + Save your key image + + + Download this image and keep it somewhere safe. You'll need it every + time you sign in. + + + {/* Key Image Display */} + + Key Image + + + Key embedded + + + + {/* Key Fingerprint */} + + + Key fingerprint (for verification) + + + + {keyFingerprint} + + + + + + {copiedFingerprint && ( + + Copied! + + )} + + + {/* Warning Items */} + + + + + + {/* Recovery Option Accordion */} + + setIsRecoveryExpanded(!isRecoveryExpanded)} + > + + + + + Recovery option (optional) + + + Add a backup way to regain access + + + + {isRecoveryExpanded ? ( + + ) : ( + + )} + + + + + {/* Enable Recovery Email Checkbox */} + setEnableRecoveryEmail(e.target.checked)} + colorScheme="blue" + mb="0.75rem" + > + + + Enable recovery email + + + Add the email of someone you trust. They can help you reset + access if you lose your key image. + + + + + {/* Email Input - Only show when checkbox is checked */} + {enableRecoveryEmail && ( + setRecoveryEmail(e.target.value)} + placeholder="Trusted contact's email" + size="md" + borderRadius="8px" + border="1px solid #E5E7EB" + bg="white" + fontSize="14px" + mt="0.75rem" + _placeholder={{ + color: "#9CA3AF", + }} + _focus={{ + borderColor: "#1B5086", + boxShadow: "0 0 0 1px #1B5086", + }} + /> + )} + + {/* Warning message when no recovery method */} + {!enableRecoveryEmail && ( + + + + + Without a recovery method, losing your key image means + permanent loss of access. + + + + )} + + + + + {/* Download Button */} + + } + onClick={handleDownload} + variant="primary" + > + Download key image + + + + {/* I've saved it link */} + + + I've saved it + + + + + ); +}; + +export default SaveKeyImageRecovery; diff --git a/client/src/components/2026/StepIndicator.tsx b/client/src/components/2026/StepIndicator.tsx new file mode 100644 index 0000000..8cf5866 --- /dev/null +++ b/client/src/components/2026/StepIndicator.tsx @@ -0,0 +1,56 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import { FiCheck } from "react-icons/fi"; + +interface StepIndicatorProps { + currentStep: number; + totalSteps?: number; +} + +const StepIndicator = ({ currentStep, totalSteps = 3 }: StepIndicatorProps) => { + const steps = Array.from({ length: totalSteps }, (_, i) => i + 1); + + return ( + + {steps.map((step, index) => ( + + {/* Step Circle */} + + {step < currentStep ? ( + + ) : ( + {step} + )} + + + {/* Connector Line */} + {index < steps.length - 1 && ( + + )} + + ))} + + ); +}; + +export default StepIndicator; diff --git a/client/src/components/2026/WarningItem.tsx b/client/src/components/2026/WarningItem.tsx new file mode 100644 index 0000000..ccaabe7 --- /dev/null +++ b/client/src/components/2026/WarningItem.tsx @@ -0,0 +1,49 @@ +import { Box, Flex, Text } from "@chakra-ui/react"; +import { FiAlertTriangle } from "react-icons/fi"; +import { ReactNode } from "react"; + +interface WarningItemProps { + title: string; + description?: string; + children?: ReactNode; +} + +const WarningItem = ({ title, description, children }: WarningItemProps) => { + return ( + + + + + + + {title} + + {description && ( + + {description} + + )} + {children} + + + ); +}; + +export default WarningItem; diff --git a/client/src/components/2026/index.ts b/client/src/components/2026/index.ts new file mode 100644 index 0000000..901c45f --- /dev/null +++ b/client/src/components/2026/index.ts @@ -0,0 +1,12 @@ +// 2026 Design Components +export { default as CreateAccountNameAndImage } from "./CreateAccountNameAndImage"; +export { default as SaveKeyImageRecovery } from "./SaveKeyImageRecovery"; +export { default as ConfirmKeyImageWorks } from "./ConfirmKeyImageWorks"; + +// Shared UI Components +export { default as StepIndicator } from "./StepIndicator"; +export { default as PageHeader } from "./PageHeader"; +export { default as InfoBox } from "./InfoBox"; +export { default as WarningItem } from "./WarningItem"; +export { default as ImageUploadBox } from "./ImageUploadBox"; +export { default as PrimaryButton } from "./PrimaryButton"; diff --git a/client/src/pages/CreateSafe2026.tsx b/client/src/pages/CreateSafe2026.tsx new file mode 100644 index 0000000..74e8fac --- /dev/null +++ b/client/src/pages/CreateSafe2026.tsx @@ -0,0 +1,265 @@ +import { useState } from "react"; +import { Container } from "@chakra-ui/react"; +import axios from "axios"; +import { toast } from "react-toastify"; +import { useTranslation } from "react-i18next"; + +import { + CreateAccountNameAndImage, + SaveKeyImageRecovery, + ConfirmKeyImageWorks, +} from "../components/2026"; + +import { + createIdentity, + embedKeyInImage, + arrayToBase64, + cleanSafeName, + deriveSessionKeyFromImage, +} from "../utils"; +import { useStoreActions } from "../hooks"; +import withLogoutOnEscPress from "../components/QuickExit"; + +// Step definitions +const STEP_CREATE_ACCOUNT = 1; +const STEP_SAVE_KEY_IMAGE = 2; +const STEP_CONFIRM_KEY_IMAGE = 3; +const STEP_SUCCESS = 4; + +interface ImagePassword { + name: string; + dataURL: string; +} + +const CreateSafe2026 = () => { + const { t } = useTranslation(); + const [step, setStep] = useState(STEP_CREATE_ACCOUNT); + const [safeName, setSafeName] = useState(""); + const [imagePassword, setImagePassword] = useState({ + name: "", + dataURL: "", + }); + const [keyFingerprint, setKeyFingerprint] = useState(""); + const [, setIsProcessing] = useState(false); + + const setIdentity = useStoreActions((actions) => actions.setIdentity); + const setSessionKeys = useStoreActions((actions) => actions.setSessionKeys); + + // Generate a fingerprint from the public key + const generateFingerprint = (pubKey: string): string => { + // Take first 16 characters and format as XXXX-XXXX-XXXX-XXXX + const clean = pubKey.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 16); + return `${clean.slice(0, 4)}-${clean.slice(4, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}`; + }; + + const handleExit = () => { + // Navigate back to landing page or reset + window.location.href = "/"; + }; + + const handleCreateAccountContinue = async (name: string, image: File) => { + setIsProcessing(true); + const cleanedName = cleanSafeName(name); + setSafeName(cleanedName); + + try { + // Create identity (cryptographic keys) + const identity = await createIdentity(); + const pubKeyBase32 = identity.stringifiedPublicKey; + + // Generate fingerprint for display + const fingerprint = generateFingerprint(pubKeyBase32); + setKeyFingerprint(fingerprint); + + // Get filename without extension + const filename = + image.name.substring(0, image.name.lastIndexOf(".")) || image.name; + + // Embed key in image + const dataURL = await embedKeyInImage( + image, + identity.stringifiedSecretKey + ); + setImagePassword({ name: filename, dataURL }); + + // Get CSRF token + let csrf = ""; + try { + const csrfResp = await axios.post(`/act/csrf`); + csrf = csrfResp.data.csrf; + } catch (error) { + console.error("CSRF request failed:", error); + toast.error(t("createImagePassword.errorAccountCreation")); + setIsProcessing(false); + return; + } + + // Create safe on server + try { + await axios.post(`/act/create-safe`, { + username: cleanedName, + pubkey_base32: pubKeyBase32, + signing_pubkey_base64: await arrayToBase64(identity.publicSigningKey), + csrf, + }); + } catch (error) { + console.error("Account creation failed:", error); + toast.error(t("createImagePassword.errorAccountCreation")); + setIsProcessing(false); + return; + } + + // Store identity + setIdentity(identity); + + // Move to next step + setStep(STEP_SAVE_KEY_IMAGE); + } catch (error) { + console.error("Image password creation failed:", error); + toast.error(t("createImagePassword.failure")); + } finally { + setIsProcessing(false); + } + }; + + const handleDownloadKeyImage = () => { + // Convert data URL to blob and download + const dataURItoBlob = (dataURI: string) => { + const byteString = atob(dataURI.split(",")[1]); + const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]; + const arrayBuffer = new ArrayBuffer(byteString.length); + const arr = new Uint8Array(arrayBuffer); + for (let i = 0; i < byteString.length; i++) { + arr[i] = byteString.charCodeAt(i); + } + return new Blob([arrayBuffer], { type: mimeString }); + }; + + // Try modern file system API first, fall back to link download + const downloadImage = async () => { + try { + // @ts-ignore - showSaveFilePicker may not be in TypeScript types + if (window.showSaveFilePicker) { + // @ts-ignore + const handle = await window.showSaveFilePicker({ + suggestedName: imagePassword.name + ".png", + }); + const writable = await handle.createWritable(); + await writable.write(dataURItoBlob(imagePassword.dataURL)); + await writable.close(); + } else { + throw new ReferenceError("showSaveFilePicker not supported"); + } + } catch (err) { + // Fallback for browsers without File System Access API + const link = document.createElement("a"); + link.href = imagePassword.dataURL; + link.setAttribute("download", imagePassword.name + ".png"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + }; + + downloadImage(); + }; + + const handleSaveKeyImageContinue = (email?: string) => { + // TODO: If email is provided, save it for recovery purposes + if (email) { + console.log("Recovery email provided:", email); + // Future: Send recovery email to backend + } + setStep(STEP_CONFIRM_KEY_IMAGE); + }; + + const handleConfirmKeyImage = async (keyImageFile: File) => { + setIsProcessing(true); + try { + // Verify the key image works by extracting session keys + const sessionKeys = await deriveSessionKeyFromImage(keyImageFile); + setSessionKeys(sessionKeys); + + toast.success("Your Digital Safe has been created successfully!"); + setStep(STEP_SUCCESS); + + // Redirect to login after a delay + setTimeout(() => { + window.location.href = "/"; + }, 2000); + } catch (error) { + console.error("Key image verification failed:", error); + toast.error( + "The key image could not be verified. Please make sure you're using the exact file you downloaded." + ); + } finally { + setIsProcessing(false); + } + }; + + const handleSignIn = () => { + // Navigate to sign in page + window.location.href = "/?login=true"; + }; + + return ( + + {step === STEP_CREATE_ACCOUNT && ( + window.history.back()} + onExit={handleExit} + onContinue={handleCreateAccountContinue} + initialName={safeName} + /> + )} + + {step === STEP_SAVE_KEY_IMAGE && ( + setStep(STEP_CREATE_ACCOUNT)} + onExit={handleExit} + onDownload={handleDownloadKeyImage} + onContinue={handleSaveKeyImageContinue} + keyImageUrl={imagePassword.dataURL} + keyFingerprint={keyFingerprint} + currentStep={2} + /> + )} + + {step === STEP_CONFIRM_KEY_IMAGE && ( + setStep(STEP_SAVE_KEY_IMAGE)} + onExit={handleExit} + onComplete={handleConfirmKeyImage} + onSignIn={handleSignIn} + currentStep={3} + /> + )} + + {step === STEP_SUCCESS && ( + +

+ Success! +

+

+ Your Digital Safe has been created. Redirecting to sign in... +

+
+ )} +
+ ); +}; + +export default withLogoutOnEscPress(CreateSafe2026);