@shopgate/engage
Version:
Shopgate's ENGAGE library.
236 lines (224 loc) • 8.29 kB
JavaScript
import "core-js/modules/es.string.replace.js";
import React, { createContext, useMemo, useEffect, useContext, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getShopSettings } from '@shopgate/engage/core/config';
import { getPreferredLocationAddress } from '@shopgate/engage/locations/selectors';
import { useRoute, i18n, historyPush, EVALIDATION } from '@shopgate/engage/core';
import { getMerchantCustomerAttributes } from '@shopgate/engage/core/selectors/merchantSettings';
import { useFormState as useForm, convertValidationErrors } from '@shopgate/engage/core/hooks/useFormState';
import showModal from '@shopgate/pwa-common/actions/modal/showModal';
import { LoadingProvider, ToastProvider } from '@shopgate/pwa-common/providers';
import UIEvents from '@shopgate/pwa-core/emitters/ui';
import { CHECKOUT_ADDRESS_BOOK_CONTACT_PATTERN, useAddressBook } from '@shopgate/engage/checkout';
import { PROFILE_ADDRESS_PATH } from "../../constants/routes";
import { fetchCustomerContacts } from "../../actions/fetchContacts";
import { fetchCustomerData } from "../../actions/fetchCustomer";
import { updateCustomerData } from "../../actions/updateCustomer";
import { deleteCustomerContact } from "../../actions/deleteContact";
import { deleteCustomer as deleteCustomerAction } from "../../actions/deleteCustomer";
import { getContacts } from "../../selectors/contacts";
import { getCustomer } from "../../selectors/customer";
import { extractAttributes, extractDefaultValues, convertPipelineValidationErrors } from "../../helper/form";
import createConstraints from "./Profile.constraints";
import { jsx as _jsx } from "react/jsx-runtime";
const ProfileContext = /*#__PURE__*/createContext();
/**
* @returns {Array}
*/
export const useProfileContext = () => useContext(ProfileContext);
/**
* @param {Object} state State.
* @returns {Object}
*/
const mapStateToProps = state => ({
contacts: getContacts(state),
customer: getCustomer(state),
merchantCustomerAttributes: getMerchantCustomerAttributes(state),
shopSettings: getShopSettings(state),
userLocation: getPreferredLocationAddress(state)
});
/**
* @param {Function} dispatch Dispatch.
* @returns {Object}
*/
const mapDispatchToProps = dispatch => ({
fetchContacts: () => dispatch(fetchCustomerContacts()),
deleteContact: customerId => dispatch(deleteCustomerContact(customerId)),
deleteCustomer: () => dispatch(deleteCustomerAction()),
fetchCustomer: () => dispatch(fetchCustomerData()),
updateCustomer: customer => dispatch(updateCustomerData(customer)),
showDialog: props => dispatch(showModal(props)),
push: props => dispatch(historyPush(props))
});
/**
* @returns {JSX}
*/
const ProfileProvider = ({
fetchContacts,
deleteContact,
fetchCustomer,
updateCustomer,
deleteCustomer,
push,
showDialog,
contacts,
customer,
merchantCustomerAttributes,
children,
shopSettings,
userLocation,
formContainerRef
}) => {
// Route
const {
pathname
} = useRoute();
const {
isCheckout,
type,
deleteContactFromOrder
} = useAddressBook();
const [requestErrors, setRequestErrors] = useState(null);
// Default state.
const defaultState = useMemo(() => customer ? {
...customer,
marketingOptIn: customer.settings.marketingOptIn || false,
...extractDefaultValues(customer.attributes)
} : null, [customer]);
// Saving the form.
const saveForm = useCallback(async values => {
LoadingProvider.setLoading(pathname);
const attributes = extractAttributes(merchantCustomerAttributes, values);
let validationErrors;
try {
await updateCustomer({
firstName: values.firstName,
...(values.middleName ? {
middleName: values.middleName
} : {}),
lastName: values.lastName,
emailAddress: values.emailAddress,
settings: {
marketingOptIn: values.marketingOptIn
},
attributes
});
await fetchCustomer();
UIEvents.emit(ToastProvider.ADD, {
id: 'account.profile.form.success',
message: 'account.profile.form.success'
});
} catch (error) {
const {
code,
errors
} = error;
if (code === EVALIDATION) {
const converted = convertPipelineValidationErrors(errors, attributes);
if (converted?.validation && Object.keys(converted.validation).length > 0) {
validationErrors = converted?.validation?.attributes;
}
}
UIEvents.emit(ToastProvider.ADD, {
id: 'account.profile.form.error',
message: 'account.profile.form.error'
});
}
setRequestErrors(validationErrors || null);
LoadingProvider.unsetLoading(pathname);
}, [fetchCustomer, merchantCustomerAttributes, pathname, updateCustomer]);
// Hold profile form state.
const constraints = useMemo(() => createConstraints(merchantCustomerAttributes), [merchantCustomerAttributes]);
const formState = useForm(defaultState, saveForm, constraints, formContainerRef, 120);
const validationErrors = useMemo(() => convertValidationErrors(formState.validationErrors || requestErrors || {}), [formState.validationErrors, requestErrors]);
useEffect(() => {
formState.scrollToError();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formState.scrollToError, requestErrors]);
/**
* Executes callback with confirmation beforehand.
* @param {string} message String
*/
const confirmation = async message => {
const confirmed = await showDialog({
title: i18n.text('account.profile.confirm.title'),
message: i18n.text(message),
confirm: i18n.text('account.profile.confirm.confirm')
});
return !!confirmed;
};
// Deletes a contact.
/* eslint-disable react-hooks/exhaustive-deps */
const deleteContactWrapper = useCallback(async contactId => {
if (!(await confirmation('account.profile.confirm.messageContact'))) {
return;
}
LoadingProvider.setLoading(pathname);
try {
await deleteContact(contactId);
await fetchContacts();
if (deleteContactFromOrder) {
await deleteContactFromOrder(contactId);
}
} catch (error) {
// taken care by regular error handling.
}
LoadingProvider.unsetLoading(pathname);
}, [deleteContact, fetchContacts, pathname]);
const deleteCustomerWrapper = useCallback(async () => {
if (!(await confirmation('account.profile.confirm.messageCustomer'))) {
return;
}
LoadingProvider.setLoading(pathname);
try {
await deleteCustomer();
} catch (error) {
// taken care by regular error handling.
}
LoadingProvider.unsetLoading(pathname);
}, [deleteContact, fetchContacts, pathname]);
/* eslint-enable react-hooks/exhaustive-deps */
const editContact = useCallback(contact => {
const editPathname = isCheckout ? `${CHECKOUT_ADDRESS_BOOK_CONTACT_PATTERN}`.replace(':type', type) : PROFILE_ADDRESS_PATH;
push({
pathname: editPathname,
state: {
contact
}
});
}, [isCheckout, push, type]);
// Store context value.
const contextValue = useMemo(() => ({
contacts,
validationErrors,
merchantCustomerAttributes,
customer: defaultState,
isCheckout,
supportedCountries: shopSettings.supportedCountries,
countrySortOrder: shopSettings.countrySortOrder,
userLocation,
formState,
saveForm: formState.handleSubmit,
editContact,
deleteContact: deleteContactWrapper,
deleteCustomer: deleteCustomerWrapper
}), [userLocation, shopSettings.supportedCountries, shopSettings.countrySortOrder, contacts, merchantCustomerAttributes, isCheckout, defaultState, deleteContactWrapper, deleteCustomerWrapper, editContact, formState, validationErrors]);
// Fetch all required data for the profile page.
useEffect(() => {
fetchContacts();
fetchCustomer();
}, [fetchContacts, fetchCustomer]);
if (!customer) {
return null;
}
return /*#__PURE__*/_jsx(ProfileContext.Provider, {
value: contextValue,
children: children
});
};
ProfileProvider.defaultProps = {
customer: null,
formContainerRef: null
};
export default connect(mapStateToProps, mapDispatchToProps)(ProfileProvider);