Merge pull request #17945 from edx/alasdair/LEARNER-4880-gdpr-modal-display
LEARNER-4880 added confirmation modal for account deletion
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable no-new */
|
||||
import { ReactRenderer } from 'ReactRenderer';
|
||||
import { StudentAccountDeletion } from './components/StudentAccountDeletion';
|
||||
|
||||
const maxWait = 60000;
|
||||
const interval = 50;
|
||||
const accountDeletionWrapperId = 'account-deletion-container';
|
||||
let currentWait = 0;
|
||||
|
||||
const wrapperRendered = setInterval(() => {
|
||||
const wrapper = document.getElementById(accountDeletionWrapperId);
|
||||
|
||||
if (wrapper) {
|
||||
clearInterval(wrapperRendered);
|
||||
new ReactRenderer({
|
||||
component: StudentAccountDeletion,
|
||||
selector: `#${accountDeletionWrapperId}`,
|
||||
componentName: 'StudentAccountDeletion',
|
||||
});
|
||||
}
|
||||
|
||||
currentWait += interval;
|
||||
|
||||
if (currentWait >= maxWait) {
|
||||
clearInterval(wrapperRendered);
|
||||
}
|
||||
}, interval);
|
||||
@@ -0,0 +1,56 @@
|
||||
/* globals gettext */
|
||||
/* eslint-disable react/no-danger, import/prefer-default-export */
|
||||
import React from 'react';
|
||||
import { Button } from '@edx/paragon/static';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
import StudentAccountDeletionModal from './StudentAccountDeletionModal';
|
||||
|
||||
export class StudentAccountDeletion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.closeDeletionModal = this.closeDeletionModal.bind(this);
|
||||
this.loadDeletionModal = this.loadDeletionModal.bind(this);
|
||||
this.state = { deletionModalOpen: false };
|
||||
}
|
||||
|
||||
closeDeletionModal() {
|
||||
this.setState({ deletionModalOpen: false });
|
||||
this.modalTrigger.focus();
|
||||
}
|
||||
|
||||
loadDeletionModal() {
|
||||
this.setState({ deletionModalOpen: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { deletionModalOpen } = this.state;
|
||||
const loseAccessText = StringUtils.interpolate(
|
||||
gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'),
|
||||
{
|
||||
htmlStart: '<a href="http://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate" target="_blank">',
|
||||
htmlEnd: '</a>',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="account-deletion-details">
|
||||
<p className="account-settings-header-subtitle">{ gettext('We’re sorry to see you go!') }</p>
|
||||
<p className="account-settings-header-subtitle">{ gettext('Please note: Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }</p>
|
||||
<p className="account-settings-header-subtitle">{ gettext('Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }</p>
|
||||
<p
|
||||
className="account-settings-header-subtitle"
|
||||
dangerouslySetInnerHTML={{ __html: loseAccessText }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
id="delete-account-btn"
|
||||
className={['btn-outline-primary']}
|
||||
label={gettext('Delete My Account')}
|
||||
inputRef={(input) => { this.modalTrigger = input; }}
|
||||
onClick={this.loadDeletionModal}
|
||||
/>
|
||||
{deletionModalOpen && <StudentAccountDeletionModal onClose={this.closeDeletionModal} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/* globals gettext */
|
||||
/* eslint-disable react/no-danger */
|
||||
import React from 'react';
|
||||
import 'whatwg-fetch';
|
||||
import PropTypes from 'prop-types';
|
||||
import Cookies from 'js-cookie';
|
||||
import { Button, Modal, Icon, InputText, StatusAlert } from '@edx/paragon/static';
|
||||
import StringUtils from 'edx-ui-toolkit/js/utils/string-utils';
|
||||
|
||||
class StudentAccountDeletionConfirmationModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.deleteAccount = this.deleteAccount.bind(this);
|
||||
this.handlePasswordInputChange = this.handlePasswordInputChange.bind(this);
|
||||
this.passwordFieldValidation = this.passwordFieldValidation.bind(this);
|
||||
this.state = {
|
||||
password: '',
|
||||
passwordSubmitted: false,
|
||||
passwordValid: true,
|
||||
validationMessage: '',
|
||||
validationErrorDetails: '',
|
||||
accountQueuedForDeletion: false,
|
||||
responseError: false,
|
||||
};
|
||||
}
|
||||
|
||||
addUserToDeletionQueue() {
|
||||
// TODO: Add API call to add user to account deletion queue
|
||||
|
||||
this.setState({
|
||||
accountQueuedForDeletion: true,
|
||||
responseError: false,
|
||||
passwordSubmitted: false,
|
||||
validationMessage: '',
|
||||
validationErrorDetails: '',
|
||||
});
|
||||
}
|
||||
|
||||
deleteAccount() {
|
||||
const { password } = this.state;
|
||||
|
||||
this.setState({ passwordSubmitted: true });
|
||||
|
||||
fetch('/accounts/verify_password', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': Cookies.get('csrftoken'),
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
return this.addUserToDeletionQueue();
|
||||
}
|
||||
return this.failedSubmission(response);
|
||||
}).catch(error => this.failedSubmission(error));
|
||||
}
|
||||
|
||||
failedSubmission(error) {
|
||||
const { status } = error;
|
||||
const title = status === 403 ? gettext('Password is incorrect') : gettext('Unable to delete account');
|
||||
const body = status === 403 ? gettext('Please re-enter your password.') : gettext('Sorry, there was an error trying to process your request. Please try again later.');
|
||||
|
||||
this.setState({
|
||||
passwordSubmitted: false,
|
||||
responseError: true,
|
||||
passwordValid: false,
|
||||
validationMessage: title,
|
||||
validationErrorDetails: body,
|
||||
});
|
||||
}
|
||||
|
||||
handlePasswordInputChange(value) {
|
||||
this.setState({ password: value });
|
||||
}
|
||||
|
||||
passwordFieldValidation(value) {
|
||||
let feedback = { passwordValid: true };
|
||||
|
||||
if (value.length < 1) {
|
||||
feedback = {
|
||||
passwordValid: false,
|
||||
validationMessage: gettext('A Password is required'),
|
||||
validationErrorDetails: '',
|
||||
};
|
||||
}
|
||||
|
||||
this.setState(feedback);
|
||||
}
|
||||
|
||||
renderConfirmationModal() {
|
||||
const {
|
||||
passwordValid,
|
||||
password,
|
||||
passwordSubmitted,
|
||||
responseError,
|
||||
validationErrorDetails,
|
||||
validationMessage,
|
||||
} = this.state;
|
||||
const { onClose } = this.props;
|
||||
const loseAccessText = StringUtils.interpolate(
|
||||
gettext('You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, follow the instructions for {htmlStart}printing or downloading a certificate{htmlEnd}.'),
|
||||
{
|
||||
htmlStart: '<a href="http://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate" target="_blank">',
|
||||
htmlEnd: '</a>',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="delete-confirmation-wrapper">
|
||||
<Modal
|
||||
title={gettext('Are you sure?')}
|
||||
renderHeaderCloseButton={false}
|
||||
onClose={onClose}
|
||||
aria-live="polite"
|
||||
open
|
||||
body={(
|
||||
<div>
|
||||
{responseError &&
|
||||
<StatusAlert
|
||||
dialog={(
|
||||
<div className="modal-alert">
|
||||
<div className="icon-wrapper">
|
||||
<Icon id="delete-confirmation-body-error-icon" className={['fa', 'fa-exclamation-circle']} />
|
||||
</div>
|
||||
<div className="alert-content">
|
||||
<h3 className="alert-title">{ validationMessage }</h3>
|
||||
<p>{ validationErrorDetails }</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
alertType="danger"
|
||||
dismissible={false}
|
||||
open
|
||||
/>
|
||||
}
|
||||
|
||||
<StatusAlert
|
||||
dialog={(
|
||||
<div className="modal-alert">
|
||||
<div className="icon-wrapper">
|
||||
<Icon id="delete-confirmation-body-warning-icon" className={['fa', 'fa-exclamation-triangle']} />
|
||||
</div>
|
||||
<div className="alert-content">
|
||||
<h3 className="alert-title">{ gettext('You have selected “Delete my account.” Deletion of your account and personal data is permanent and cannot be undone. EdX will not be able to recover your account or the data that is deleted.') }</h3>
|
||||
<p>{ gettext('If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.') }</p>
|
||||
<p dangerouslySetInnerHTML={{ __html: loseAccessText }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
dismissible={false}
|
||||
open
|
||||
/>
|
||||
<p className="next-steps">{ gettext('If you still wish to continue and delete your account, please enter your account password:') }</p>
|
||||
<InputText
|
||||
name="confirm-password"
|
||||
label="Password"
|
||||
type="password"
|
||||
className={['confirm-password-input']}
|
||||
onBlur={this.passwordFieldValidation}
|
||||
isValid={passwordValid}
|
||||
validationMessage={validationMessage}
|
||||
onChange={this.handlePasswordInputChange}
|
||||
autoComplete="new-password"
|
||||
themes={['danger']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
closeText={gettext('Cancel')}
|
||||
buttons={[
|
||||
<Button
|
||||
label={gettext('Yes, Delete')}
|
||||
onClick={this.deleteAccount}
|
||||
disabled={password.length === 0 || passwordSubmitted}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSuccessModal() {
|
||||
const { onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className="delete-success-wrapper">
|
||||
<Modal
|
||||
title={gettext('We\'re sorry to see you go! Your account will be deleted shortly.')}
|
||||
renderHeaderCloseButton={false}
|
||||
body={gettext('Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.')}
|
||||
onClose={onClose}
|
||||
aria-live="polite"
|
||||
open
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { accountQueuedForDeletion } = this.state;
|
||||
|
||||
return accountQueuedForDeletion ? this.renderSuccessModal() : this.renderConfirmationModal();
|
||||
}
|
||||
}
|
||||
|
||||
StudentAccountDeletionConfirmationModal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
StudentAccountDeletionConfirmationModal.defaultProps = {
|
||||
onClose: () => {},
|
||||
};
|
||||
|
||||
export default StudentAccountDeletionConfirmationModal;
|
||||
@@ -22,17 +22,17 @@
|
||||
contactEmail,
|
||||
allowEmailChange,
|
||||
socialPlatforms,
|
||||
|
||||
syncLearnerProfileData,
|
||||
enterpriseName,
|
||||
enterpriseReadonlyAccountFields,
|
||||
edxSupportUrl,
|
||||
extendedProfileFields
|
||||
extendedProfileFields,
|
||||
enableGDPRFlag
|
||||
) {
|
||||
var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
|
||||
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
|
||||
showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField,
|
||||
emailFieldView, socialFields, platformData,
|
||||
emailFieldView, socialFields, accountDeletionFields, platformData,
|
||||
aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView,
|
||||
fullNameFieldData, emailFieldData, countryFieldData, additionalFields, fieldItem;
|
||||
|
||||
@@ -291,6 +291,18 @@
|
||||
}
|
||||
aboutSectionsData.push(socialFields);
|
||||
|
||||
// Add account deletion fields
|
||||
if (enableGDPRFlag) {
|
||||
accountDeletionFields = {
|
||||
title: gettext('Delete My Account'),
|
||||
fields: [],
|
||||
// Used so content can be rendered external to Backbone
|
||||
domHookId: 'account-deletion-container'
|
||||
};
|
||||
aboutSectionsData.push(accountDeletionFields);
|
||||
}
|
||||
|
||||
|
||||
// set TimeZoneField to listen to CountryField
|
||||
getUserField = function(list, search) {
|
||||
return _.find(list, function(field) {
|
||||
|
||||
@@ -535,6 +535,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
.account-deletion-details {
|
||||
.btn-outline-primary {
|
||||
@extend %ui-clear-button;
|
||||
|
||||
// set styles
|
||||
@extend %btn-pl-default-base;
|
||||
|
||||
@include font-size(18);
|
||||
|
||||
border: 1px solid $blue;
|
||||
color: $blue;
|
||||
padding: 11px 14px;
|
||||
line-height: normal;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.paragon__modal-open {
|
||||
overflow-y: scroll;
|
||||
color: $dark-gray;
|
||||
|
||||
.paragon__modal-title {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.paragon__modal-body {
|
||||
line-height: 1.5;
|
||||
|
||||
.alert-title {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.paragon__alert-warning {
|
||||
color: $dark-gray;
|
||||
}
|
||||
|
||||
.next-steps {
|
||||
margin-bottom: 10px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.confirm-password-input {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.paragon__btn:not(.cancel-btn) {
|
||||
@extend %btn-primary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-alert {
|
||||
display: flex;
|
||||
|
||||
.icon-wrapper {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.alert-content {
|
||||
.alert-title {
|
||||
color: $dark-gray;
|
||||
margin-bottom: 10px;
|
||||
font: {
|
||||
size: 1rem;
|
||||
weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue-u1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-confirmation-wrapper {
|
||||
.paragon__modal-footer {
|
||||
.paragon__btn-outline-primary {
|
||||
@extend %ui-clear-button;
|
||||
|
||||
// set styles
|
||||
@extend %btn-pl-default-base;
|
||||
|
||||
@include margin-left(25px);
|
||||
|
||||
border-color: $blue;
|
||||
color: $blue;
|
||||
padding: 11px 14px;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
%>
|
||||
|
||||
<!--<%namespace name='static' file='/static_content.html'/>-->
|
||||
# GDPR Flag
|
||||
from openedx.features.course_experience import ENABLE_GDPR_COMPAT_FLAG
|
||||
%>
|
||||
|
||||
<%inherit file="/main.html" />
|
||||
<%def name="online_help_token()"><% return "learneraccountsettings" %></%def>
|
||||
@@ -25,9 +26,9 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
|
||||
% endif
|
||||
|
||||
<div class="wrapper-account-settings"></div>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='style-course'/>
|
||||
<link type="text/css" rel="stylesheet" href="${STATIC_URL}paragon/static/paragon.min.css">
|
||||
</%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
@@ -44,7 +45,8 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
|
||||
enterpriseName = '${ enterprise_name | n, js_escaped_string }',
|
||||
enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json },
|
||||
edxSupportUrl = '${ edx_support_url | n, js_escaped_string }',
|
||||
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json };
|
||||
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json },
|
||||
enableGDPRFlag = ${ ENABLE_GDPR_COMPAT_FLAG.is_enabled_without_course_context() | n, dump_js_escaped_json };
|
||||
|
||||
AccountSettingsFactory(
|
||||
fieldsData,
|
||||
@@ -63,7 +65,11 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str
|
||||
enterpriseName,
|
||||
enterpriseReadonlyAccountFields,
|
||||
edxSupportUrl,
|
||||
extendedProfileFields
|
||||
extendedProfileFields,
|
||||
enableGDPRFlag
|
||||
);
|
||||
</%static:require_module>
|
||||
% if ENABLE_GDPR_COMPAT_FLAG.is_enabled_without_course_context():
|
||||
<%static:webpack entry="StudentAccountDeletionInitializer"></%static:webpack>
|
||||
% endif
|
||||
</%block>
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (section.domHookId) { %>
|
||||
<div id="<%- section.domHookId %>"></div>
|
||||
<% } %>
|
||||
|
||||
<div class="account-settings-section-body <%- tabName %>-section-body">
|
||||
<div class="ui-loading-error is-hidden">
|
||||
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"integrity": "sha512-MIrtCbJj7CZdW+FQ1hLaNcbqbgcoalxIRobBfgYiLa3oaBnmMh3IkwsYUsYz4hjvzXrUQvL+FsJqq/RB8SAZlg==",
|
||||
"requires": {
|
||||
"@edx/edx-bootstrap": "0.4.3",
|
||||
"@edx/paragon": "2.5.6",
|
||||
"@edx/paragon": "2.6.4",
|
||||
"classnames": "2.2.5",
|
||||
"prop-types": "15.6.1",
|
||||
"universal-cookie": "2.1.2"
|
||||
@@ -46,9 +46,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "2.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.5.6.tgz",
|
||||
"integrity": "sha512-ASKTOWTZvBUo8ev2SJ9apWBa7u7UKc3UNNdGlqgBzLzj2hNVJ2urxrgATaa1LaMazT23gMZkZM5iKCToQE5Pvw==",
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.6.4.tgz",
|
||||
"integrity": "sha512-E3n4fzTv2pzhCwhbTfmc7roJ7lEaEqetIRls7xK4U1dVvWW+yra4cyQioLkU30fNc8p8aA3Li3P9zstkqBnq6w==",
|
||||
"requires": {
|
||||
"@edx/edx-bootstrap": "1.0.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"dependencies": {
|
||||
"@edx/cookie-policy-banner": "^1.1.3",
|
||||
"@edx/edx-bootstrap": "0.4.3",
|
||||
"@edx/paragon": "2.5.6",
|
||||
"@edx/paragon": "2.6.4",
|
||||
"@edx/studio-frontend": "1.7.3",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-loader": "6.4.1",
|
||||
|
||||
@@ -31,6 +31,8 @@ module.exports = {
|
||||
PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx',
|
||||
EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx',
|
||||
PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx',
|
||||
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
|
||||
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
|
||||
|
||||
// Learner Dashboard
|
||||
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',
|
||||
|
||||
Reference in New Issue
Block a user