Skip to content

Commit

Permalink
feat: custom register fields
Browse files Browse the repository at this point in the history
  • Loading branch information
mirovladimitrovski committed May 31, 2023
1 parent 9e1b8da commit f7a5249
Show file tree
Hide file tree
Showing 18 changed files with 506 additions and 59 deletions.
1 change: 1 addition & 0 deletions .depcheckrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ignores: [
'\#types',
'\#components',
'\#utils',
'\#static',
# This is used in src/styles, which recognizes absolute paths from the repo root
'src',
# To support e2e-reports
Expand Down
4 changes: 2 additions & 2 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { type ReactNode } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

Expand All @@ -8,7 +8,7 @@ import HelperText from '#components/HelperText/HelperText';
import useOpaqueId from '#src/hooks/useOpaqueId';

type Props = {
label?: string | JSX.Element;
label?: ReactNode;
name: string;
value?: string;
checked?: boolean;
Expand Down
71 changes: 71 additions & 0 deletions src/components/CustomRegisterField/CustomRegisterField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type FC, type ChangeEventHandler, type ReactNode, useMemo, useCallback } from 'react';
import type { GetRegisterFieldOption } from '@inplayer-org/inplayer.js';

import Checkbox from '#components/Checkbox/Checkbox';
import TextField from '#components/TextField/TextField';
import Radio from '#components/Radio/Radio';
import Dropdown from '#components/Dropdown/Dropdown';
import { ConsentFieldVariants } from '#src/services/inplayer.account.service';
import { countries, usStates } from '#static/json';

type Props = {
type: ConsentFieldVariants;
name: string;
value: string;
onChange: (name: string, value: string) => void;
} & Partial<{
label: ReactNode;
placeholder: string;
error: boolean;
helperText: string;
disabled: boolean;
required: boolean;
options: GetRegisterFieldOption;
}>;

export type CustomRegisterFieldCommonProps = Props;

export const CustomRegisterField: FC<Props> = ({ type, name, value = '', onChange, ...props }) => {
const changeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
if (type === ConsentFieldVariants.CHECKBOX) {
onChange(name, `${e.target.checked}`);
} else {
onChange(name, e.target.value);
}
},
[type, name, onChange],
);

const optionsSet = useMemo(() => {
switch (type) {
case ConsentFieldVariants.COUNTRY_SELECT:
return countries;
case ConsentFieldVariants.US_STATE_SELECT:
return usStates;
default:
return props.options || {};
}
}, [type, props.options]);

const dropdownOptions = useMemo(() => Object.entries(optionsSet).map(([value, label]) => ({ value, label })), [optionsSet]);

const commonProps = { ...props, name, onChange: changeHandler };

switch (type) {
case ConsentFieldVariants.CHECKBOX:
return <Checkbox {...commonProps} checked={value === 'true'} />;
case ConsentFieldVariants.INPUT:
return <TextField {...commonProps} value={value} />;
case ConsentFieldVariants.RADIO:
return <Radio {...commonProps} values={dropdownOptions} value={value} header={props.label} />;
case ConsentFieldVariants.GENERAL_SELECT:
case ConsentFieldVariants.COUNTRY_SELECT:
case ConsentFieldVariants.US_STATE_SELECT:
return <Dropdown {...commonProps} options={dropdownOptions} value={value} defaultLabel={props.placeholder} fullWidth />;
}

return null;
};

export default CustomRegisterField;
4 changes: 2 additions & 2 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { type ReactNode } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

Expand All @@ -15,7 +15,7 @@ type Props = {
options?: (string | { value: string; label: string })[];
optionsStyle?: string;
valuePrefix?: string;
label?: string;
label?: ReactNode;
fullWidth?: boolean;
size?: 'small' | 'medium';
error?: boolean;
Expand Down
17 changes: 12 additions & 5 deletions src/components/PersonalDetailsForm/PersonalDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ const PersonalDetailsForm: React.FC<Props> = ({
}: Props) => {
const { t } = useTranslation('account');
const renderQuestion = ({ value, key, question, required }: CleengCaptureQuestionField) => {
const values = value?.split(';') || [];
const values = value?.split(';').map((question) => {
const [value, label = value] = question.split(':');

return { value, label };
});

const props = {
name: key,
onChange: onQuestionChange,
Expand All @@ -55,11 +60,13 @@ const PersonalDetailsForm: React.FC<Props> = ({
key,
};

if (values.length === 1) {
return <Checkbox checked={!!questionValues[key]} value={values[0]} header={question} label={values[0]} {...props} />;
} else if (values.length === 2) {
const optionsKeys = Object.keys(values);

if (optionsKeys.length === 1) {
return <Checkbox checked={!!questionValues[key]} value={values[0].value} header={question} label={values[0].label} {...props} />;
} else if (optionsKeys.length === 2) {
return <Radio values={values} value={questionValues[key]} header={question} {...props} />;
} else if (values.length > 2) {
} else if (optionsKeys.length > 2) {
return <Dropdown options={values} value={questionValues[key]} label={question} defaultLabel={t('personal_details.select_answer')} {...props} fullWidth />;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('<Radio>', () => {
header={'Choose a Value'}
onChange={vi.fn()}
value="value1"
values={Array.of('value1', 'value2', 'value3')}
values={Array.of({ value: 'value1', label: 'Label 1 ' }, { value: 'value2', label: 'Label 2 ' }, { value: 'value3', label: 'Label 3 ' })}
helperText={'This is required!'}
error={false}
/>,
Expand Down
10 changes: 5 additions & 5 deletions src/components/Radio/Radio.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';

import styles from './Radio.module.scss';
Expand All @@ -9,9 +9,9 @@ import useOpaqueId from '#src/hooks/useOpaqueId';
type Props = {
name: string;
value?: string;
values: string[];
values: { value: string; label: string }[];
onChange: React.ChangeEventHandler<HTMLInputElement>;
header?: string;
header?: ReactNode;
helperText?: string;
error?: boolean;
required?: boolean;
Expand All @@ -29,10 +29,10 @@ const Radio: React.FC<Props> = ({ name, onChange, header, value, values, helperT
{!required ? <span>{t('optional')}</span> : null}
</div>
) : null}
{values.map((optionValue, index) => (
{values.map(({ value: optionValue, label: optionLabel }, index) => (
<div className={styles.radio} key={index}>
<input value={optionValue} name={name} type="radio" id={id + index} onChange={onChange} checked={value === optionValue} required={required} />
<label htmlFor={id + index}>{optionValue}</label>
<label htmlFor={id + index}>{optionLabel ?? optionValue}</label>
</div>
))}
<HelperText error={error}>{helperText}</HelperText>
Expand Down
24 changes: 14 additions & 10 deletions src/components/RegistrationForm/RegistrationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import IconButton from '#components/IconButton/IconButton';
import Visibility from '#src/icons/Visibility';
import VisibilityOff from '#src/icons/VisibilityOff';
import PasswordStrength from '#components/PasswordStrength/PasswordStrength';
import Checkbox from '#components/Checkbox/Checkbox';
import CustomRegisterField from '#components/CustomRegisterField/CustomRegisterField';
import FormFeedback from '#components/FormFeedback/FormFeedback';
import LoadingOverlay from '#components/LoadingOverlay/LoadingOverlay';
import Link from '#components/Link/Link';
Expand All @@ -20,16 +20,17 @@ import useToggle from '#src/hooks/useToggle';
import { addQueryParam } from '#src/utils/location';
import type { FormErrors } from '#types/form';
import type { RegistrationFormData, Consent } from '#types/account';
import type { ConsentFieldVariants } from '#src/services/inplayer.account.service';

type Props = {
onSubmit: React.FormEventHandler<HTMLFormElement>;
onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onConsentChange: React.ChangeEventHandler<HTMLInputElement>;
onConsentChange: (name: string, value: string) => void;
errors: FormErrors<RegistrationFormData>;
values: RegistrationFormData;
loading: boolean;
consentValues: Record<string, boolean>;
consentValues: Record<string, string>;
consentErrors: string[];
submitting: boolean;
canSubmit: boolean;
Expand Down Expand Up @@ -77,6 +78,7 @@ const RegistrationForm: React.FC<Props> = ({

return (
<form onSubmit={onSubmit} data-testid={testId('registration-form')} noValidate>
vvv
<h2 className={styles.title}>{t('registration.sign_up')}</h2>
{errors.form ? <FormFeedback variant="error">{errors.form}</FormFeedback> : null}
<TextField
Expand Down Expand Up @@ -113,17 +115,19 @@ const RegistrationForm: React.FC<Props> = ({
}
required
/>
{publisherConsents?.map((consent, index) => (
<Checkbox
key={index}
{publisherConsents?.map((consent) => (
<CustomRegisterField
key={consent.name}
type={consent.type as ConsentFieldVariants}
name={consent.name}
value={consent.value || ''}
options={consent.options}
label={formatConsentLabel(consent.label)}
placeholder={consent.placeholder}
value={consentValues[consent.name]}
required={consent.required}
error={consentErrors?.includes(consent.name)}
helperText={consentErrors?.includes(consent.name) ? t('registration.consent_required') : undefined}
required={consent.required}
checked={consentValues[consent.name] || false}
onChange={onConsentChange}
label={formatConsentLabel(consent.label)}
/>
))}
<Button
Expand Down
4 changes: 2 additions & 2 deletions src/components/TextField/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { RefObject } from 'react';
import React, { type RefObject, type ReactNode } from 'react';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

Expand All @@ -17,7 +17,7 @@ type InputOrTextAreaProps =

type Props = {
className?: string;
label?: string;
label?: ReactNode;
helperText?: React.ReactNode;
leftControl?: React.ReactNode;
rightControl?: React.ReactNode;
Expand Down
11 changes: 7 additions & 4 deletions src/containers/AccountModal/forms/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ const Registration = () => {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation('account');
const [consentValues, setConsentValues] = useState<Record<string, boolean>>({});
const [consentValues, setConsentValues] = useState<Record<string, string>>({});
const [consentErrors, setConsentErrors] = useState<string[]>([]);

const { data, isLoading: publisherConsentsLoading } = useQuery(['consents'], getPublisherConsents);
const publisherConsents = useMemo(() => data?.consents || [], [data]);

const handleChangeConsent = (event: React.ChangeEvent<HTMLInputElement>) => {
setConsentValues((current) => ({ ...current, [event.target.name]: event.target.checked }));
const handleChangeConsent = (name: string, value: string) => {
setConsentValues((current) => ({
...current,
[name]: value,
}));

// Clear the errors for any checkbox that's toggled
setConsentErrors((errors) => errors.filter((e) => e !== event.target.name));
setConsentErrors((errors) => errors.filter((e) => e !== name));
};

useEffect(() => {
Expand Down
77 changes: 57 additions & 20 deletions src/services/inplayer.account.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import InPlayer, { AccountData, Env, RegisterField, UpdateAccountData, FavoritesData, WatchHistory } from '@inplayer-org/inplayer.js';
import InPlayer, { AccountData, Env, RegisterField, UpdateAccountData, FavoritesData, WatchHistory, GetRegisterFieldOption } from '@inplayer-org/inplayer.js';
import i18next from 'i18next';

import type {
Expand Down Expand Up @@ -35,6 +35,16 @@ enum InPlayerEnv {
Daily = 'daily',
}

export enum ConsentFieldVariants {
INPUT = 'input',
GENERAL_SELECT = 'select',
COUNTRY_SELECT = 'country',
US_STATE_SELECT = 'us_state',
RADIO = 'radio',
CHECKBOX = 'checkbox',
DATE_PICKER = 'datepicker', // not implemented yet
}

export const setEnvironment = (config: Config) => {
const env: string = config.integrations?.jwp?.useSandbox ? InPlayerEnv.Development : InPlayerEnv.Production;
InPlayer.setConfig(env as Env);
Expand Down Expand Up @@ -132,16 +142,27 @@ export const updateCustomer: UpdateCustomer = async (customer) => {
}
};

export const getPublisherConsents: GetPublisherConsents = async (config) => {
export const getPublisherConsents: GetPublisherConsents<ConsentFieldVariants> = async (config) => {
try {
const { jwp } = config.integrations;
const { data } = await InPlayer.Account.getRegisterFields(jwp?.clientId || '');

const result: Consent[] = data?.collection.filter((field) => field.type === 'checkbox').map((consent) => formatPublisherConsents(consent));

return {
consents: [getTermsConsent(), ...result],
};
const result = data?.collection
// todo 1: update RegisterField.type: string to RegisterField.type: ConsentFieldVariants
// todo 2: implement DATE_PICKER at some point
.filter((field) => (field.type as ConsentFieldVariants) !== ConsentFieldVariants.DATE_PICKER && field.name !== 'email_confirmation')
.map((field) =>
formatPublisherConsents({
...field,
type: field.type as ConsentFieldVariants,
// todo 3: field.option type in SDK is incorrect, remove this line entirely after fixing that
options: field.options as unknown as GetRegisterFieldOption,
}),
);

const consents = [getTermsConsent(), ...result];

return { consents };
} catch {
throw new Error('Failed to fetch publisher consents.');
}
Expand Down Expand Up @@ -402,21 +423,37 @@ function formatAuth(auth: InPlayerAuthData): AuthData {
};
}

function formatPublisherConsents(consent: Partial<RegisterField>) {
return {
broadcasterId: 0,
enabledByDefault: false,
label: consent.label,
name: consent.name,
required: consent.required,
value: '',
version: '1',
} as Consent;
}

function getTermsConsent(): Consent {
const formatPublisherConsents = <T = string>({
type,
label,
placeholder,
name,
required,
default_value: defaultValue = '',
options,
}: Pick<RegisterField, 'label' | 'name' | 'required'> & {
type: T;
default_value?: string;
placeholder?: string;
options?: GetRegisterFieldOption;
}): Consent<T> => ({
type: type as T,
label,
placeholder,
name,
required,
defaultValue,
broadcasterId: 0,
value: '',
version: '1',
options,
});

function getTermsConsent() {
const termsUrl = '<a href="https://inplayer.com/legal/terms" target="_blank">Terms and Conditions</a>';

return formatPublisherConsents({
type: ConsentFieldVariants.CHECKBOX,
required: true,
name: 'terms',
label: i18next.t('account:registration.terms_consent', { termsUrl }),
Expand Down
Loading

0 comments on commit f7a5249

Please sign in to comment.