plugin updates

This commit is contained in:
Tony Volpe
2024-06-17 15:33:26 -04:00
parent 3751a5a1a6
commit e4e274a9a7
2674 changed files with 0 additions and 507851 deletions

View File

@@ -1,82 +0,0 @@
import $ from 'jquery';
import Raven from '../lib/Raven';
import { restNonce, restUrl } from '../constants/leadinConfig';
import { addQueryObjectToUrl } from '../utils/queryParams';
function makeRequest(
method: string,
path: string,
data: any = {},
queryParams = {}
): Promise<any> {
// eslint-disable-next-line compat/compat
const restApiUrl = new URL(`${restUrl}leadin/v1${path}`);
addQueryObjectToUrl(restApiUrl, queryParams);
return new Promise((resolve, reject) => {
const payload: { [key: string]: any } = {
url: restApiUrl.toString(),
method,
contentType: 'application/json',
beforeSend: (xhr: any) => xhr.setRequestHeader('X-WP-Nonce', restNonce),
success: resolve,
error: (response: any) => {
Raven.captureMessage(
`HTTP Request to ${restApiUrl} failed with error ${response.status}: ${response.responseText}`,
{
fingerprint: [
'{{ default }}',
path,
response.status,
response.responseText,
],
}
);
reject(response);
},
};
if (method !== 'get') {
payload.data = JSON.stringify(data);
}
$.ajax(payload);
});
}
export function healthcheckRestApi() {
return makeRequest('get', '/healthcheck');
}
export function disableInternalTracking(value: boolean) {
return makeRequest('put', '/internal-tracking', value ? '1' : '0');
}
export function fetchDisableInternalTracking() {
return makeRequest('get', '/internal-tracking').then(message => ({
message,
}));
}
export function updateHublet(hublet: string) {
return makeRequest('put', '/hublet', { hublet });
}
export function skipReview() {
return makeRequest('post', '/skip-review');
}
export function trackConsent(canTrack: boolean) {
return makeRequest('post', '/track-consent', { canTrack }).then(message => ({
message,
}));
}
export function setBusinessUnitId(businessUnitId: number) {
return makeRequest('put', '/business-unit', { businessUnitId });
}
export function getBusinessUnitId() {
return makeRequest('get', '/business-unit');
}

View File

@@ -1,35 +0,0 @@
import { __ } from '@wordpress/i18n';
const REGISTRATION_FORM = 'REGISTRATION_FORM';
const CONTACT_US_FORM = 'CONTACT_US_FORM';
const NEWSLETTER_FORM = 'NEWSLETTER_FORM';
const SUPPORT_FORM = 'SUPPORT_FORM';
const EVENT_FORM = 'EVENT_FORM';
export type FormType =
| typeof REGISTRATION_FORM
| typeof CONTACT_US_FORM
| typeof NEWSLETTER_FORM
| typeof SUPPORT_FORM
| typeof EVENT_FORM;
export const DEFAULT_OPTIONS = {
label: __('Templates', 'leadin'),
options: [
{ label: __('Registration Form', 'leadin'), value: REGISTRATION_FORM },
{ label: __('Contact us Form', 'leadin'), value: CONTACT_US_FORM },
{ label: __('Newsletter sign-up Form', 'leadin'), value: NEWSLETTER_FORM },
{ label: __('Support Form', 'leadin'), value: SUPPORT_FORM },
{ label: __('Event Registration Form', 'leadin'), value: EVENT_FORM },
],
};
export function isDefaultForm(value: FormType) {
return (
value === REGISTRATION_FORM ||
value === CONTACT_US_FORM ||
value === NEWSLETTER_FORM ||
value === SUPPORT_FORM ||
value === EVENT_FORM
);
}

View File

@@ -1,138 +0,0 @@
interface KeyStringObject {
[key: string]: string;
}
export type ContentEmbedDetails = {
activated: boolean;
installed: boolean;
canActivate: boolean;
canInstall: boolean;
nonce: string;
};
export interface LeadinConfig {
accountName: string;
adminUrl: string;
activationTime: string;
connectionStatus?: 'Connected' | 'NotConnected';
deviceId: string;
didDisconnect: '1' | '0';
env: string;
formsScript: string;
meetingsScript: string;
formsScriptPayload: string;
hublet: string;
hubspotBaseUrl: string;
hubspotNonce: string;
iframeUrl: string;
impactLink?: string;
lastAuthorizeTime: string;
lastDeauthorizeTime: string;
lastDisconnectTime: string;
leadinPluginVersion: string;
leadinQueryParams: KeyStringObject;
loginUrl: string;
locale: string;
phpVersion: string;
pluginPath: string;
plugins: KeyStringObject;
portalDomain: string;
portalEmail: string;
portalId: number;
redirectNonce: string;
restNonce: string;
restUrl: string;
reviewSkippedDate: string;
refreshToken?: string;
theme: string;
trackConsent?: boolean | string;
wpVersion: string;
contentEmbed: ContentEmbedDetails;
requiresContentEmbedScope?: boolean;
refreshTokenError?: string;
}
const {
accountName,
adminUrl,
activationTime,
connectionStatus,
deviceId,
didDisconnect,
env,
formsScript,
meetingsScript,
formsScriptPayload,
hublet,
hubspotBaseUrl,
hubspotNonce,
iframeUrl,
impactLink,
lastAuthorizeTime,
lastDeauthorizeTime,
lastDisconnectTime,
leadinPluginVersion,
leadinQueryParams,
locale,
loginUrl,
phpVersion,
pluginPath,
plugins,
portalDomain,
portalEmail,
portalId,
redirectNonce,
restNonce,
restUrl,
refreshToken,
reviewSkippedDate,
theme,
trackConsent,
wpVersion,
contentEmbed,
requiresContentEmbedScope,
refreshTokenError,
}: //@ts-expect-error global
LeadinConfig = window.leadinConfig;
export {
accountName,
adminUrl,
activationTime,
connectionStatus,
deviceId,
didDisconnect,
env,
formsScript,
meetingsScript,
formsScriptPayload,
hublet,
hubspotBaseUrl,
hubspotNonce,
iframeUrl,
impactLink,
lastAuthorizeTime,
lastDeauthorizeTime,
lastDisconnectTime,
leadinPluginVersion,
leadinQueryParams,
loginUrl,
locale,
phpVersion,
pluginPath,
plugins,
portalDomain,
portalEmail,
portalId,
redirectNonce,
restNonce,
restUrl,
refreshToken,
reviewSkippedDate,
theme,
trackConsent,
wpVersion,
contentEmbed,
requiresContentEmbedScope,
refreshTokenError,
};

View File

@@ -1,19 +0,0 @@
export const domElements = {
iframe: '#leadin-iframe',
subMenu: '.toplevel_page_leadin > ul',
subMenuLinks: '.toplevel_page_leadin > ul a',
subMenuButtons: '.toplevel_page_leadin > ul > li',
deactivatePluginButton: '[data-slug="leadin"] .deactivate a',
deactivateFeedbackForm: 'form.leadin-deactivate-form',
deactivateFeedbackSubmit: 'button#leadin-feedback-submit',
deactivateFeedbackSkip: 'button#leadin-feedback-skip',
thickboxModalClose: '.leadin-modal-close',
thickboxModalWindow: 'div#TB_window.thickbox-loading',
thickboxModalContent: 'div#TB_ajaxContent.TB_modal',
reviewBannerContainer: '#leadin-review-banner',
reviewBannerLeaveReviewLink: 'a#leave-review-button',
reviewBannerDismissButton: 'a#dismiss-review-banner-button',
leadinIframeContainer: 'leadin-iframe-container',
leadinIframe: 'leadin-iframe',
leadinIframeFallbackContainer: 'leadin-iframe-fallback-container',
};

View File

@@ -1,22 +0,0 @@
import React from 'react';
import ElementorBanner from './ElementorBanner';
import { __ } from '@wordpress/i18n';
export default function ConnectPluginBanner() {
return (
<ElementorBanner>
<b
dangerouslySetInnerHTML={{
__html: __(
'The HubSpot plugin is not connected right now To use HubSpot tools on your WordPress site, %1$sconnect the plugin now%2$s'
)
.replace(
'%1$s',
'<a class="leadin-banner__link" href="admin.php?page=leadin&bannerClick=true">'
)
.replace('%2$s', '</a>'),
}}
></b>
</ElementorBanner>
);
}

View File

@@ -1,20 +0,0 @@
import React from 'react';
interface IElementorBannerProps {
type?: string;
}
export default function ElementorBanner({
type = 'warning',
children,
}: React.PropsWithChildren<IElementorBannerProps>) {
return (
<div className="elementor-control-content">
<div
className={`elementor-control-raw-html elementor-panel-alert elementor-panel-alert-${type}`}
>
{children}
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import { styled } from '@linaria/react';
import React from 'react';
const Container = styled.div`
display: flex;
justify-content: center;
padding-bottom: 8px;
`;
export default function ElementorButton({
children,
...params
}: React.PropsWithChildren<React.ButtonHTMLAttributes<HTMLButtonElement>>) {
return (
<Container className="elementor-button-wrapper">
<button
className="elementor-button elementor-button-default"
type="button"
{...params}
>
{children}
</button>
</Container>
);
}

View File

@@ -1,25 +0,0 @@
import { styled } from '@linaria/react';
interface IElementorWrapperProps {
pluginPath?: string;
}
export default styled.div<IElementorWrapperProps>`
background-image: ${props =>
`url(${props.pluginPath}/plugin/assets/images/hubspot.svg)`};
background-color: #f5f8fa;
background-repeat: no-repeat;
background-position: center 25px;
background-size: 120px;
color: #33475b;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-size: 14px;
padding: ${(props: any) => props.padding || '90px 20% 25px'};
p {
font-size: inherit !important;
line-height: 24px;
margin: 4px 0;
}
`;

View File

@@ -1,86 +0,0 @@
import React, { useState, useEffect, Fragment } from 'react';
import { portalId, refreshToken } from '../../constants/leadinConfig';
import ElementorBanner from '../Common/ElementorBanner';
import UISpinner from '../../shared/UIComponents/UISpinner';
import { __ } from '@wordpress/i18n';
import {
BackgroudAppContext,
useBackgroundAppContext,
} from '../../iframe/useBackgroundApp';
import useForms from './hooks/useForms';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IElementorFormSelectProps {
formId: string;
setAttributes: Function;
}
function ElementorFormSelect({
formId,
setAttributes,
}: IElementorFormSelectProps) {
const { hasError, forms, loading } = useForms();
return loading ? (
<div>
<UISpinner />
</div>
) : hasError ? (
<ElementorBanner type="danger">
{__('Please refresh your forms or try again in a few minutes', 'leadin')}
</ElementorBanner>
) : (
<select
value={formId}
onChange={event => {
const selectedForm = forms.find(
form => form.value === event.target.value
);
if (selectedForm) {
setAttributes({
portalId,
formId: selectedForm.value,
formName: selectedForm.label,
});
}
}}
>
<option value="" disabled={true} selected={true}>
{__('Search for a form', 'leadin')}
</option>
{forms.map(form => (
<option key={form.value} value={form.value}>
{form.label}
</option>
))}
</select>
);
}
function ElementorFormSelectWrapper(props: IElementorFormSelectProps) {
const isBackgroundAppReady = useBackgroundAppContext();
return (
<Fragment>
{!isBackgroundAppReady ? (
<div>
<UISpinner />
</div>
) : (
<ElementorFormSelect {...props} />
)}
</Fragment>
);
}
export default function ElementorFormSelectContainer(
props: IElementorFormSelectProps
) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<ElementorFormSelectWrapper {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,31 +0,0 @@
import React, { Fragment } from 'react';
import { connectionStatus } from '../../constants/leadinConfig';
import ConnectPluginBanner from '../Common/ConnectPluginBanner';
import ElementorFormSelect from './ElementorFormSelect';
import { IFormAttributes } from './registerFormWidget';
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
};
export default function FormControlController(
attributes: IFormAttributes,
setValue: Function
) {
return () => {
const render = () => {
if (connectionStatus === ConnectionStatus.Connected) {
return (
<ElementorFormSelect
formId={attributes.formId}
setAttributes={setValue}
/>
);
} else {
return <ConnectPluginBanner />;
}
};
return <Fragment>{render()}</Fragment>;
};
}

View File

@@ -1,30 +0,0 @@
import React, { Fragment } from 'react';
import { connectionStatus } from '../../constants/leadinConfig';
import ErrorHandler from '../../shared/Common/ErrorHandler';
import FormEdit from '../../shared/Form/FormEdit';
import ConnectionStatus from '../../shared/enums/connectionStatus';
import { IFormAttributes } from './registerFormWidget';
export default function FormWidgetController(
attributes: IFormAttributes,
setValue: Function
) {
return () => {
const render = () => {
if (connectionStatus === ConnectionStatus.Connected) {
return (
<FormEdit
attributes={attributes}
isSelected={true}
setAttributes={setValue}
preview={false}
origin="elementor"
/>
);
} else {
return <ErrorHandler status={401} />;
}
};
return <Fragment>{render()}</Fragment>;
};
}

View File

@@ -1,45 +0,0 @@
import { useState, useEffect } from 'react';
import LoadState, { LoadStateType } from '../../../shared/enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import { IForm } from '../../../shared/types';
interface FormOption {
label: string;
value: string;
}
export default function useForms() {
const proxy = usePostAsyncBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [hasError, setError] = useState(null);
const [forms, setForms] = useState<FormOption[]>([]);
useEffect(() => {
if (loadState === LoadState.NotLoaded) {
proxy({
key: ProxyMessages.FetchForms,
payload: {
search: '',
},
})
.then(data => {
setForms(
data.map((form: IForm) => ({
label: form.name,
value: form.guid,
}))
);
setLoadState(LoadState.Loaded);
})
.catch(error => {
setError(error);
setLoadState(LoadState.Failed);
});
}
}, [loadState]);
return { forms, loading: loadState === LoadState.Loading, hasError };
}

View File

@@ -1,44 +0,0 @@
import ReactDOM from 'react-dom';
import FormControlController from './FormControlController';
import FormWidgetController from './FormWidgetController';
export interface IFormAttributes {
formId: string;
formName: string;
portalId: string;
}
export default class registerFormWidget {
widgetContainer: Element;
attributes: IFormAttributes;
controlContainer: Element;
setValue: Function;
constructor(controlContainer: any, widgetContainer: any, setValue: Function) {
const attributes = widgetContainer.dataset.attributes
? JSON.parse(widgetContainer.dataset.attributes)
: {};
this.widgetContainer = widgetContainer;
this.controlContainer = controlContainer;
this.setValue = setValue;
this.attributes = attributes;
}
render() {
ReactDOM.render(
FormWidgetController(this.attributes, this.setValue)(),
this.widgetContainer
);
ReactDOM.render(
FormControlController(this.attributes, this.setValue)(),
this.controlContainer
);
}
done() {
ReactDOM.unmountComponentAtNode(this.widgetContainer);
ReactDOM.unmountComponentAtNode(this.controlContainer);
}
}

View File

@@ -1,122 +0,0 @@
import React, { Fragment, useState } from 'react';
import ElementorBanner from '../Common/ElementorBanner';
import UISpinner from '../../shared/UIComponents/UISpinner';
import ElementorMeetingWarning from './ElementorMeetingWarning';
import useMeetings, {
useSelectedMeetingCalendar,
} from '../../shared/Meeting/hooks/useMeetings';
import { __ } from '@wordpress/i18n';
import Raven from 'raven-js';
import {
BackgroudAppContext,
useBackgroundAppContext,
} from '../../iframe/useBackgroundApp';
import { refreshToken } from '../../constants/leadinConfig';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IElementorMeetingSelectProps {
url: string;
setAttributes: Function;
}
function ElementorMeetingSelect({
url,
setAttributes,
}: IElementorMeetingSelectProps) {
const {
mappedMeetings: meetings,
loading,
error,
reload,
connectCalendar,
} = useMeetings();
const selectedMeetingCalendar = useSelectedMeetingCalendar(url);
const [localUrl, setLocalUrl] = useState(url);
const handleConnectCalendar = () => {
return connectCalendar()
.then(() => {
reload();
})
.catch(error => {
Raven.captureMessage('Unable to connect calendar', {
extra: { error },
});
});
};
return (
<Fragment>
{loading ? (
<div>
<UISpinner />
</div>
) : error ? (
<ElementorBanner type="danger">
{__(
'Please refresh your meetings or try again in a few minutes',
'leadin'
)}
</ElementorBanner>
) : (
<Fragment>
{selectedMeetingCalendar && (
<ElementorMeetingWarning
status={selectedMeetingCalendar}
onConnectCalendar={connectCalendar}
/>
)}
{meetings.length > 1 && (
<select
value={localUrl}
onChange={event => {
const newUrl = event.target.value;
setLocalUrl(newUrl);
setAttributes({
url: newUrl,
});
}}
>
<option value="" disabled={true} selected={true}>
{__('Select a meeting', 'leadin')}
</option>
{meetings.map(item => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
)}
</Fragment>
)}
</Fragment>
);
}
function ElementorMeetingSelectWrapper(props: IElementorMeetingSelectProps) {
const isBackgroundAppReady = useBackgroundAppContext();
return (
<Fragment>
{!isBackgroundAppReady ? (
<div>
<UISpinner />
</div>
) : (
<ElementorMeetingSelect {...props} />
)}
</Fragment>
);
}
export default function ElementorMeetingsSelectContainer(
props: IElementorMeetingSelectProps
) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<ElementorMeetingSelectWrapper {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,53 +0,0 @@
import React, { Fragment } from 'react';
import { CURRENT_USER_CALENDAR_MISSING } from '../../shared/Meeting/constants';
import ElementorButton from '../Common/ElementorButton';
import ElementorBanner from '../Common/ElementorBanner';
import { styled } from '@linaria/react';
import { __ } from '@wordpress/i18n';
const Container = styled.div`
padding-bottom: 8px;
`;
interface IMeetingWarningPros {
onConnectCalendar: React.MouseEventHandler<HTMLButtonElement>;
status: string;
}
export default function MeetingWarning({
onConnectCalendar,
status,
}: IMeetingWarningPros) {
const isMeetingOwner = status === CURRENT_USER_CALENDAR_MISSING;
const titleText = isMeetingOwner
? __('Your calendar is not connected', 'leadin')
: __('Calendar is not connected', 'leadin');
const titleMessage = isMeetingOwner
? __(
'Please connect your calendar to activate your scheduling pages',
'leadin'
)
: __(
'Make sure that everybody in this meeting has connected their calendar from the Meetings page in HubSpot',
'leadin'
);
return (
<Fragment>
<Container>
<ElementorBanner type="warning">
<b>{titleText}</b>
<br />
{titleMessage}
</ElementorBanner>
</Container>
{isMeetingOwner && (
<ElementorButton
id="meetings-connect-calendar"
onClick={onConnectCalendar}
>
{__('Connect calendar', 'leadin')}
</ElementorButton>
)}
</Fragment>
);
}

View File

@@ -1,31 +0,0 @@
import React, { Fragment } from 'react';
import { connectionStatus } from '../../constants/leadinConfig';
import ConnectPluginBanner from '../Common/ConnectPluginBanner';
import ElementorMeetingSelect from './ElementorMeetingSelect';
import { IMeetingAttributes } from './registerMeetingWidget';
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
};
export default function MeetingControlController(
attributes: IMeetingAttributes,
setValue: Function
) {
return () => {
const render = () => {
if (connectionStatus === ConnectionStatus.Connected) {
return (
<ElementorMeetingSelect
url={attributes.url}
setAttributes={setValue}
/>
);
} else {
return <ConnectPluginBanner />;
}
};
return <Fragment>{render()}</Fragment>;
};
}

View File

@@ -1,34 +0,0 @@
import React, { Fragment } from 'react';
import { connectionStatus } from '../../constants/leadinConfig';
import ErrorHandler from '../../shared/Common/ErrorHandler';
import MeetingsEdit from '../../shared/Meeting/MeetingEdit';
import { IMeetingAttributes } from './registerMeetingWidget';
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
};
export default function MeetingWidgetController(
attributes: IMeetingAttributes,
setValue: Function
) {
return () => {
const render = () => {
if (connectionStatus === ConnectionStatus.Connected) {
return (
<MeetingsEdit
attributes={attributes}
isSelected={true}
setAttributes={setValue}
preview={false}
origin="elementor"
/>
);
} else {
return <ErrorHandler status={401} />;
}
};
return <Fragment>{render()}</Fragment>;
};
}

View File

@@ -1,42 +0,0 @@
import ReactDOM from 'react-dom';
import MeetingControlController from './MeetingControlController';
import MeetingWidgetController from './MeetingWidgetController';
export interface IMeetingAttributes {
url: string;
}
export default class registerMeetingsWidget {
widgetContainer: Element;
controlContainer: Element;
setValue: Function;
attributes: IMeetingAttributes;
constructor(controlContainer: any, widgetContainer: any, setValue: Function) {
const attributes = widgetContainer.dataset.attributes
? JSON.parse(widgetContainer.dataset.attributes)
: {};
this.widgetContainer = widgetContainer;
this.controlContainer = controlContainer;
this.setValue = setValue;
this.attributes = attributes;
}
render() {
ReactDOM.render(
MeetingWidgetController(this.attributes, this.setValue)(),
this.widgetContainer
);
ReactDOM.render(
MeetingControlController(this.attributes, this.setValue)(),
this.controlContainer
);
}
done() {
ReactDOM.unmountComponentAtNode(this.widgetContainer);
ReactDOM.unmountComponentAtNode(this.controlContainer);
}
}

View File

@@ -1,46 +0,0 @@
export default function elementorWidget(
elementor: any,
options: any,
callback: Function,
done = () => {}
) {
return elementor.modules.controls.BaseData.extend({
onReady() {
const self = this;
const controlContainer = this.ui.contentEditable.prevObject[0].querySelector(
options.controlSelector
);
let widgetContainer = this.options.element.$el[0].querySelector(
options.containerSelector
);
if (widgetContainer) {
callback(controlContainer, widgetContainer, (args: any) =>
self.setValue(args)
);
} else {
//@ts-expect-error global
window.elementorFrontend.hooks.addAction(
`frontend/element_ready/${options.widgetName}.default`,
(element: HTMLElement[]) => {
widgetContainer = element[0].querySelector(
options.containerSelector
);
callback(controlContainer, widgetContainer, (args: any) =>
self.setValue(args)
);
}
);
}
},
saveValue(props: any) {
this.setValue(props);
},
onBeforeDestroy() {
//@ts-expect-error global
window.elementorFrontend.hooks.removeAction(
`frontend/element_ready/${options.widgetName}.default`
);
done();
},
});
}

View File

@@ -1,4 +0,0 @@
import { initAppOnReady } from '../utils/appUtils';
import renderIframeApp from '../iframe/renderIframeApp';
initAppOnReady(renderIframeApp);

View File

@@ -1,79 +0,0 @@
import elementorWidget from '../elementor/elementorWidget';
import registerFormWidget from '../elementor/FormWidget/registerFormWidget';
import { initBackgroundApp } from '../utils/backgroundAppUtils';
import registerMeetingsWidget from '../elementor/MeetingWidget/registerMeetingWidget';
const ELEMENTOR_READY_INTERVAL = 500;
const MAX_POLL_TIMEOUT = 30000;
const registerElementorWidgets = () => {
initBackgroundApp(() => {
let FormWidget: any;
let MeetingsWidget: any;
const leadinSelectFormItemView = elementorWidget(
//@ts-expect-error global
window.elementor,
{
widgetName: 'hubspot-form',
controlSelector: '.elementor-hbspt-form-selector',
containerSelector: '.hubspot-form-edit-mode',
},
(controlContainer: any, widgetContainer: any, setValue: Function) => {
FormWidget = new registerFormWidget(
controlContainer,
widgetContainer,
setValue
);
FormWidget.render();
},
() => {
FormWidget.done();
}
);
const leadinSelectMeetingItemView = elementorWidget(
//@ts-expect-error global
window.elementor,
{
widgetName: 'hubspot-meeting',
controlSelector: '.elementor-hbspt-meeting-selector',
containerSelector: '.hubspot-meeting-edit-mode',
},
(controlContainer: any, widgetContainer: any, setValue: Function) => {
MeetingsWidget = new registerMeetingsWidget(
controlContainer,
widgetContainer,
setValue
);
MeetingsWidget.render();
},
() => {
MeetingsWidget.done();
}
);
//@ts-expect-error global
window.elementor.addControlView(
'leadinformselect',
leadinSelectFormItemView
);
//@ts-expect-error global
window.elementor.addControlView(
'leadinmeetingselect',
leadinSelectMeetingItemView
);
});
};
const pollForElementorReady = setInterval(() => {
const elementorFrontend = (window as any).elementorFrontend;
if (elementorFrontend) {
registerElementorWidgets();
clearInterval(pollForElementorReady);
}
}, ELEMENTOR_READY_INTERVAL);
setTimeout(() => {
clearInterval(pollForElementorReady);
}, MAX_POLL_TIMEOUT);

View File

@@ -1,68 +0,0 @@
import $ from 'jquery';
import Raven from '../lib/Raven';
import { domElements } from '../constants/selectors';
import ThickBoxModal from '../feedback/ThickBoxModal';
import { submitFeedbackForm } from '../feedback/feedbackFormApi';
import {
getOrCreateBackgroundApp,
initBackgroundApp,
} from '../utils/backgroundAppUtils';
import { refreshToken } from '../constants/leadinConfig';
import { ProxyMessages } from '../iframe/integratedMessages';
function deactivatePlugin() {
const href = $(domElements.deactivatePluginButton).attr('href');
if (href) {
window.location.href = href;
}
}
function setLoadingState() {
$(domElements.deactivateFeedbackSubmit).addClass('loading');
}
function submitAndDeactivate(e: Event) {
e.preventDefault();
setLoadingState();
const feedback = $(domElements.deactivateFeedbackForm)
.serializeArray()
.find(field => field.name === 'feedback');
submitFeedbackForm(domElements.deactivateFeedbackForm)
.then(() => {
if (feedback && refreshToken) {
const embedder = getOrCreateBackgroundApp(refreshToken);
embedder.postMessage({
key: ProxyMessages.TrackPluginDeactivation,
payload: {
type: feedback.value.trim().replace(/[\s']+/g, '_'),
},
});
}
})
.catch((err: Error) => {
Raven.captureException(err);
})
.finally(() => {
deactivatePlugin();
});
}
function init() {
// eslint-disable-next-line no-new
new ThickBoxModal(
domElements.deactivatePluginButton,
'leadin-feedback-container',
'leadin-feedback-window',
'leadin-feedback-content'
);
$(domElements.deactivateFeedbackForm)
.off('submit')
.on('submit', submitAndDeactivate);
$(domElements.deactivateFeedbackSkip)
.off('click')
.on('click', deactivatePlugin);
}
initBackgroundApp(init);

View File

@@ -1,10 +0,0 @@
import registerFormBlock from '../gutenberg/FormBlock/registerFormBlock';
import { registerHubspotSidebar } from '../gutenberg/Sidebar/contentType';
import registerMeetingBlock from '../gutenberg/MeetingsBlock/registerMeetingBlock';
import { initBackgroundApp } from '../utils/backgroundAppUtils';
initBackgroundApp([
registerFormBlock,
registerMeetingBlock,
registerHubspotSidebar,
]);

View File

@@ -1,64 +0,0 @@
import $ from 'jquery';
import {
getOrCreateBackgroundApp,
initBackgroundApp,
} from '../utils/backgroundAppUtils';
import { domElements } from '../constants/selectors';
import { refreshToken, activationTime } from '../constants/leadinConfig';
import { ProxyMessages } from '../iframe/integratedMessages';
const REVIEW_BANNER_INTRO_PERIOD_DAYS = 15;
const userIsAfterIntroductoryPeriod = () => {
const activationDate = new Date(+activationTime * 1000);
const currentDate = new Date();
const timeElapsed = new Date(
currentDate.getTime() - activationDate.getTime()
);
return timeElapsed.getUTCDate() - 1 >= REVIEW_BANNER_INTRO_PERIOD_DAYS;
};
/**
* Adds some methods to window when review banner is
* displayed to monitor events
*/
export function initMonitorReviewBanner() {
if (refreshToken) {
const embedder = getOrCreateBackgroundApp(refreshToken);
const container = $(domElements.reviewBannerContainer);
if (container && userIsAfterIntroductoryPeriod()) {
$(domElements.reviewBannerLeaveReviewLink)
.off('click')
.on('click', () => {
embedder.postMessage({
key: ProxyMessages.TrackReviewBannerInteraction,
});
});
$(domElements.reviewBannerDismissButton)
.off('click')
.on('click', () => {
embedder.postMessage({
key: ProxyMessages.TrackReviewBannerDismissed,
});
});
embedder
.postAsyncMessage({
key: ProxyMessages.FetchContactsCreateSinceActivation,
payload: +activationTime * 1000,
})
.then(({ total }: any) => {
if (total >= 5) {
container.removeClass('leadin-review-banner--hide');
embedder.postMessage({
key: ProxyMessages.TrackReviewBannerRender,
});
}
});
}
}
}
initBackgroundApp(initMonitorReviewBanner);

View File

@@ -1,50 +0,0 @@
import $ from 'jquery';
import { domElements } from '../constants/selectors';
export default class ThickBoxModal {
openTriggerSelector: string;
inlineContentId: string;
windowCssClass: string;
contentCssClass: string;
constructor(
openTriggerSelector: string,
inlineContentId: string,
windowCssClass: string,
contentCssClass: string
) {
this.openTriggerSelector = openTriggerSelector;
this.inlineContentId = inlineContentId;
this.windowCssClass = windowCssClass;
this.contentCssClass = contentCssClass;
$(openTriggerSelector).on('click', this.init.bind(this));
}
close() {
//@ts-expect-error global
window.tb_remove();
}
init(e: Event) {
//@ts-expect-error global
window.tb_show(
'',
`#TB_inline?inlineId=${this.inlineContentId}&modal=true`
);
// thickbox doesn't respect the width and height url parameters https://core.trac.wordpress.org/ticket/17249
// We override thickboxes css with !important in the css
$(domElements.thickboxModalWindow).addClass(this.windowCssClass);
// have to modify the css of the thickbox content container as well
$(domElements.thickboxModalContent).addClass(this.contentCssClass);
// we unbind previous handlers because a thickbox modal is a single global object.
// Everytime it is re-opened, it still has old handlers bound
$(domElements.thickboxModalClose)
.off('click')
.on('click', this.close);
e.preventDefault();
}
}

View File

@@ -1,23 +0,0 @@
import $ from 'jquery';
const portalId = '6275621';
const formId = '0e8807f8-2ac3-4664-b742-44552bfa09e2';
const formSubmissionUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formId}`;
export function submitFeedbackForm(formSelector: string) {
const formSubmissionPayload = {
fields: $(formSelector).serializeArray(),
skipValidation: true,
};
return new Promise((resolve, reject) => {
$.ajax({
type: 'POST',
url: formSubmissionUrl,
contentType: 'application/json',
data: JSON.stringify(formSubmissionPayload),
success: resolve,
error: reject,
});
});
}

View File

@@ -1,27 +0,0 @@
import React from 'react';
export default function CalendarIcon() {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_903_1965)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.519 2.48009H15.069H15.0697C16.2619 2.48719 17.2262 3.45597 17.2262 4.65016V12.7434C17.223 12.9953 17.1203 13.2226 16.9549 13.3886L12.6148 17.7287C12.4488 17.8941 12.2214 17.9968 11.9689 18H3.29508C2.09637 18 1.125 17.0286 1.125 15.8299V4.65016C1.125 3.45404 2.09314 2.48396 3.28862 2.48009H4.83867V0.930032C4.83867 0.416577 5.25525 0 5.7687 0C6.28216 0 6.69874 0.416577 6.69874 0.930032V2.48009H11.6589V0.930032C11.6589 0.416577 12.0755 0 12.5889 0C13.1024 0 13.519 0.416577 13.519 0.930032V2.48009ZM2.98506 15.8312C2.99863 15.9882 3.12909 16.1115 3.28862 16.1141H11.5814L11.6589 16.0366V13.634C11.6589 12.9494 12.2143 12.394 12.899 12.394H15.2951L15.3726 12.3165V7.4338H2.98506V15.8312ZM4.83868 8.68029H6.07873H6.07937C6.42684 8.68029 6.71037 8.95478 6.72458 9.30032V14.2863C6.72458 14.6428 6.43524 14.9322 6.07873 14.9322H4.83868C4.48217 14.9322 4.19283 14.6428 4.19283 14.2863V9.32615C4.19283 8.96964 4.48217 8.68029 4.83868 8.68029ZM8.53298 8.68029H9.82469H9.82534C10.1728 8.68029 10.4563 8.95478 10.4705 9.30032V14.2863C10.4705 14.6428 10.1812 14.9322 9.82469 14.9322H8.53298C8.17647 14.9322 7.88712 14.6428 7.88712 14.2863V9.32615C7.88712 8.96964 8.17647 8.68029 8.53298 8.68029ZM13.519 8.68029H12.2789C11.9366 8.68029 11.6589 8.95801 11.6589 9.30032V10.5404C11.6589 10.8827 11.9366 11.1604 12.2789 11.1604H13.519C13.8613 11.1604 14.139 10.8827 14.139 10.5404V9.30032C14.139 8.95801 13.8613 8.68029 13.519 8.68029Z"
fill="#FF7A59"
/>
</g>
<defs>
<clipPath id="clip0_903_1965">
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@@ -1,20 +0,0 @@
import React from 'react';
export default function SidebarSprocketIcon() {
return (
<svg
width="20px"
height="20px"
version="1.1"
viewBox="0 0 40 42"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<path
d="M28.8989809,30.0402293 C25.817707,30.0402293 23.319363,27.5423949 23.319363,24.461121 C23.319363,21.3798471 25.817707,18.881758 28.8989809,18.881758 C31.98,18.881758 34.4780892,21.3798471 34.4780892,24.461121 C34.4780892,27.5423949 31.98,30.0402293 28.8989809,30.0402293 M30.5692994,13.7199745 L30.5692994,8.75717196 C31.864586,8.14519744 32.7723567,6.8346242 32.7723567,5.31360508 L32.7723567,5.1989554 C32.7723567,3.10010191 31.0546497,1.38264968 28.956051,1.38264968 L28.8414013,1.38264968 C26.7425478,1.38264968 25.0248408,3.10010191 25.0248408,5.1989554 L25.0248408,5.31360508 C25.0248408,6.8346242 25.9328662,8.14519744 27.2281529,8.75717196 L27.2281529,13.7202293 C25.2994904,14.0180637 23.5371974,14.8137325 22.0829299,15.9844331 L8.45643312,5.38417836 C8.54611464,5.0392102 8.6090446,4.6835414 8.60955416,4.310293 C8.61261148,1.93271338 6.68777072,0.00303184713 4.31019108,-2.5477707e-05 C1.93286624,-0.00308280255 0.0029299363,1.92175796 0.000127388535,4.29933756 C-0.0029299363,6.67666244 1.92191083,8.60634396 4.29949044,8.60940128 C5.07426752,8.6104204 5.7912102,8.390293 6.42,8.03284076 L19.8243312,18.4603567 C18.6842038,20.181121 18.0166879,22.2422675 18.0166879,24.461121 C18.0166879,26.7841784 18.7504458,28.9327134 19.9907006,30.7001019 L15.9142675,34.776535 C15.5919745,34.6799745 15.2574522,34.6122038 14.9033121,34.6122038 C12.9499363,34.6122038 11.3659873,36.1961529 11.3659873,38.1497834 C11.3659873,40.103414 12.9499363,41.6871084 14.9033121,41.6871084 C16.8571974,41.6871084 18.4408917,40.103414 18.4408917,38.1497834 C18.4408917,37.7958981 18.3733758,37.461121 18.2765605,37.1390828 L22.3089172,33.1067261 C24.1392357,34.5041784 26.4184713,35.3431592 28.8989809,35.3431592 C34.9089172,35.3431592 39.7810191,30.4710573 39.7810191,24.461121 C39.7810191,19.0203567 35.7840764,14.5255796 30.5692994,13.7199745"
id="Fill-1"
fillRule="evenodd"
/>
</svg>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
export default function SprocketIcon() {
return (
<svg
width="40px"
height="42px"
viewBox="0 0 40 42"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<polygon
id="path-1"
points="0.000123751494 0 39.7808917 0 39.7808917 41.6871084 0.000123751494 41.6871084"
/>
</defs>
<g
id="Page-1"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<g id="HubSpot-Sprocket---Full-Color">
<mask id="mask-2" fill="white">
<use xlinkHref="#path-1" />
</mask>
<g id="path-1" />
<path
d="M28.8989809,30.0402293 C25.817707,30.0402293 23.319363,27.5423949 23.319363,24.461121 C23.319363,21.3798471 25.817707,18.881758 28.8989809,18.881758 C31.98,18.881758 34.4780892,21.3798471 34.4780892,24.461121 C34.4780892,27.5423949 31.98,30.0402293 28.8989809,30.0402293 M30.5692994,13.7199745 L30.5692994,8.75717196 C31.864586,8.14519744 32.7723567,6.8346242 32.7723567,5.31360508 L32.7723567,5.1989554 C32.7723567,3.10010191 31.0546497,1.38264968 28.956051,1.38264968 L28.8414013,1.38264968 C26.7425478,1.38264968 25.0248408,3.10010191 25.0248408,5.1989554 L25.0248408,5.31360508 C25.0248408,6.8346242 25.9328662,8.14519744 27.2281529,8.75717196 L27.2281529,13.7202293 C25.2994904,14.0180637 23.5371974,14.8137325 22.0829299,15.9844331 L8.45643312,5.38417836 C8.54611464,5.0392102 8.6090446,4.6835414 8.60955416,4.310293 C8.61261148,1.93271338 6.68777072,0.00303184713 4.31019108,-2.5477707e-05 C1.93286624,-0.00308280255 0.0029299363,1.92175796 0.000127388535,4.29933756 C-0.0029299363,6.67666244 1.92191083,8.60634396 4.29949044,8.60940128 C5.07426752,8.6104204 5.7912102,8.390293 6.42,8.03284076 L19.8243312,18.4603567 C18.6842038,20.181121 18.0166879,22.2422675 18.0166879,24.461121 C18.0166879,26.7841784 18.7504458,28.9327134 19.9907006,30.7001019 L15.9142675,34.776535 C15.5919745,34.6799745 15.2574522,34.6122038 14.9033121,34.6122038 C12.9499363,34.6122038 11.3659873,36.1961529 11.3659873,38.1497834 C11.3659873,40.103414 12.9499363,41.6871084 14.9033121,41.6871084 C16.8571974,41.6871084 18.4408917,40.103414 18.4408917,38.1497834 C18.4408917,37.7958981 18.3733758,37.461121 18.2765605,37.1390828 L22.3089172,33.1067261 C24.1392357,34.5041784 26.4184713,35.3431592 28.8989809,35.3431592 C34.9089172,35.3431592 39.7810191,30.4710573 39.7810191,24.461121 C39.7810191,19.0203567 35.7840764,14.5255796 30.5692994,13.7199745"
id="Fill-1"
fill="#F3785B"
fillRule="nonzero"
mask="url(#mask-2)"
/>
</g>
</g>
</svg>
);
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import { RawHTML } from '@wordpress/element';
import { IFormBlockAttributes } from './registerFormBlock';
export default function FormSaveBlock({ attributes }: IFormBlockAttributes) {
const { portalId, formId } = attributes;
if (portalId && formId) {
return (
<RawHTML className="wp-block-leadin-hubspot-form-block">
{`[hubspot portal="${portalId}" id="${formId}" type="form"]`}
</RawHTML>
);
}
return null;
}

View File

@@ -1,14 +0,0 @@
import React, { Fragment } from 'react';
import { pluginPath } from '../../constants/leadinConfig';
import UIImage from '../UIComponents/UIImage';
export default function FormGutenbergPreview() {
return (
<Fragment>
<UIImage
alt="Create a new Hubspot Form"
src={`${pluginPath}/public/assets/images/hubspot-form.png`}
/>
</Fragment>
);
}

View File

@@ -1,73 +0,0 @@
import React from 'react';
import * as WpBlocksApi from '@wordpress/blocks';
import SprocketIcon from '../Common/SprocketIcon';
import FormBlockSave from './FormBlockSave';
import { connectionStatus } from '../../constants/leadinConfig';
import FormGutenbergPreview from './FormGutenbergPreview';
import ErrorHandler from '../../shared/Common/ErrorHandler';
import FormEdit from '../../shared/Form/FormEdit';
import ConnectionStatus from '../../shared/enums/connectionStatus';
import { __ } from '@wordpress/i18n';
import { isFullSiteEditor } from '../../utils/withMetaData';
export interface IFormBlockAttributes {
attributes: {
portalId: string;
formId: string;
preview?: boolean;
formName: string;
};
}
export interface IFormBlockProps extends IFormBlockAttributes {
setAttributes: Function;
isSelected: boolean;
context?: any;
}
export default function registerFormBlock() {
const editComponent = (props: IFormBlockProps) => {
if (props.attributes.preview) {
return <FormGutenbergPreview />;
} else if (connectionStatus === ConnectionStatus.Connected) {
return <FormEdit {...props} origin="gutenberg" preview={true} />;
} else {
return <ErrorHandler status={401} />;
}
};
// We do not support the full site editor: https://issues.hubspotcentral.com/browse/WP-1033
if (!WpBlocksApi || isFullSiteEditor()) {
return null;
}
WpBlocksApi.registerBlockType('leadin/hubspot-form-block', {
title: __('HubSpot Form', 'leadin'),
description: __('Select and embed a HubSpot form', 'leadin'),
icon: SprocketIcon,
category: 'leadin-blocks',
attributes: {
portalId: {
type: 'string',
default: '',
} as WpBlocksApi.BlockAttribute<string>,
formId: {
type: 'string',
} as WpBlocksApi.BlockAttribute<string>,
formName: {
type: 'string',
} as WpBlocksApi.BlockAttribute<string>,
preview: {
type: 'boolean',
default: false,
} as WpBlocksApi.BlockAttribute<boolean>,
},
example: {
attributes: {
preview: true,
},
},
edit: editComponent,
save: props => <FormBlockSave {...props} />,
});
}

View File

@@ -1,15 +0,0 @@
import React, { Fragment } from 'react';
import { pluginPath } from '../../constants/leadinConfig';
import UIImage from '../UIComponents/UIImage';
export default function MeetingGutenbergPreview() {
return (
<Fragment>
<UIImage
alt="Create a new Hubspot Meeting"
width="100%"
src={`${pluginPath}/public/assets/images/hubspot-meetings.png`}
/>
</Fragment>
);
}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import { RawHTML } from '@wordpress/element';
import { IMeetingBlockAttributes } from './registerMeetingBlock';
export default function MeetingSaveBlock({
attributes,
}: IMeetingBlockAttributes) {
const { url } = attributes;
if (url) {
return (
<RawHTML className="wp-block-leadin-hubspot-meeting-block">{`[hubspot url="${url}" type="meeting"]`}</RawHTML>
);
}
return null;
}

View File

@@ -1,71 +0,0 @@
import React from 'react';
import * as WpBlocksApi from '@wordpress/blocks';
import CalendarIcon from '../Common/CalendarIcon';
import { connectionStatus } from '../../constants/leadinConfig';
import MeetingGutenbergPreview from './MeetingGutenbergPreview';
import MeetingSaveBlock from './MeetingSaveBlock';
import MeetingEdit from '../../shared/Meeting/MeetingEdit';
import ErrorHandler from '../../shared/Common/ErrorHandler';
import { __ } from '@wordpress/i18n';
import { isFullSiteEditor } from '../../utils/withMetaData';
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
};
export interface IMeetingBlockAttributes {
attributes: {
url: string;
preview?: boolean;
};
}
export interface IMeetingBlockProps extends IMeetingBlockAttributes {
setAttributes: Function;
isSelected: boolean;
}
export default function registerMeetingBlock() {
const editComponent = (props: IMeetingBlockProps) => {
if (props.attributes.preview) {
return <MeetingGutenbergPreview />;
} else if (connectionStatus === ConnectionStatus.Connected) {
return <MeetingEdit {...props} preview={true} origin="gutenberg" />;
} else {
return <ErrorHandler status={401} />;
}
};
// We do not support the full site editor: https://issues.hubspotcentral.com/browse/WP-1033
if (!WpBlocksApi || isFullSiteEditor()) {
return null;
}
WpBlocksApi.registerBlockType('leadin/hubspot-meeting-block', {
title: __('Hubspot Meetings Scheduler', 'leadin'),
description: __(
'Schedule meetings faster and forget the back-and-forth emails Your calendar stays full, and you stay productive',
'leadin'
),
icon: CalendarIcon,
category: 'leadin-blocks',
attributes: {
url: {
type: 'string',
default: '',
} as WpBlocksApi.BlockAttribute<string>,
preview: {
type: 'boolean',
default: false,
} as WpBlocksApi.BlockAttribute<boolean>,
},
example: {
attributes: {
preview: true,
},
},
edit: editComponent,
save: props => <MeetingSaveBlock {...props} />,
});
}

View File

@@ -1,83 +0,0 @@
import React from 'react';
import * as WpPluginsLib from '@wordpress/plugins';
import { PluginSidebar } from '@wordpress/edit-post';
import { PanelBody, Icon } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import UISidebarSelectControl from '../UIComponents/UISidebarSelectControl';
import SidebarSprocketIcon from '../Common/SidebarSprocketIcon';
import styled from 'styled-components';
import { __ } from '@wordpress/i18n';
import { BackgroudAppContext } from '../../iframe/useBackgroundApp';
import { refreshToken } from '../../constants/leadinConfig';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
export function registerHubspotSidebar() {
const ContentTypeLabelStyle = styled.div`
white-space: normal;
text-transform: none;
`;
const ContentTypeLabel = (
<ContentTypeLabelStyle>
{__(
'Select the content type HubSpot Analytics uses to track this page',
'leadin'
)}
</ContentTypeLabelStyle>
);
const LeadinPluginSidebar = ({ postType }: { postType: string }) =>
postType ? (
<PluginSidebar
name="leadin"
title="HubSpot"
icon={
<Icon
className="hs-plugin-sidebar-sprocket"
icon={SidebarSprocketIcon()}
/>
}
>
<PanelBody title={__('HubSpot Analytics', 'leadin')} initialOpen={true}>
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<UISidebarSelectControl
metaKey="content-type"
className="select-content-type"
label={ContentTypeLabel}
options={[
{ label: __('Detect Automatically', 'leadin'), value: '' },
{ label: __('Blog Post', 'leadin'), value: 'blog-post' },
{
label: __('Knowledge Article', 'leadin'),
value: 'knowledge-article',
},
{ label: __('Landing Page', 'leadin'), value: 'landing-page' },
{ label: __('Listing Page', 'leadin'), value: 'listing-page' },
{
label: __('Standard Page', 'leadin'),
value: 'standard-page',
},
]}
/>
</BackgroudAppContext.Provider>
</PanelBody>
</PluginSidebar>
) : null;
const LeadinPluginSidebarWrapper = withSelect((select: Function) => {
const data = select('core/editor');
return {
postType:
data &&
data.getCurrentPostType() &&
data.getEditedPostAttribute('meta'),
};
})(LeadinPluginSidebar);
if (WpPluginsLib) {
WpPluginsLib.registerPlugin('leadin', {
render: LeadinPluginSidebarWrapper,
icon: SidebarSprocketIcon,
});
}
}

View File

@@ -1,6 +0,0 @@
import { styled } from '@linaria/react';
export default styled.img`
height: ${props => (props.height ? props.height : 'auto')};
width: ${props => (props.width ? props.width : 'auto')};
`;

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { SelectControl } from '@wordpress/components';
import withMetaData from '../../utils/withMetaData';
import {
useBackgroundAppContext,
usePostBackgroundMessage,
} from '../../iframe/useBackgroundApp';
import { ProxyMessages } from '../../iframe/integratedMessages';
interface IOption {
label: string;
value: string;
disabled?: boolean;
}
interface IUISidebarSelectControlProps {
metaValue?: string;
metaKey: string;
setMetaValue?: Function;
options: IOption[];
className: string;
label: JSX.Element;
}
const UISidebarSelectControl = (props: IUISidebarSelectControlProps) => {
const isBackgroundAppReady = useBackgroundAppContext();
const monitorSidebarMetaChange = usePostBackgroundMessage();
return (
<SelectControl
value={props.metaValue}
onChange={content => {
if (props.setMetaValue) {
props.setMetaValue(content);
}
isBackgroundAppReady &&
monitorSidebarMetaChange({
key: ProxyMessages.TrackSidebarMetaChange,
payload: {
metaKey: props.metaKey,
},
});
}}
{...props}
/>
);
};
export default withMetaData(UISidebarSelectControl);

View File

@@ -1,54 +0,0 @@
import React from 'react';
import { __ } from '@wordpress/i18n';
import { styled } from '@linaria/react';
const IframeErrorContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120px;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-weight: 400;
font-size: 14px;
font-size: 0.875rem;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-smoothing: antialiased;
line-height: 1.5rem;
`;
const ErrorHeader = styled.h1`
text-shadow: 0 0 1px transparent;
margin-bottom: 1.25rem;
color: #33475b;
font-size: 1.25rem;
`;
export const IframeErrorPage = () => (
<IframeErrorContainer>
<img
alt="Cannot find page"
width="175"
src="//static.hsappstatic.net/ui-images/static-1.14/optimized/errors/map.svg"
/>
<ErrorHeader>
{__(
'The HubSpot for WordPress plugin is not able to load pages',
'leadin'
)}
</ErrorHeader>
<p>
{__(
'Try disabling your browser extensions and ad blockers, then refresh the page',
'leadin'
)}
</p>
<p>
{__(
'Or open the HubSpot for WordPress plugin in a different browser',
'leadin'
)}
</p>
</IframeErrorContainer>
);

View File

@@ -1,15 +0,0 @@
export enum App {
Forms,
LiveChat,
Plugin,
PluginSettings,
Background,
}
export const AppIframe = {
[App.Forms]: 'integrated-form-app',
[App.LiveChat]: 'integrated-livechat-app',
[App.Plugin]: 'integrated-plugin-app',
[App.PluginSettings]: 'integrated-plugin-app',
[App.Background]: 'integrated-plugin-proxy',
} as const;

View File

@@ -1,11 +0,0 @@
export const CoreMessages = {
HandshakeReceive: 'INTEGRATED_APP_EMBEDDER_HANDSHAKE_RECEIVED',
SendRefreshToken: 'INTEGRATED_APP_EMBEDDER_SEND_REFRESH_TOKEN',
ReloadParentFrame: 'INTEGRATED_APP_EMBEDDER_RELOAD_PARENT_FRAME',
RedirectParentFrame: 'INTEGRATED_APP_EMBEDDER_REDIRECT_PARENT_FRAME',
SendLocale: 'INTEGRATED_APP_EMBEDDER_SEND_LOCALE',
SendDeviceId: 'INTEGRATED_APP_EMBEDDER_SEND_DEVICE_ID',
SendIntegratedAppConfig: 'INTEGRATED_APP_EMBEDDER_CONFIG',
} as const;
export type CoreMessageType = typeof CoreMessages[keyof typeof CoreMessages];

View File

@@ -1,5 +0,0 @@
export const FormMessages = {
CreateFormAppNavigation: 'CREATE_FORM_APP_NAVIGATION',
} as const;
export type FormMessageType = typeof FormMessages[keyof typeof FormMessages];

View File

@@ -1,18 +0,0 @@
import * as Core from './core/CoreMessages';
import * as Forms from './forms/FormsMessages';
import * as LiveChat from './livechat/LiveChatMessages';
import * as Plugin from './plugin/PluginMessages';
import * as Proxy from './proxy/ProxyMessages';
export type MessageType =
| Core.CoreMessageType
| Forms.FormMessageType
| LiveChat.LiveChatMessageType
| Plugin.PluginMessageType
| Proxy.ProxyMessageType;
export * from './core/CoreMessages';
export * from './forms/FormsMessages';
export * from './livechat/LiveChatMessages';
export * from './plugin/PluginMessages';
export * from './proxy/ProxyMessages';

View File

@@ -1,5 +0,0 @@
export const LiveChatMessages = {
CreateLiveChatAppNavigation: 'CREATE_LIVE_CHAT_APP_NAVIGATION',
} as const;
export type LiveChatMessageType = typeof LiveChatMessages[keyof typeof LiveChatMessages];

View File

@@ -1,27 +0,0 @@
export const PluginMessages = {
PluginSettingsNavigation: 'PLUGIN_SETTINGS_NAVIGATION',
PluginLeadinConfig: 'PLUGIN_LEADIN_CONFIG',
TrackConsent: 'INTEGRATED_APP_EMBEDDER_TRACK_CONSENT',
InternalTrackingFetchRequest: 'INTEGRATED_TRACKING_FETCH_REQUEST',
InternalTrackingFetchResponse: 'INTEGRATED_TRACKING_FETCH_RESPONSE',
InternalTrackingFetchError: 'INTEGRATED_TRACKING_FETCH_ERROR',
InternalTrackingChangeRequest: 'INTEGRATED_TRACKING_CHANGE_REQUEST',
InternalTrackingChangeError: 'INTEGRATED_TRACKING_CHANGE_ERROR',
BusinessUnitFetchRequest: 'BUSINESS_UNIT_FETCH_REQUEST',
BusinessUnitFetchResponse: 'BUSINESS_UNIT_FETCH_FETCH_RESPONSE',
BusinessUnitFetchError: 'BUSINESS_UNIT_FETCH_FETCH_ERROR',
BusinessUnitChangeRequest: 'BUSINESS_UNIT_CHANGE_REQUEST',
BusinessUnitChangeError: 'BUSINESS_UNIT_CHANGE_ERROR',
SkipReviewRequest: 'SKIP_REVIEW_REQUEST',
SkipReviewResponse: 'SKIP_REVIEW_RESPONSE',
SkipReviewError: 'SKIP_REVIEW_ERROR',
RemoveParentQueryParam: 'REMOVE_PARENT_QUERY_PARAM',
ContentEmbedInstallRequest: 'CONTENT_EMBED_INSTALL_REQUEST',
ContentEmbedInstallResponse: 'CONTENT_EMBED_INSTALL_RESPONSE',
ContentEmbedInstallError: 'CONTENT_EMBED_INSTALL_ERROR',
ContentEmbedActivationRequest: 'CONTENT_EMBED_ACTIVATION_REQUEST',
ContentEmbedActivationResponse: 'CONTENT_EMBED_ACTIVATION_RESPONSE',
ContentEmbedActivationError: 'CONTENT_EMBED_ACTIVATION_ERROR',
} as const;
export type PluginMessageType = typeof PluginMessages[keyof typeof PluginMessages];

View File

@@ -1,21 +0,0 @@
export const ProxyMessages = {
FetchForms: 'FETCH_FORMS',
FetchForm: 'FETCH_FORM',
CreateFormFromTemplate: 'CREATE_FORM_FROM_TEMPLATE',
FetchAuth: 'FETCH_AUTH',
FetchMeetingsAndUsers: 'FETCH_MEETINGS_AND_USERS',
FetchContactsCreateSinceActivation: 'FETCH_CONTACTS_CREATED_SINCE_ACTIVATION',
FetchOrCreateMeetingUser: 'FETCH_OR_CREATE_MEETING_USER',
ConnectMeetingsCalendar: 'CONNECT_MEETINGS_CALENDAR',
TrackFormPreviewRender: 'TRACK_FORM_PREVIEW_RENDER',
TrackFormCreatedFromTemplate: 'TRACK_FORM_CREATED_FROM_TEMPLATE',
TrackFormCreationFailed: 'TRACK_FORM_CREATION_FAILED',
TrackMeetingPreviewRender: 'TRACK_MEETING_PREVIEW_RENDER',
TrackSidebarMetaChange: 'TRACK_SIDEBAR_META_CHANGE',
TrackReviewBannerRender: 'TRACK_REVIEW_BANNER_RENDER',
TrackReviewBannerInteraction: 'TRACK_REVIEW_BANNER_INTERACTION',
TrackReviewBannerDismissed: 'TRACK_REVIEW_BANNER_DISMISSED',
TrackPluginDeactivation: 'TRACK_PLUGIN_DEACTIVATION',
} as const;
export type ProxyMessageType = typeof ProxyMessages[keyof typeof ProxyMessages];

View File

@@ -1,161 +0,0 @@
import { MessageType, PluginMessages } from '../iframe/integratedMessages';
import {
fetchDisableInternalTracking,
trackConsent,
disableInternalTracking,
getBusinessUnitId,
setBusinessUnitId,
skipReview,
} from '../api/wordpressApiClient';
import { removeQueryParamFromLocation } from '../utils/queryParams';
import { startActivation, startInstall } from '../utils/contentEmbedInstaller';
export type Message = { key: MessageType; payload?: any };
const messageMapper: Map<MessageType, Function> = new Map([
[
PluginMessages.TrackConsent,
(message: Message) => {
trackConsent(message.payload);
},
],
[
PluginMessages.InternalTrackingChangeRequest,
(message: Message, embedder: any) => {
disableInternalTracking(message.payload)
.then(() => {
embedder.postMessage({
key: PluginMessages.InternalTrackingFetchResponse,
payload: message.payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.InternalTrackingChangeError,
payload,
});
});
},
],
[
PluginMessages.InternalTrackingFetchRequest,
(__message: Message, embedder: any) => {
fetchDisableInternalTracking()
.then(({ message: payload }) => {
embedder.postMessage({
key: PluginMessages.InternalTrackingFetchResponse,
payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.InternalTrackingFetchError,
payload,
});
});
},
],
[
PluginMessages.BusinessUnitFetchRequest,
(__message: Message, embedder: any) => {
getBusinessUnitId()
.then(payload => {
embedder.postMessage({
key: PluginMessages.BusinessUnitFetchResponse,
payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.BusinessUnitFetchError,
payload,
});
});
},
],
[
PluginMessages.BusinessUnitChangeRequest,
(message: Message, embedder: any) => {
setBusinessUnitId(message.payload)
.then(payload => {
embedder.postMessage({
key: PluginMessages.BusinessUnitFetchResponse,
payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.BusinessUnitChangeError,
payload,
});
});
},
],
[
PluginMessages.SkipReviewRequest,
(__message: Message, embedder: any) => {
skipReview()
.then(payload => {
embedder.postMessage({
key: PluginMessages.SkipReviewResponse,
payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.SkipReviewError,
payload,
});
});
},
],
[
PluginMessages.RemoveParentQueryParam,
(message: Message) => {
removeQueryParamFromLocation(message.payload);
},
],
[
PluginMessages.ContentEmbedInstallRequest,
(message: Message, embedder: any) => {
startInstall(message.payload.nonce)
.then(payload => {
embedder.postMessage({
key: PluginMessages.ContentEmbedInstallResponse,
payload: payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.ContentEmbedInstallError,
payload,
});
});
},
],
[
PluginMessages.ContentEmbedActivationRequest,
(message: Message, embedder: any) => {
startActivation(message.payload.activateAjaxUrl)
.then(payload => {
embedder.postMessage({
key: PluginMessages.ContentEmbedActivationResponse,
payload: payload,
});
})
.catch(payload => {
embedder.postMessage({
key: PluginMessages.ContentEmbedActivationError,
payload,
});
});
},
],
]);
export const messageMiddleware = (embedder: any) => (message: Message) => {
const next = messageMapper.get(message.key);
if (next) {
next(message, embedder);
}
};

View File

@@ -1,64 +0,0 @@
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
import { domElements } from '../constants/selectors';
import useAppEmbedder from './useAppEmbedder';
import { App } from './constants';
import { IframeErrorPage } from './IframeErrorPage';
interface PortalProps extends React.PropsWithChildren {
app: App;
createRoute: boolean;
}
const IntegratedIframePortal = (props: PortalProps) => {
const container = document.getElementById(domElements.leadinIframeContainer);
const iframeNotRendered = useAppEmbedder(
props.app,
props.createRoute,
container
);
if (container && !iframeNotRendered) {
return ReactDOM.createPortal(props.children, container);
}
return (
<Fragment>
{(!container || iframeNotRendered) && <IframeErrorPage />}
</Fragment>
);
};
const renderIframeApp = () => {
const iframeFallbackContainer = document.getElementById(
domElements.leadinIframeContainer
);
let app: App;
const queryParams = new URLSearchParams(location.search);
const page = queryParams.get('page');
const createRoute = queryParams.get('leadin_route[0]') === 'create';
switch (page) {
case 'leadin_forms':
app = App.Forms;
break;
case 'leadin_chatflows':
app = App.LiveChat;
break;
case 'leadin_settings':
app = App.PluginSettings;
break;
case 'leadin_user_guide':
default:
app = App.Plugin;
break;
}
ReactDOM.render(
<IntegratedIframePortal app={app} createRoute={createRoute} />,
iframeFallbackContainer
);
};
export default renderIframeApp;

View File

@@ -1,233 +0,0 @@
import { useEffect } from 'react';
import Raven from '../lib/Raven';
import {
accountName,
adminUrl,
connectionStatus,
deviceId,
hubspotBaseUrl,
leadinQueryParams,
locale,
plugins,
portalDomain,
portalEmail,
portalId,
reviewSkippedDate,
refreshToken,
impactLink,
theme,
lastAuthorizeTime,
lastDeauthorizeTime,
lastDisconnectTime,
leadinPluginVersion,
phpVersion,
wpVersion,
contentEmbed,
requiresContentEmbedScope,
refreshTokenError,
LeadinConfig,
} from '../constants/leadinConfig';
import { App, AppIframe } from './constants';
import { messageMiddleware } from './messageMiddleware';
import { resizeWindow, useIframeNotRendered } from '../utils/iframe';
type PartialLeadinConfig = Pick<
LeadinConfig,
| 'accountName'
| 'adminUrl'
| 'connectionStatus'
| 'deviceId'
| 'plugins'
| 'portalDomain'
| 'portalEmail'
| 'portalId'
| 'reviewSkippedDate'
| 'refreshToken'
| 'impactLink'
| 'theme'
| 'trackConsent'
| 'lastAuthorizeTime'
| 'lastDeauthorizeTime'
| 'lastDisconnectTime'
| 'leadinPluginVersion'
| 'phpVersion'
| 'wpVersion'
| 'contentEmbed'
| 'requiresContentEmbedScope'
| 'refreshTokenError'
>;
type AppIntegrationConfig = Pick<LeadinConfig, 'adminUrl'>;
const getIntegrationConfig = (): AppIntegrationConfig => {
return {
adminUrl: leadinQueryParams.adminUrl,
};
};
/**
* A modified version of the original leadinConfig that is passed to some integrated apps.
*
* Important:
* Try not to add new fields here.
* This config is already too large and broad in scope.
* It tightly couples the apps that use it with the WordPress plugin.
* Consider instead passing new required fields as new entry to PluginAppOptions or app-specific options.
*/
type AppLeadinConfig = {
admin: string;
company: string;
email: string;
firstName: string;
irclickid: string;
justConnected: string;
lastName: string;
mpid: string;
nonce: string;
websiteName: string;
} & PartialLeadinConfig;
const getLeadinConfig = (): AppLeadinConfig => {
const utm_query_params = Object.keys(leadinQueryParams)
.filter(x => /^utm/.test(x))
.reduce(
(p: { [key: string]: string }, c: string) => ({
[c]: leadinQueryParams[c],
...p,
}),
{}
);
return {
accountName,
admin: leadinQueryParams.admin,
adminUrl,
company: leadinQueryParams.company,
connectionStatus,
deviceId,
email: leadinQueryParams.email,
firstName: leadinQueryParams.firstName,
irclickid: leadinQueryParams.irclickid,
justConnected: leadinQueryParams.justConnected,
lastName: leadinQueryParams.lastName,
lastAuthorizeTime,
lastDeauthorizeTime,
lastDisconnectTime,
leadinPluginVersion,
mpid: leadinQueryParams.mpid,
nonce: leadinQueryParams.nonce,
phpVersion,
plugins,
portalDomain,
portalEmail,
portalId,
reviewSkippedDate,
theme,
trackConsent: leadinQueryParams.trackConsent,
websiteName: leadinQueryParams.websiteName,
wpVersion,
contentEmbed,
requiresContentEmbedScope,
refreshTokenError,
...utm_query_params,
};
};
const getAppOptions = (app: App, createRoute = false) => {
const {
IntegratedAppOptions,
FormsAppOptions,
LiveChatAppOptions,
PluginAppOptions,
}: any = window;
let options;
switch (app) {
case App.Plugin:
options = new PluginAppOptions().setLeadinConfig(getLeadinConfig());
break;
case App.PluginSettings:
options = new PluginAppOptions()
.setLeadinConfig(getLeadinConfig())
.setPluginSettingsInit();
break;
case App.Forms:
options = new FormsAppOptions().setIntegratedAppConfig(
getIntegrationConfig()
);
if (createRoute) {
options = options.setCreateFormAppInit();
}
break;
case App.LiveChat:
options = new LiveChatAppOptions();
if (createRoute) {
options = options.setCreateLiveChatAppInit();
}
break;
default:
options = new IntegratedAppOptions();
}
return options;
};
export default function useAppEmbedder(
app: App,
createRoute: boolean,
container: HTMLElement | null
) {
console.info(
'HubSpot plugin - starting app embedder for:',
AppIframe[app],
container
);
const iframeNotRendered = useIframeNotRendered(AppIframe[app]);
useEffect(() => {
const { IntegratedAppEmbedder }: any = window;
if (IntegratedAppEmbedder) {
const options = getAppOptions(app, createRoute)
.setLocale(locale)
.setDeviceId(deviceId)
.setRefreshToken(refreshToken);
const embedder = new IntegratedAppEmbedder(
AppIframe[app],
portalId,
hubspotBaseUrl,
resizeWindow,
refreshToken ? '' : impactLink
).setOptions(options);
embedder.subscribe(messageMiddleware(embedder));
embedder.attachTo(container, true);
embedder.postStartAppMessage(); // lets the app know all all data has been passed to it
(window as any).embedder = embedder;
}
}, []);
if (iframeNotRendered) {
console.error('HubSpot plugin Iframe not rendered', {
portalId,
container,
appName: AppIframe[app],
hasIntegratedAppEmbedder: !!(window as any).IntegratedAppEmbedder,
});
Raven.captureException(new Error('Leadin Iframe not rendered'), {
fingerprint: ['USE_APP_EMBEDDER', 'IFRAME_SETUP_ERROR'],
extra: {
portalId,
container,
app,
hubspotBaseUrl,
impactLink,
appName: AppIframe[app],
hasRefreshToken: !!refreshToken,
},
});
}
return iframeNotRendered;
}

View File

@@ -1,29 +0,0 @@
import { createContext, useContext } from 'react';
import {
deviceId,
hubspotBaseUrl,
locale,
portalId,
} from '../constants/leadinConfig';
import { Message } from './messageMiddleware';
export const BackgroudAppContext = createContext<any>(null);
export function useBackgroundAppContext() {
return useContext(BackgroudAppContext);
}
export function usePostBackgroundMessage() {
const app = useBackgroundAppContext();
return (message: Message) => {
app.postMessage(message);
};
}
export function usePostAsyncBackgroundMessage(): (
message: Message
) => Promise<any> {
const app = useBackgroundAppContext();
return (message: Message) => app.postAsyncMessage(message);
}

View File

@@ -1,40 +0,0 @@
import Raven from 'raven-js';
import {
hubspotBaseUrl,
phpVersion,
wpVersion,
leadinPluginVersion,
portalId,
plugins,
} from '../constants/leadinConfig';
export function configureRaven() {
if (hubspotBaseUrl.indexOf('app.hubspot.com') === -1) {
return;
}
Raven.config(
'https://e9b8f382cdd130c0d415cd977d2be56f@exceptions.hubspot.com/1',
{
instrument: {
tryCatch: false,
},
release: leadinPluginVersion,
}
).install();
Raven.setTagsContext({
v: leadinPluginVersion,
php: phpVersion,
wordpress: wpVersion,
});
Raven.setExtraContext({
hub: portalId,
plugins: Object.keys(plugins)
.map(name => `${name}#${plugins[name]}`)
.join(','),
});
}
export default Raven;

View File

@@ -1,311 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { styled } from '@linaria/react';
import {
CALYPSO,
CALYPSO_LIGHT,
CALYPSO_MEDIUM,
OBSIDIAN,
} from '../UIComponents/colors';
import UISpinner from '../UIComponents/UISpinner';
import LoadState, { LoadStateType } from '../enums/loadState';
const Container = styled.div`
color: ${OBSIDIAN};
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-size: 14px;
position: relative;
`;
interface IControlContainerProps {
focused: boolean;
}
const ControlContainer = styled.div<IControlContainerProps>`
align-items: center;
background-color: hsl(0, 0%, 100%);
border-color: hsl(0, 0%, 80%);
border-radius: 4px;
border-style: solid;
border-width: ${props => (props.focused ? '0' : '1px')};
cursor: default;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
min-height: 38px;
outline: 0 !important;
position: relative;
transition: all 100ms;
box-sizing: border-box;
box-shadow: ${props =>
props.focused ? `0 0 0 2px ${CALYPSO_MEDIUM}` : 'none'};
&:hover {
border-color: hsl(0, 0%, 70%);
}
`;
const ValueContainer = styled.div`
align-items: center;
display: flex;
flex: 1;
flex-wrap: wrap;
padding: 2px 8px;
position: relative;
overflow: hidden;
box-sizing: border-box;
`;
const Placeholder = styled.div`
color: hsl(0, 0%, 50%);
margin-left: 2px;
margin-right: 2px;
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
font-size: 16px;
`;
const SingleValue = styled.div`
color: hsl(0, 0%, 20%);
margin-left: 2px;
margin-right: 2px;
max-width: calc(100% - 8px);
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
white-space: nowrap;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box;
`;
const IndicatorContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex-shrink: 0;
box-sizing: border-box;
`;
const DropdownIndicator = styled.div`
border-top: 8px solid ${CALYPSO};
border-left: 6px solid transparent;
border-right: 6px solid transparent;
width: 0px;
height: 0px;
margin: 10px;
`;
const InputContainer = styled.div`
margin: 2px;
padding-bottom: 2px;
padding-top: 2px;
visibility: visible;
color: hsl(0, 0%, 20%);
box-sizing: border-box;
`;
const Input = styled.input`
box-sizing: content-box;
background: rgba(0, 0, 0, 0) none repeat scroll 0px center;
border: 0px none;
font-size: inherit;
opacity: 1;
outline: currentcolor none 0px;
padding: 0px;
color: inherit;
font-family: inherit;
`;
const InputShadow = styled.div`
position: absolute;
opacity: 0;
font-size: inherit;
`;
const MenuContainer = styled.div`
position: absolute;
top: 100%;
background-color: #fff;
border-radius: 4px;
margin-bottom: 8px;
margin-top: 8px;
z-index: 9999;
box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1);
width: 100%;
`;
const MenuList = styled.div`
max-height: 300px;
overflow-y: auto;
padding-bottom: 4px;
padding-top: 4px;
position: relative;
`;
const MenuGroup = styled.div`
padding-bottom: 8px;
padding-top: 8px;
`;
const MenuGroupHeader = styled.div`
color: #999;
cursor: default;
font-size: 75%;
font-weight: 500;
margin-bottom: 0.25em;
text-transform: uppercase;
padding-left: 12px;
padding-left: 12px;
`;
interface IMenuItemProps {
selected: boolean;
}
const MenuItem = styled.div<IMenuItemProps>`
display: block;
background-color: ${props =>
props.selected ? CALYPSO_MEDIUM : 'transparent'};
color: ${props => (props.selected ? '#fff' : 'inherit')};
cursor: default;
font-size: inherit;
width: 100%;
padding: 8px 12px;
&:hover {
background-color: ${props =>
props.selected ? CALYPSO_MEDIUM : CALYPSO_LIGHT};
}
`;
interface IAsyncSelectProps {
placeholder: string;
value: any;
loadOptions?: Function;
defaultOptions?: any[];
onChange: Function;
}
export default function AsyncSelect({
placeholder,
value,
loadOptions,
onChange,
defaultOptions,
}: IAsyncSelectProps) {
const inputEl = useRef<HTMLInputElement>(null);
const inputShadowEl = useRef<HTMLDivElement>(null);
const [isFocused, setFocus] = useState(false);
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [localValue, setLocalValue] = useState('');
const [options, setOptions] = useState(defaultOptions);
const inputSize = `${
inputShadowEl.current ? inputShadowEl.current.clientWidth + 10 : 2
}px`;
useEffect(() => {
if (loadOptions && loadState === LoadState.NotLoaded) {
loadOptions('', (result: any) => {
setOptions(result);
setLoadState(LoadState.Idle);
});
}
}, [loadOptions, loadState]);
const renderItems = (items: any[] = [], parentKey?: number) => {
return items.map((item, index) => {
if (item.options) {
return (
<MenuGroup key={`async-select-item-${index}`}>
<MenuGroupHeader id={`${index}-heading`}>
{item.label}
</MenuGroupHeader>
<div>{renderItems(item.options, index)}</div>
</MenuGroup>
);
} else {
const key = `async-select-item-${
parentKey !== undefined ? `${parentKey}-${index}` : index
}`;
return (
<MenuItem
key={key}
id={key}
selected={value && item.value === value.value}
onClick={() => {
onChange(item);
setFocus(false);
}}
>
{item.label}
</MenuItem>
);
}
});
};
return (
<Container>
<ControlContainer
id="leadin-async-selector"
focused={isFocused}
onClick={() => {
if (isFocused) {
if (inputEl.current) {
inputEl.current.blur();
}
setFocus(false);
setLocalValue('');
} else {
if (inputEl.current) {
inputEl.current.focus();
}
setFocus(true);
}
}}
>
<ValueContainer>
{localValue === '' &&
(!value ? (
<Placeholder>{placeholder}</Placeholder>
) : (
<SingleValue>{value.label}</SingleValue>
))}
<InputContainer>
<Input
ref={inputEl}
onFocus={() => {
setFocus(true);
}}
onChange={e => {
setLocalValue(e.target.value);
setLoadState(LoadState.Loading);
loadOptions &&
loadOptions(e.target.value, (result: any) => {
setOptions(result);
setLoadState(LoadState.Idle);
});
}}
value={localValue}
width={inputSize}
id="asycn-select-input"
/>
<InputShadow ref={inputShadowEl}>{localValue}</InputShadow>
</InputContainer>
</ValueContainer>
<IndicatorContainer>
{loadState === LoadState.Loading && <UISpinner />}
<DropdownIndicator />
</IndicatorContainer>
</ControlContainer>
{isFocused && (
<MenuContainer>
<MenuList>{renderItems(options)}</MenuList>
</MenuContainer>
)}
</Container>
);
}

View File

@@ -1,55 +0,0 @@
import React from 'react';
import UIButton from '../UIComponents/UIButton';
import UIContainer from '../UIComponents/UIContainer';
import HubspotWrapper from './HubspotWrapper';
import { adminUrl, redirectNonce } from '../../constants/leadinConfig';
import { pluginPath } from '../../constants/leadinConfig';
import { __ } from '@wordpress/i18n';
interface IErrorHandlerProps {
status: number;
resetErrorState?: React.MouseEventHandler<HTMLButtonElement>;
errorInfo?: {
header: string;
message: string;
action: string;
};
}
function redirectToPlugin() {
window.location.href = `${adminUrl}admin.php?page=leadin&leadin_expired=${redirectNonce}`;
}
export default function ErrorHandler({
status,
resetErrorState,
errorInfo = { header: '', message: '', action: '' },
}: IErrorHandlerProps) {
const isUnauthorized = status === 401 || status === 403;
const errorHeader = isUnauthorized
? __("Your plugin isn't authorized", 'leadin')
: errorInfo.header;
const errorMessage = isUnauthorized
? __('Reauthorize your plugin to access your free HubSpot tools', 'leadin')
: errorInfo.message;
return (
<HubspotWrapper pluginPath={pluginPath}>
<UIContainer textAlign="center">
<h4>{errorHeader}</h4>
<p>
<b>{errorMessage}</b>
</p>
{isUnauthorized ? (
<UIButton data-test-id="authorize-button" onClick={redirectToPlugin}>
{__('Go to plugin', 'leadin')}
</UIButton>
) : (
<UIButton data-test-id="retry-button" onClick={resetErrorState}>
{errorInfo.action}
</UIButton>
)}
</UIContainer>
</HubspotWrapper>
);
}

View File

@@ -1,26 +0,0 @@
import { styled } from '@linaria/react';
interface IHubspotWrapperProps {
pluginPath: string;
padding?: string;
}
export default styled.div<IHubspotWrapperProps>`
background-image: ${props =>
`url(${props.pluginPath}/public/assets/images/hubspot.svg)`};
background-color: #f5f8fa;
background-repeat: no-repeat;
background-position: center 25px;
background-size: 120px;
color: #33475b;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-size: 14px;
padding: ${(props: any) => props.padding || '90px 20% 25px'};
p {
font-size: inherit !important;
line-height: 24px;
margin: 4px 0;
}
`;

View File

@@ -1,12 +0,0 @@
import React from 'react';
import HubspotWrapper from './HubspotWrapper';
import UISpinner from '../UIComponents/UISpinner';
import { pluginPath } from '../../constants/leadinConfig';
export default function LoadingBlock() {
return (
<HubspotWrapper pluginPath={pluginPath}>
<UISpinner size={50} />
</HubspotWrapper>
);
}

View File

@@ -1,81 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import { portalId, refreshToken } from '../../constants/leadinConfig';
import UISpacer from '../UIComponents/UISpacer';
import PreviewForm from './PreviewForm';
import FormSelect from './FormSelect';
import { IFormBlockProps } from '../../gutenberg/FormBlock/registerFormBlock';
import {
usePostBackgroundMessage,
BackgroudAppContext,
useBackgroundAppContext,
} from '../../iframe/useBackgroundApp';
import { ProxyMessages } from '../../iframe/integratedMessages';
import LoadingBlock from '../Common/LoadingBlock';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IFormEditProps extends IFormBlockProps {
preview: boolean;
origin: 'gutenberg' | 'elementor';
}
function FormEdit({
attributes,
isSelected,
setAttributes,
preview = true,
origin = 'gutenberg',
}: IFormEditProps) {
const { formId, formName } = attributes;
const formSelected = portalId && formId;
const isBackgroundAppReady = useBackgroundAppContext();
const monitorFormPreviewRender = usePostBackgroundMessage();
const handleChange = (selectedForm: { value: string; label: string }) => {
setAttributes({
portalId,
formId: selectedForm.value,
formName: selectedForm.label,
});
};
useEffect(() => {
monitorFormPreviewRender({
key: ProxyMessages.TrackFormPreviewRender,
payload: {
origin,
},
});
}, [origin]);
return !isBackgroundAppReady ? (
<LoadingBlock />
) : (
<Fragment>
{(isSelected || !formSelected) && (
<FormSelect
formId={formId}
formName={formName}
handleChange={handleChange}
origin={origin}
/>
)}
{formSelected && (
<Fragment>
{isSelected && <UISpacer />}
{preview && <PreviewForm portalId={portalId} formId={formId} />}
</Fragment>
)}
</Fragment>
);
}
export default function FormEditContainer(props: IFormEditProps) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<FormEdit {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,80 +0,0 @@
import React from 'react';
import FormSelector from './FormSelector';
import LoadingBlock from '../Common/LoadingBlock';
import { __ } from '@wordpress/i18n';
import useForms from './hooks/useForms';
import useCreateFormFromTemplate from './hooks/useCreateFormFromTemplate';
import { FormType, isDefaultForm } from '../../constants/defaultFormOptions';
import ErrorHandler from '../Common/ErrorHandler';
interface IFormSelectProps {
formId: string;
formName: string;
handleChange: Function;
origin: 'gutenberg' | 'elementor';
}
export default function FormSelect({
formId,
formName,
handleChange,
origin = 'gutenberg',
}: IFormSelectProps) {
const { search, formApiError, reset } = useForms();
const {
createFormByTemplate,
reset: createReset,
isCreating,
hasError,
formApiError: createApiError,
} = useCreateFormFromTemplate(origin);
const value =
formId && formName
? {
label: formName,
value: formId,
}
: null;
const handleLocalChange = (option: { value: FormType }) => {
if (isDefaultForm(option.value)) {
createFormByTemplate(option.value).then(({ guid, name }) => {
handleChange({
value: guid,
label: name,
});
});
} else {
handleChange(option);
}
};
return isCreating ? (
<LoadingBlock />
) : formApiError || createApiError ? (
<ErrorHandler
status={formApiError ? formApiError.status : createApiError.status}
resetErrorState={() => {
if (hasError) {
createReset();
} else {
reset();
}
}}
errorInfo={{
header: __('There was a problem retrieving your forms', 'leadin'),
message: __(
'Please refresh your forms or try again in a few minutes',
'leadin'
),
action: __('Refresh forms', 'leadin'),
}}
/>
) : (
<FormSelector
loadOptions={search}
onChange={(option: { value: FormType }) => handleLocalChange(option)}
value={value}
/>
);
}

View File

@@ -1,36 +0,0 @@
import React from 'react';
import HubspotWrapper from '../Common/HubspotWrapper';
import { pluginPath } from '../../constants/leadinConfig';
import AsyncSelect from '../Common/AsyncSelect';
import { __ } from '@wordpress/i18n';
interface IFormSelectorProps {
loadOptions: Function;
onChange: Function;
value: any;
}
export default function FormSelector({
loadOptions,
onChange,
value,
}: IFormSelectorProps) {
return (
<HubspotWrapper pluginPath={pluginPath}>
<p data-test-id="leadin-form-select">
<b>
{__(
'Select an existing form or create a new one from a template',
'leadin'
)}
</b>
</p>
<AsyncSelect
placeholder={__('Search for a form', 'leadin')}
value={value}
loadOptions={loadOptions}
onChange={onChange}
/>
</HubspotWrapper>
);
}

View File

@@ -1,29 +0,0 @@
import React, { useEffect, useRef } from 'react';
import UIOverlay from '../UIComponents/UIOverlay';
import { formsScriptPayload, hublet } from '../../constants/leadinConfig';
import useFormScript from './hooks/useFormsScript';
export default function PreviewForm({
portalId,
formId,
}: {
portalId: number;
formId: string;
}) {
const inputEl = useRef<HTMLDivElement>(null);
const ready = useFormScript();
useEffect(() => {
if (!ready) {
return;
}
if (inputEl.current) {
inputEl.current.innerHTML = '';
const embedScript = document.createElement('script');
embedScript.innerHTML = `hbspt.forms.create({ portalId: '${portalId}', formId: '${formId}', region: '${hublet}', ${formsScriptPayload} });`;
inputEl.current.appendChild(embedScript);
}
}, [formId, portalId, ready, inputEl]);
return <UIOverlay ref={inputEl} />;
}

View File

@@ -1,53 +0,0 @@
import { useState } from 'react';
import {
usePostAsyncBackgroundMessage,
usePostBackgroundMessage,
} from '../../../iframe/useBackgroundApp';
import { FormType } from '../../../constants/defaultFormOptions';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
export default function useCreateFormFromTemplate(origin = 'gutenberg') {
const proxy = usePostAsyncBackgroundMessage();
const track = usePostBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(LoadState.Idle);
const [formApiError, setFormApiError] = useState<any>(null);
const createFormByTemplate = (type: FormType) => {
setLoadState(LoadState.Loading);
track({
key: ProxyMessages.TrackFormCreatedFromTemplate,
payload: {
type,
origin,
},
});
return proxy({
key: ProxyMessages.CreateFormFromTemplate,
payload: type,
})
.then(form => {
setLoadState(LoadState.Idle);
return form;
})
.catch(err => {
setFormApiError(err);
track({
key: ProxyMessages.TrackFormCreationFailed,
payload: {
origin,
},
});
setLoadState(LoadState.Failed);
});
};
return {
isCreating: loadState === LoadState.Loading,
hasError: loadState === LoadState.Failed,
formApiError,
createFormByTemplate,
reset: () => setLoadState(LoadState.Idle),
};
}

View File

@@ -1,42 +0,0 @@
import { useState } from 'react';
import debounce from 'lodash/debounce';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import { DEFAULT_OPTIONS } from '../../../constants/defaultFormOptions';
import { ProxyMessages } from '../../../iframe/integratedMessages';
import { IForm } from '../../types';
export default function useForms() {
const proxy = usePostAsyncBackgroundMessage();
const [formApiError, setFormApiError] = useState<any>(null);
const search = debounce(
(search: string, callback: Function) => {
return proxy({
key: ProxyMessages.FetchForms,
payload: {
search,
},
})
.then(forms => {
callback([
...forms.map((form: IForm) => ({
label: form.name,
value: form.guid,
})),
DEFAULT_OPTIONS,
]);
})
.catch(error => {
setFormApiError(error);
});
},
300,
{ trailing: true }
);
return {
search,
formApiError,
reset: () => setFormApiError(null),
};
}

View File

@@ -1,30 +0,0 @@
import $ from 'jquery';
import { useEffect, useState } from 'react';
import { formsScript } from '../../../constants/leadinConfig';
import Raven from '../../../lib/Raven';
let promise: Promise<string | undefined>;
function loadFormsScript() {
if (!promise) {
promise = new Promise((resolve, reject) =>
$.getScript(formsScript)
.done(resolve)
.fail(reject)
);
}
return promise;
}
export default function useFormScript() {
const [ready, setReady] = useState(false);
useEffect(() => {
loadFormsScript()
.then(() => setReady(true))
.catch(error => Raven.captureException(error));
}, []);
return ready;
}

View File

@@ -1,95 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import LoadingBlock from '../Common/LoadingBlock';
import MeetingSelector from './MeetingSelector';
import MeetingWarning from './MeetingWarning';
import useMeetings, {
useSelectedMeeting,
useSelectedMeetingCalendar,
} from './hooks/useMeetings';
import HubspotWrapper from '../Common/HubspotWrapper';
import ErrorHandler from '../Common/ErrorHandler';
import { pluginPath } from '../../constants/leadinConfig';
import { __ } from '@wordpress/i18n';
import Raven from 'raven-js';
interface IMeetingControllerProps {
url: string;
handleChange: Function;
}
export default function MeetingController({
handleChange,
url,
}: IMeetingControllerProps) {
const {
mappedMeetings: meetings,
loading,
error,
reload,
connectCalendar,
} = useMeetings();
const selectedMeetingOption = useSelectedMeeting(url);
const selectedMeetingCalendar = useSelectedMeetingCalendar(url);
useEffect(() => {
if (!url && meetings.length > 0) {
handleChange(meetings[0].value);
}
}, [meetings, url, handleChange]);
const handleLocalChange = (option: { value: string }) => {
handleChange(option.value);
};
const handleConnectCalendar = () => {
return connectCalendar()
.then(() => {
reload();
})
.catch(error => {
Raven.captureMessage('Unable to connect calendar', {
extra: { error },
});
});
};
return (
<Fragment>
{loading ? (
<LoadingBlock />
) : error ? (
<ErrorHandler
status={(error && error.status) || error}
resetErrorState={() => reload()}
errorInfo={{
header: __(
'There was a problem retrieving your meetings',
'leadin'
),
message: __(
'Please refresh your meetings or try again in a few minutes',
'leadin'
),
action: __('Refresh meetings', 'leadin'),
}}
/>
) : (
<HubspotWrapper padding="90px 32px 24px" pluginPath={pluginPath}>
{selectedMeetingCalendar && (
<MeetingWarning
status={selectedMeetingCalendar}
onConnectCalendar={handleConnectCalendar}
/>
)}
{meetings.length > 1 && (
<MeetingSelector
onChange={handleLocalChange}
options={meetings}
value={selectedMeetingOption}
/>
)}
</HubspotWrapper>
)}
</Fragment>
);
}

View File

@@ -1,65 +0,0 @@
import React, { Fragment, useEffect } from 'react';
import { IMeetingBlockProps } from '../../gutenberg/MeetingsBlock/registerMeetingBlock';
import MeetingController from './MeetingController';
import PreviewMeeting from './PreviewMeeting';
import {
BackgroudAppContext,
useBackgroundAppContext,
usePostBackgroundMessage,
} from '../../iframe/useBackgroundApp';
import { refreshToken } from '../../constants/leadinConfig';
import { ProxyMessages } from '../../iframe/integratedMessages';
import LoadingBlock from '../Common/LoadingBlock';
import { getOrCreateBackgroundApp } from '../../utils/backgroundAppUtils';
interface IMeetingEditProps extends IMeetingBlockProps {
preview?: boolean;
origin?: 'gutenberg' | 'elementor';
}
function MeetingEdit({
attributes: { url },
isSelected,
setAttributes,
preview = true,
origin = 'gutenberg',
}: IMeetingEditProps) {
const isBackgroundAppReady = useBackgroundAppContext();
const monitorFormPreviewRender = usePostBackgroundMessage();
const handleChange = (newUrl: string) => {
setAttributes({
url: newUrl,
});
};
useEffect(() => {
monitorFormPreviewRender({
key: ProxyMessages.TrackMeetingPreviewRender,
payload: {
origin,
},
});
}, [origin]);
return !isBackgroundAppReady ? (
<LoadingBlock />
) : (
<Fragment>
{(isSelected || !url) && (
<MeetingController url={url} handleChange={handleChange} />
)}
{preview && url && <PreviewMeeting url={url} />}
</Fragment>
);
}
export default function MeetingsEditContainer(props: IMeetingEditProps) {
return (
<BackgroudAppContext.Provider
value={refreshToken && getOrCreateBackgroundApp(refreshToken)}
>
<MeetingEdit {...props} />
</BackgroudAppContext.Provider>
);
}

View File

@@ -1,38 +0,0 @@
import React, { Fragment } from 'react';
import AsyncSelect from '../Common/AsyncSelect';
import UISpacer from '../UIComponents/UISpacer';
import { __ } from '@wordpress/i18n';
interface IMeetingSelectorProps {
options: any[];
onChange: Function;
value: any;
}
export default function MeetingSelector({
options,
onChange,
value,
}: IMeetingSelectorProps) {
const optionsWrapper = [
{
label: __('Meeting name', 'leadin'),
options,
},
];
return (
<Fragment>
<UISpacer />
<p data-test-id="leadin-meeting-select">
<b>{__('Select a meeting scheduling page', 'leadin')}</b>
</p>
<AsyncSelect
defaultOptions={optionsWrapper}
onChange={onChange}
placeholder={__('Select a meeting', 'leadin')}
value={value}
/>
</Fragment>
);
}

View File

@@ -1,42 +0,0 @@
import React from 'react';
import UIAlert from '../UIComponents/UIAlert';
import UIButton from '../UIComponents/UIButton';
import { CURRENT_USER_CALENDAR_MISSING } from './constants';
import { __ } from '@wordpress/i18n';
interface IMeetingWarningProps {
status: string;
onConnectCalendar: React.MouseEventHandler<HTMLButtonElement>;
}
export default function MeetingWarning({
status,
onConnectCalendar,
}: IMeetingWarningProps) {
const isMeetingOwner = status === CURRENT_USER_CALENDAR_MISSING;
const titleText = isMeetingOwner
? __('Your calendar is not connected', 'leadin')
: __('Calendar is not connected', 'leadin');
const titleMessage = isMeetingOwner
? __(
'Please connect your calendar to activate your scheduling pages',
'leadin'
)
: __(
'Make sure that everybody in this meeting has connected their calendar from the Meetings page in HubSpot',
'leadin'
);
return (
<UIAlert titleText={titleText} titleMessage={titleMessage}>
{isMeetingOwner && (
<UIButton
use="tertiary"
id="meetings-connect-calendar"
onClick={onConnectCalendar}
>
{__('Connect calendar', 'leadin')}
</UIButton>
)}
</UIAlert>
);
}

View File

@@ -1,106 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import useCurrentUserFetch from './hooks/useCurrentUserFetch';
import useMeetingsFetch from './hooks/useMeetingsFetch';
import LoadState from '../enums/loadState';
interface IMeetingsContextWrapperState {
loading: boolean;
error: any;
meetings: any[];
currentUser: any;
meetingUsers: any;
selectedMeeting: string;
}
interface IMeetingsContext extends IMeetingsContextWrapperState {
reload: Function;
}
interface IMeetingsContextWrapperProps {
url: string;
}
export const MeetingsContext = React.createContext<IMeetingsContext>({
loading: true,
error: null,
meetings: [],
currentUser: null,
meetingUsers: {},
selectedMeeting: '',
reload: () => {},
});
export default function MeetingsContextWrapper({
url,
children,
}: React.PropsWithChildren<IMeetingsContextWrapperProps>) {
const [state, setState] = useState<IMeetingsContextWrapperState>({
loading: true,
error: null,
meetings: [],
currentUser: null,
meetingUsers: {},
selectedMeeting: url,
});
const {
meetings,
meetingUsers,
loadMeetingsState,
error: errorMeeting,
reload: reloadMeetings,
} = useMeetingsFetch();
const {
user: currentUser,
loadUserState,
error: errorUser,
reload: reloadUser,
} = useCurrentUserFetch();
const reload = useCallback(() => {
reloadUser();
reloadMeetings();
}, [reloadUser, reloadMeetings]);
useEffect(() => {
if (
!state.loading &&
!state.error &&
state.currentUser &&
state.meetings.length === 0
) {
reloadMeetings();
}
}, [state, reloadMeetings]);
useEffect(() => {
setState(previous => ({
...previous,
loading:
loadUserState === LoadState.Loading ||
loadMeetingsState === LoadState.Loading,
currentUser,
meetings,
meetingUsers: meetingUsers.reduce((p, c) => ({ ...p, [c.id]: c }), {}),
error: errorMeeting || errorUser,
selectedMeeting: url,
}));
}, [
loadUserState,
loadMeetingsState,
currentUser,
meetings,
meetingUsers,
errorMeeting,
errorUser,
url,
setState,
]);
return (
<MeetingsContext.Provider value={{ ...state, reload }}>
{children}
</MeetingsContext.Provider>
);
}

View File

@@ -1,31 +0,0 @@
import React, { Fragment, useEffect, useRef } from 'react';
import UIOverlay from '../UIComponents/UIOverlay';
import useMeetingsScript from './hooks/useMeetingsScript';
interface IPreviewForm {
url: string;
}
export default function PreviewForm({ url }: IPreviewForm) {
const ready = useMeetingsScript();
const inputEl = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ready) {
return;
}
if (inputEl.current) {
inputEl.current.innerHTML = '';
const container = document.createElement('div');
container.dataset.src = `${url}?embed=true`;
container.classList.add('meetings-iframe-container');
inputEl.current.appendChild(container);
const embedScript = document.createElement('script');
embedScript.innerHTML =
'hbspt.meetings.create(".meetings-iframe-container");';
inputEl.current.appendChild(embedScript);
}
}, [url, ready, inputEl]);
return <Fragment>{url && <UIOverlay ref={inputEl}></UIOverlay>}</Fragment>;
}

View File

@@ -1,2 +0,0 @@
export const OTHER_USER_CALENDAR_MISSING = 'OTHER_USER_CALENDAR_MISSING';
export const CURRENT_USER_CALENDAR_MISSING = 'CURRENT_USER_CALENDAR_MISSING';

View File

@@ -1,45 +0,0 @@
import { useEffect, useState } from 'react';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
let user: any = null;
export default function useCurrentUserFetch() {
const proxy = usePostAsyncBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [error, setError] = useState<null | Error>(null);
const createUser = () => {
if (!user) {
setLoadState(LoadState.NotLoaded);
}
};
const reload = () => {
user = null;
setLoadState(LoadState.NotLoaded);
setError(null);
};
useEffect(() => {
if (loadState === LoadState.NotLoaded && !user) {
setLoadState(LoadState.Loading);
proxy({
key: ProxyMessages.FetchOrCreateMeetingUser,
})
.then(data => {
user = data;
setLoadState(LoadState.Idle);
})
.catch(err => {
setError(err);
setLoadState(LoadState.Failed);
});
}
}, [loadState]);
return { user, loadUserState: loadState, error, createUser, reload };
}

View File

@@ -1,121 +0,0 @@
import { useCallback } from 'react';
import { __ } from '@wordpress/i18n';
import {
CURRENT_USER_CALENDAR_MISSING,
OTHER_USER_CALENDAR_MISSING,
} from '../constants';
import useMeetingsFetch, { MeetingUser } from './useMeetingsFetch';
import useCurrentUserFetch from './useCurrentUserFetch';
import LoadState from '../../enums/loadState';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import { ProxyMessages } from '../../../iframe/integratedMessages';
function getDefaultMeetingName(
meeting: any,
currentUser: any,
meetingUsers: any
) {
const [meetingOwnerId] = meeting.meetingsUserIds;
let result = __('Default', 'leadin');
if (
currentUser &&
meetingOwnerId !== currentUser.id &&
meetingUsers[meetingOwnerId]
) {
const user = meetingUsers[meetingOwnerId];
result += ` (${user.userProfile.fullName})`;
}
return result;
}
function hasCalendarObject(user: any) {
return (
user &&
user.meetingsUserBlob &&
user.meetingsUserBlob.calendarSettings &&
user.meetingsUserBlob.calendarSettings.email
);
}
export default function useMeetings() {
const proxy = usePostAsyncBackgroundMessage();
const {
meetings,
meetingUsers,
error: meetingsError,
loadMeetingsState,
reload: reloadMeetings,
} = useMeetingsFetch();
const {
user: currentUser,
error: userError,
loadUserState,
reload: reloadUser,
} = useCurrentUserFetch();
const reload = useCallback(() => {
reloadUser();
reloadMeetings();
}, [reloadUser, reloadMeetings]);
const connectCalendar = () => {
return proxy({
key: ProxyMessages.ConnectMeetingsCalendar,
});
};
return {
mappedMeetings: meetings.map(meet => ({
label:
meet.name || getDefaultMeetingName(meet, currentUser, meetingUsers),
value: meet.link,
})),
meetings,
meetingUsers,
currentUser,
error: meetingsError || (userError as any),
loading:
loadMeetingsState == LoadState.Loading ||
loadUserState === LoadState.Loading,
reload,
connectCalendar,
};
}
export function useSelectedMeeting(url: string) {
const { mappedMeetings: meetings } = useMeetings();
const option = meetings.find(({ value }) => value === url);
return option;
}
export function useSelectedMeetingCalendar(url: string) {
const { meetings, meetingUsers, currentUser } = useMeetings();
const meeting = meetings.find(meet => meet.link === url);
const mappedMeetingUsersId: {
[key: number]: MeetingUser;
} = meetingUsers.reduce((p, c) => ({ ...p, [c.id]: c }), {});
if (!meeting) {
return null;
} else {
const { meetingsUserIds } = meeting;
if (
currentUser &&
meetingsUserIds.includes(currentUser.id) &&
!hasCalendarObject(currentUser)
) {
return CURRENT_USER_CALENDAR_MISSING;
} else if (
meetingsUserIds
.map(id => mappedMeetingUsersId[id])
.some((user: any) => !hasCalendarObject(user))
) {
return OTHER_USER_CALENDAR_MISSING;
} else {
return null;
}
}
}

View File

@@ -1,58 +0,0 @@
import { useEffect, useState } from 'react';
import { usePostAsyncBackgroundMessage } from '../../../iframe/useBackgroundApp';
import LoadState, { LoadStateType } from '../../enums/loadState';
import { ProxyMessages } from '../../../iframe/integratedMessages';
export interface Meeting {
meetingsUserIds: number[];
name: string;
link: string;
}
export interface MeetingUser {
id: string;
}
let meetings: Meeting[] = [];
let meetingUsers: MeetingUser[] = [];
export default function useMeetingsFetch() {
const proxy = usePostAsyncBackgroundMessage();
const [loadState, setLoadState] = useState<LoadStateType>(
LoadState.NotLoaded
);
const [error, setError] = useState(null);
const reload = () => {
meetings = [];
setError(null);
setLoadState(LoadState.NotLoaded);
};
useEffect(() => {
if (loadState === LoadState.NotLoaded && meetings.length === 0) {
setLoadState(LoadState.Loading);
proxy({
key: ProxyMessages.FetchMeetingsAndUsers,
})
.then(data => {
setLoadState(LoadState.Loaded);
meetings = data && data.meetingLinks;
meetingUsers = data && data.meetingUsers;
})
.catch(e => {
setError(e);
setLoadState(LoadState.Failed);
});
}
}, [loadState]);
return {
meetings,
meetingUsers,
loadMeetingsState: loadState,
error,
reload,
};
}

View File

@@ -1,30 +0,0 @@
import $ from 'jquery';
import { useState, useEffect } from 'react';
import { meetingsScript } from '../../../constants/leadinConfig';
import Raven from '../../../lib/Raven';
let promise: Promise<any>;
function loadMeetingsScript() {
if (!promise) {
promise = new Promise((resolve, reject) =>
$.getScript(meetingsScript)
.done(resolve)
.fail(reject)
);
}
return promise;
}
export default function useMeetingsScript() {
const [ready, setReady] = useState(false);
useEffect(() => {
loadMeetingsScript()
.then(() => setReady(true))
.catch(error => Raven.captureException(error));
}, []);
return ready;
}

View File

@@ -1,69 +0,0 @@
import React from 'react';
import { styled } from '@linaria/react';
import { MARIGOLD_LIGHT, MARIGOLD_MEDIUM, OBSIDIAN } from './colors';
const AlertContainer = styled.div`
background-color: ${MARIGOLD_LIGHT};
border-color: ${MARIGOLD_MEDIUM};
color: ${OBSIDIAN};
font-size: 14px;
align-items: center;
justify-content: space-between;
display: flex;
border-style: solid;
border-top-style: solid;
border-right-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-width: 1px;
min-height: 60px;
padding: 8px 20px;
position: relative;
text-align: left;
`;
const Title = styled.p`
font-family: 'Lexend Deca';
font-style: normal;
font-weight: 700;
font-size: 16px;
line-height: 19px;
color: ${OBSIDIAN};
margin: 0;
padding: 0;
`;
const Message = styled.p`
font-family: 'Lexend Deca';
font-style: normal;
font-weight: 400;
font-size: 14px;
margin: 0;
padding: 0;
`;
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
`;
interface IUIAlertProps {
titleText: string;
titleMessage: string;
}
export default function UIAlert({
titleText,
titleMessage,
children,
}: React.PropsWithChildren<IUIAlertProps>) {
return (
<AlertContainer>
<MessageContainer>
<Title>{titleText}</Title>
<Message>{titleMessage}</Message>
</MessageContainer>
{children}
</AlertContainer>
);
}

View File

@@ -1,19 +0,0 @@
import { styled } from '@linaria/react';
import { HEFFALUMP, LORAX, OLAF } from './colors';
interface IButtonProps {
use?: string;
}
export default styled.button<IButtonProps>`
background-color:${props => (props.use === 'tertiary' ? HEFFALUMP : LORAX)};
border: 3px solid ${props => (props.use === 'tertiary' ? HEFFALUMP : LORAX)};
color: ${OLAF}
border-radius: 3px;
font-size: 14px;
line-height: 14px;
padding: 12px 24px;
font-family: 'Lexend Deca', Helvetica, Arial, sans-serif;
font-weight: 500;
white-space: nowrap;
`;

View File

@@ -1,9 +0,0 @@
import { styled } from '@linaria/react';
interface IUIContainerProps {
textAlign?: string;
}
export default styled.div<IUIContainerProps>`
text-align: ${props => (props.textAlign ? props.textAlign : 'inherit')};
`;

View File

@@ -1,14 +0,0 @@
import { styled } from '@linaria/react';
export default styled.div`
position: relative;
&:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
`;

View File

@@ -1,5 +0,0 @@
import { styled } from '@linaria/react';
export default styled.div`
height: 30px;
`;

View File

@@ -1,78 +0,0 @@
import React from 'react';
import { styled } from '@linaria/react';
import { CALYPSO_MEDIUM, CALYPSO } from './colors';
const SpinnerOuter = styled.div`
align-items: center;
color: #00a4bd;
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
margin: '2px';
`;
const SpinnerInner = styled.div`
align-items: center;
display: flex;
justify-content: center;
width: 100%;
height: 100%;
`;
interface IColorProp {
color: string;
}
const Circle = styled.circle<IColorProp>`
fill: none;
stroke: ${props => props.color};
stroke-width: 5;
stroke-linecap: round;
transform-origin: center;
`;
const AnimatedCircle = styled.circle<IColorProp>`
fill: none;
stroke: ${props => props.color};
stroke-width: 5;
stroke-linecap: round;
transform-origin: center;
animation: dashAnimation 2s ease-in-out infinite,
spinAnimation 2s linear infinite;
@keyframes dashAnimation {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -50;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -140;
}
}
@keyframes spinAnimation {
transform: rotate(360deg);
}
`;
export default function UISpinner({ size = 20 }) {
return (
<SpinnerOuter>
<SpinnerInner>
<svg height={size} width={size} viewBox="0 0 50 50">
<Circle color={CALYPSO_MEDIUM} cx="25" cy="25" r="22.5" />
<AnimatedCircle color={CALYPSO} cx="25" cy="25" r="22.5" />
</svg>
</SpinnerInner>
</SpinnerOuter>
);
}

View File

@@ -1,9 +0,0 @@
export const CALYPSO = '#00a4bd';
export const CALYPSO_MEDIUM = '#7fd1de';
export const CALYPSO_LIGHT = '#e5f5f8';
export const LORAX = '#ff7a59';
export const OLAF = '#ffffff';
export const HEFFALUMP = '#425b76';
export const MARIGOLD_LIGHT = '#fef8f0';
export const MARIGOLD_MEDIUM = '#fae0b5';
export const OBSIDIAN = '#33475b';

View File

@@ -1,6 +0,0 @@
const ConnectionStatus = {
Connected: 'Connected',
NotConnected: 'NotConnected',
} as const;
export default ConnectionStatus;

View File

@@ -1,11 +0,0 @@
const LoadState = {
NotLoaded: 'NotLoaded',
Loading: 'Loading',
Loaded: 'Loaded',
Idle: 'Idle',
Failed: 'Failed',
} as const;
export type LoadStateType = typeof LoadState[keyof typeof LoadState];
export default LoadState;

View File

@@ -1,4 +0,0 @@
export interface IForm {
guid: string;
name: string;
}

View File

@@ -1,14 +0,0 @@
import $ from 'jquery';
import Raven, { configureRaven } from '../lib/Raven';
export function initApp(initFn: Function) {
configureRaven();
Raven.context(initFn);
}
export function initAppOnReady(initFn: (...args: any[]) => void) {
function main() {
$(initFn);
}
initApp(main);
}

View File

@@ -1,44 +0,0 @@
import {
deviceId,
hubspotBaseUrl,
locale,
portalId,
} from '../constants/leadinConfig';
import { initApp } from './appUtils';
type CallbackFn = (...args: any[]) => void;
export function initBackgroundApp(initFn: CallbackFn | CallbackFn[]) {
function main() {
if (Array.isArray(initFn)) {
initFn.forEach(callback => callback());
} else {
initFn();
}
}
initApp(main);
}
export const getOrCreateBackgroundApp = (refreshToken: string) => {
if ((window as any).LeadinBackgroundApp) {
return (window as any).LeadinBackgroundApp;
}
const { IntegratedAppEmbedder, IntegratedAppOptions }: any = window;
const options = new IntegratedAppOptions()
.setLocale(locale)
.setDeviceId(deviceId)
.setRefreshToken(refreshToken);
const embedder = new IntegratedAppEmbedder(
'integrated-plugin-proxy',
portalId,
hubspotBaseUrl,
() => {}
).setOptions(options);
embedder.attachTo(document.body, false);
embedder.postStartAppMessage(); // lets the app know all all data has been passed to it
(window as any).LeadinBackgroundApp = embedder;
return (window as any).LeadinBackgroundApp;
};

View File

@@ -1,27 +0,0 @@
type ContentEmbedInfoResponse = {
success: boolean;
data?: {
// Empty if user doesn't have permissions or plugin already activated
activateAjaxUrl?: string;
message: string;
};
};
export function startInstall(nonce: string) {
const formData = new FormData();
const ajaxUrl = (window as any).ajaxurl;
formData.append('_wpnonce', nonce);
formData.append('action', 'content_embed_install');
return fetch(ajaxUrl, {
method: 'POST',
body: formData,
keepalive: true,
}).then<ContentEmbedInfoResponse>(res => res.json());
}
export function startActivation(requestUrl: string) {
return fetch(requestUrl, {
method: 'POST',
keepalive: true,
}).then<ContentEmbedInfoResponse>(res => res.json());
}

View File

@@ -1,36 +0,0 @@
import { useEffect, useState } from 'react';
const IFRAME_DISPLAY_TIMEOUT = 5000;
export function useIframeNotRendered(app: string) {
const [iframeNotRendered, setIframeNotRendered] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
const iframe = document.getElementById(app);
if (!iframe) {
setIframeNotRendered(true);
}
}, IFRAME_DISPLAY_TIMEOUT);
return () => {
if (timer) {
clearTimeout(timer);
}
};
}, []);
return iframeNotRendered;
}
export const resizeWindow = () => {
const adminMenuWrap = document.getElementById('adminmenuwrap');
const sideMenuHeight = adminMenuWrap ? adminMenuWrap.offsetHeight : 0;
const adminBar = document.getElementById('wpadminbar');
const adminBarHeight = (adminBar && adminBar.offsetHeight) || 0;
const offset = 4;
if (window.innerHeight < sideMenuHeight) {
return sideMenuHeight - offset;
} else {
return window.innerHeight - adminBarHeight - offset;
}
};

View File

@@ -1,14 +0,0 @@
export function addQueryObjectToUrl(
urlObject: URL,
queryParams: { [key: string]: any }
) {
Object.keys(queryParams).forEach(key => {
urlObject.searchParams.append(key, queryParams[key]);
});
}
export function removeQueryParamFromLocation(key: string) {
const location = new URL(window.location.href);
location.searchParams.delete(key);
window.history.replaceState(null, '', location.href);
}

View File

@@ -1,30 +0,0 @@
import { withSelect, withDispatch, select } from '@wordpress/data';
// from answer here: https://github.com/WordPress/gutenberg/issues/44477#issuecomment-1263026599
export const isFullSiteEditor = () => {
return select && !!select('core/edit-site');
};
const applyWithSelect = withSelect((select: Function, props: any): any => {
return {
metaValue: select('core/editor').getEditedPostAttribute('meta')[
props.metaKey
],
};
});
const applyWithDispatch = withDispatch(
(dispatch: Function, props: any): any => {
return {
setMetaValue(value: string) {
dispatch('core/editor').editPost({ meta: { [props.metaKey]: value } });
},
};
}
);
function apply<T>(el: T): T {
return applyWithSelect(applyWithDispatch(el));
}
export default apply;