From 497bd49c0af6f9cece9a85f31a2e13f2b4523859 Mon Sep 17 00:00:00 2001 From: Roy Schut Date: Fri, 23 Jul 2021 11:46:21 +0200 Subject: [PATCH] feat(user): add consent validation --- src/components/Account/Account.module.scss | 68 ++++++- src/components/Account/Account.test.tsx | 7 +- src/components/Account/Account.tsx | 112 +++++++---- .../__snapshots__/Account.test.tsx.snap | 190 ++++++++---------- src/components/Form/Form.tsx | 53 +++-- .../LoadingOverlay/LoadingOverlay.module.scss | 40 ---- .../LoadingOverlay/LoadingOverlay.tsx | 9 +- src/components/LoginForm/LoginForm.tsx | 2 +- .../MarkdownComponent/MarkdownComponent.tsx | 5 +- src/components/Spinner/Spinner.module.scss | 45 +++++ src/components/Spinner/Spinner.tsx | 21 ++ src/containers/Customer/CustomerContainer.ts | 51 +++-- src/hooks/useForm.ts | 11 +- src/screens/User/User.tsx | 17 +- src/services/account.service.ts | 19 +- src/styles/_theme.scss | 10 +- src/utils/collection.ts | 38 +++- types/account.d.ts | 31 +++ types/form.d.ts | 10 + types/general.d.ts | 1 - 20 files changed, 503 insertions(+), 237 deletions(-) create mode 100644 src/components/Spinner/Spinner.module.scss create mode 100644 src/components/Spinner/Spinner.tsx create mode 100644 types/form.d.ts delete mode 100644 types/general.d.ts diff --git a/src/components/Account/Account.module.scss b/src/components/Account/Account.module.scss index e8edcb69f..937cc59fa 100644 --- a/src/components/Account/Account.module.scss +++ b/src/components/Account/Account.module.scss @@ -19,9 +19,75 @@ button { } .checkbox { - display: flex; + position: relative; + display: block; align-items: center; + margin-top: variables.$base-spacing / 2; + margin-bottom: 12px; + padding-left: 35px; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + > input { + position: absolute; + width: 0; + height: 0; margin-right: 10px; + opacity: 0; + :not(&:disabled) { + &:hover ~ .checkmark:hover { + background-color: theme.$forms-checkbox-bg-hover; + cursor: pointer; + } + &:hover ~ .checkLabel { + cursor: pointer; + } + &:checked ~ .checkmark:hover { + background-color: theme.$forms-primary-color-hover; + } + } + &:checked ~ .checkmark { + background-color: theme.$forms-primary-color; + } + &:checked ~ .checkmark::after { + display: block; + } + + &:disabled ~ .checkmark, + &:disabled ~ .checkLabel { + cursor: default; + opacity: 0.6; + } + } +} + +.checkmark { + position: absolute; + top: 0; + left: 0; + width: 15px; + height: 15px; + background-color: theme.$forms-checkbox-bg; + &::after { + content: ''; + position: absolute; + top: 2px; + left: 4px; + display: none; + + width: 6px; + height: 10px; + border: solid black; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); } } + +.submitConsents { + margin-top: variables.$base-spacing; +} diff --git a/src/components/Account/Account.test.tsx b/src/components/Account/Account.test.tsx index ed1f514ed..0a7a09cbc 100644 --- a/src/components/Account/Account.test.tsx +++ b/src/components/Account/Account.test.tsx @@ -17,8 +17,11 @@ describe('', () => { const { container } = render( console.info(values)} - onUpdateInfoSubmit={(values) => console.info(values)} + isLoading={false} + consentsLoading={false} + onUpdateEmailSubmit={() => null} + onUpdateInfoSubmit={() => null} + onUpdateConsentsSubmit={() => null} onDeleteAccountClick={() => null} />, ); diff --git a/src/components/Account/Account.tsx b/src/components/Account/Account.tsx index 1a579fdd2..ff9fc4627 100644 --- a/src/components/Account/Account.tsx +++ b/src/components/Account/Account.tsx @@ -1,55 +1,70 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { Customer, UpdateCustomerPayload } from 'types/account'; +import type { Consent, Customer, CustomerConsent, UpdateCustomerPayload } from 'types/account'; +import type { CustomerFormValues, FormErrors, GenericFormValues } from 'types/form'; +import { formatConsentsFromValues, formatConsentValues } from '../../utils/collection'; import Visibility from '../../icons/Visibility'; import VisibilityOff from '../../icons/VisibilityOff'; -import type { FormErrors } from '../../hooks/useForm'; import useToggle from '../../hooks/useToggle'; import Button from '../../components/Button/Button'; +import Spinner from '../../components/Spinner/Spinner'; import Form from '../Form/Form'; import IconButton from '../IconButton/IconButton'; import LoadingOverlay from '../LoadingOverlay/LoadingOverlay'; import TextField from '../TextField/TextField'; +import MarkdownComponent from '../MarkdownComponent/MarkdownComponent'; import styles from './Account.module.scss'; type Props = { customer: Customer; - errors: FormErrors; + errors?: FormErrors; isLoading: boolean; - onUpdateEmailSubmit: (data: Record) => void; - onUpdateInfoSubmit: (data: Record) => void; + consentsLoading: boolean; + publisherConsents?: Consent[]; + customerConsents?: CustomerConsent[]; + onUpdateEmailSubmit: (data: CustomerFormValues) => void; + onUpdateInfoSubmit: (data: CustomerFormValues) => void; onDeleteAccountClick: () => void; - onReset: () => void; + onUpdateConsentsSubmit: (consents: CustomerConsent[]) => void; + onReset?: () => void; panelClassName?: string; panelHeaderClassName?: string; }; -type Editing = 'none' | 'account' | 'password' | 'info'; -type FormValues = Record; +type Editing = 'none' | 'account' | 'password' | 'info' | 'consents'; const Account = ({ customer, errors, isLoading, + consentsLoading, + publisherConsents, + customerConsents, panelClassName, panelHeaderClassName, onUpdateEmailSubmit, onUpdateInfoSubmit, onDeleteAccountClick, + onUpdateConsentsSubmit, onReset, }: Props): JSX.Element => { const { t } = useTranslation('user'); const [editing, setEditing] = useState('none'); const [viewPassword, toggleViewPassword] = useToggle(); + const consentValues = useMemo(() => formatConsentValues(publisherConsents, customerConsents), [publisherConsents, customerConsents]); + const initialValues = useMemo(() => ({ ...customer, consents: consentValues }), [customer, consentValues]); - const handleSubmit = (values: FormValues) => { + const handleSubmit = (values: GenericFormValues) => { switch (editing) { case 'account': - return onUpdateEmailSubmit(values); + return onUpdateEmailSubmit(values as CustomerFormValues); case 'info': - return onUpdateInfoSubmit(values); + return onUpdateInfoSubmit(values as CustomerFormValues); + case 'consents': + return onUpdateConsentsSubmit(formatConsentsFromValues(publisherConsents, values)); + break; default: return; } @@ -57,7 +72,7 @@ const Account = ({ const onCancelClick = (formResetHandler?: () => void): void => { formResetHandler && formResetHandler(); setEditing('none'); - onReset(); + onReset && onReset(); }; useEffect(() => { @@ -65,8 +80,8 @@ const Account = ({ }, [isLoading]); return ( -
- {({ values, handleChange, handleSubmit, handleReset }) => ( + + {({ values, handleChange, handleReset, handleSubmit, hasChanged }) => ( <> {isLoading && }
@@ -78,7 +93,7 @@ const Account = ({ {editing === 'account' ? ( <> -
@@ -127,7 +147,7 @@ const Account = ({
{t('account.password')}

****************

-
@@ -139,7 +159,7 @@ const Account = ({ {editing === 'info' ? ( <> -
@@ -173,15 +193,35 @@ const Account = ({

{'Terms & tracking'}

-
- -
+ {consentsLoading ? ( + + ) : publisherConsents ? ( +
+ {publisherConsents.map((consent, index) => ( + + ))} +
+ ) : null} )} diff --git a/src/components/Account/__snapshots__/Account.test.tsx.snap b/src/components/Account/__snapshots__/Account.test.tsx.snap index a22664e24..dccaf587a 100644 --- a/src/components/Account/__snapshots__/Account.test.tsx.snap +++ b/src/components/Account/__snapshots__/Account.test.tsx.snap @@ -2,132 +2,112 @@ exports[` renders and matches snapshot 1`] = `
-
+
-

- account.email -

-
-
-

- todo@test.nl -

+
+

+ account.email +

+
+

+ todo@test.nl +

+
+ +
+
+
+
+
+

+ account.security +

+
+
+ + account.password + +

+ **************** +

-
-
-
-

- account.security -

-
-
- - account.password - -

- **************** -

- -
-
-
-

- account.about_you -

-
-
-
+
+

+ account.about_you +

+
+
- -

-

-
- +

+

+
- account.lastname - -

-

-
-
+
- - account.edit_information - - + +
-
-
-
-

- Terms & tracking -

-
- - +
+

+ Terms & tracking +

+
-
+
`; diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 6faba2397..5d35467ed 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -1,43 +1,66 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import type { GenericFormValues } from 'types/form'; type Return = { - values: FormValues; - handleChange?: React.ChangeEventHandler; + values: GenericFormValues; + handleChange?: (event: React.FormEvent, options?: HandleChangeOptions | undefined) => void; handleSubmit?: () => void; handleReset?: () => void; + hasChanged?: boolean; }; type Props = { - initialValues: FormValues; + initialValues: GenericFormValues; editing?: boolean; children: ({ values, handleChange }: Return) => JSX.Element; - onSubmit: (values: FormValues) => void; + onSubmit: (values: GenericFormValues) => void; +}; + +type HandleChangeOptions = { + nestInto: string; }; const Form = ({ initialValues, editing = true, children, onSubmit }: Props): JSX.Element => { - const [values, setValues] = useState(initialValues); + const [values, setValues] = useState(initialValues); + const [hasChanged, setHasChanged] = useState(false); - const handleChange = (event: React.FormEvent) => { + const handleChange = (event: React.FormEvent, options?: HandleChangeOptions) => { if (!event.currentTarget) return; - setValues({ ...values, [event.currentTarget.name]: event.currentTarget.value }); + + const name = event.currentTarget.name; + const value = event.currentTarget.type === 'checkbox' ? (event.currentTarget as HTMLInputElement).checked : event.currentTarget.value; + + setHasChanged(true); + + if (options?.nestInto) { + const oldSubValues = values[options.nestInto]; + const subValues = { ...oldSubValues, [name]: value }; + return setValues({ ...values, [options.nestInto]: subValues }); + } + + setValues({ ...values, [name]: value }); }; const handleSubmit = (event?: React.FormEvent) => { event && event.preventDefault(); - onSubmit(values); + onSubmit && onSubmit(values); + }; + + const handleReset = () => { + setValues(initialValues); + setHasChanged(false); }; - const handleReset = () => setValues(initialValues); + useEffect(() => { + // todo: only update new key/values? + setValues(initialValues); + }, [initialValues]); if (!editing) { return children({ values }); } - return ( -
- {children({ values, handleChange, handleSubmit, handleReset })} -
- ); + return
{children({ values, handleChange, handleSubmit, handleReset, hasChanged })}
; }; export default Form; diff --git a/src/components/LoadingOverlay/LoadingOverlay.module.scss b/src/components/LoadingOverlay/LoadingOverlay.module.scss index 7536804cb..89a0858f5 100644 --- a/src/components/LoadingOverlay/LoadingOverlay.module.scss +++ b/src/components/LoadingOverlay/LoadingOverlay.module.scss @@ -17,43 +17,3 @@ .transparent { opacity: 0.5; } - -.buffer { - position: relative; - display: inline-block; - width: 80px; - height: 80px; -} - -.buffer div { - position: absolute; - display: block; - width: 64px; - height: 64px; - margin: 8px; - border: 4px solid variables.$white; - border-color: variables.$white transparent transparent transparent; - border-radius: 50%; - animation: buffer 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; -} - -.buffer div:nth-child(1) { - animation-delay: -0.45s; -} - -.buffer div:nth-child(2) { - animation-delay: -0.3s; -} - -.buffer div:nth-child(3) { - animation-delay: -0.15s; -} - -@keyframes buffer { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} diff --git a/src/components/LoadingOverlay/LoadingOverlay.tsx b/src/components/LoadingOverlay/LoadingOverlay.tsx index 79c4221aa..83bad81cc 100644 --- a/src/components/LoadingOverlay/LoadingOverlay.tsx +++ b/src/components/LoadingOverlay/LoadingOverlay.tsx @@ -1,6 +1,8 @@ import React from 'react'; import classNames from 'classnames'; +import Spinner from '../Spinner/Spinner'; + import styles from './LoadingOverlay.module.scss'; type Props = { @@ -10,12 +12,7 @@ type Props = { const LoadingOverlay = ({ transparentBackground = false }: Props): JSX.Element => { return (
-
-
-
-
-
-
+
); }; diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx index 5722ed811..c45c70256 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/LoginForm.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useHistory } from 'react-router'; import { useTranslation } from 'react-i18next'; import type { LoginFormData } from 'types/account'; +import type { FormErrors } from 'types/form'; import useToggle from '../../hooks/useToggle'; import { addQueryParam } from '../../utils/history'; @@ -11,7 +12,6 @@ import Link from '../Link/Link'; import IconButton from '../IconButton/IconButton'; import Visibility from '../../icons/Visibility'; import VisibilityOff from '../../icons/VisibilityOff'; -import type { FormErrors } from '../../hooks/useForm'; import styles from './LoginForm.module.scss'; diff --git a/src/components/MarkdownComponent/MarkdownComponent.tsx b/src/components/MarkdownComponent/MarkdownComponent.tsx index f47143de1..556e17a36 100644 --- a/src/components/MarkdownComponent/MarkdownComponent.tsx +++ b/src/components/MarkdownComponent/MarkdownComponent.tsx @@ -9,6 +9,7 @@ const LINEBREAK_REGEX = /(?:\r\n|\r|\n)/g; type Props = { markdownString: string; + className?: string; }; const parseMarkdown = (value: string): string => @@ -29,8 +30,8 @@ const parseMarkdown = (value: string): string => }) .replace(LINEBREAK_REGEX, '
'); // linebreak formatter should run last -const MarkdownComponent: React.FC = ({ markdownString }: Props) => { - return
; +const MarkdownComponent: React.FC = ({ markdownString, className }: Props) => { + return
; }; export default MarkdownComponent; diff --git a/src/components/Spinner/Spinner.module.scss b/src/components/Spinner/Spinner.module.scss new file mode 100644 index 000000000..be8976cb5 --- /dev/null +++ b/src/components/Spinner/Spinner.module.scss @@ -0,0 +1,45 @@ +@use '../../styles/variables'; +@use '../../styles/theme'; + +.buffer { + position: relative; + display: inline-block; + width: 80px; + height: 80px; +} + +.buffer div { + position: absolute; + display: block; + width: variables.$base-spacing * 4; + height: variables.$base-spacing * 4; + margin: 8px; + border: 4px solid variables.$white; + border-color: variables.$white transparent transparent transparent; + border-radius: 50%; + animation: buffer 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; +} +.small { + transform: scale(0.6); +} + +.buffer div:nth-child(1) { + animation-delay: -0.45s; +} + +.buffer div:nth-child(2) { + animation-delay: -0.3s; +} + +.buffer div:nth-child(3) { + animation-delay: -0.15s; +} + +@keyframes buffer { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/components/Spinner/Spinner.tsx b/src/components/Spinner/Spinner.tsx new file mode 100644 index 000000000..e4a962518 --- /dev/null +++ b/src/components/Spinner/Spinner.tsx @@ -0,0 +1,21 @@ +import classNames from 'classnames'; +import React from 'react'; + +import styles from './Spinner.module.scss'; + +type Props = { + size?: 'small' | 'medium'; +}; + +const Spinner = ({ size = 'medium' }: Props): JSX.Element => { + return ( +
+
+
+
+
+
+ ); +}; + +export default Spinner; diff --git a/src/containers/Customer/CustomerContainer.ts b/src/containers/Customer/CustomerContainer.ts index d352b0442..991323751 100644 --- a/src/containers/Customer/CustomerContainer.ts +++ b/src/containers/Customer/CustomerContainer.ts @@ -1,39 +1,60 @@ -import type { Customer, UpdateCustomerPayload } from 'types/account'; -import { useMutation } from 'react-query'; +import type { Consent, Customer, CustomerConsent, UpdateCustomerPayload } from 'types/account'; +import { useMutation, useQuery } from 'react-query'; +import type { CustomerFormErrors, CustomerFormValues, FormErrors } from 'types/form'; import { ConfigStore } from '../../stores/ConfigStore'; import { AccountStore } from '../../stores/AccountStore'; -import { updateCustomer } from '../../services/account.service'; -import type { FormErrors, FormValues } from '../../hooks/useForm'; +import { getCustomerConsents, getPublisherConsents, updateCustomer } from '../../services/account.service'; type ChildrenParams = { customer: Customer; errors: FormErrors; isLoading: boolean; + consentsLoading: boolean; + publisherConsents?: Consent[]; + customerConsents?: CustomerConsent[]; onUpdateEmailSubmit: (values: CustomerFormValues) => void; onUpdateInfoSubmit: (values: CustomerFormValues) => void; + onUpdateConsentsSubmit: (consents: CustomerConsent[]) => void; onReset: () => void; }; -type CustomerFormValues = FormValues; -type CustomerFormErrors = FormErrors; - type Props = { children: (data: ChildrenParams) => JSX.Element; + fetchConsents?: boolean; }; -const CustomerContainer = ({ children }: Props): JSX.Element => { +const CustomerContainer = ({ children, fetchConsents = true }: Props): JSX.Element => { const customer = AccountStore.useState((state) => state.user); const auth = AccountStore.useState((state) => state.auth); - const { - config: { cleengSandbox }, - } = ConfigStore.getRawState(); + const { config } = ConfigStore.getRawState(); + const { cleengId, cleengSandbox } = config; + const jwt = auth?.jwt || ''; + const publisherId = cleengId || ''; + const customerId = customer?.id || ''; + + const { mutate, isLoading, data, reset } = useMutation((values: CustomerFormValues) => updateCustomer(values, cleengSandbox, jwt)); - const { mutate, isLoading, data, reset } = useMutation((values: CustomerFormValues) => updateCustomer(values, cleengSandbox, auth?.jwt || '')); + const { data: publisherConsents, isLoading: publisherConsentsLoading } = useQuery( + ['publisherConsents'], + () => getPublisherConsents({ publisherId }, cleengSandbox), + { + enabled: fetchConsents && !!publisherId, + }, + ); + const { data: customerConsents, isLoading: customerConsentsLoading } = useQuery( + ['customerConsents'], + () => getCustomerConsents({ customerId }, cleengSandbox, jwt), + { + enabled: fetchConsents && !!customer?.id, + }, + ); const onUpdateEmailSubmit = ({ id, email, confirmationPassword }: CustomerFormValues) => mutate({ id, email, confirmationPassword }); const onUpdateInfoSubmit = ({ id, firstName, lastName }: CustomerFormValues) => mutate({ id, firstName, lastName }); + const onUpdateConsentsSubmit = (consents: CustomerConsent[]) => mutate({ id: customerId, consents }); + const translateErrors = (errors?: string[]) => { const formErrors: CustomerFormErrors = {}; @@ -62,10 +83,14 @@ const CustomerContainer = ({ children }: Props): JSX.Element => { return children({ customer, - isLoading, + isLoading: isLoading, errors: translateErrors(data?.errors), + publisherConsents: publisherConsents?.responseData?.consents, + customerConsents: customerConsents?.responseData?.consents, + consentsLoading: publisherConsentsLoading || customerConsentsLoading, onUpdateEmailSubmit, onUpdateInfoSubmit, + onUpdateConsentsSubmit, onReset: reset, } as ChildrenParams); }; diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index 6b70ef106..b2fedea12 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -1,14 +1,7 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; +import type { FormErrors, GenericFormValues, UseFormChangeHandler, UseFormSubmitHandler } from 'types/form'; import { ValidationError, AnySchema } from 'yup'; -type UseFormChangeHandler = React.ChangeEventHandler; -type UseFormSubmitHandler = React.FormEventHandler; - -export type GenericFormErrors = { form: string }; -export type GenericFormValues = Record; -export type FormErrors = Partial; -export type FormValues = Partial; - export type UseFormReturnValue = { values: T; errors: FormErrors; diff --git a/src/screens/User/User.tsx b/src/screens/User/User.tsx index 4f4c5db56..2b62d49bc 100644 --- a/src/screens/User/User.tsx +++ b/src/screens/User/User.tsx @@ -68,13 +68,28 @@ const User = (): JSX.Element => { - {({ customer, errors, isLoading, onUpdateEmailSubmit, onUpdateInfoSubmit, onReset }) => ( + {({ + customer, + errors, + isLoading, + consentsLoading, + publisherConsents, + customerConsents, + onUpdateEmailSubmit, + onUpdateInfoSubmit, + onUpdateConsentsSubmit, + onReset, + }) => ( { return post(sandbox, '/auths', JSON.stringify(payload)); }; +export const getPublisherConsents: GetPublisherConsents = async (payload, sandbox) => { + return get(sandbox, `/publishers/${payload.publisherId}/consents`); +}; + +export const getCustomerConsents: GetCustomerConsents = async (payload, sandbox, jwt) => { + return get(sandbox, `/customers/${payload.customerId}/consents`, jwt); +}; + export const resetPassword: ResetPassword = async (payload, sandbox) => { return put(sandbox, '/customers/passwords', JSON.stringify(payload)); }; diff --git a/src/styles/_theme.scss b/src/styles/_theme.scss index 1b31f0632..872bed636 100644 --- a/src/styles/_theme.scss +++ b/src/styles/_theme.scss @@ -91,7 +91,7 @@ $cookies-color: $dark-color !default; $cookies-bg: variables.$white !default; // Form -$form-error-bg-color: #FF0C3E !default; +$form-error-bg-color: #ff0c3e !default; // Footer @@ -151,7 +151,7 @@ $text-field-hover-bg-color: rgba(variables.$white, 0.08) !default; $text-field-active-color: variables.$white !default; $text-field-active-border-color: variables.$white !default; $text-field-placeholder-color: rgba(variables.$white, 0.7) !default; -$text-field-error-color: #FF0C3E !default; +$text-field-error-color: #ff0c3e !default; // Toggle @@ -188,3 +188,9 @@ $video-details-tag-shadow: 0 1px 0 variables.$black !default; $panel-bg: rgba(255, 255, 255, 0.08); $panel-box-shadow: 0 6px 10px rgb(0 0 0 / 14%), 0 1px 18px rgb(0 0 0 / 12%), 0 3px 5px rgb(0 0 0 / 20%); $panel-header-border-bottom: 1px solid rgba(255, 255, 255, 0.32); + +// Forms +$forms-primary-color: rgb(1, 163, 157); +$forms-primary-color-hover: rgba(1, 163, 157, 0.8); +$forms-checkbox-bg: #cccccc; +$forms-checkbox-bg-hover: #eeeeee; diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 92385a899..063cae751 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -1,4 +1,6 @@ +import type { Consent, CustomerConsent } from 'types/account'; import type { Config } from 'types/Config'; +import type { GenericFormValues } from 'types/form'; import type { Playlist, PlaylistItem } from 'types/playlist'; const getFiltersFromConfig = (config: Config, playlistId: string): string[] => { @@ -16,8 +18,7 @@ const filterPlaylist = (playlist: PlaylistItem[], filter: string) => { const getFiltersFromSeries = (series: PlaylistItem[]): string[] => series.reduce( - (filters: string[], item) => - item.seasonNumber && filters.includes(item.seasonNumber) ? filters : filters.concat(item.seasonNumber || ''), + (filters: string[], item) => (item.seasonNumber && filters.includes(item.seasonNumber) ? filters : filters.concat(item.seasonNumber || '')), [], ); @@ -59,6 +60,37 @@ const generatePlaylistPlaceholder = (playlistLength: number = 15): Playlist => ( ), }); +const formatConsentValues = (publisherConsents?: Consent[], customerConsents?: CustomerConsent[]) => { + if (!publisherConsents || !customerConsents) { + return {}; + } + + const values: Record = {}; + publisherConsents?.forEach((publisherConsent) => { + if (customerConsents?.find((customerConsent) => customerConsent.name === 'terms' && customerConsent.state === 'accepted')) { + values[publisherConsent.name] = true; + } + }); + + return values; +}; + +const formatConsentsFromValues = (publisherConsents?: Consent[], values?: GenericFormValues) => { + const consents: CustomerConsent[] = []; + + if (!publisherConsents || !values) return consents; + + publisherConsents.forEach((consent) => { + consents.push({ + name: consent.name, + version: consent.version, + state: values.consents[consent.name] ? 'accepted' : 'declined', + }); + }); + + return consents; +}; + export { getFiltersFromConfig, getFiltersFromSeries, @@ -67,4 +99,6 @@ export { chunk, findPlaylistImageForWidth, generatePlaylistPlaceholder, + formatConsentValues, + formatConsentsFromValues, }; diff --git a/types/account.d.ts b/types/account.d.ts index f3d66521c..64a6c2091 100644 --- a/types/account.d.ts +++ b/types/account.d.ts @@ -35,6 +35,14 @@ export type RegisterPayload = { externalData?: string; }; +export type GetPublisherConsentsPayload = { + publisherId: string; +}; + +export type GetCustomerConsentsPayload = { + customerId: string; +}; + export type ResetPasswordPayload = { customerEmail: string; offerId?: string; @@ -74,8 +82,31 @@ export type Customer = { externalData?: string; }; +export type Consent = { + broadcasterId: number; + name: string; + version: string; + value: string; + label: string; + required: boolean; +}; +export type CustomerConsent = { + customerId?: string; + date?: number; + label?: string; + name: string; + needsUpdate?: boolean; + newestVersion?: string; + required?: boolean; + state: 'accepted' | 'declined'; + value?: string; + version: string; +}; + type Login = CleengRequest; type Register = CleengRequest; +type GetPublisherConsents = CleengRequest>; +type GetCustomerConsents = CleengAuthRequest>; type ResetPassword = CleengRequest>; type ChangePassword = CleengRequest>; type GetCustomer = CleengAuthRequest; diff --git a/types/form.d.ts b/types/form.d.ts new file mode 100644 index 000000000..332c72ec6 --- /dev/null +++ b/types/form.d.ts @@ -0,0 +1,10 @@ +export type UseFormChangeHandler = React.ChangeEventHandler; +export type UseFormSubmitHandler = React.FormEventHandler; + +export type GenericFormErrors = { form: string }; +export type GenericFormValues = Record; +export type FormErrors = Partial; +export type FormValues = Partial; + +export type CustomerFormValues = FormValues; +export type CustomerFormErrors = FormErrors; diff --git a/types/general.d.ts b/types/general.d.ts deleted file mode 100644 index aafebb521..000000000 --- a/types/general.d.ts +++ /dev/null @@ -1 +0,0 @@ -type FormValues = Record;