diff --git a/.env b/.env index 7b5b903..3b40baf 100644 --- a/.env +++ b/.env @@ -26,6 +26,7 @@ STUDIO_BASE_URL='' SUPPORT_URL='' USER_INFO_COOKIE_NAME='' ENABLE_COPPA_COMPLIANCE='' +ENABLE_DOB_UPDATE='' MARKETING_EMAILS_OPT_IN='' APP_ID= MFE_CONFIG_API_URL= diff --git a/.env.development b/.env.development index 9456202..40e4c89 100644 --- a/.env.development +++ b/.env.development @@ -27,6 +27,7 @@ STUDIO_BASE_URL='' SUPPORT_URL='http://localhost:18000/support' USER_INFO_COOKIE_NAME='edx-user-info' ENABLE_COPPA_COMPLIANCE='' +ENABLE_DOB_UPDATE='' MARKETING_EMAILS_OPT_IN='' APP_ID= MFE_CONFIG_API_URL= diff --git a/.env.test b/.env.test index ee8e1b6..7b20d23 100644 --- a/.env.test +++ b/.env.test @@ -26,6 +26,7 @@ STUDIO_BASE_URL='' SUPPORT_URL='http://localhost:18000/support' USER_INFO_COOKIE_NAME='edx-user-info' ENABLE_COPPA_COMPLIANCE='' +ENABLE_DOB_UPDATE='' MARKETING_EMAILS_OPT_IN='' APP_ID= MFE_CONFIG_API_URL= diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 8163063..80d4bd2 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -37,11 +37,13 @@ import ThirdPartyAuth from './third-party-auth'; import BetaLanguageBanner from './BetaLanguageBanner'; import EmailField from './EmailField'; import OneTimeDismissibleAlert from './OneTimeDismissibleAlert'; +import DOBModal from './DOBForm'; import { YEAR_OF_BIRTH_OPTIONS, EDUCATION_LEVELS, GENDER_OPTIONS, COUNTRY_WITH_STATES, + COPPA_COMPLIANCE_YEAR, getStatesList, } from './data/constants'; import { fetchSiteLanguages } from './site-language'; @@ -494,13 +496,37 @@ class AccountSettingsPage extends React.Component { ); const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0; + + // if user is under 13 and does not have cookie set + const shouldUpdateDOB = ( + getConfig().ENABLE_COPPA_COMPLIANCE + && getConfig().ENABLE_DOB_UPDATE + && this.props.formValues.year_of_birth.toString() >= COPPA_COMPLIANCE_YEAR.toString() + && !localStorage.getItem('submittedDOB') + ); return ( <> + { shouldUpdateDOB + && ( + + )}
{ this.props.mostRecentVerifiedName && this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName) } + {localStorage.getItem('submittedDOB') + && ( + + )}

{this.props.intl.formatMessage(messages['account.settings.section.account.information'])} diff --git a/src/account-settings/AccountSettingsPage.messages.jsx b/src/account-settings/AccountSettingsPage.messages.jsx index f5994d8..21b7508 100644 --- a/src/account-settings/AccountSettingsPage.messages.jsx +++ b/src/account-settings/AccountSettingsPage.messages.jsx @@ -266,6 +266,46 @@ const messages = defineMessages({ defaultMessage: 'Select a year of birth', description: 'Option for empty value on account settings year of birth field.', }, + 'account.settings.field.dob.month': { + id: 'account.settings.field.dob.month', + defaultMessage: 'Month', + description: 'Label for account settings month of birth field.', + }, + 'account.settings.field.dob.year': { + id: 'account.settings.field.dob.year', + defaultMessage: 'Year', + description: 'Label for account settings year of birth field.', + }, + 'account.settings.field.dob.form.button': { + id: 'account.settings.field.dob.form.button', + defaultMessage: 'Please confirm your date of birth', + description: 'Message to prompt user to enter dob', + }, + 'account.settings.field.dob.form.title': { + id: 'account.settings.field.dob.form.title', + defaultMessage: 'Confirm your date of birth', + description: 'Title of DOB form', + }, + 'account.settings.field.dob.form.help.text': { + id: 'account.settings.field.dob.form.help.text', + defaultMessage: 'We ask for birthday information to ensure that underage people aren’t using edX.', + description: 'Help text for DOB form', + }, + 'account.settings.field.dob.form.success': { + id: 'account.settings.field.dob.form.success', + defaultMessage: 'Thank you for entering your birthday information.', + description: 'Title of banner when date of birth is successfully entered', + }, + 'account.settings.field.month_of_birth.options.empty': { + id: 'account.settings.field.month_of_birth.options.empty', + defaultMessage: 'Select a month of birth', + description: 'Option for empty value on account settings month of birth field.', + }, + 'account.settingsfield.dob.error.general': { + id: 'account.settingsfield.dob.error.general', + defaultMessage: 'A technical error occurred. Please try again.', + description: 'Generic error message.', + }, 'account.settings.field.country': { id: 'account.settings.field.country', defaultMessage: 'Country', diff --git a/src/account-settings/DOBForm.jsx b/src/account-settings/DOBForm.jsx new file mode 100644 index 0000000..d93828a --- /dev/null +++ b/src/account-settings/DOBForm.jsx @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { + Form, StatefulButton, ModalDialog, ActionRow, useToggle, Button, +} from '@edx/paragon'; +import React, { useCallback, useEffect } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import messages from './AccountSettingsPage.messages'; +import { YEAR_OF_BIRTH_OPTIONS } from './data/constants'; +import { editableFieldSelector } from './data/selectors'; +import { saveSettingsReset } from './data/actions'; + +function DOBModal(props) { + const { + saveState, + error, + onSubmit, + intl, + } = props; + + const dispatch = useDispatch(); + + // eslint-disable-next-line no-unused-vars + const [isOpen, open, close, toggle] = useToggle(true, {}); + + const handleSubmit = (e) => { + e.preventDefault(); + + const month = e.target.month.value; + const year = e.target.year.value; + const data = month !== '' && year !== '' ? [{ field_name: 'DOB', field_value: `${year}-${month}` }] : []; + onSubmit('extended_profile', data); + }; + + const handleComplete = useCallback(() => { + localStorage.setItem('submittedDOB', 'true'); + close(); + dispatch(saveSettingsReset()); + }, [dispatch, close]); + + const handleClose = useCallback(() => { + close(); + dispatch(saveSettingsReset()); + }, [dispatch, close]); + + function renderErrors() { + if (saveState === 'error' || error) { + return ( + + {intl.formatMessage(messages['account.settingsfield.dob.error.general'])} + + ); + } + return null; + } + + useEffect(() => { + if (saveState === 'complete' && isOpen) { + handleComplete(); + } + }, [handleComplete, saveState, isOpen]); + + return ( + <> + + +
+ + + + {intl.formatMessage(messages['account.settings.field.dob.form.title'])} + + + + +

{intl.formatMessage(messages['account.settings.field.dob.form.help.text'])}

+ + + {intl.formatMessage(messages['account.settings.field.dob.month'])} + + + {[...Array(12).keys()].map(month => ( + + ))} + + + + + {intl.formatMessage(messages['account.settings.field.dob.year'])} + + + {YEAR_OF_BIRTH_OPTIONS.map(year => ( + + ))} + + + {renderErrors()} +
+ + + + + Cancel + + + + + +
+
+ + ); +} + +DOBModal.propTypes = { + saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']), + error: PropTypes.string, + onSubmit: PropTypes.func.isRequired, + intl: intlShape.isRequired, +}; + +DOBModal.defaultProps = { + saveState: undefined, + error: undefined, +}; + +export default connect(editableFieldSelector)(injectIntl(DOBModal)); diff --git a/src/account-settings/data/constants.js b/src/account-settings/data/constants.js index e905754..d94becf 100644 --- a/src/account-settings/data/constants.js +++ b/src/account-settings/data/constants.js @@ -10,6 +10,11 @@ export const YEAR_OF_BIRTH_OPTIONS = (() => { return years.reverse(); })(); +export const COPPA_COMPLIANCE_YEAR = (() => { + const currentYear = new Date().getFullYear(); + return currentYear - 13; +})(); + export const EDUCATION_LEVELS = [ '', 'p', diff --git a/src/index.jsx b/src/index.jsx index 4975935..fe60d86 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -66,6 +66,7 @@ initialize({ ENABLE_DEMOGRAPHICS_COLLECTION: (process.env.ENABLE_DEMOGRAPHICS_COLLECTION || false), DEMOGRAPHICS_BASE_URL: process.env.DEMOGRAPHICS_BASE_URL, ENABLE_COPPA_COMPLIANCE: (process.env.ENABLE_COPPA_COMPLIANCE || false), + ENABLE_DOB_UPDATE: (process.env.ENABLE_DOB_UPDATE || false), MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false), }, 'App loadConfig override handler'); },