Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const OnboardingView: FC<OnboardingViewProps> = ({
...steps[OnboardingStepIndex.EMAIL_VERIFICATION],
component: (
<d.EmailVerificationContainer onComplete={() => onStepChange(OnboardingStepIndex.PAYMENT_METHOD)}>
{props => <d.EmailVerificationStep {...props} />}
{({ sendCode, verifyCode }) => <d.EmailVerificationStep sendCode={sendCode} verifyCode={verifyCode} />}
</d.EmailVerificationContainer>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,36 @@ import { describe, expect, it, vi } from "vitest";

import { VerifyEmailPage } from "./VerifyEmailPage";

import { act, render, screen } from "@testing-library/react";
import { render, screen } from "@testing-library/react";

describe(VerifyEmailPage.name, () => {
it("calls verifyEmail with the email from search params", () => {
const { mockVerifyEmail } = setup({ email: "test@example.com" });
it("shows redirect loading text", () => {
setup();

expect(mockVerifyEmail).toHaveBeenCalledWith("test@example.com");
expect(screen.queryByText("Redirecting to email verification...")).toBeInTheDocument();
});

it("does not call verifyEmail when email param is missing", () => {
const { mockVerifyEmail } = setup({ email: null });
it("redirects to onboarding with email verification step", () => {
const { redirect } = setup();

expect(mockVerifyEmail).not.toHaveBeenCalled();
expect(redirect).toHaveBeenCalledWith("/signup?return-to=%2F");
});

it("shows loading text when verification is pending", () => {
setup({ email: "test@example.com", isPending: true });

expect(screen.queryByText("Just a moment while we finish verifying your email.")).toBeInTheDocument();
});

it("shows success message when email is verified", () => {
const { capturedOnSuccess } = setup({ email: "test@example.com" });

act(() => capturedOnSuccess?.(true));

expect(screen.queryByTestId("CheckCircleIcon")).toBeInTheDocument();
});

it("shows error message when email verification fails", () => {
const { capturedOnError } = setup({ email: "test@example.com" });

act(() => capturedOnError?.());

expect(screen.queryByText("Your email was not verified. Please try again.")).toBeInTheDocument();
});

it("shows error message when isVerified is null", () => {
setup({ email: "test@example.com" });

expect(screen.queryByText("Your email was not verified. Please try again.")).toBeInTheDocument();
});

function setup(input: { email?: string | null; isPending?: boolean }) {
const mockVerifyEmail = vi.fn();
let capturedOnSuccess: ((isVerified: boolean) => void) | undefined;
let capturedOnError: (() => void) | undefined;

const mockUseVerifyEmail = vi.fn().mockImplementation((options: { onSuccess?: (v: boolean) => void; onError?: () => void }) => {
capturedOnSuccess = options.onSuccess;
capturedOnError = options.onError;
return { mutate: mockVerifyEmail, isPending: input.isPending || false };
});

const mockUseWhen = vi.fn().mockImplementation((condition: unknown, run: () => void) => {
if (condition) {
run();
}
});
function setup(input: { onboardingUrl?: string } = {}) {
const redirect = vi.fn();

const dependencies = {
useSearchParams: vi.fn().mockReturnValue(new URLSearchParams(input.email ? `email=${input.email}` : "")),
useVerifyEmail: mockUseVerifyEmail,
useWhen: mockUseWhen,
Layout: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Loading: ({ text }: { text: string }) => <div>{text}</div>,
NextSeo: () => null,
UrlService: {
onboarding: vi.fn(() => "/signup")
}
onboarding: vi.fn(() => input.onboardingUrl ?? "/signup?return-to=%2F")
},
redirect
} as unknown as ComponentProps<typeof VerifyEmailPage>["dependencies"];

render(<VerifyEmailPage dependencies={dependencies} />);

return { mockVerifyEmail, capturedOnSuccess, capturedOnError };
return { redirect };
}
});
Original file line number Diff line number Diff line change
@@ -1,93 +1,35 @@
import React, { useCallback, useState } from "react";
import { AutoButton } from "@akashnetwork/ui/components";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { ArrowRight } from "iconoir-react";
import { useSearchParams } from "next/navigation";
import React, { useEffect } from "react";
import { NextSeo } from "next-seo";

import Layout, { Loading } from "@src/components/layout/Layout";
import { OnboardingStepIndex } from "@src/components/onboarding/OnboardingContainer/OnboardingContainer";
import { useWhen } from "@src/hooks/useWhen";
import { useVerifyEmail } from "@src/queries/useVerifyEmailQuery";
import { ONBOARDING_STEP_KEY } from "@src/services/storage/keys";
import { UrlService } from "@src/utils/urlUtils";

const DEPENDENCIES = {
useSearchParams,
useVerifyEmail,
useWhen,
Layout,
Loading,
UrlService
NextSeo,
UrlService,
redirect: (url: string) => {
window.localStorage.setItem(ONBOARDING_STEP_KEY, OnboardingStepIndex.EMAIL_VERIFICATION.toString());
window.location.href = url;
Comment on lines +14 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use location.replace for this redirect page.

window.location.href adds /verify-email to history, so Back lands here and immediately redirects again. For a transient page like this, that creates a back-button loop.

Suggested change
   redirect: (url: string) => {
     window.localStorage.setItem(ONBOARDING_STEP_KEY, OnboardingStepIndex.EMAIL_VERIFICATION.toString());
-    window.location.href = url;
+    window.location.replace(url);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/deploy-web/src/components/onboarding/VerifyEmailPage/VerifyEmailPage.tsx`
around lines 14 - 16, The redirect implementation in the redirect function
currently sets window.location.href which adds a history entry and causes a
back-button loop for the transient VerifyEmailPage; change it to use
location.replace(url) so the /verify-email step is not kept in history. Update
the redirect function that writes ONBOARDING_STEP_KEY and currently calls
window.location.href to call window.location.replace(url) (preserving the
existing localStorage write to OnboardingStepIndex.EMAIL_VERIFICATION).

}
};

type VerifyEmailPageProps = {
dependencies?: typeof DEPENDENCIES;
};

type VerificationResultProps = {
isVerified: boolean;
dependencies: Pick<typeof DEPENDENCIES, "UrlService">;
};

function VerificationResult({ isVerified, dependencies: d }: VerificationResultProps) {
const gotoOnboarding = useCallback(() => {
window.localStorage?.setItem(ONBOARDING_STEP_KEY, OnboardingStepIndex.PAYMENT_METHOD.toString());
window.location.href = d.UrlService.onboarding({ returnTo: "/" });
}, [d.UrlService]);

return (
<div className="mt-10 text-center">
{isVerified ? (
<>
<CheckCircleIcon className="mb-2 h-16 w-16 text-green-500" />
<h5>
Your email was verified.
<br />
You can continue using the application.
</h5>
<AutoButton
onClick={gotoOnboarding}
text={
<>
Continue <ArrowRight className="ml-4" />
</>
}
timeout={5000}
/>
</>
) : (
<>
<ErrorOutlineIcon className="mb-2 h-16 w-16 text-red-500" />
<h5>Your email was not verified. Please try again.</h5>
</>
)}
</div>
);
}

export function VerifyEmailPage({ dependencies: d = DEPENDENCIES }: VerifyEmailPageProps) {
const email = d.useSearchParams().get("email");
const [isVerified, setIsVerified] = useState<boolean | null>(null);
const { mutate: verifyEmail, isPending: isVerifying } = d.useVerifyEmail({ onSuccess: setIsVerified, onError: () => setIsVerified(false) });

d.useWhen(email, () => {
if (email) {
verifyEmail(email);
}
});
useEffect(() => {
d.redirect(d.UrlService.onboarding({ returnTo: "/" }));
}, [d]);
Comment on lines +25 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: could you please clarify the purpose of this redirect?


return (
<d.Layout>
<NextSeo title="Verifying your email" />
{isVerifying ? (
<d.Loading text="Just a moment while we finish verifying your email." />
) : (
<>
<VerificationResult isVerified={isVerified === true} dependencies={d} />
</>
)}
<d.NextSeo title="Email Verification" />
<d.Loading text="Redirecting to email verification..." />
</d.Layout>
);
}
Loading
Loading