diff --git a/biome.json b/biome.json index 07c7ff8..1dca93a 100644 --- a/biome.json +++ b/biome.json @@ -42,7 +42,7 @@ "noUnusedVariables": "error" } }, - "includes": ["**/src", "!backend"] + "includes": ["**", "!backend"] }, "assist": { "actions": { diff --git a/frontend/src/components/editor/PostSealModal.tsx b/frontend/src/components/editor/PostSealModal.tsx new file mode 100644 index 0000000..1f1c2de --- /dev/null +++ b/frontend/src/components/editor/PostSealModal.tsx @@ -0,0 +1,54 @@ +import { LockIcon } from "@phosphor-icons/react"; +import type { NavigateFunction } from "react-router-dom"; +import { PATHS, ROUTES } from "../../config/routes"; + +interface PostSealModalProps { + sealedTargetId: string | null; + navigate: NavigateFunction; +} + +export function PostSealModal({ + sealedTargetId, + navigate, +}: PostSealModalProps) { + if (!sealedTargetId) return null; + return ( +
+
+ +

Your letter is sealed

+

+ It's encrypted and always safe in your drawer. +

+

+ When you're ready, +
+ you can{" "} + read it,{" "} + send it to + someone, or{" "} + burn it to + release +

+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/editor/ToolBar.tsx b/frontend/src/components/editor/ToolBar.tsx new file mode 100644 index 0000000..efe4c3d --- /dev/null +++ b/frontend/src/components/editor/ToolBar.tsx @@ -0,0 +1,195 @@ +import { + ImageIcon, + LockIcon, + QuestionIcon, + StampIcon, + TrayIcon, + VaultIcon, +} from "@phosphor-icons/react"; + +interface ToolBarProps { + fileInputRef: React.RefObject; + sealBtnClicked: boolean; + setSealBtnClicked: (v: boolean) => void; + onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise; + setConfirmModal: (v: "VAULT" | "SEAL" | null) => void; +} + +export function ToolBar({ + fileInputRef, + sealBtnClicked, + setSealBtnClicked, + onSave, + setConfirmModal, +}: ToolBarProps) { + return ( +
+
+ +
+ +
+ + +
+ + +
+ +
+ +
+ or +
+ +
+ +
+ ); +} + +export function LetterHead() { + return ( +
+
+ + + Sealed & View Only + +
+
+ ); +} + +interface VaultConfirmModalProps { + onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise; + setConfirmModal: (v: "VAULT" | "SEAL" | null) => void; + setUnlockDate: (d: Date | null) => void; +} + +export function VaultConfirmModal({ + onSave, + setConfirmModal, + setUnlockDate, +}: VaultConfirmModalProps) { + return ( +
+
+ +

Vault this letter?

+

+ Vaulting locks the letter permanently and will be{" "} + mailed to you + automatically on the unlock date. +
+ + You cannot edit or view the contents of the letter until then. + +

+
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const unlockDateStr = formData.get("vault-date") as string; + const newUnlockDate = new Date(unlockDateStr); + setUnlockDate(newUnlockDate); + await onSave("VAULT", newUnlockDate); + setConfirmModal(null); + }} + id="vault-form" + > +
+ Set an unlock date +
+ + + + +
+
+
+ ); +} diff --git a/frontend/src/components/ui/EnvelopeReveal.tsx b/frontend/src/components/ui/EnvelopeReveal.tsx index ed0e1b7..6078b70 100644 --- a/frontend/src/components/ui/EnvelopeReveal.tsx +++ b/frontend/src/components/ui/EnvelopeReveal.tsx @@ -66,8 +66,10 @@ export function EnvelopeReveal({ src={waxSeal} alt="Seal" onClick={() => flapCheckbox.current?.click()} + onKeyDown={() => flapCheckbox.current?.click()} />
-
setIsFlipped((prev) => !prev)} > @@ -113,7 +117,7 @@ export function EnvelopeReveal({ className={"absolute mt-0 mr-4 top-18 right-8 text-primary"} size={50} /> -
+ {ignite && ( diff --git a/frontend/src/components/ui/PasskeyModal.tsx b/frontend/src/components/ui/PasskeyModal.tsx new file mode 100644 index 0000000..16376bb --- /dev/null +++ b/frontend/src/components/ui/PasskeyModal.tsx @@ -0,0 +1,52 @@ +import { LockKeyIcon } from "@phosphor-icons/react"; + +interface PasskeyModalProps { + onUnlock: (password: string) => Promise; +} + +export function PasskeyModal({ onUnlock }: PasskeyModalProps) { + return ( +
+
+ +

+ Authentication Required +

+

+ We need your passkey to open your letters +

+
+

+ Your passkey is used to decrypt your data locally. +

+
+
) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const password = formData.get("password") as string; + if (!password) return; + await onUnlock(password); + }} + > + +
+ +
+
+
+
+ ); +} diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts index e915f28..9cd9815 100644 --- a/frontend/src/hooks/useAuth.ts +++ b/frontend/src/hooks/useAuth.ts @@ -48,9 +48,7 @@ export const useAuth = () => { try { const masterKey = await loadMasterKey(); if (masterKey) setMasterKey(masterKey); - } catch { - console.error("Master key restoration failed"); - } + } catch {} // If session in memory, don't trigger refresh/me again if (accessToken && user) { @@ -82,9 +80,7 @@ export const useAuth = () => { ); await saveMasterKey(masterKey); setMasterKey(masterKey); - } catch { - console.error("Master key restoration failed"); - } + } catch {} }; return { diff --git a/frontend/src/pages/Drawer.tsx b/frontend/src/pages/Drawer.tsx index b43971f..2efc859 100644 --- a/frontend/src/pages/Drawer.tsx +++ b/frontend/src/pages/Drawer.tsx @@ -1,9 +1,10 @@ -import { FeatherIcon, LockKeyIcon } from "@phosphor-icons/react"; +import { FeatherIcon } from "@phosphor-icons/react"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import Logo from "../components/Logo"; import { DrawerSection } from "../components/ui/DrawerSection"; import { LetterItem } from "../components/ui/LetterItem"; +import { PasskeyModal } from "../components/ui/PasskeyModal"; import { PATHS } from "../config/routes"; import { useAuth } from "../hooks/useAuth"; import { useLetters } from "../hooks/useLetters"; @@ -12,57 +13,6 @@ import { formatRelativeDateWithoutTime, } from "../utils/dateFormat.ts"; -interface PasskeyModalProps { - onUnlock: (password: string) => Promise; -} - -function PasskeyModal({ onUnlock }: PasskeyModalProps) { - return ( -
-
- -

- Authentication Required -

-

- We need you to re-enter your passkey to open your letters -

-
-

- P.S. We don't validate your input at the moment. -

-
-
) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const password = formData.get("password") as string; - if (!password) return; - await onUnlock(password); - }} - > - -
- -
-
-
-
- ); -} - export default function Drawer() { const { user, logout, unlock } = useAuth(); diff --git a/frontend/src/pages/Editor.test.tsx b/frontend/src/pages/Editor.test.tsx index 63768d0..4f4a8d4 100644 --- a/frontend/src/pages/Editor.test.tsx +++ b/frontend/src/pages/Editor.test.tsx @@ -24,21 +24,20 @@ vi.mock("../components/ui/ComposeCanvas", () => ({ // Mock CryptoUtils to avoid real crypto calls in UI tests vi.mock("../utils/crypto", () => { return { - CryptoUtils: vi.fn().mockImplementation(function () { - return { - initialize: vi.fn().mockResolvedValue(undefined), - encryptLetter: vi.fn().mockResolvedValue({ - encrypted_content: "enc-content", - encrypted_dek: "enc-dek", - sharingKey: "share-key", - }), - encryptMetadata: vi.fn().mockResolvedValue({ - encrypted_content: "enc-meta", - encrypted_dek: "enc-dek", - }), - decryptMetadata: vi.fn().mockResolvedValue({ recipient: "Test User" }), - decryptLetter: vi.fn().mockResolvedValue("{}"), - }; + CryptoUtils: () => ({ + initialize: vi.fn().mockResolvedValue(undefined), + encryptLetter: vi.fn().mockResolvedValue({ + encrypted_content: "enc-content", + encrypted_dek: "enc-dek", + sharingKey: "share-key", + }), + encryptMetadata: vi.fn().mockResolvedValue({ + encrypted_content: "enc-meta", + encrypted_dek: "enc-dek", + }), + decryptMetadata: vi.fn().mockResolvedValue({ recipient: "Test User" }), + decryptLetter: vi.fn().mockResolvedValue("{}"), + extractSharingKey: vi.fn().mockResolvedValue("share-key"), }), }; }); @@ -160,7 +159,7 @@ describe("Editor Page", () => { fireEvent.click(secondarySealBtn); await waitFor(() => { - expect(screen.getByText(/Sealed & Ready/i)).toBeInTheDocument(); + expect(screen.getByText(/Your letter is saved/i)).toBeInTheDocument(); }); expect(canvas.getAttribute("data-readonly")).toBe("true"); diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index fa75902..6ca5057 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -1,13 +1,7 @@ import { ClockIcon, DownloadSimpleIcon, - ImageIcon, - LockIcon, - QuestionIcon, SpinnerGapIcon, - StampIcon, - TrayIcon, - VaultIcon, XIcon, } from "@phosphor-icons/react"; import { useEffect, useRef, useState } from "react"; @@ -17,6 +11,12 @@ import { useParams, } from "react-router-dom"; import { api } from "../api/apiClient"; +import { PostSealModal } from "../components/editor/PostSealModal"; +import { + LetterHead, + ToolBar, + VaultConfirmModal, +} from "../components/editor/ToolBar"; import { type CanvasTools, ComposeCanvas, @@ -24,8 +24,9 @@ import { import DateDisplay from "../components/ui/DateDisplay"; import { LogModal } from "../components/ui/LogModal"; import { Navbar } from "../components/ui/Navbar"; + import { endpoints } from "../config/endpoints"; -import { PATHS, ROUTES } from "../config/routes"; +import { PATHS } from "../config/routes"; import { useKeyStore } from "../store/useKeyStore"; import { CryptoUtils } from "../utils/crypto"; import { formatRelativeDate } from "../utils/dateFormat"; @@ -43,241 +44,6 @@ const toPlaceholderList = [ "Something to bear...", ]; -interface SealedModalProps { - sealedTargetId: string | null; - navigate: NavigateFunction; -} - -function SealedModal({ sealedTargetId, navigate }: SealedModalProps) { - if (!sealedTargetId) return null; - return ( -
-
- -

Your letter is sealed

-

- It's encrypted and always safe in your drawer. -

-

- When you're ready, -
- you can{" "} - read it,{" "} - send it to - someone, or{" "} - burn it to - release -

-
- - -
-
-
- ); -} - -interface ToolBarProps { - fileInputRef: React.RefObject; - sealBtnClicked: boolean; - setSealBtnClicked: (v: boolean) => void; - onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise; - setConfirmModal: (v: "VAULT" | "SEAL" | null) => void; -} - -function ToolBar({ - fileInputRef, - sealBtnClicked, - setSealBtnClicked, - onSave, - setConfirmModal, -}: ToolBarProps) { - return ( -
-
- -
- -
- - -
- - -
- -
- -
- or -
- -
- -
- ); -} - -function LetterHead() { - return ( -
-
- - - Sealed & View Only - -
-
- ); -} - -interface VaultConfirmProps { - onSave: (status: "SEALED" | "DRAFT" | "VAULT", date?: Date) => Promise; - setConfirmModal: (v: "VAULT" | "SEAL" | null) => void; - setUnlockDate: (d: Date | null) => void; -} - -function VaultConfirm({ - onSave, - setConfirmModal, - setUnlockDate, -}: VaultConfirmProps) { - return ( -
-
- -

Vault this letter?

-

- Vaulting locks the letter permanently and will be{" "} - mailed to you - automatically on the unlock date. -
- - You cannot edit or view the contents of the letter until then. - -

-
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const unlockDateStr = formData.get("vault-date") as string; - const newUnlockDate = new Date(unlockDateStr); - setUnlockDate(newUnlockDate); - await onSave("VAULT", newUnlockDate); - setConfirmModal(null); - }} - id="vault-form" - > -
- Set an unlock date -
- - - - -
-
-
- ); -} - export default function Editor() { const navigate = useNavigate(); const navigateRef = useRef(navigate); @@ -646,14 +412,14 @@ export default function Editor() { )} {confirmModal === "VAULT" && ( - )} {sealedTargetId && ( - + )}
diff --git a/frontend/src/pages/Reader.tsx b/frontend/src/pages/Reader.tsx index 11471ed..e1a4fe6 100644 --- a/frontend/src/pages/Reader.tsx +++ b/frontend/src/pages/Reader.tsx @@ -65,13 +65,12 @@ export default function Reader() { const isAuthor = !!masterKey && !sharingKey; const handleShare = async () => { - if (!encryptedDek || !masterKey || !public_id) return; + if (!(encryptedDek && masterKey && public_id)) return; const cryptoUtils = new CryptoUtils(); const key = await cryptoUtils.extractSharingKey(encryptedDek, masterKey); try { await api.patch(`${endpoints.LETTERS}${public_id}/`, { type: "SENT" }); - } catch (err) { - console.error("Failed to update letter:", err); + } catch (_err) { } finally { setShareLink(`${window.location.origin}${PATHS.read(public_id)}#${key}`); } @@ -145,14 +144,13 @@ export default function Reader() { } const burnLetter = async () => { - console.log("Burning letter..."); if (!public_id || isBurning) return; setIsBurning(true); try { await api.patch(`${endpoints.LETTERS}${public_id}/`, { status: "BURNED", }); - } catch (err) { + } catch (_err) { } finally { setIsBurning(false); setShowBurnModal(false); @@ -447,6 +445,7 @@ export default function Reader() {