Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create a Modal to inform the update of terms of service #9

Merged
merged 3 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 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,46 @@ 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint": "fedx-scripts eslint --ext .js --ext .jsx . --fix",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"start:with-theme": "paragon install-theme && npm start && npm install",
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 @@ -134,6 +138,9 @@ class SiteFooter extends React.Component {
</div>
</section>
<AdditionalLogosSection />
{
config.MODAL_UPDATE_TERMS_OF_SERVICE && <ModalToS />
}
</footer>
);
}
Expand All @@ -157,5 +164,9 @@ SiteFooter.defaultProps = {
supportedLanguages: [],
};

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

export default injectIntl(SiteFooter);
export { EVENT_NAMES };
173 changes: 173 additions & 0 deletions src/components/modal-tos/ModalToS.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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,
useWindowSize,
} 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 { width } = useWindowSize();
const checkboxLabelStyle = (width < 768) ? 'd-inline-block' : null;
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" labelClassName={checkboxLabelStyle}>
<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" labelClassName={checkboxLabelStyle}>
<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" labelClassName={checkboxLabelStyle}>
<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
Loading