import React, { FC, ReactType, RefObject, useEffect, useRef, useState, } from 'react'; import Layout from '../../components/Layout'; import { Button, Checkbox, LinkProps } from '../../index'; import ReactModal from 'react-modal'; import 'font-awesome/css/font-awesome.min.css'; import ArrowSVG from '../../assets/icons/icon_angle.svg'; import { checkIfAddressIsValid } from '../../helpers/addressHelper'; import { CoordsProps } from '../../components/AddressMap'; import { FormCustomerDetailsValues } from '../../components/FormCustomerDetails'; import { FormCustomerBillingDetailsValues } from '../../components/FormCustomerBillingDetails'; import { PaymentFormValues } from '../../components/FormPayment'; import classnames from 'classnames'; import { AddressModal, BookingHeader, LoginModal, PasswordResetModal, SummaryContent, SummaryGiftCard, } from '../../helpers/bookingFunnelHelper'; import Modal from '../../blocks/Modal'; import css from '../BookingFunnel/index.module.css'; import * as R from 'ramda'; import { getAmountWithCurrency } from '../../helpers/priceHelper'; import FormGiftCard from '../../components/FormGiftCard'; export interface DraftServiceCall { price: number; currency: string; } export interface BookingFunnelGiftCardSubmitValues { serviceLocationData: FormCustomerDetailsValues; serviceBillingData: FormCustomerBillingDetailsValues; paymentData: PaymentFormValues; additionalData: { position: CoordsProps; receivesNewsletter: boolean; }; physical: boolean; discountAmount: number; } export interface BookingFunnelGiftCardProps { BookingForm?: ReactType; PaymentForm?: ReactType; ServiceDetailsForm?: ReactType; BillingForm?: ReactType; translations: { steps?: { step1: string; step2: string; step3: string; step4: string; }; serviceContactAndLocation: string; alreadyCustomer: string; loginHere: string; differingBillingAddress: string; invoiceAddress: string; payment: string; summary: string; dateTime: string; serviceLocation: string; voucher: string; total: string; summaryInfo: string; newsUpdates: string; accept: string; continueButton: string; bookFor: string; login: string; mapTitle: string; mapText: string; mapMarkerName: string; editAddressButton: string; useAddressButton: string; priceHappiness: string; bookError: string; close: string; resetPassword: string; selectAmount: string; byMail: string; byPdf: string; shipping: string; }; LoginForm?: ReactType; PasswordResetForm?: ReactType; onSubmit: (data: BookingFunnelGiftCardSubmitValues) => void; terms: LinkProps; LinkProps?: LinkProps; locale: string; googleApiKey?: string; deviceType?: string; isAuthenticated?: boolean; currencyCode: string; isPdf: boolean; availableAmounts: Array; } //add viual viewport to window so that typescript doesn't throw an errror declare global { interface Window { visualViewport: EventTarget; } } const BookingFunnelGiftCardTemplate: FC = ({ BookingForm, PaymentForm, BillingForm, translations, LoginForm, PasswordResetForm, onSubmit, terms, LinkProps, locale, googleApiKey, deviceType, isAuthenticated, currencyCode, isPdf = true, availableAmounts, }) => { // Reverse order for scrolling const isUk = locale.slice(3, 5).toLowerCase() === 'gb'; const steps = { step4: false, step3: false, step2: false, step1: true }; const forms = { select: true, customerDetails: true, billingDetails: true, paymentDetails: true, terms: true, }; const [getSteps, setSteps] = useState(steps); const [isDisabled, setDisabled] = useState(false); const [isLoading, setLoading] = useState(false); const [isBillingAddress, setBillingAddress] = useState(false); const [validForms, setValidForms] = useState(forms); const [position, setPosition] = useState(null); const [receivesNewsletter, setReceivesNewsletter] = useState(false); const [hideStickyFooter, setStickyFooter] = useState(false); const [isKeyboardOpenFallback, setKeyboardOpenFallback] = useState(false); const [submitError, setSubmitError] = useState(null); const isMobileOrTablet = deviceType === 'mobile' || deviceType === 'tablet'; const [amount, setAmount] = useState(availableAmounts[0]); const [serviceBillingData, setServiceBillingData] = useState< FormCustomerBillingDetailsValues >(Object({})); const [serviceLocationData, setServiceLocationData] = useState< FormCustomerDetailsValues >(Object({})); const [paymentData, setPaymentData] = useState(); const [initialValues, setInitialValues] = useState({ serviceBillingData: null, serviceLocationData: null, paymentMethods: null, paymentData: { paymentMethod: 'Invoice', } as unknown, }); const [stepTitle, setStepTitle] = useState(translations.steps.step1); const stepRefs: { [key: string]: RefObject } = {}; stepRefs['refStep1'] = useRef(null); stepRefs['refStep2'] = useRef(null); stepRefs['refStep3'] = useRef(null); stepRefs['refStep4'] = useRef(null); useEffect(() => { for (const key of Object.keys(getSteps)) { if (getSteps[key] == true) { const item = stepRefs[`${'ref' + key.charAt(0).toUpperCase() + key.slice(1)}`] .current; // Substract header height in offfset window.scrollTo({ top: item.offsetTop - 60, behavior: 'smooth', }); break; } } }, [getSteps]); const isValid = (valid): void => { setDisabled(!valid); }; useEffect(() => { isValid(Object.values(validForms).every(Boolean)); }, [validForms]); const setLocationData = (values: FormCustomerDetailsValues): void => { setServiceLocationData(values); if (!isBillingAddress) setServiceBillingData(values); }; const setBillingData = (values: FormCustomerBillingDetailsValues): void => { setServiceBillingData(values); }; const setPaymentFormData = (values: PaymentFormValues): void => { setPaymentData(values); }; const [addressModalProps, setAddressModalProps] = useState<{ isOpen: boolean; address: FormCustomerDetailsValues | FormCustomerBillingDetailsValues; isValidAddressCallback: Function; }>({ isOpen: false, address: null, isValidAddressCallback: Function(), }); const isValidGoogleAddress = useRef(true); const isValidBillingAddress = useRef(true); const openAddressValidationModal = (address, validRef): void => { setAddressModalProps({ isOpen: true, address: address, isValidAddressCallback: () => { validRef.current = true; }, }); }; const closeAddressValidationModal = (): void => { setAddressModalProps({ isOpen: false, address: null, isValidAddressCallback: Function(), }); }; const continueEvent = async (): Promise => { const updatedSteps = Object.assign({}, getSteps); const updateForms = Object.assign({}, validForms); if (isDisabled) return; if (!updatedSteps.step2) { updatedSteps.step2 = true; setDisabled(true); setSteps(updatedSteps); setStepTitle(translations.steps.step2); updateForms.customerDetails = false; setValidForms(updateForms); return; } if (updatedSteps.step2 && !isValidGoogleAddress.current) { await checkIfAddressIsValid( continueEvent, serviceLocationData, isValidGoogleAddress, openAddressValidationModal, setPosition, ); return; } if ( updatedSteps.step2 && !isValidBillingAddress.current && isBillingAddress ) { await checkIfAddressIsValid( continueEvent, serviceBillingData, isValidBillingAddress, openAddressValidationModal, setPosition, ); return; } if ( updatedSteps.step2 && !updatedSteps.step3 && isValidGoogleAddress.current ) { updatedSteps.step3 = true; setSteps(updatedSteps); setStepTitle(translations.steps.step3); return; } if (updatedSteps.step3 && !updatedSteps.step4) { updatedSteps.step4 = true; updateForms.terms = false; setValidForms(updateForms); setSteps(updatedSteps); setStepTitle(translations.steps.step4); return; } }; const [loginModalIsOpen, setLoginModalIsOpen] = useState(false); const [passwordResetModalIsOpen, setPasswordResetModalIsOpen] = useState( false, ); const openLoginModal = (): void => { setLoginModalIsOpen(true); }; const closeLoginModal = (): void => { setLoginModalIsOpen(false); }; const openPasswordResetModal = (): void => { setPasswordResetModalIsOpen(true); }; const closePasswordResetModal = (): void => { setPasswordResetModalIsOpen(false); }; const submitServiceCall = async (): Promise => { if (!isValidGoogleAddress.current) { await checkIfAddressIsValid( submitServiceCall, serviceLocationData, isValidGoogleAddress, openAddressValidationModal, setPosition, ); return; } if (!isValidBillingAddress.current && isBillingAddress) { await checkIfAddressIsValid( submitServiceCall, serviceBillingData, isValidBillingAddress, openAddressValidationModal, setPosition, ); return; } setSubmitError(null); setDisabled(true); setLoading(true); try { await onSubmit({ serviceLocationData, serviceBillingData, paymentData, discountAmount: parseInt(amount), physical: !isPdf, additionalData: { position, receivesNewsletter, }, }); } catch (e) { console.error(e); setSubmitError(e.message); setLoading(false); setDisabled(false); } }; // Check if virtual keyboard is open const [resizeHeight, setResizeHeight] = useState(null); const keyboardResizeEvent = (event): void => { const ua = navigator.userAgent.toLowerCase(); const isAndroid = ua.indexOf('android') > -1; if (isAndroid) { if (window.screen.height - event.target.height > 250) { setStickyFooter(true); } else { setStickyFooter(false); } } else { setResizeHeight(event.target.height); } }; useEffect(() => { if (resizeHeight != null && resizeHeight !== window.innerHeight) { setStickyFooter(true); } else { setStickyFooter(false); } }, [resizeHeight]); useEffect(() => { if (window.visualViewport && isMobileOrTablet) { window.visualViewport.addEventListener('resize', keyboardResizeEvent); return (): void => window.visualViewport.removeEventListener( 'resize', keyboardResizeEvent, ); } else { setKeyboardOpenFallback(true); } }, []); return (
{getSteps.step1 && (
{translations.steps.step1} { setAmount(amount); }} isPrint={isPdf} />
)} {getSteps.step2 && (
{translations.serviceContactAndLocation} {!isAuthenticated && (
{translations.alreadyCustomer} {translations.loginHere}
)} { closeLoginModal(); openPasswordResetModal(); }} /> { closePasswordResetModal(); openLoginModal(); }} closePasswordResetModal={closePasswordResetModal} />
{BookingForm && ( { isValidGoogleAddress.current = false; const updatedForms = Object.assign({}, validForms); updatedForms.customerDetails = bool; setValidForms(updatedForms); }} getFormData={setLocationData} initialValues={initialValues.serviceLocationData} checkIfKeyBoardOpen={(bool): void => { if (isMobileOrTablet && isKeyboardOpenFallback) setStickyFooter(bool); }} /> )}
{ setBillingAddress(e.target.checked); const updatedForms = Object.assign({}, validForms); updatedForms.billingDetails = !e.target.checked; setValidForms(updatedForms); if (!e.target.checked) setBillingData(Object.assign({}, serviceLocationData)); }} value={isBillingAddress} name="isBillingAddress" label={translations.differingBillingAddress} />
)} {isBillingAddress && (
{translations.invoiceAddress}
{BillingForm && ( { isValidBillingAddress.current = false; const updatedForms = Object.assign({}, validForms); updatedForms.billingDetails = bool; setValidForms(updatedForms); }} getFormData={setBillingData} initialValues={initialValues.serviceBillingData} checkIfKeyBoardOpen={(bool): void => { if (isMobileOrTablet && isKeyboardOpenFallback) setStickyFooter(bool); }} /> )}
)} {getSteps.step3 && (
{translations.payment}
{PaymentForm && ( { const updatedForms = Object.assign({}, validForms); updatedForms.paymentDetails = bool; updatedForms.terms = true; setValidForms(updatedForms); const updatedSteps = Object.assign({}, getSteps); if (updatedSteps.step4) { updatedSteps.step4 = false; setSteps(updatedSteps); setStepTitle(translations.steps.step3); } }} getFormData={setPaymentFormData} invoiceAddress={serviceBillingData} paymentMethods={initialValues.paymentMethods} checkIfKeyBoardOpen={(bool): void => { if (isMobileOrTablet && isKeyboardOpenFallback) setStickyFooter(bool); }} inputErrors={{}} /> )}
)} {getSteps.step4 && ( <>
{ setReceivesNewsletter(e.target.checked); }} /> { const updateForms = Object.assign({}, validForms); updateForms.terms = e.target.checked; setValidForms(updateForms); }} link={terms} />
)}
{!hideStickyFooter && (
{!getSteps.step4 ? ( <>
{isPdf ? translations.byPdf : translations.byMail}
{ {getAmountWithCurrency( isPdf ? amount : parseInt(amount) + 3, currencyCode, )} }
) : (
{ setSubmitError(null); }} actions={ } > {submitError}
)}
)}
); }; export default BookingFunnelGiftCardTemplate;