diff --git a/CHANGELOG.md b/CHANGELOG.md index 951d236d3d..5cc79cbb1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ The types of changes are: * Adds users and owners configuration for Hubspot connector [#1091](https://github.com/ethyca/fidesops/pull/1091) * Foundations for a new email connector type [#1142](https://github.com/ethyca/fidesops/pull/1142) * Adds endpoint for GET identity verification config [#1221](https://github.com/ethyca/fidesops/pull/1221) +* Add user identification flow to privacy center [#1231](https://github.com/ethyca/fidesops/pull/1231) * Access support for Shopify [#1220](https://github.com/ethyca/fidesops/pull/1220) ## [1.7.1](https://github.com/ethyca/fidesops/compare/1.7.0...1.7.1) diff --git a/clients/ops/privacy-center/.eslintignore b/clients/ops/privacy-center/.eslintignore new file mode 100644 index 0000000000..acaeb30a90 --- /dev/null +++ b/clients/ops/privacy-center/.eslintignore @@ -0,0 +1,5 @@ +node_modules +dist +.eslintrc.json +next.config.js +jest.config.js \ No newline at end of file diff --git a/clients/ops/privacy-center/__tests__/RequestModal.test.tsx b/clients/ops/privacy-center/__tests__/RequestModal.test.tsx index 982abfe60e..52fafa5870 100644 --- a/clients/ops/privacy-center/__tests__/RequestModal.test.tsx +++ b/clients/ops/privacy-center/__tests__/RequestModal.test.tsx @@ -11,23 +11,42 @@ import { act } from "react-dom/test-utils"; import { rest } from "msw"; import { setupServer } from "msw/node"; -import { RequestModal } from "../components/RequestModal"; +import { + RequestModal, + RequestModalProps, +} from "../components/modals/RequestModal"; import IndexPage from "../pages/index"; import mockConfig from "../config/__mocks__/config.json"; +import { hostUrl } from "../constants"; +import { ModalViews } from "../components/modals/types"; jest.mock("../config/config.json"); -const server = setupServer(); +const server = setupServer( + rest.get(`${hostUrl}/id-verification/config`, (req, res, ctx) => + res( + ctx.json({ + identity_verification_required: false, + valid_email_config_exists: false, + }) + ) + ) +); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); -const defaultModalProperties = { +const defaultModalProperties: RequestModalProps = { isOpen: true, onClose: () => {}, openAction: mockConfig.actions[0].policy_key, setAlert: () => {}, + currentView: ModalViews.PrivacyRequest, + setCurrentView: () => {}, + privacyRequestId: "", + setPrivacyRequestId: () => {}, + isVerificationRequired: false, }; describe("RequestModal", () => { diff --git a/clients/ops/privacy-center/common/CommonHeaders.ts b/clients/ops/privacy-center/common/CommonHeaders.ts new file mode 100644 index 0000000000..c72511eee4 --- /dev/null +++ b/clients/ops/privacy-center/common/CommonHeaders.ts @@ -0,0 +1,17 @@ +/* eslint-disable import/prefer-default-export */ + +/** + * Adds common headers to all api calls to fidesops + */ +export function addCommonHeaders(headers: Headers, token: string | null) { + headers.set("Access-Control-Allow-Origin", "*"); + headers.set("X-Fides-Source", "fidesops-privacy-center"); + headers.set("Accept", "application/json"); + headers.set("Content-Type", "application/json"); + if (token) { + headers.set("authorization", `Bearer ${token}`); + } + return headers; +} + + diff --git a/clients/ops/privacy-center/components/PrivacyCard.tsx b/clients/ops/privacy-center/components/PrivacyCard.tsx new file mode 100644 index 0000000000..2d670d2516 --- /dev/null +++ b/clients/ops/privacy-center/components/PrivacyCard.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Heading, Text, Stack, Box, Image, HStack } from "@fidesui/react"; + +type PrivacyCardProps = { + title: string; + policyKey: string; + iconPath: string; + description: string; + onOpen: (policyKey: string) => void; +}; + +const PrivacyCard: React.FC = ({ + title, + policyKey, + iconPath, + description, + onOpen, +}) => ( + onOpen(policyKey)} + > + + + {description} + + + + + {title} + + + {description} + + + + +); + +export default PrivacyCard; diff --git a/clients/ops/privacy-center/components/RequestModal.tsx b/clients/ops/privacy-center/components/RequestModal.tsx deleted file mode 100644 index f6ef10229f..0000000000 --- a/clients/ops/privacy-center/components/RequestModal.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalFooter, - ModalBody, - Text, - Button, - chakra, - Stack, - FormControl, - Input, - FormErrorMessage, -} from "@fidesui/react"; - -import { useFormik } from "formik"; - -import type { AlertState } from "../types/AlertState"; - -import config from "../config/config.json"; - -export const useRequestModal = () => { - const [isOpen, setIsOpen] = useState(false); - const [openAction, setOpenAction] = useState(null); - - const onOpen = (action: string) => { - setOpenAction(action); - setIsOpen(true); - }; - - const onClose = () => { - setIsOpen(false); - setOpenAction(null); - }; - - return { isOpen, onClose, onOpen, openAction }; -}; - -const useRequestForm = ({ - onClose, - action, - setAlert, -}: { - onClose: () => void; - action: typeof config.actions[0] | null; - setAlert: (state: AlertState) => void; -}) => { - const [isLoading, setIsLoading] = useState(false); - const formik = useFormik({ - initialValues: { - name: "", - email: "", - phone: "", - }, - onSubmit: async (values) => { - if (!action) { - // somehow we've reached a broken state, return - return; - } - - setIsLoading(true); - const host = - process.env.NODE_ENV === "development" - ? config.fidesops_host_development - : config.fidesops_host_production; - const body = [ - { - identity: { - email: values.email, - phone_number: values.phone, - // enable this when name field is supported on the server - // name: values.name - }, - policy_key: action.policy_key, - }, - ]; - - const handleError = () => { - setAlert({ - status: "error", - description: "Your request has failed. Please try again.", - }); - onClose(); - }; - - try { - const response = await fetch(`${host}/privacy-request`, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "X-Fides-Source": "fidesops-privacy-center", - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - handleError(); - return; - } - - const data = await response.json(); - - if (data.succeeded.length) { - setAlert({ - status: "success", - description: - "Your request was successful, please await further instructions.", - }); - } else { - handleError(); - } - } catch (error) { - handleError(); - return; - } - - onClose(); - }, - validate: (values) => { - if (!action) return {}; - const errors: { - name?: string; - email?: string; - phone?: string; - } = {}; - - if (!values.email && action.identity_inputs.email === "required") { - errors.email = "Required"; - } - - if (!values.name && action.identity_inputs.name === "required") { - errors.name = "Required"; - } - - if (!values.phone && action.identity_inputs.phone === "required") { - errors.phone = "Required"; - } - - return errors; - }, - }); - - return { ...formik, isLoading }; -}; - -export const RequestModal: React.FC<{ - isOpen: boolean; - onClose: () => void; - openAction: string | null; - setAlert: (state: AlertState) => void; -}> = ({ isOpen, onClose, openAction, setAlert }) => { - const action = openAction - ? config.actions.filter(({ policy_key }) => policy_key === openAction)[0] - : null; - - const { - errors, - handleBlur, - handleChange, - handleSubmit, - touched, - values, - isValid, - dirty, - resetForm, - } = useRequestForm({ onClose, action, setAlert }); - - useEffect(() => resetForm(), [isOpen, resetForm]); - - if (!action) return null; - - return ( - - - - - {action.title} - - - - - {action.description} - - - {action.identity_inputs.name ? ( - - - {errors.name} - - ) : null} - {action.identity_inputs.email ? ( - - - {errors.email} - - ) : null} - {action.identity_inputs.phone ? ( - - - {errors.phone} - - ) : null} - - - - - - - - - - - ); -}; diff --git a/clients/ops/privacy-center/components/modals/PrivacyRequestForm.tsx b/clients/ops/privacy-center/components/modals/PrivacyRequestForm.tsx new file mode 100644 index 0000000000..f951c250f8 --- /dev/null +++ b/clients/ops/privacy-center/components/modals/PrivacyRequestForm.tsx @@ -0,0 +1,299 @@ +import React, { useEffect, useState } from "react"; +import { + Button, + chakra, + FormControl, + FormErrorMessage, + Input, + ModalBody, + ModalFooter, + ModalHeader, + Stack, + Text, +} from "@fidesui/react"; + +import { useFormik } from "formik"; + +import { Headers } from "headers-polyfill"; +import type { AlertState } from "../../types/AlertState"; +import { PrivacyRequestStatus } from "../../types"; +import { addCommonHeaders } from "../../common/CommonHeaders"; + +import config from "../../config/config.json"; + +import { ModalViews } from "./types"; +import { hostUrl } from "../../constants"; + +const usePrivacyRequestForm = ({ + onClose, + action, + setAlert, + setCurrentView, + setPrivacyRequestId, + isVerificationRequired, +}: { + onClose: () => void; + action: typeof config.actions[0] | null; + setAlert: (state: AlertState) => void; + setCurrentView: (view: ModalViews) => void; + setPrivacyRequestId: (id: string) => void; + isVerificationRequired: boolean; +}) => { + const [isLoading, setIsLoading] = useState(false); + const formik = useFormik({ + initialValues: { + name: "", + email: "", + phone: "", + }, + onSubmit: async (values) => { + if (!action) { + // somehow we've reached a broken state, return + return; + } + + setIsLoading(true); + + const body = [ + { + identity: { + email: values.email, + phone_number: values.phone, + // enable this when name field is supported on the server + // name: values.name + }, + policy_key: action.policy_key, + }, + ]; + + const handleError = () => { + setAlert({ + status: "error", + description: "Your request has failed. Please try again.", + }); + onClose(); + }; + + try { + const headers: Headers = new Headers(); + addCommonHeaders(headers, null); + + const response = await fetch(`${hostUrl}/privacy-request`, { + method: "POST", + headers, + body: JSON.stringify(body), + }); + + if (!response.ok) { + handleError(); + return; + } + + const data = await response.json(); + + if (!isVerificationRequired && data.succeeded.length) { + setAlert({ + status: "success", + description: + "Your request was successful, please await further instructions.", + }); + } else if ( + isVerificationRequired && + data.succeeded.length && + data.succeeded[0].status === PrivacyRequestStatus.IDENTITY_UNVERIFIED + ) { + setPrivacyRequestId(data.succeeded[0].id); + setCurrentView(ModalViews.IdentityVerification); + } else { + handleError(); + } + } catch (error) { + handleError(); + return; + } + + if (!isVerificationRequired) { + onClose(); + } + }, + validate: (values) => { + if (!action) return {}; + const errors: { + name?: string; + email?: string; + phone?: string; + } = {}; + + if (!values.email && action.identity_inputs.email === "required") { + errors.email = "Required"; + } + + if (!values.name && action.identity_inputs.name === "required") { + errors.name = "Required"; + } + + if (!values.phone && action.identity_inputs.phone === "required") { + errors.phone = "Required"; + } + + return errors; + }, + }); + + return { ...formik, isLoading }; +}; + +type PrivacyRequestFormProps = { + isOpen: boolean; + onClose: () => void; + openAction: string | null; + setAlert: (state: AlertState) => void; + setCurrentView: (view: ModalViews) => void; + setPrivacyRequestId: (id: string) => void; + isVerificationRequired: boolean; +}; + +const PrivacyRequestForm: React.FC = ({ + isOpen, + onClose, + openAction, + setAlert, + setCurrentView, + setPrivacyRequestId, + isVerificationRequired, +}) => { + const action = openAction + ? config.actions.filter(({ policy_key }) => policy_key === openAction)[0] + : null; + + const { + errors, + handleBlur, + handleChange, + handleSubmit, + touched, + values, + isValid, + dirty, + resetForm, + } = usePrivacyRequestForm({ + onClose, + action, + setAlert, + setCurrentView, + setPrivacyRequestId, + isVerificationRequired, + }); + + useEffect(() => resetForm(), [isOpen, resetForm]); + + if (!action) return null; + + return ( + <> + + {action.title} + + + + + {action.description} + + + {action.identity_inputs.name ? ( + + + {errors.name} + + ) : null} + {action.identity_inputs.email ? ( + + + {errors.email} + + ) : null} + {action.identity_inputs.phone ? ( + + + {errors.phone} + + ) : null} + + + + + + + + + + ); +}; + +export default PrivacyRequestForm; diff --git a/clients/ops/privacy-center/components/modals/RequestModal.tsx b/clients/ops/privacy-center/components/modals/RequestModal.tsx new file mode 100644 index 0000000000..93a5ac28f3 --- /dev/null +++ b/clients/ops/privacy-center/components/modals/RequestModal.tsx @@ -0,0 +1,115 @@ +import React, { useState } from "react"; +import { Modal, ModalContent, ModalOverlay } from "@fidesui/react"; + +import type { AlertState } from "../../types/AlertState"; + +import config from "../../config/config.json"; + +import { ModalViews } from "./types"; +import PrivacyRequestForm from "./PrivacyRequestForm"; +import VerificationForm from "./VerificationForm"; +import RequestSubmitted from "./RequestSubmitted"; + +export const useRequestModal = () => { + const [isOpen, setIsOpen] = useState(false); + const [openAction, setOpenAction] = useState(null); + const [currentView, setCurrentView] = useState( + ModalViews.PrivacyRequest + ); + const [privacyRequestId, setPrivacyRequestId] = useState(""); + + const onOpen = (action: string) => { + setOpenAction(action); + setIsOpen(true); + }; + + const onClose = () => { + setIsOpen(false); + setOpenAction(null); + setCurrentView(ModalViews.PrivacyRequest); + setPrivacyRequestId(""); + }; + + return { + isOpen, + onClose, + onOpen, + openAction, + currentView, + setCurrentView, + privacyRequestId, + setPrivacyRequestId, + }; +}; + +export type RequestModalProps = { + isOpen: boolean; + onClose: () => void; + openAction: string | null; + setAlert: (state: AlertState) => void; + currentView: ModalViews; + setCurrentView: (view: ModalViews) => void; + privacyRequestId: string; + setPrivacyRequestId: (id: string) => void; + isVerificationRequired: boolean; +}; + +export const RequestModal: React.FC = ({ + isOpen, + onClose, + openAction, + setAlert, + currentView, + setCurrentView, + privacyRequestId, + setPrivacyRequestId, + isVerificationRequired, +}) => { + const action = openAction + ? config.actions.filter(({ policy_key }) => policy_key === openAction)[0] + : null; + + if (!action) return null; + + let form = null; + + if (currentView === ModalViews.PrivacyRequest) { + form = ( + + ); + } + + if (currentView === ModalViews.IdentityVerification) { + form = ( + + ); + } + + if (currentView === ModalViews.RequestSubmitted) { + form = ; + } + + return ( + + + + {form} + + + ); +}; diff --git a/clients/ops/privacy-center/components/modals/RequestSubmitted.tsx b/clients/ops/privacy-center/components/modals/RequestSubmitted.tsx new file mode 100644 index 0000000000..d4cd9be72d --- /dev/null +++ b/clients/ops/privacy-center/components/modals/RequestSubmitted.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { + ModalHeader, + ModalFooter, + ModalBody, + Text, + Button, + Image, + HStack, +} from "@fidesui/react"; + +type RequestSubmittedProps = { + onClose: () => void; + action: any; +}; + +const RequestSubmitted: React.FC = ({ + onClose, + action, +}) => ( + <> + + green-checkmark + + + Request submitted + + + + + Thanks for your {action.policy_key} Request. A member of our team will + review and be in contact with you shortly. + + + + + + + +); + +export default RequestSubmitted; diff --git a/clients/ops/privacy-center/components/modals/VerificationForm.tsx b/clients/ops/privacy-center/components/modals/VerificationForm.tsx new file mode 100644 index 0000000000..3f686e242d --- /dev/null +++ b/clients/ops/privacy-center/components/modals/VerificationForm.tsx @@ -0,0 +1,237 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { + Button, + chakra, + FormControl, + FormErrorMessage, + HStack, + Input, + ModalBody, + ModalFooter, + ModalHeader, + Stack, + Text, + VStack, +} from "@fidesui/react"; + +import { useFormik } from "formik"; + +import { Headers } from "headers-polyfill"; +import type { AlertState } from "../../types/AlertState"; +import { ModalViews } from "./types"; +import { addCommonHeaders } from "../../common/CommonHeaders"; + +import config from "../../config/config.json"; +import { hostUrl } from "../../constants"; + +const useVerificationForm = ({ + onClose, + action, + setAlert, + privacyRequestId, + setCurrentView, +}: { + onClose: () => void; + action: typeof config.actions[0] | null; + setAlert: (state: AlertState) => void; + privacyRequestId: string; + setCurrentView: (view: ModalViews) => void; +}) => { + const [isLoading, setIsLoading] = useState(false); + const resetVerificationProcess = useCallback(() => { + setCurrentView(ModalViews.PrivacyRequest); + }, [setCurrentView]); + + const formik = useFormik({ + initialValues: { + code: "", + }, + onSubmit: async (values) => { + if (!action) { + // somehow we've reached a broken state, return + return; + } + + setIsLoading(true); + + const body = { + code: values.code, + }; + + const handleError = (detail: string | undefined) => { + const fallbackMessage = + "An error occured while verifying your request."; + setAlert({ + status: "error", + description: detail || fallbackMessage, + }); + onClose(); + }; + try { + const headers: Headers = new Headers(); + addCommonHeaders(headers, null); + + const response = await fetch( + `${hostUrl}/privacy-request/${privacyRequestId}/verify`, + { + method: "POST", + headers, + body: JSON.stringify(body), + } + ); + const data = await response.json(); + + if (!response.ok) { + handleError(data?.detail); + return; + } + + setCurrentView(ModalViews.RequestSubmitted); + } catch (error) { + handleError(""); + } + }, + validate: (values) => { + const errors: { + code?: string; + } = {}; + + if (!values.code) { + errors.code = "Required"; + return errors; + } + if (!values.code.match(/^\d+$/g)) { + errors.code = "Verification code must be all numbers"; + } + + return errors; + }, + }); + + return { ...formik, isLoading, resetVerificationProcess }; +}; + +type VerificationFormProps = { + isOpen: boolean; + onClose: () => void; + openAction: string | null; + setAlert: (state: AlertState) => void; + privacyRequestId: string; + setCurrentView: (view: ModalViews) => void; +}; + +const VerificationForm: React.FC = ({ + isOpen, + onClose, + openAction, + setAlert, + privacyRequestId, + setCurrentView, +}) => { + const action = openAction + ? config.actions.filter(({ policy_key }) => policy_key === openAction)[0] + : null; + + const { + errors, + handleBlur, + handleChange, + handleSubmit, + touched, + values, + isValid, + dirty, + resetForm, + resetVerificationProcess, + } = useVerificationForm({ + onClose, + action, + setAlert, + privacyRequestId, + setCurrentView, + }); + + useEffect(() => resetForm(), [isOpen, resetForm]); + + if (!action) return null; + + return ( + <> + + Enter verification code + + + + + We have sent a verification code to your email address. Please check + your email, then return to this window and the code below. + + + {action.identity_inputs.name ? ( + + + {errors.code} + + ) : null} + + + + + + + + + + + Didn't receive a code? + {" "} + + Click here to try again + + + + + + + ); +}; + +export default VerificationForm; diff --git a/clients/ops/privacy-center/components/modals/types.ts b/clients/ops/privacy-center/components/modals/types.ts new file mode 100644 index 0000000000..ba0f625422 --- /dev/null +++ b/clients/ops/privacy-center/components/modals/types.ts @@ -0,0 +1,7 @@ +/* eslint-disable import/prefer-default-export */ + +export enum ModalViews { + PrivacyRequest = "privacyRequest", + IdentityVerification = "identityVerification", + RequestSubmitted = "requestSubmitted", +} diff --git a/clients/ops/privacy-center/constants/index.ts b/clients/ops/privacy-center/constants/index.ts new file mode 100644 index 0000000000..589a239151 --- /dev/null +++ b/clients/ops/privacy-center/constants/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/prefer-default-export */ + + +import config from "../config/config.json"; + +export const hostUrl = + process.env.NODE_ENV === "development" + ? config.fidesops_host_development + : config.fidesops_host_production; \ No newline at end of file diff --git a/clients/ops/privacy-center/package-lock.json b/clients/ops/privacy-center/package-lock.json index 0ca6fa380e..8ea53fd089 100644 --- a/clients/ops/privacy-center/package-lock.json +++ b/clients/ops/privacy-center/package-lock.json @@ -14,6 +14,7 @@ "@fontsource/inter": "^4.5.4", "formik": "^2.2.9", "framer-motion": "^5", + "headers-polyfill": "^3.0.10", "next": "12.1.0", "next-auth": "^4.10.3", "react": "^17.0.2", @@ -6967,10 +6968,9 @@ } }, "node_modules/headers-polyfill": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.4.tgz", - "integrity": "sha512-I1DOM1EdWYntdrnCvqQtcKwSSuiTzoqOExy4v1mdcFixFZABlWP4IPHdmoLtPda0abMHqDOY4H9svhQ10DFR4w==", - "dev": true + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.10.tgz", + "integrity": "sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ==" }, "node_modules/hey-listen": { "version": "1.0.8", @@ -16296,10 +16296,9 @@ } }, "headers-polyfill": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.4.tgz", - "integrity": "sha512-I1DOM1EdWYntdrnCvqQtcKwSSuiTzoqOExy4v1mdcFixFZABlWP4IPHdmoLtPda0abMHqDOY4H9svhQ10DFR4w==", - "dev": true + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.10.tgz", + "integrity": "sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ==" }, "hey-listen": { "version": "1.0.8", diff --git a/clients/ops/privacy-center/package.json b/clients/ops/privacy-center/package.json index 9a74070f08..896241af8a 100644 --- a/clients/ops/privacy-center/package.json +++ b/clients/ops/privacy-center/package.json @@ -5,7 +5,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "eslint . --fix --ext .ts,.tsx", "format": "prettier --write types/ theme/ pages/ config/ components/ __tests__/", "format:ci": "prettier --check types/ theme/ pages/ config/ components/ __tests__/", "test": "jest --watch", @@ -24,6 +25,7 @@ "@fontsource/inter": "^4.5.4", "formik": "^2.2.9", "framer-motion": "^5", + "headers-polyfill": "^3.0.10", "next": "12.1.0", "next-auth": "^4.10.3", "react": "^17.0.2", @@ -52,8 +54,8 @@ "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "msw": "^0.39.0", + "prettier": "^2.6.2", "typescript": "4.5.5", - "whatwg-fetch": "^3.6.2", - "prettier": "^2.6.2" + "whatwg-fetch": "^3.6.2" } } diff --git a/clients/ops/privacy-center/pages/index.tsx b/clients/ops/privacy-center/pages/index.tsx index eb876f7689..121405e68b 100644 --- a/clients/ops/privacy-center/pages/index.tsx +++ b/clients/ops/privacy-center/pages/index.tsx @@ -6,7 +6,6 @@ import { Heading, Text, Stack, - Box, Alert, AlertIcon, AlertDescription, @@ -14,14 +13,30 @@ import { Image, } from "@fidesui/react"; -import { useRequestModal, RequestModal } from "../components/RequestModal"; +import { + useRequestModal, + RequestModal, +} from "../components/modals/RequestModal"; +import PrivacyCard from "../components/PrivacyCard"; import type { AlertState } from "../types/AlertState"; import config from "../config/config.json"; +import { hostUrl } from "../constants"; const Home: NextPage = () => { const [alert, setAlert] = useState(null); - const { isOpen, onClose, onOpen, openAction } = useRequestModal(); + const [isVerificationRequired, setIsVerificationRequired] = + useState(false); + const { + isOpen, + onClose, + onOpen, + openAction, + currentView, + setCurrentView, + privacyRequestId, + setPrivacyRequestId, + } = useRequestModal(); useEffect(() => { if (alert?.status) { @@ -31,6 +46,19 @@ const Home: NextPage = () => { return () => false; }, [alert]); + useEffect(() => { + const getConfig = async () => { + const response = await fetch(`${hostUrl}/id-verification/config`, { + headers: { + "X-Fides-Source": "fidesops-privacy-center", + }, + }); + const data = await response.json(); + setIsVerificationRequired(data.identity_verification_required); + }; + getConfig(); + }, [setIsVerificationRequired]); + return (
@@ -97,50 +125,14 @@ const Home: NextPage = () => { {config.actions.map((action) => ( - onOpen(action.policy_key)} - > - - {action.description} - - - {action.title} - - - {action.description} - - - - + title={action.title} + policyKey={action.policy_key} + iconPath={action.icon_path} + description={action.description} + onOpen={onOpen} + /> ))} @@ -149,6 +141,11 @@ const Home: NextPage = () => { onClose={onClose} openAction={openAction} setAlert={setAlert} + currentView={currentView} + setCurrentView={setCurrentView} + privacyRequestId={privacyRequestId} + setPrivacyRequestId={setPrivacyRequestId} + isVerificationRequired={isVerificationRequired} />
diff --git a/clients/ops/privacy-center/public/green-check.svg b/clients/ops/privacy-center/public/green-check.svg new file mode 100644 index 0000000000..30db47950c --- /dev/null +++ b/clients/ops/privacy-center/public/green-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/clients/ops/privacy-center/types/index.ts b/clients/ops/privacy-center/types/index.ts new file mode 100644 index 0000000000..0597a4df0f --- /dev/null +++ b/clients/ops/privacy-center/types/index.ts @@ -0,0 +1,13 @@ +/* eslint-disable import/prefer-default-export */ + +export enum PrivacyRequestStatus { + APPROVED = "approved", + COMPLETE = "complete", + DENIED = "denied", + ERROR = "error", + IN_PROCESSING = "in_processing", + PAUSED = "paused", + CANCELED = "canceled", + PENDING = "pending", + IDENTITY_UNVERIFIED = "identity_unverified", +}