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
+
+
+ navigate(ROUTES.DRAWER)}
+ >
+ Keep it to myself
+
+
+ navigate(PATHS.read(sealedTargetId), { replace: true })
+ }
+ >
+ View letter
+
+
+
+
+ );
+}
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 (
+
+ );
+}
+
+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.
+
+
+
+
+
+ );
+}
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.
+
+
+
+
+ );
+}
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.
-
-
-
-
- );
-}
-
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
-
-
- navigate(ROUTES.DRAWER)}
- >
- Keep it to myself
-
-
- navigate(PATHS.read(sealedTargetId), { replace: true })
- }
- >
- View letter
-
-
-
-
- );
-}
-
-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 (
-
- );
-}
-
-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.
-
-
-
-
-
- );
-}
-
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() {
navigate(ROUTES.DRAWER)}
>