feat: implement dynamic redirection after login based on location state

This commit is contained in:
ramvignesh-b
2026-04-24 18:43:13 +05:30
parent db31be4ec8
commit 42493a950c
5 changed files with 89 additions and 29 deletions
@@ -61,7 +61,7 @@ export function EnvelopeReveal({
</div> </div>
<img <img
className={ className={
"translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1500 cursor-pointer" "translate-y-24 delay-2000 absolute z-6 peer-has-checked:pointer-events-none peer-has-checked:opacity-0 peer-has-checked:delay-0 transition-opacity duration-1000 cursor-pointer"
} }
src={waxSeal} src={waxSeal}
alt="Seal" alt="Seal"
@@ -71,7 +71,7 @@ export function EnvelopeReveal({
<button <button
type="button" type="button"
id="letter" id="letter"
className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-2000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`} className={`absolute mx-auto transition-all peer-has-checked:delay-800 peer-has-checked:duration-1000 duration-1000 mt-2 h-55 w-105 bg-paper peer-has-checked:-mt-12 hover:-mt-24 cursor-pointer ${revealLetter ? "duration-1000 peer-has-checked:duration-3000 w-screen max-w-4xl h-screen z-101 -translate-y-90" : "peer-has-checked:z-1"}`}
onClick={handleClick} onClick={handleClick}
></button> ></button>
@@ -91,8 +91,9 @@ export function EnvelopeReveal({
</div> </div>
<button <button
id="env-front"
type="button" type="button"
className="p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2" className="text-left p-10 absolute inset-0 backface-hidden w-110 bg-base-200 z-99 rounded-md -translate-x-2"
onClick={() => setIsFlipped((prev) => !prev)} onClick={() => setIsFlipped((prev) => !prev)}
> >
<span className={"text-neutral-content/60 font-xs font-display"}> <span className={"text-neutral-content/60 font-xs font-display"}>
+23 -3
View File
@@ -44,7 +44,19 @@ describe("Login Page", () => {
expect(await screen.findByText(/technical issues/i)).toBeInTheDocument(); expect(await screen.findByText(/technical issues/i)).toBeInTheDocument();
}); });
it("should redirect to the drawer when login is successful", async () => { it.each([
{
locationState: undefined,
nextRoute: "Drawer",
},
{
locationState: { redirectUrl: "/read/123" },
nextRoute: "Reader",
},
])("should redirect to the next route when login is successful", async ({
locationState,
nextRoute,
}) => {
const mockUser = { const mockUser = {
public_id: "user-123", public_id: "user-123",
email: "test@example.com", email: "test@example.com",
@@ -61,10 +73,18 @@ describe("Login Page", () => {
); );
render( render(
<MemoryRouter initialEntries={["/login"]}> <MemoryRouter
initialEntries={[
{
pathname: "/login",
state: locationState,
},
]}
>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/drawer" element={<div>Drawer</div>} /> <Route path="/drawer" element={<div>Drawer</div>} />
<Route path="/read/:publicId" element={<div>Reader</div>} />
</Routes> </Routes>
</MemoryRouter>, </MemoryRouter>,
); );
@@ -73,6 +93,6 @@ describe("Login Page", () => {
await userEvent.type(screen.getByLabelText(/password/i), "password123"); await userEvent.type(screen.getByLabelText(/password/i), "password123");
await userEvent.click(screen.getByRole("button", { name: /sign in/i })); await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
expect(await screen.findByText(/Drawer/i)).toBeInTheDocument(); expect(await screen.findByText(nextRoute)).toBeInTheDocument();
}); });
}); });
+2 -1
View File
@@ -78,6 +78,7 @@ export default function Login() {
const [apiError, setApiError] = useState<string | null>(null); const [apiError, setApiError] = useState<string | null>(null);
const { setAuthStore } = useAuth(); const { setAuthStore } = useAuth();
const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime); const [showWelcome, setShowWelcome] = useState(!!location.state?.firstTime);
const nextRoute = location.state?.redirectUrl || ROUTES.DRAWER;
const { const {
register, register,
@@ -110,7 +111,7 @@ export default function Login() {
// store the auth related data // store the auth related data
await setAuthStore(authData.access, userData, masterKey); await setAuthStore(authData.access, userData, masterKey);
navigate(ROUTES.DRAWER, { replace: true }); navigate(nextRoute, { replace: true });
} catch (err) { } catch (err) {
let message = let message =
"Sorry, we're experiencing technical issues.\nPlease try again later."; "Sorry, we're experiencing technical issues.\nPlease try again later.";
+50 -15
View File
@@ -1,9 +1,10 @@
import { render, screen, waitFor } from "@testing-library/react"; import { render, screen, waitFor } from "@testing-library/react";
import { HttpResponse, http } from "msw"; import { HttpResponse, http } from "msw";
import { MemoryRouter, Route, Routes } from "react-router-dom"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { server } from "../../test/mocks/server"; import { server } from "../../test/mocks/server";
import { endpoints } from "../config/endpoints"; import { endpoints } from "../config/endpoints";
import { useKeyStore } from "../store/useKeyStore";
import { CryptoUtils } from "../utils/crypto"; import { CryptoUtils } from "../utils/crypto";
import Reader from "./Reader"; import Reader from "./Reader";
@@ -19,6 +20,15 @@ describe("Reader Page", () => {
let masterKey: CryptoKey; let masterKey: CryptoKey;
let utils: CryptoUtils; let utils: CryptoUtils;
const LocationTest = () => {
const location = useLocation();
return (
<div data-testid="location-state">
{JSON.stringify(location.state || {})}
</div>
);
};
beforeEach(async () => { beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -26,6 +36,8 @@ describe("Reader Page", () => {
await utils.initialize(); await utils.initialize();
const bundle = await CryptoUtils.deriveKeyBundle("password", "salt"); const bundle = await CryptoUtils.deriveKeyBundle("password", "salt");
masterKey = bundle.masterKey; masterKey = bundle.masterKey;
// User is logged in by default
useKeyStore.setState({ masterKey });
// Clear the URL hash // Clear the URL hash
vi.stubGlobal("location", { vi.stubGlobal("location", {
@@ -34,24 +46,12 @@ describe("Reader Page", () => {
}); });
}); });
it("should notify the user if the sharing key is missing from the URL", async () => {
render(
<MemoryRouter initialEntries={["/read/123"]}>
<Routes>
<Route path="/read/:public_id" element={<Reader />} />
</Routes>
</MemoryRouter>,
);
expect(
await screen.findByText(/No sharing key provided/i),
).toBeInTheDocument();
});
it("should load and decrypt the letter when a valid key is provided and display the envelope", async () => { it("should load and decrypt the letter when a valid key is provided and display the envelope", async () => {
const mockPublicId = "test-uuid"; const mockPublicId = "test-uuid";
const letterContent = JSON.stringify({ objects: [] }); const letterContent = JSON.stringify({ objects: [] });
const metadata = { recipient: "Guest" }; const metadata = { recipient: "Guest" };
// simulate guest
useKeyStore.setState({ masterKey: null });
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey); const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey); const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
@@ -103,4 +103,39 @@ describe("Reader Page", () => {
await screen.findByText(/Failed to load letter/i), await screen.findByText(/Failed to load letter/i),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it("should navigate to the login page with redirect url when the letter has no sharing key and the user is not logged in", async () => {
const mockPublicId = "4ef9f25f-4f37-477a-891a-4b10541e350c";
const letterContent = JSON.stringify({ objects: [] });
const metadata = { recipient: "Guest" };
useKeyStore.setState({ masterKey: null });
const encryptedLetter = await utils.encryptLetter(letterContent, masterKey);
const encryptedMetadata = await utils.encryptMetadata(metadata, masterKey);
server.use(
http.get(`${API_URL}${endpoints.LETTERS}${mockPublicId}/`, () => {
return HttpResponse.json({
encrypted_content: encryptedLetter.encrypted_content,
encrypted_metadata: encryptedMetadata.encrypted_content,
encrypted_dek: encryptedLetter.encrypted_dek,
images: [],
});
}),
);
render(
<MemoryRouter initialEntries={[`/read/${mockPublicId}`]}>
<Routes>
<Route path="/read/:public_id" element={<Reader />} />
<Route path="/login" element={<LocationTest />} />
</Routes>
</MemoryRouter>,
);
const stateComponent = screen.getByTestId("location-state");
expect(stateComponent).toHaveTextContent(
`"redirectUrl":"/read/${mockPublicId}"`,
);
});
}); });
+10 -7
View File
@@ -6,7 +6,12 @@ import {
XCircleIcon, XCircleIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom"; import {
type NavigateFunction,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { api } from "../api/apiClient"; import { api } from "../api/apiClient";
import { import {
type CanvasJSON, type CanvasJSON,
@@ -37,6 +42,7 @@ export default function Reader() {
const navigate = useNavigate(); const navigate = useNavigate();
const sharingKey = location.hash.replace("#", ""); const sharingKey = location.hash.replace("#", "");
const navigateRef = useRef<NavigateFunction>(navigate);
const canvasRef = useRef<CanvasTools>(null); const canvasRef = useRef<CanvasTools>(null);
const [isDecrypting, setIsDecrypting] = useState(true); const [isDecrypting, setIsDecrypting] = useState(true);
@@ -256,12 +262,9 @@ export default function Reader() {
useEffect(() => { useEffect(() => {
if (!(sharingKey || masterKey)) { if (!(sharingKey || masterKey)) {
setError({ navigateRef.current("/login", {
message: state: { redirectUrl: `/read/${public_id}` },
"No sharing key provided. Please check the link or log in if you are the author.",
log: "",
}); });
setIsDecrypting(false);
return; return;
} }
@@ -373,7 +376,7 @@ export default function Reader() {
if (isDecrypting) { if (isDecrypting) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-base-100 font-serif"> <div className="flex items-center justify-center bg-base-100 font-serif">
<div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" /> <div className="fixed inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,rgba(0,0,0,0.4)_100%)] pointer-events-none z-0" />
<div className="text-center space-y-6 z-10"> <div className="text-center space-y-6 z-10">
<Logo /> <Logo />