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
4 changes: 4 additions & 0 deletions changelog/7238.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: Changed # One of: Added, Changed, Developer Experience, Deprecated, Docs, Fixed, Removed, Security
description: Updated privacy request and ID verification flow to use separate pages instead of a modal
pr: 7238 # PR number
labels: [] # Optional: ["high-risk", "db-migration"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use server";

import {
getPageMetadata,
getPrivacyCenterEnvironmentCached,
} from "~/app/server-utils";
import LoadServerEnvironmentIntoStores from "~/components/LoadServerEnvironmentIntoStores";
import PrivacyRequestFormPage from "~/components/privacy-request/PrivacyRequestFormPage";
import { PrivacyRequestLayout } from "~/components/privacy-request/PrivacyRequestLayout";
import { NextSearchParams } from "~/types/next";

export const generateMetadata = getPageMetadata;

/**
* Privacy Request Form Page
* Full-page view for submitting privacy requests (replaces modal)
*/
const PrivacyRequestPage = async ({
params,
searchParams,
}: {
params: Promise<{ actionIndex: string }>;
searchParams: NextSearchParams;
}) => {
const { actionIndex } = await params;
const serverEnvironment = await getPrivacyCenterEnvironmentCached({
searchParams,
});

return (
<LoadServerEnvironmentIntoStores serverEnvironment={serverEnvironment}>
<PrivacyRequestLayout>
<PrivacyRequestFormPage actionIndex={actionIndex} />
</PrivacyRequestLayout>
</LoadServerEnvironmentIntoStores>
);
};

export default PrivacyRequestPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use server";

import {
getPageMetadata,
getPrivacyCenterEnvironmentCached,
} from "~/app/server-utils";
import LoadServerEnvironmentIntoStores from "~/components/LoadServerEnvironmentIntoStores";
import { PrivacyRequestLayout } from "~/components/privacy-request/PrivacyRequestLayout";
import RequestSubmittedPage from "~/components/privacy-request/RequestSubmittedPage";
import { NextSearchParams } from "~/types/next";

export const generateMetadata = getPageMetadata;

/**
* Privacy Request Success Page
* Full-page view showing request submitted confirmation
*/
const PrivacyRequestSuccessPage = async ({
searchParams,
}: {
searchParams: NextSearchParams;
}) => {
const serverEnvironment = await getPrivacyCenterEnvironmentCached({
searchParams,
});

return (
<LoadServerEnvironmentIntoStores serverEnvironment={serverEnvironment}>
<PrivacyRequestLayout title="Request submitted">
<RequestSubmittedPage />
</PrivacyRequestLayout>
</LoadServerEnvironmentIntoStores>
);
};

export default PrivacyRequestSuccessPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use server";

import {
getPageMetadata,
getPrivacyCenterEnvironmentCached,
} from "~/app/server-utils";
import LoadServerEnvironmentIntoStores from "~/components/LoadServerEnvironmentIntoStores";
import { PrivacyRequestLayout } from "~/components/privacy-request/PrivacyRequestLayout";
import VerificationPage from "~/components/privacy-request/VerificationPage";
import { NextSearchParams } from "~/types/next";

export const generateMetadata = getPageMetadata;

/**
* Privacy Request Verification Page
* Full-page view for OTP verification (replaces modal)
*/
const PrivacyRequestVerifyPage = async ({
params,
searchParams,
}: {
params: Promise<{ actionIndex: string }>;
searchParams: NextSearchParams;
}) => {
const { actionIndex } = await params;
const serverEnvironment = await getPrivacyCenterEnvironmentCached({
searchParams,
});

return (
<LoadServerEnvironmentIntoStores serverEnvironment={serverEnvironment}>
<PrivacyRequestLayout title="Enter verification code">
<VerificationPage actionIndex={actionIndex} />
</PrivacyRequestLayout>
</LoadServerEnvironmentIntoStores>
);
};

export default PrivacyRequestVerifyPage;
40 changes: 10 additions & 30 deletions clients/privacy-center/components/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
useChakraToast as useToast,
} from "fidesui";
import type { NextPage } from "next";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { ReactNode, useEffect, useState } from "react";

import { useAppSelector } from "~/app/hooks";
import sanitizeHTML from "~/common/sanitize-html";
Expand All @@ -24,10 +24,6 @@ import {
useConsentRequestModal,
} from "~/components/modals/consent-request-modal/ConsentRequestModal";
import NoticeEmptyStateModal from "~/components/modals/NoticeEmptyStateModal";
import {
PrivacyRequestModal,
usePrivacyRequestModal,
} from "~/components/modals/privacy-request-modal/PrivacyRequestModal";
import PrivacyCard from "~/components/PrivacyCard";
import { useConfig } from "~/features/common/config.slice";
import {
Expand Down Expand Up @@ -62,22 +58,12 @@ const TextOrHtml = ({

const HomePage: NextPage = () => {
const config = useConfig();
const router = useRouter();
const [isVerificationRequired, setIsVerificationRequired] =
useState<boolean>(false);
const [isConsentVerificationDisabled, setIsConsentVerificationDisabled] =
useState<boolean>(false);
const toast = useToast();
const {
isOpen: isPrivacyModalOpen,
onClose: onPrivacyModalClose,
onOpen: onPrivacyModalOpen,
openAction,
currentView: currentPrivacyModalView,
setCurrentView: setCurrentPrivacyModalView,
privacyRequestId,
setPrivacyRequestId,
successHandler: privacyModalSuccessHandler,
} = usePrivacyRequestModal();

const {
isOpen: isConsentModalOpenConst,
Expand Down Expand Up @@ -142,7 +128,12 @@ const HomePage: NextPage = () => {
toast,
]);

const content: any = [];
// Navigate to privacy request page instead of opening modal
const handlePrivacyRequestOpen = (index: number) => {
router.push(`/privacy-request/${index}`);
};

const content: ReactNode[] = [];

config.actions.forEach((action, index) => {
content.push(
Expand All @@ -152,7 +143,7 @@ const HomePage: NextPage = () => {
title={action.title}
iconPath={action.icon_path}
description={action.description}
onOpen={onPrivacyModalOpen}
onOpen={handlePrivacyRequestOpen}
/>,
);
});
Expand Down Expand Up @@ -255,17 +246,6 @@ const HomePage: NextPage = () => {
</Stack>
)}
</Stack>
<PrivacyRequestModal
isOpen={isPrivacyModalOpen}
onClose={onPrivacyModalClose}
openAction={openAction}
currentView={currentPrivacyModalView}
setCurrentView={setCurrentPrivacyModalView}
privacyRequestId={privacyRequestId}
setPrivacyRequestId={setPrivacyRequestId}
isVerificationRequired={isVerificationRequired}
successHandler={privacyModalSuccessHandler}
/>

<ConsentRequestModal
isOpen={isConsentModalOpen}
Expand Down
101 changes: 101 additions & 0 deletions clients/privacy-center/components/common/AuthFormLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* eslint-disable @next/next/no-img-element */

"use client";

/**
* Shared Auth Form Layout Component
*
* Provides a reusable full-page layout for authentication and form flows.
* Used by both ExternalAuthLayout and PrivacyRequestLayout to avoid duplication.
*/

import { Flex, Space, Typography } from "fidesui";
import React from "react";

import { useConfig } from "~/features/common/config.slice";

interface AuthFormLayoutProps {
children: React.ReactNode;
title?: string;
maxWidth?: string;
showTitleOnDesktop?: boolean;
dataTestId?: string;
}

export const AuthFormLayout = ({
children,
title,
maxWidth = "640px",
showTitleOnDesktop = true,
dataTestId = "auth-form-layout",
}: AuthFormLayoutProps) => {
const config = useConfig();

return (
<Flex
justify="center"
align="center"
style={{
width: "100%",
minHeight: "100vh",
backgroundColor: "#f5f5f5", // neutral-75 from palette
padding: "32px 16px",
}}
data-testid={dataTestId}
>
<div style={{ width: "100%", maxWidth, padding: "48px 24px" }}>
Copy link
Contributor

Choose a reason for hiding this comment

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

style: uses div instead of semantic components - per coding standards, prefer Ant Design's Flex component or other semantic elements

Suggested change
<div style={{ width: "100%", maxWidth, padding: "48px 24px" }}>
<Flex style={{ width: "100%", maxWidth, padding: "48px 24px" }}>

Context Used: Rule from dashboard - Avoid using div elements when possible. Use semantic HTML elements or component library alternativ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

<Space direction="vertical" size={48} style={{ width: "100%" }}>
{/* Logo */}
<Flex justify="center">
<img
src={config?.logo_path || "/logo.svg"}
alt="Logo"
width={205}
height={46}
/>
</Flex>

{/* Form Container */}
<div
style={{
backgroundColor: "white",
padding: "48px",
width: "100%",
borderRadius: "4px",
boxShadow:
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
}}
Comment on lines +59 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

style: uses div for form container - prefer semantic or Flex component

Suggested change
<div
style={{
backgroundColor: "white",
padding: "48px",
width: "100%",
borderRadius: "4px",
boxShadow:
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
}}
<Flex
style={{
backgroundColor: "white",
padding: "48px",
width: "100%",
borderRadius: "4px",
boxShadow:
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
}}
>

Context Used: Rule from dashboard - Avoid using div elements when possible. Use semantic HTML elements or component library alternativ... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

>
{title && (
<Space
direction="vertical"
size={16}
style={{ width: "100%", marginBottom: "32px" }}
>
{/* Desktop Title - conditionally shown */}
{showTitleOnDesktop && (
<Flex justify="center">
<Typography.Title
level={2}
style={{
fontSize: "1.875rem", // 3xl
color: "#2b2e35", // minos
marginBottom: 0,
textAlign: "center",
}}
>
{title}
</Typography.Title>
</Flex>
)}
</Space>
)}

{/* Content */}
<div style={{ width: "100%" }}>{children}</div>
</div>
</Space>
</div>
</Flex>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type PrivacyRequestFormProps = {
setCurrentView: (view: ModalViews) => void;
setPrivacyRequestId: (id: string) => void;
isVerificationRequired: boolean;
onSuccessWithoutVerification?: () => void;
};

const PrivacyRequestForm = ({
Expand All @@ -25,6 +26,7 @@ const PrivacyRequestForm = ({
setCurrentView,
setPrivacyRequestId,
isVerificationRequired,
onSuccessWithoutVerification,
}: PrivacyRequestFormProps) => {
const config = useConfig();

Expand Down Expand Up @@ -55,6 +57,7 @@ const PrivacyRequestForm = ({
setCurrentView,
setPrivacyRequestId,
isVerificationRequired,
onSuccessWithoutVerification,
});

if (!action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ const usePrivacyRequestForm = ({
setCurrentView,
setPrivacyRequestId,
isVerificationRequired,
onSuccessWithoutVerification,
}: {
onClose: () => void;
action?: ConfigPrivacyRequestOption;
setCurrentView: (view: ModalViews) => void;
setPrivacyRequestId: (id: string) => void;
isVerificationRequired: boolean;
onSuccessWithoutVerification?: () => void;
}) => {
const settings = useSettings();
const {
Expand Down Expand Up @@ -219,6 +221,12 @@ const usePrivacyRequestForm = ({
"Your request was successful, please await further instructions.",
...SuccessToastOptions,
});
// Call success handler if provided (for page flow), otherwise just close
if (onSuccessWithoutVerification) {
onSuccessWithoutVerification();
} else {
onClose();
}
} else if (
(isVerificationRequired &&
data.succeeded.length &&
Expand All @@ -239,11 +247,6 @@ const usePrivacyRequestForm = ({
title:
"An unhandled error occurred while creating your privacy request",
});
return;
}

if (!isVerificationRequired) {
onClose();
}
},
validationSchema: Yup.object().shape({
Expand Down
Loading
Loading