@@ -106,22 +125,8 @@ const FormView = ({
{data.subblocks?.map((subblock, index) => {
let name = getFieldName(subblock.label, subblock.id);
- var fields_to_send = [];
- var fieldSchemaProperties = FieldSchema(subblock)?.properties;
- for (var key in fieldSchemaProperties) {
- if (fieldSchemaProperties[key].send_to_backend) {
- fields_to_send.push(key);
- }
- }
-
- var fields_to_send_with_value = Object.assign(
- {},
- ...fields_to_send.map((field) => {
- return {
- [field]: subblock[field],
- };
- }),
- );
+ const fields_to_send_with_value =
+ getFieldsToSendWithValue(subblock);
return (
@@ -150,6 +155,47 @@ const FormView = ({
);
})}
+
+ {/*OTP*/}
+ {data.subblocks
+ .filter((subblock) => subblock.use_as_bcc)
+ .map((subblock, index) => {
+ const fieldName = getFieldName(subblock.label, subblock.id);
+ const name = fieldName + OTP_FIELDNAME_EXTENDER;
+ const fieldValue = formData[fieldName]?.value;
+ const value = formData[fieldName]?.otp;
+ const fields_to_send_with_value =
+ getFieldsToSendWithValue(subblock);
+
+ return (
+
+
+
+ onChangeFormData(
+ subblock.id,
+ fieldName,
+ fieldValue,
+ {
+ ...fields_to_send_with_value,
+ otp: value,
+ },
+ )
+ }
+ value={value}
+ valid={isValidField(name)}
+ errorMessage={getErrorMessage(name)}
+ formHasErrors={formErrors?.length > 0}
+ path={path}
+ block_id={block_id}
+ />
+
+
+ );
+ })}
+
{captcha.render()}
{formErrors.length > 0 && (
@@ -159,7 +205,6 @@ const FormView = ({
{intl.formatMessage(messages.form_errors)}
)}
-
{formState.error && (
diff --git a/src/components/View.jsx b/src/components/View.jsx
index 627fddf..5042264 100644
--- a/src/components/View.jsx
+++ b/src/components/View.jsx
@@ -10,6 +10,7 @@ import config from '@plone/volto/registry';
import { Captcha } from 'volto-form-block/components/Widget';
import { isValidEmail } from 'volto-form-block/helpers/validators';
import ValidateConfigForm from 'volto-form-block/components/ValidateConfigForm';
+import { OTP_FIELDNAME_EXTENDER } from 'volto-form-block/components/Widget';
const messages = defineMessages({
formSubmitted: {
@@ -28,6 +29,10 @@ const messages = defineMessages({
id: 'formblock_invalidEmailMessage',
defaultMessage: 'The email is incorrect',
},
+ insertOtp: {
+ id: 'formblock_insertOtp_error',
+ defaultMessage: 'Please, insert the OTP code recived via email.',
+ },
});
const initialState = {
@@ -132,6 +137,7 @@ const View = ({ data, id, path }) => {
data.subblocks.forEach((subblock, index) => {
const name = getFieldName(subblock.label, subblock.id);
const fieldType = subblock.field_type;
+ const isBCC = subblock.use_as_bcc;
const additionalField =
config.blocks.blocksConfig.form.additionalFields?.filter(
(f) => f.id === fieldType && f.isValid !== undefined,
@@ -172,14 +178,20 @@ const View = ({ data, id, path }) => {
message: intl.formatMessage(messages.requiredFieldMessage),
});
} else if (
- fieldType === 'from' &&
- formData[name]?.value &&
- !isValidEmail(formData[name].value)
+ (fieldType === 'from' || fieldType === 'email') &&
+ formData[name]?.value
) {
- v.push({
- field: name,
- message: intl.formatMessage(messages.invalidEmailMessage),
- });
+ if (!isValidEmail(formData[name].value)) {
+ v.push({
+ field: name,
+ message: intl.formatMessage(messages.invalidEmailMessage),
+ });
+ } else if (isBCC && !formData[name].otp) {
+ v.push({
+ field: name + OTP_FIELDNAME_EXTENDER,
+ message: intl.formatMessage(messages.insertOtp),
+ });
+ }
}
});
@@ -327,6 +339,8 @@ const View = ({ data, id, path }) => {
resetFormState={resetFormState}
resetFormOnError={resetFormOnError}
getErrorMessage={getErrorMessage}
+ path={path}
+ block_id={id}
/>
);
diff --git a/src/components/Widget/Button.jsx b/src/components/Widget/Button.jsx
new file mode 100644
index 0000000..94ceeef
--- /dev/null
+++ b/src/components/Widget/Button.jsx
@@ -0,0 +1,13 @@
+/**
+ * Button component.
+ * This is a wrapper for Buttons, to eventually customize Button component if you don't like to use semantic-ui, for example.
+ * @module components/Widget/OTPWidget
+ */
+
+import { Button as SemanticButton } from 'semantic-ui-react';
+
+const Button = (props) => {
+ return ;
+};
+
+export default Button;
diff --git a/src/components/Widget/FileWidget.jsx b/src/components/Widget/FileWidget.jsx
index 6cb80d2..a0d1b8e 100644
--- a/src/components/Widget/FileWidget.jsx
+++ b/src/components/Widget/FileWidget.jsx
@@ -6,7 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Button, Image, Dimmer } from 'semantic-ui-react';
+import { Image, Dimmer } from 'semantic-ui-react';
import { readAsDataURL } from 'promise-file-reader';
import { injectIntl } from 'react-intl';
import deleteSVG from '@plone/volto/icons/delete.svg';
@@ -14,6 +14,7 @@ import { Icon, FormFieldWrapper } from '@plone/volto/components';
import loadable from '@loadable/component';
import { flattenToAppURL } from '@plone/volto/helpers';
import { defineMessages, useIntl } from 'react-intl';
+import { Button } from 'volto-form-block/components/Widget';
const imageMimetypes = [
'image/png',
@@ -85,8 +86,8 @@ const FileWidget = (props) => {
const imgsrc = value?.download
? `${flattenToAppURL(value?.download)}?id=${Date.now()}`
: null || value?.data
- ? `data:${value['content-type']};${value.encoding},${value.data}`
- : null;
+ ? `data:${value['content-type']};${value.encoding},${value.data}`
+ : null;
/**
* Drop handler
diff --git a/src/components/Widget/OTPWidget.css b/src/components/Widget/OTPWidget.css
new file mode 100644
index 0000000..8c4978f
--- /dev/null
+++ b/src/components/Widget/OTPWidget.css
@@ -0,0 +1,49 @@
+.otp-widget .otp-widget-field-wrapper {
+ display: flex;
+ gap: 1rem;
+ align-items: start;
+}
+.otp-widget .otp-widget-field-wrapper .field {
+ flex-grow: 1;
+}
+
+.otp-widget .otp-alert {
+ position: relative;
+ padding: 0.5rem 1rem;
+ margin-bottom: 1rem;
+ border: 1px solid #5d7083;
+ border-left: 8px solid #5d7083;
+ background-color: #fff;
+ border-radius: 0;
+ margin-top: -3rem;
+ margin-bottom: 3rem;
+}
+
+.otp-widget .otp-alert.otp-error {
+ border-left: 8px solid #cc334d;
+}
+
+.otp-widget .otp-alert.otp-success {
+ border-left: 8px solid #008055;
+}
+
+.otp-widget .otp-widget-field-wrapper .button-wrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.otp-widget .otp-widget-field-wrapper .button-wrapper .otp-button-message {
+ font-size: 0.8rem;
+ color: #555555;
+ text-align: center;
+}
+@media (max-width: 1024px) {
+ .otp-widget .otp-widget-field-wrapper {
+ flex-direction: column;
+ }
+ .otp-widget .otp-widget-field-wrapper .field,
+ .otp-widget .otp-widget-field-wrapper .button-wrapper,
+ .otp-widget .otp-widget-field-wrapper .button-wrapper button {
+ width: 100%;
+ }
+}
diff --git a/src/components/Widget/OTPWidget.jsx b/src/components/Widget/OTPWidget.jsx
new file mode 100644
index 0000000..defa629
--- /dev/null
+++ b/src/components/Widget/OTPWidget.jsx
@@ -0,0 +1,200 @@
+/**
+ * OTPWidget component.
+ * @module components/Widget/OTPWidget
+ */
+
+import PropTypes from 'prop-types';
+import React, { useState, useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { useIntl, defineMessages } from 'react-intl';
+import { isValidEmail } from 'volto-form-block/helpers/validators';
+import Field from 'volto-form-block/components/Field';
+import { Button } from 'volto-form-block/components/Widget';
+import { sendOTP } from 'volto-form-block/actions';
+
+import 'volto-form-block/components/Widget/OTPWidget.css';
+export const OTP_FIELDNAME_EXTENDER = '_otp';
+
+const messages = defineMessages({
+ send_otp_to: {
+ id: 'form_send_otp_to',
+ defaultMessage: 'Send OTP code to {email}',
+ },
+ insert_otp: {
+ id: 'form_insert_otp',
+ defaultMessage: 'Insert here the OTP code received at {email}',
+ },
+ otp_sent: {
+ id: 'form_otp_send',
+ defaultMessage:
+ 'OTP code was sent to {email}. Check your email and insert the received OTP code into the field above.',
+ },
+ otp_countdown: {
+ id: 'form_otp_countdown',
+ defaultMessage: 'You can send a new OTP code in',
+ },
+});
+const getCountDownValues = (countDown) => {
+ // calculate time left
+ const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
+ const hours = Math.floor(
+ (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
+ );
+ const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
+ const seconds = Math.floor((countDown % (1000 * 60)) / 1000);
+
+ return [days, hours, minutes, seconds];
+};
+
+const OTPWidget = (props) => {
+ const OTP_EXIPIRE_MINUTES = 5;
+ const {
+ id,
+ title,
+ fieldValue,
+ onChange,
+ value,
+ valid,
+ disabled,
+ isOnEdit,
+ formHasErrors,
+ errorMessage,
+ path,
+ block_id,
+ } = props;
+ const intl = useIntl();
+ const dispatch = useDispatch();
+ const _id = id + OTP_FIELDNAME_EXTENDER;
+ const sendOTPResponse = useSelector(
+ (state) => state.sendOTP?.subrequests?.[block_id + '_' + fieldValue],
+ );
+ const [countDownEnd, setCountDownEnd] = useState(null);
+ const [countDown, setCountDown] = useState(null);
+
+ const displayWidget = isValidEmail(fieldValue);
+
+ const sendOTPCode = () => {
+ dispatch(sendOTP(path, block_id, fieldValue));
+ };
+
+ useEffect(() => {
+ if (sendOTPResponse?.loaded) {
+ const end = new Date().getTime() + OTP_EXIPIRE_MINUTES * 60000;
+ setCountDownEnd(end);
+ setCountDown(end - new Date().getTime());
+
+ const interval = setInterval(() => {
+ setCountDown(countDownEnd - new Date().getTime());
+ }, 1000);
+
+ return () => clearInterval(interval);
+ } else {
+ setCountDown(null);
+ setCountDownEnd(null);
+ }
+ }, [sendOTPResponse, countDownEnd]);
+
+ return displayWidget ? (
+
+
+
+
+ {countDown > 0 && (
+
+ {intl.formatMessage(messages.otp_countdown)}{' '}
+ {getCountDownValues(countDown)
+ .filter((v, index) => v > 0 || index === 2 || index === 3)
+ .map((v) => (v < 10 ? '0' + v : v))
+ .join(':')}
+ .
+
+ )}
+
+
+
+
+
+ {sendOTPResponse?.loaded && !sendOTPResponse?.loading && (
+
+ {intl.formatMessage(messages.otp_sent, { email: fieldValue })}
+
+ )}
+
+ {sendOTPResponse?.error && (
+
+ {JSON.stringify(sendOTPResponse.error)}
+
+ )}
+
+ ) : (
+ <>>
+ );
+};
+
+/**
+ * Property types
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+OTPWidget.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ description: PropTypes.string,
+ required: PropTypes.bool,
+ error: PropTypes.arrayOf(PropTypes.string),
+ value: PropTypes.string,
+ onChange: PropTypes.func.isRequired,
+ onBlur: PropTypes.func,
+ onClick: PropTypes.func,
+ minLength: PropTypes.number,
+ maxLength: PropTypes.number,
+ placeholder: PropTypes.string,
+};
+
+/**
+ * Default properties.
+ * @property {Object} defaultProps Default properties.
+ * @static
+ */
+OTPWidget.defaultProps = {
+ description: null,
+ required: false,
+ error: [],
+ value: null,
+ onChange: () => {},
+ onBlur: () => {},
+ onClick: () => {},
+ minLength: null,
+ maxLength: null,
+};
+
+export default OTPWidget;
diff --git a/src/components/Widget/index.js b/src/components/Widget/index.js
index 1b2cb56..a5c031b 100644
--- a/src/components/Widget/index.js
+++ b/src/components/Widget/index.js
@@ -13,3 +13,6 @@ export { default as RadioWidget } from 'volto-form-block/components/Widget/Radio
export { default as SelectWidget } from 'volto-form-block/components/Widget/SelectWidget';
export { default as TextareaWidget } from 'volto-form-block/components/Widget/TextareaWidget';
export { default as TextWidget } from 'volto-form-block/components/Widget/TextWidget';
+export { default as OTPWidget } from 'volto-form-block/components/Widget/OTPWidget';
+export { OTP_FIELDNAME_EXTENDER } from 'volto-form-block/components/Widget/OTPWidget';
+export { default as Button } from 'volto-form-block/components/Widget/Button';
diff --git a/src/index.js b/src/index.js
index 07fe390..8b4439d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -16,6 +16,7 @@ import {
getFormData,
exportCsvFormData,
clearFormData,
+ sendOTP,
} from 'volto-form-block/reducers';
import FormSchema from 'volto-form-block/formSchema';
import FieldSchema from 'volto-form-block/fieldSchema';
@@ -34,6 +35,7 @@ export {
submitForm,
getFormData,
exportCsvFormData,
+ sendOTP,
} from 'volto-form-block/actions';
export { isValidEmail };
@@ -83,13 +85,14 @@ const applyConfig = (config) => {
formData: getFormData,
exportCsvFormData,
clearFormData,
+ sendOTP,
};
- config.settings.loadables['HCaptcha'] = loadable(() =>
- import('@hcaptcha/react-hcaptcha'),
+ config.settings.loadables['HCaptcha'] = loadable(
+ () => import('@hcaptcha/react-hcaptcha'),
);
- config.settings.loadables['GoogleReCaptcha'] = loadable.lib(() =>
- import('react-google-recaptcha-v3'),
+ config.settings.loadables['GoogleReCaptcha'] = loadable.lib(
+ () => import('react-google-recaptcha-v3'),
);
return config;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 4467e1c..1574475 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -7,6 +7,7 @@ import {
EXPORT_CSV_FORMDATA,
GET_FORM_DATA,
CLEAR_FORM_DATA,
+ SEND_OTP,
} from 'volto-form-block/actions';
function download(filename, text) {
@@ -95,6 +96,7 @@ export const submitForm = (state = initialState, action = {}) => {
...state.subrequests,
[action.subrequest]: {
...(state.subrequests[action.subrequest] || {}),
+ error: action.error,
result: null,
loading: false,
loaded: false,
@@ -226,3 +228,82 @@ export const clearFormData = (state = initialState, action = {}) => {
return state;
}
};
+
+/**
+ * sendOTP reducer.
+ * @function sendOTP
+ * @param {Object} state Current state.
+ * @param {Object} action Action to be handled.
+ * @returns {Object} New state.
+ */
+export const sendOTP = (state = initialState, action = {}) => {
+ switch (action.type) {
+ case `${SEND_OTP}_PENDING`:
+ return action.subrequest
+ ? {
+ ...state,
+ subrequests: {
+ ...state.subrequests,
+ [action.subrequest]: {
+ ...(state.subrequests[action.subrequest] || {
+ items: [],
+ total: 0,
+ batching: {},
+ }),
+ error: null,
+ loaded: false,
+ loading: true,
+ },
+ },
+ }
+ : {
+ ...state,
+ error: null,
+ loading: true,
+ loaded: false,
+ };
+ case `${SEND_OTP}_SUCCESS`:
+ return action.subrequest
+ ? {
+ ...state,
+ subrequests: {
+ ...state.subrequests,
+ [action.subrequest]: {
+ ...(state.subrequests[action.subrequest] || {}),
+ error: null,
+ loaded: true,
+ loading: false,
+ },
+ },
+ }
+ : {
+ ...state,
+ error: null,
+ loaded: true,
+ loading: false,
+ };
+ case `${SEND_OTP}_FAIL`:
+ return action.subrequest
+ ? {
+ ...state,
+ subrequests: {
+ ...state.subrequests,
+ [action.subrequest]: {
+ ...(state.subrequests[action.subrequest] || {}),
+ error: action.error,
+ loading: false,
+ loaded: false,
+ },
+ },
+ }
+ : {
+ ...state,
+ error: action.error,
+ loading: false,
+ loaded: false,
+ };
+
+ default:
+ return state;
+ }
+};