UNPKG

@proca/widget

Version:

Proca is an open-source campaign toolkit designed to empower activists and organisations in their digital advocacy efforts. It provides a flexible and customisable platform for creating and managing online petitions, email campaigns, and other forms of di

601 lines (556 loc) 18.1 kB
import React, { useRef, useEffect, useState } from "react"; /*import Backdrop from '@material-ui/core/Backdrop'; import CircularProgress from '@material-ui/core/CircularProgress'; <Backdrop className={classes.backdrop} open={open} onClick={handleClose}> <CircularProgress color="inherit" /> </Backdrop> */ import { useCompactLayout } from "@hooks/useElementWidth"; import Url from "@lib/urlparser"; import { setCookie } from "@lib/cookie"; import { getDomain } from "@lib/checkMail"; import { useCampaignConfig } from "@hooks/useConfig"; import useData from "@hooks/useData"; import { makeStyles } from "@material-ui/core/styles"; import { Container, Box, Button, Snackbar, Grid } from "@material-ui/core"; import TextField from "@components/TextField"; import Alert from "@material-ui/lab/Alert"; import EmailField from "@components/field/Email"; import PhoneField from "@components/field/Phone"; import ProcaIcon from "../images/Proca"; import SvgIcon from "@material-ui/core/SvgIcon"; import DoneIcon from "@material-ui/icons/Done"; import SkipNextIcon from "@material-ui/icons/SkipNext"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Consent, { ConsentProcessing } from "@components/Consent"; import ImplicitConsent from "@components/ImplicitConsent"; import Salutation from "@components/field/Salutation"; import WelcomeSupporter from "@components/WelcomeSupporter"; import CustomField from "@components/field/CustomField"; import Address from "@components/field/Address"; import { addActionContact, addAction } from "@lib/server.js"; import dispatch from "@lib/event.js"; import uuid, { isSet as isUuid } from "@lib/uuid.js"; const useStyles = makeStyles(theme => ({ container: { display: "flex", flexWrap: "wrap", }, hidden: { display: "none", }, field: { margin: "0 !important", }, textField: { marginLeft: theme.spacing(0), marginRight: theme.spacing(0), width: "100%", }, next: { width: "100%", marginTop: "10px", }, withSubText: { flexDirection: "column", }, subText: { display: "inline", fontSize: "0.9em", textTransform: "none", }, act: { "&:hover": { "& .flame": { fill: "url(#a)" }, "& .arrow": { fill: "#fff" }, "& nope.circle": { fill: "#ff5c39", fillOpacity: 1 }, }, }, "@global": { "select:-moz-focusring": { color: "transparent", textShadow: "0 0 0 #000", }, "input:invalid + fieldset": {}, }, })); const ConditionalDisabled = props => { if (props.disabled === true) return <fieldset disabled="disabled">{props.children}</fieldset>; return props.children; }; const SubmitButton = props => { const classes = useStyles(); const config = useCampaignConfig(); const { formState, setValue, register } = props.form; const { t } = useTranslation(); const handleClick = (e, privacy) => { setValue("privacy", privacy); return props.handleClick(e); }; if (config.component.consent?.buttons) { return ( <> <input type="hidden" {...register("privacy", { required: false })} /> <Grid item xs={12} sm={6}> <Button variant="contained" classes={{ label: classes.withSubText }} fullWidth onClick={e => handleClick(e, "opt-out")} disabled={ formState.isSubmitting || config.component.register?.disabled === true } > {props.buttonText || t(config.component.register?.button || "action.register")} <span className={classes.subText}> {t("consent.subButton.opt-out")} </span> </Button> </Grid> <Grid item xs={12} sm={6}> <Button color="primary" variant="contained" classes={{ label: classes.withSubText }} fullWidth onClick={e => handleClick(e, "opt-in")} disabled={ formState.isSubmitting || config.component.register?.disabled === true } > {props.buttonText || t(config.component.register?.button || "action.register")} <span className={classes.subText}> {t("consent.subButton.opt-in")} </span> </Button> </Grid> </> ); } return ( <Grid item xs={12}> <Button color="primary" variant="contained" className={classes.act} fullWidth onClick={props.handleClick} size="large" disabled={ formState.isSubmitting || config.component.register?.disabled === true } endIcon={ <SvgIcon> <ProcaIcon /> </SvgIcon> } > {" "} {props.buttonText || t(config.component.register?.button || "action.register")} </Button> </Grid> ); }; export default function Register(props) { const classes = useStyles(); const config = useCampaignConfig(); const [data, setData] = useData(); const [beforeSubmit, _setBeforeSubmit] = useState(null); const customField = React.useRef({}); const setBeforeSubmit = fct => { if (!beforeSubmit) { _setBeforeSubmit(() => fct); // you can't put a function or promise in useState directly, it's taken as a setter instead } }; let emailProvider = useRef(undefined); // we don't know the email provider const { t } = useTranslation(); if (props.emailProvider) emailProvider = props.emailProvider; // use case: if Register is called from a parent component that wants to store the email provider const compact = useCompactLayout("#proca-register", 380); let buttonNext = "Next"; const [status, setStatus] = useState("default"); const _form = useForm({ mode: "onBlur", // nativeValidation: true, defaultValues: props.form ? null : data, }); const form = props.form || _form; const { trigger, handleSubmit, setError, getValues, setValue } = form; const comment = data.comment; useEffect(() => { setValue("comment", comment); }, [comment, setValue]); const onSubmit = async formData => { config.data && Object.entries(config.data).forEach(([key, value]) => { if (!formData[key]) formData[key] = value; }); if (emailProvider.current === false) { setError("email", { type: "mx", message: t("email.invalid_domain", { defaultValue: "{{domain}} cannot receive emails", domain: getDomain(formData.email), }), }); // the email domain is checked and invalid return false; } else { if (emailProvider.current) formData.emailProvider = emailProvider.current; } formData.tracking = Url.utm(config.component.register?.tracking); if (config.component.consent?.implicit) { formData.privacy = config.component.consent.implicit === true ? null : config.component.consent.implicit; // implicit true or opt-in or opt-out } let actionType = config.component.register?.actionType || "register"; if (props.targets) { formData.targets = props.targets; actionType = "mail2target"; } if (props.beforeSubmit && typeof props.beforeSubmit === "function") { formData = await props.beforeSubmit(formData); } if (customField.current.beforeSubmit) { formData = await customField.current.beforeSubmit(formData); } if (!formData) { console.error("missing data"); return false; } if (isUuid()) { // they were previous actions, we associate them with the contact recorded now formData.uuid = uuid(); } if (data.uuid) { // the contact is known, but the contact details possibly not set formData.uuid = data.uuid; } let result = null; if (data.uuid) { const expected = "uuid,firstname,lastname,email,phone,country,postcode,locality,address,region,birthdate,privacy,tracking,donation".split( "," ); const payload = {}; for (const [key, value] of Object.entries(formData)) { if (value && !expected.includes(key)) payload[key] = value; } result = await addAction( config.actionPage, actionType, { uuid: data.uuid, tracking: Url.utm(config.component?.register?.tracking), payload: payload, }, config.test ); } else { result = await addActionContact( actionType, config.actionPage, formData, config.test ); } if (result.errors) { let handled = false; if (result.errors.fields) { result.errors.fields.forEach(field => { if (field.name in formData) { setError(field.name, { type: "server", message: field.message }); handled = true; } else if (field.name.toLowerCase() in formData) { setError(field.name.toLowerCase(), { type: "server", message: field.message, }); handled = true; } }); } !handled && setStatus("error"); return; } if (result.addAction) { result = result.addAction; } dispatch( `${config.component?.register?.actionType || "register"}:complete`, { uuid: result.contactRef, test: !!config.test, firstname: formData.firstname, country: formData.country, comment: formData.comment, privacy: formData.privacy, }, formData, config ); if (config.component.register?.remember) { setCookie("proca_firstname", formData.firstname); setCookie("proca_uuid", result.contactRef); } setStatus("success"); setData(formData); if (!config.component.share?.anonymous) { uuid(result.contactRef); // set the global uuid as signature's fingerprint } props.done && props.done({ errors: result.errors, uuid: uuid(), firstname: formData.firstname, country: formData.country, privacy: formData.privacy, comment: formData.comment, }); }; const handleClick = async () => { const result = await trigger(); if (result) { if (props.onClick) { // do not await it, it would open a warning 'firefox prevented this page to open a pop up window... setTimeout(() => handleSubmit(onSubmit)(), 1); props.onClick(getValues()); // how to get the data updated? } else { await handleSubmit(onSubmit)(); } } }; /* useEffect(() => { const inputs = document.querySelectorAll("input, select, textarea"); // todo: workaround until the feature is native react-form ? inputs.forEach((input) => { input.oninvalid = (e) => { setError(e.target.attributes.name.nodeValue, { type: e.type, message: e.target.validationMessage, }); }; }); }, [setError]); */ function ErrorS(props) { if (props.display) return ( <Snackbar open={true} autoHideDuration={6000}> <Alert severity="error">{t("Sorry, we couldn't save")}</Alert> </Snackbar> ); return null; } function Success(props) { if (props.display) return ( <Snackbar open={true} autoHideDuration={6000}> <Alert severity="success">Done, Thank you for your support!</Alert> </Snackbar> ); return null; } if (status === "success") { return ( <Container component="div" maxWidth="sm"> <Grid container spacing={1}> <Grid item xs={12}> <DoneIcon color="action" fontSize="large" my={4} /> </Grid> </Grid> </Container> ); } let ConsentBlock = config.component.consent?.implicit ? ImplicitConsent : Consent; if (config.component.consent?.buttons) { ConsentBlock = function NoConsent() { return null; }; } const isValid = Object.keys(form.formState.errors).length === 0; const classField = data.uuid && isValid ? classes.hidden : classes.field; //const classField = classes.field; const enforceRequired = !data.uuid; // if the user took action, no fields are required const withSalutation = config.component?.register?.field?.salutation; const nameWidth = field => { if (compact) return 12; if (withSalutation && field === "firstname") return 4; if (withSalutation) return 5; return 6; }; if (typeof props.buttonNext === "string") { buttonNext = props.buttonNext; } const next = () => { const d = getValues(); setData(d); dispatch( `${config.component?.register?.actionType || "register"}:skip`, { test: !!config.test, country: d.country, }, d, config ); props.done(); }; return ( <form className={classes.container} id="proca-register" onSubmit={handleSubmit(onSubmit)} method="post" action="http://localhost" > <Success display={status === "success"} /> <ErrorS display={status === "error"} /> <Container component="div" maxWidth="sm"> <ConditionalDisabled disabled={config.component.register?.disabled === true} > <WelcomeSupporter /> <Box marginBottom={1}> <Grid container spacing={1}> {config.component.register?.custom?.top && ( <CustomField compact={compact} form={form} position="top" myref={customField} handleBeforeSubmit={setBeforeSubmit} classes={classes} /> )} {config.component.register?.field?.organisation && ( <Grid item xs={12} className={classField}> <TextField type="organisation" form={form} name="organisation" label={t("Organisation")} required={ enforceRequired && config.component.register.field.organisation.required } /> </Grid> )} {withSalutation && ( <Salutation form={form} compact={compact} classes={classes} /> )} <Grid item xs={12} sm={nameWidth("firstname")} className={classField} > <TextField form={form} name="firstname" label={t("First name")} autoComplete="given-name" required /> </Grid> {config.component.register?.field?.lastname !== false && ( <Grid item xs={12} sm={nameWidth()} className={classField}> <TextField form={form} name="lastname" label={t("Last name")} autoComplete="family-name" required={ enforceRequired && config.component.register?.field?.lastname?.required } /> </Grid> )} <Grid item xs={12} sm={ compact || config.component.register?.field?.lastname !== false ? 12 : 6 } className={classField} > <EmailField form={form} required={enforceRequired} /> </Grid> <Address form={form} compact={compact} classField={classField} /> <PhoneField form={form} classField={classField} /> {config.component.register?.field?.comment !== false && ( <Grid item xs={12} className={classField}> <TextField form={form} name="comment" multiline maxRows="10" required={ enforceRequired && config.component.register?.field?.comment?.required } label={t("Comment")} helperText={t("help.comment", "")} /> </Grid> )} {props.extraFields && props.extraFields({ form: form, classes: classes })} {config.component.register?.custom?.bottom && ( <CustomField compact={compact} form={form} classes={classes} myref={customField} /> )} {!data.uuid && ( <ConsentBlock organisation={props.organisation} privacy_url={config.privacyUrl} intro={props.consentIntro} form={form} /> )} <SubmitButton handleClick={handleClick} form={form} buttonText={props.buttonText} /> <Grid item xs={12}> <ConsentProcessing /> {config.component.register?.next && ( <Button endIcon={<SkipNextIcon />} className={classes.next} variant="contained" onClick={next} > {t([buttonNext])} </Button> )} </Grid> </Grid> </Box> </ConditionalDisabled> </Container> </form> ); }