@selfcommunity/react-ui
Version:
React UI Components to integrate a Community created with SelfCommunity Platform.
307 lines (302 loc) • 18.7 kB
JavaScript
import { __rest } from "tslib";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { forwardRef, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useThemeProps } from '@mui/system';
import { styled } from '@mui/material/styles';
import { Alert, Button, Checkbox, FormControlLabel, Typography } from '@mui/material';
import classNames from 'classnames';
import Icon from '@mui/material/Icon';
import { LegalPageService, UserService } from '@selfcommunity/api-services';
import { arraysEqual, capitalize, Logger } from '@selfcommunity/utils';
import { SCPreferences, SCUserContext, useSCPreferences } from '@selfcommunity/react-core';
import ConsentSolutionSwitch from '../../shared/ConsentSolutionSwitch';
import { SCOPE_SC_UI } from '../../constants/Errors';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import Slide from '@mui/material/Slide';
import { LEGAL_POLICIES } from './../../constants/LegalPolicies';
import ConsentSolutionSkeleton from './Skeleton';
import { getDocumentBody, isDocumentApproved, isEmptyDocumentBody } from '../../utils/legalPages';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { elementScrollTo } from 'seamless-scroll-polyfill';
import AccountDataPortability from '../AccountDataPortability';
import { PREFIX } from './constants';
const classes = {
root: `${PREFIX}-root`,
title: `${PREFIX}-title`,
titleBack: `${PREFIX}-title-back`,
content: `${PREFIX}-content`,
consent: `${PREFIX}-consent`,
consentSwitch: `${PREFIX}-consent-switch`,
actions: `${PREFIX}-actions`,
backButton: `${PREFIX}-back-button`,
nextButton: `${PREFIX}-next-button`,
closeButton: `${PREFIX}-close-button`,
confirmDeleteAccountButton: `${PREFIX}-confirm-delete-account-button`,
logoutAccountButton: `${PREFIX}-logout-account-button`,
deleteAccountButton: `${PREFIX}-delete-account-button`,
dataPortability: `${PREFIX}-data-portability`,
dataPortabilityCheck: `${PREFIX}-download-data-portability-check`,
alertAcceptDocument: `${PREFIX}-alert-accept-document`,
acceptConditions: `${PREFIX}-accept-conditions`
};
const Root = styled(Dialog, {
name: PREFIX,
slot: 'Root'
})(() => ({}));
/**
* Translations
*/
const messages = defineMessages({
deleteAccountDpSectionInfo: {
id: 'ui.consentSolution.deleteAccountDpSectionInfo',
defaultMessage: 'ui.consentSolution.deleteAccountDpSectionInfo'
},
deleteAccountDpSectionTitle: {
id: 'ui.consentSolution.deleteAccountDpSectionTitle',
defaultMessage: 'ui.consentSolution.deleteAccountDpSectionTitle'
}
});
/**
* Dialog default transition
*/
const DialogTransition = forwardRef(function Transition(props, ref) {
return _jsx(Slide, Object.assign({ direction: "up", ref: ref }, props));
});
/**
* Dialog views
*/
export const ACCEPT_VIEW = 'accept';
export const REJECTION_VIEW = 'rejection';
export const CONFIRM_DELETE_ACCOUNT = 'delete_account';
/**
* > API documentation for the Community-JS ConsentSolution component. Learn about the available props and the CSS API.
*
#### Import
```jsx
import {ConsentSolution} from '@selfcommunity/react-ui';
```
#### Component Name
The name `SCConsentSolution` can be used when providing style overrides in the theme.
#### CSS
|Rule Name|Global class|Description|
|---|---|---|
|root|.SCConsentSolution-root|Styles applied to the root element.|
|title|.SCConsentSolution-title|Styles applied to the title element.|
|titleBack|.SCConsentSolution-title-back|Styles applied to the title with the back button element.|
|content|.SCConsentSolution-content|Styles applied to the content element section.|
|consent|.SCConsentSolution-consent|Styles applied to the consent element section.|
|consentSwitch|.SCConsentSolution-consent-switch|Styles applied to the switch element. |
|alertAcceptDocument|.SCConsentSolution-alert-accept-document|Styles applied to the alert box in the consent section.|nt.|
|actions|.SCConsentSolution-actions|Styles applied to the actions section.|
|backButton|.SCConsentSolution-back-button|Styles applied to the back button in the title and action sections.|
|nextButton|.SCConsentSolution-next-button|Styles applied to the next button in the actions section.|
|closeButton|.SCConsentSolution-close-button|Styles applied to the close button in the actions section.|
|confirmDeleteAccountButton|.SCConsentSolution-confirm-delete-account-button|Styles applied to the confirm delete account button in the rejection section.|
|logoutAccountButton|.SCConsentSolution-logout-account-button|Styles applied to the exit account button in the rejection section.|
|deleteAccountButton|.SCConsentSolution-delete-account-button|Styles applied to the delete account button in the rejection section.|
|dataPortability|.SCConsentSolution-data-portability|Styles applied to the data portability component in the rejection section.|
|dataPortabilityCheck|.SCConsentSolution-data-portability-check|Styles applied to the checkbox in the rejection section.|
* @param inProps
*/
export default function ConsentSolution(inProps) {
// PROPS
const props = useThemeProps({
props: inProps,
name: PREFIX
});
const { className, open = true, onClose, legalPolicies = LEGAL_POLICIES, onLogout, onDeleteAccount } = props, rest = __rest(props, ["className", "open", "onClose", "legalPolicies", "onLogout", "onDeleteAccount"]);
// CONTEXT
const scUserContext = useContext(SCUserContext);
// PREFERENCES
const scPreferences = useSCPreferences();
const communityName = useMemo(() => {
return scPreferences.preferences && SCPreferences.TEXT_APPLICATION_NAME in scPreferences.preferences
? scPreferences.preferences[SCPreferences.TEXT_APPLICATION_NAME].value
: null;
}, [scPreferences.preferences]);
// STATE
const [ready, setReady] = React.useState(false);
const [_view, setView] = useState(ACCEPT_VIEW);
const [documents, setDocuments] = useState([]);
const [currentDocument, setCurrentDocument] = useState(null);
const [loading, setLoading] = useState(true);
const [loadingAck, setLoadingAck] = useState(false);
const [rejected, setRejected] = useState(false);
const [dataPortability, setDataPortability] = useState(null);
const [dataPortabilityChecked, setDataPortabilityChecked] = useState(false);
const [loadingDeleteAccount, setLoadingDeleteAccount] = useState(false);
// CONST
const authUserId = scUserContext.user ? scUserContext.user.id : null;
const doc = documents[currentDocument];
// REFS
const contentDialog = useRef(null);
// INTL
const intl = useIntl();
/**
* Handle close dialog
*/
const handleClose = (e, s) => {
if (doc.ack && doc.ack.accepted_at) {
setReady(false);
// Call dialog callback if exist
onClose && onClose(e, s);
}
};
/**
* Handle close dialog
*/
const handleNext = () => {
if (doc.ack && doc.ack.accepted_at) {
contentDialog.current && elementScrollTo(contentDialog.current, { top: 0, behavior: 'smooth' });
setCurrentDocument((prev) => prev + 1);
}
else {
setRejected(true);
}
};
/**
* Handle accept (ack a policy document)
*/
const handleChangeConsent = (accept) => {
setLoadingAck(true);
let legalPages = [...documents];
LegalPageService.ackLegalPage(legalPages[currentDocument].id, Number(accept).toString())
.then((ack) => {
if (accept) {
legalPages[currentDocument].ack = ack;
setRejected(false);
}
else {
legalPages[currentDocument].ack.accepted_at = null;
setRejected(true);
}
setLoadingAck(false);
setDocuments(legalPages);
})
.catch((_error) => {
setLoadingAck(false);
Logger.error(SCOPE_SC_UI, _error);
});
};
/**
* Handle confirm delete account
*/
const handleConfirmDeleteAccount = () => {
setLoadingDeleteAccount(true);
UserService.userDelete(scUserContext.user.id, 0)
.then(() => {
setLoadingDeleteAccount(false);
onDeleteAccount && onDeleteAccount(scUserContext.user);
handleLogout();
})
.catch((_error) => {
setLoadingDeleteAccount(false);
Logger.error(SCOPE_SC_UI, _error);
});
};
/**
* Handle logout
*/
const handleLogout = () => {
scUserContext.logout();
onLogout && onLogout();
};
/**
* Fetch tec and privacy document status
*/
const fetchLegalPages = () => {
setLoading(true);
return LegalPageService.getAllLastRevisionsOfLegalPages()
.then((documents) => {
// filter documents (show only privacy and tec)
let docs = documents.filter((lp) => legalPolicies.filter((p) => lp.slug.startsWith(p)).length > 0);
// if initial legalPolicies !== LEGAL_POLICIES
if (arraysEqual(legalPolicies, LEGAL_POLICIES)) {
docs = docs.filter((d) => !isEmptyDocumentBody(d) && !isDocumentApproved(d));
}
setDocuments(docs);
setLoading(false);
if (docs.length > 0) {
setCurrentDocument(0);
setReady(true);
}
Promise.resolve(docs);
})
.catch((_error) => {
Logger.error(SCOPE_SC_UI, _error);
});
};
/**
* On mount, fetches legal and custom documents
*/
useEffect(() => {
if (!authUserId) {
return;
}
fetchLegalPages();
}, [authUserId]);
/**
* Render main view
*/
const renderMainView = () => {
if (!doc) {
return null;
}
const isAccept = Boolean(doc.ack && doc.ack.accepted_at);
return (_jsxs(_Fragment, { children: [_jsx(DialogTitle, Object.assign({ className: classes.title }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.acceptDocumentTitle", defaultMessage: "ui.consentSolution.acceptDocumentTitle", values: { label: doc.title } }) })), _jsx(DialogContent, Object.assign({ className: classes.content, dividers: true, ref: contentDialog }, { children: _jsx(Typography, { component: "div", gutterBottom: true, dangerouslySetInnerHTML: {
__html: getDocumentBody(doc)
} }) })), _jsxs(DialogContent, Object.assign({ className: classes.consent, dividers: true }, { children: [_jsx(FormControlLabel, { className: classes.consentSwitch, control: _jsx(ConsentSolutionSwitch, { loading: loadingAck, disabled: loadingAck, checked: isAccept, onChange: () => handleChangeConsent(!isAccept), name: "consent" }), label: _jsx("b", { children: _jsx(FormattedMessage, { id: "ui.consentSolution.consentSwitchLabel", defaultMessage: "ui.consentSolution.consentSwitchLabel" }) }) }), rejected ? (_jsxs(Alert, Object.assign({ severity: "error", className: classes.alertAcceptDocument }, { children: [_jsx(FormattedMessage, { id: "ui.consentSolution.deleteAccountAlert", defaultMessage: "ui.consentSolution.deleteAccountAlert" }), _jsx("a", Object.assign({ onClick: () => setView(REJECTION_VIEW), className: classes.deleteAccountButton }, { children: _jsx("b", { children: _jsx(FormattedMessage, { id: "ui.consentSolution.deleteAccount", defaultMessage: "ui.consentSolution.deleteAccount" }) }) }))] }))) : (_jsx(_Fragment, { children: _jsx(Typography, Object.assign({ variant: "body2", className: classes.acceptConditions }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.consentSwitchDetail", defaultMessage: "ui.consentSolution.consentSwitchDetail" }) })) }))] })), _jsxs(DialogActions, Object.assign({ className: classes.actions }, { children: [currentDocument > 0 && (_jsx(Button, Object.assign({ size: "small", onClick: () => setCurrentDocument((prev) => prev - 1), className: classes.backButton }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.backButton", defaultMessage: "ui.consentSolution.backButton" }) }))), currentDocument < documents.length - 1 ? (_jsx(Button, Object.assign({ size: "small", variant: 'outlined', disabled: !isAccept, onClick: handleNext, className: classes.nextButton }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.nextButton", defaultMessage: "ui.consentSolution.nextButton" }) }))) : (_jsx(Button, Object.assign({ size: "small", variant: 'outlined', disabled: !isAccept, onClick: (e) => handleClose(e, null), className: classes.closeButton }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.closeButton", defaultMessage: "ui.consentSolution.closeButton" }) })))] }))] }));
};
/**
* Render document detail view
*/
const renderRejectionView = () => {
const doc = documents[currentDocument];
if (!doc) {
return null;
}
return (_jsxs(_Fragment, { children: [_jsx(DialogTitle, Object.assign({ className: classNames(classes.title, classes.titleBack) }, { children: _jsx(Button, Object.assign({ size: "small", variant: 'outlined', onClick: () => setView(ACCEPT_VIEW), className: classes.backButton, startIcon: _jsx(Icon, { children: "arrow_back" }) }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.backButton", defaultMessage: "ui.consentSolution.backButton" }) })) })), _jsxs(DialogContent, Object.assign({ className: classes.content, dividers: true }, { children: [_jsx(AccountDataPortability, { className: classes.dataPortability }), _jsx(Typography, Object.assign({ variant: "h6" }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.deleteAccountDpSectionTitle", defaultMessage: "ui.consentSolution.deleteAccountDpSectionTitle", values: { username: capitalize(scUserContext.user.username) } }) })), _jsx("ul", { children: intl.formatMessage(messages.deleteAccountDpSectionInfo, { communityName, li: (chunks) => _jsx("li", { children: chunks }) }) }), _jsx(FormControlLabel, { control: _jsx(Checkbox, { className: classes.dataPortabilityCheck, checked: dataPortabilityChecked, onChange: () => setDataPortabilityChecked((p) => !p) }), label: _jsx(FormattedMessage, { id: "ui.consentSolution.deleteAccountDpSectionCheckboxLabel", defaultMessage: "ui.consentSolution.deleteAccountDpSectionCheckboxLabel" }) }), _jsx("br", {}), _jsx(Button, Object.assign({ size: "small", startIcon: _jsx(Icon, { children: "delete_outlined" }), disabled: !dataPortabilityChecked, variant: 'contained', className: classes.confirmDeleteAccountButton, onClick: () => setView(CONFIRM_DELETE_ACCOUNT) }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.confirmDeleteAccountButton", defaultMessage: "ui.consentSolution.confirmDeleteAccountButton" }) })), _jsx(Button, Object.assign({ size: "small", variant: 'outlined', className: classes.logoutAccountButton, startIcon: _jsx(Icon, { children: "upload" }), onClick: handleLogout }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.logoutImmediatelyButton", defaultMessage: "ui.consentSolution.logoutImmediatelyButton" }) }))] }))] }));
};
/**
* Render delete account view
*/
const renderDeleteAccountView = () => {
return (_jsxs(_Fragment, { children: [_jsx(DialogTitle, Object.assign({ className: classNames(classes.title, classes.titleBack) }, { children: _jsx(Button, Object.assign({ size: "small", disabled: loadingDeleteAccount, variant: 'outlined', onClick: () => setView(REJECTION_VIEW), className: classes.backButton, startIcon: _jsx(Icon, { children: "arrow_back" }) }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.backButton", defaultMessage: "ui.consentSolution.backButton" }) })) })), _jsxs(DialogContent, Object.assign({ className: classes.content, dividers: true }, { children: [_jsx(Typography, Object.assign({ variant: "body2" }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.removeAccountTitle", defaultMessage: "ui.consentSolution.removeAccountTitle" }) })), _jsx("ul", { children: intl.formatMessage(messages.deleteAccountDpSectionInfo, { communityName, li: (chunks) => _jsx("li", { children: chunks }) }) }), _jsx("br", {}), _jsx(Typography, Object.assign({ variant: "body2" }, { children: _jsx("b", { children: _jsx(FormattedMessage, { id: "ui.consentSolution.removeAccountConfirm", defaultMessage: "ui.consentSolution.removeAccountConfirm" }) }) }))] })), _jsxs(DialogActions, Object.assign({ className: classes.actions }, { children: [_jsx(Button, Object.assign({ size: "small", disabled: loadingDeleteAccount, variant: 'outlined', onClick: () => setView(REJECTION_VIEW) }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.removeAccountCancelButton", defaultMessage: "ui.consentSolution.removeAccountCancelButton" }) })), _jsx(Button, Object.assign({ size: "small", disabled: loadingDeleteAccount, variant: 'outlined', onClick: handleConfirmDeleteAccount }, { children: _jsx(FormattedMessage, { id: "ui.consentSolution.removeAccountConfirmButton", defaultMessage: "ui.consentSolution.removeAccountConfirmButton" }) }))] }))] }));
};
/**
* If there's no authUserId, component is hidden.
*/
if (!authUserId) {
return null;
}
/**
* Set content dialog component
*/
let content = () => null;
if (ready && doc && !loading) {
switch (_view) {
case ACCEPT_VIEW:
content = renderMainView;
break;
case REJECTION_VIEW:
content = renderRejectionView;
break;
case CONFIRM_DELETE_ACCOUNT:
content = renderDeleteAccountView;
break;
default:
content = renderMainView;
break;
}
}
else {
content = () => _jsx(ConsentSolutionSkeleton, {});
}
/**
* Renders root object
*/
return (_jsx(_Fragment, { children: open && (_jsx(Root, Object.assign({ "aria-describedby": "consent--solution-dialog", className: classNames(classes.root, className), TransitionComponent: DialogTransition, maxWidth: 'md', fullWidth: true, open: ready, disableEscapeKeyDown: true, onClose: handleClose, scroll: 'paper' }, rest, { children: content() }))) }));
}