Skip to content

Commit

Permalink
feat: create a Modal to inform the update uf terms of service
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoa committed Oct 16, 2024
1 parent 1adca56 commit 13ea862
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 3 deletions.
56 changes: 56 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ This component requires that the following environment variable be set by the co
* ``LMS_BASE_URL`` - The URL of the LMS of your Open edX instance.
* ``LOGO_TRADEMARK_URL`` - This is a URL to a logo for use in the footer. This is a different environment variable than ``LOGO_URL`` (used in frontend-component-header) to accommodate sites that would like to have additional trademark information on a logo in the footer, such as a (tm) or (r) symbol.

Optionally, use the following variables to configure the Terms of Service Modal for the MFEs:

* ``MODAL_UPDATE_TERMS_OF_SERVICE`` - Object that reppresent the text and checkbox configured for the TOS Modal
* ``PRIVACY_POLICY_URL`` - The URL for the privacy policy.
* ``TERMS_OF_SERVICE_URL`` - The URL for the terms of service.
* ``TOS_AND_HONOR_CODE`` - The URL for the honor code.


Installation
============

Expand Down Expand Up @@ -93,6 +101,54 @@ This library has the following exports:
language from its dropdown.
* supportedLanguages: An array of objects representing available languages. See example below for object shape.

Terms of Service Modal
=======================

The Terms of Service Modal allows configuring a modal that prompts users to accept updated terms and conditions,
including Data Authorization, Terms of Service, and/or an Honor Code.

To configure this modal, use either the MFE build-time configuration (via ``.env``, ``.env.config.js``) or the
runtime MFE Config API to set the MODAL_UPDATE_TERMS_OF_SERVICE object. Example:

.. code-block:: python
MFE_CONFIG["MODAL_UPDATE_TERMS_OF_SERVICE"] = {
"date_iso_8601": "2025-06-08",
"title": {
"en": "English modal title",
"pt-pt": "Portuguese modal title"
},
"body": {
"en": "English modal text",
"pt-pt": "Portuguese modal text"
},
"data_authorization": true,
"terms_of_service": true,
"honor_code": true,
}
Where:
* **date_iso_8601** *(required)*: This is a required field representing the date of the terms of service update
in ISO 8601 format.
It is used to track whether the user has accepted the new terms since the last update.
* **title** *(optional)*: It is an object that provides the modal title text for different languages.
* **body** *(optional)*: It is an object that provides the body content of the modal for different languages.
* **data_authorization** *(optional)*: Boolean that determines whether the Privacy Policy checkbox should be
displayed in the modal.
* **terms_of_service** *(optional)*: Boolean that controls whether the Terms of Service checkbox should be
shown in the modal.
* **honor_code** *(optional)*: Boolean that specifies whether the Honor Code checkbox should be displayed
in the modal.

The modal conditions:

* The modal will be displayed if the user has not yet accepted the latest terms and conditions as defined
by date_iso_8601.
* If any of the optional fields (data_authorization, terms_of_service, honor_code) are not specified, the
corresponding checkboxes will not appear in the modal.
The modal is multilingual, and the content for both the title and body can be customized for different
locales using language keys like en (English), pt-pt (Portuguese), etc.

Plugin
======
The footer can be replaced using using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
Expand Down
13 changes: 12 additions & 1 deletion src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import {
APP_CONFIG_INITIALIZED, ensureConfig, getConfig, subscribe,
} from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';

import { hydrateAuthenticatedUser } from '@edx/frontend-platform/auth';
import messages from './Footer.messages';
import LanguageSelector from './LanguageSelector';
import FooterLinks from './footer-links/FooterNavLinks';
import FooterSocial from './footer-links/FooterSocialLinks';
import parseEnvSettings from '../utils/parseData';
import ModalToS from './modal-tos';

ensureConfig([
'LMS_BASE_URL',
Expand Down Expand Up @@ -113,6 +117,9 @@ class SiteFooter extends React.Component {
</section>
<AdditionalLogosSection />
<FooterCopyrightSection intl={intl} />
{
config.MODAL_UPDATE_TERMS_OF_SERVICE && <ModalToS />
}
</footer>
);
}
Expand All @@ -136,5 +143,9 @@ SiteFooter.defaultProps = {
supportedLanguages: [],
};

subscribe(APP_CONFIG_INITIALIZED, async () => {
await hydrateAuthenticatedUser();
});

export default injectIntl(SiteFooter);
export { EVENT_NAMES };
171 changes: 171 additions & 0 deletions src/components/modal-tos/ModalToS.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useEffect, useState } from 'react';

import { convertKeyNames, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { FormattedMessage, getLocale, injectIntl } from '@edx/frontend-platform/i18n';
import {
Button, Form, Hyperlink, ModalDialog, useToggle, useCheckboxSetValues,
ActionRow,
} from '@openedx/paragon';

import { getUserTOSPreference, updateUserTOSPreference } from './data/api';
import { CAMEL_CASE_KEYS } from './data/constants';
import parseEnvSettings from '../../utils/parseData';

const createTOSLink = (chunks, url) => (
<Hyperlink
destination={url}
target="_blank"
>{chunks}
</Hyperlink>
);

const ModalToS = () => {
const [tosPreference, setTosPreference] = useState(undefined);
const [isOpen, open, close] = useToggle(false);

const {
MODAL_UPDATE_TERMS_OF_SERVICE,
PRIVACY_POLICY_URL,
SITE_NAME,
TERMS_OF_SERVICE_URL,
TOS_AND_HONOR_CODE,
} = getConfig();

const modalSettings = parseEnvSettings(MODAL_UPDATE_TERMS_OF_SERVICE) || MODAL_UPDATE_TERMS_OF_SERVICE || {};
const {
body = {},
title = {},
dateIso8601,
dataAuthorization = false,
honorCode = false,
termsOfService = false,
} = convertKeyNames(modalSettings, CAMEL_CASE_KEYS);

const {
dateJoined,
username,
} = getAuthenticatedUser();

const lang = getLocale() || 'en';
const tosKey = `update_terms_of_service_${dateIso8601?.replaceAll('-', '_')}`;
const [checkboxValues, { add, remove }] = useCheckboxSetValues([]);

useEffect(() => {
if (username && dateIso8601) {
getUserTOSPreference(username, tosKey).then(userTos => {
setTosPreference(userTos);
if (userTos === null) {
open();
}
});
}
}, [dateIso8601, tosKey, username, open]);

const setAcceptance = () => {
updateUserTOSPreference(username, tosKey);
close();
};

const numCheckBox = [dataAuthorization, termsOfService, honorCode]
.reduce((prev, curr) => (curr ? prev + 1 : prev), 0);

const handleChange = e => {
if (e.target.checked) {
add(e.target.value);
} else {
remove(e.target.value);
}
};

if (tosPreference || !dateIso8601 || !username
|| new Date(dateIso8601) < new Date(dateJoined)) {
return null;
}

return (
<ModalDialog
title="Modal Terms of Service"
isBlocking
isOpen={isOpen}
onClose={close}
hasCloseButton={false}
>
{title[lang] && (
<ModalDialog.Header>
<ModalDialog.Title>
{title[lang]}
</ModalDialog.Title>
</ModalDialog.Header>
)}
<ModalDialog.Body>
{body[lang]}
<Form className="my-4">
<Form.CheckboxSet
name="TOSCheckbox"
onChange={handleChange}
value={checkboxValues}
>
{dataAuthorization
&& (
<Form.Checkbox value="dataAuthorization">
<FormattedMessage
id="modalToS.dataAuthorization.checkbox.label"
description="The label for the data authorization checkbox inside the TOS modal."
defaultMessage="I have read and understood the&nbsp;<a>Privacy Policy</a>"
values={{
a: chunks => createTOSLink(chunks, PRIVACY_POLICY_URL),
}}
/>
</Form.Checkbox>
)}
{termsOfService
&& (
<Form.Checkbox value="termsOfService">
<FormattedMessage
id="modalToS.termsOfService.checkbox.label"
description="The label for the terms of service checkbox inside the TOS modal."
defaultMessage="I agree to the {platformName}&nbsp;<a>Terms of Service</a>"
values={{
a: chunks => createTOSLink(chunks, TERMS_OF_SERVICE_URL),
platformName: SITE_NAME,
}}
/>
</Form.Checkbox>
)}
{honorCode
&& (
<Form.Checkbox value="honorCode">
<FormattedMessage
id="modalToS.honorCode.checkbox.label"
description="The label for the honor code checkbox inside the TOS modal."
defaultMessage="I agree to the {platformName}&nbsp;<a>Honor Code</a>"
values={{
a: chunks => createTOSLink(chunks, TOS_AND_HONOR_CODE),
platformName: SITE_NAME,
}}
/>
</Form.Checkbox>
)}
</Form.CheckboxSet>
</Form>
<ActionRow isStacked>
<Button
variant="primary"
disabled={numCheckBox !== checkboxValues.length}
onClick={setAcceptance}
data-testid="modalToSButton"
>
<FormattedMessage
id="modalToS.acceptance.button"
description="The label for the button inside the TOS modal."
defaultMessage="Accept new terms of service"
/>
</Button>
</ActionRow>
</ModalDialog.Body>
</ModalDialog>
);
};

export default injectIntl(ModalToS);
Loading

0 comments on commit 13ea862

Please sign in to comment.