Skip to content

Instantly share code, notes, and snippets.

@alettieri
Last active October 12, 2023 16:44
Show Gist options
  • Save alettieri/f382458694b8c73a321f3ccad36f69f2 to your computer and use it in GitHub Desktop.
Save alettieri/f382458694b8c73a321f3ccad36f69f2 to your computer and use it in GitHub Desktop.
HubSpot React/Next Scheduler Widget Component
import { useHubSpotMeetingsSchedulerListener } from '../lib/HubSpot/useHubSpotMeetingsSchedulerListener';
const Component = () => {
useHubSpotMeetingsSchedulerListener({
onMeetingBookedSuccess(event) {
console.log('success!', event);
},
});
return (<InlineWidget url="https://meetings.hubspot.com/[path-to-calendar]" />)
}
import React from 'react';
import {
Box,
Typography,
Card,
CardActions,
CardContent,
ButtonBase,
Button,
} from '@mui/material';
import * as Sentry from '@sentry/nextjs';
import Script from 'next/script';
import { useIntercom } from 'react-use-intercom';
import { Link } from '../../components/Link';
declare global {
interface Window {
hbspt: {
meetings: {
create: (selector: string) => void;
};
};
}
}
const WidgetFallback = (props: { url: string }) => {
const intercom = useIntercom();
const handleClick = React.useCallback(() => {
intercom.showNewMessages("Hi, I'm interested in scheduling a demo.");
}, [intercom]);
return (
<Card sx={{ m: 4, maxWidth: 400 }}>
<CardContent>
<Typography variant="h4" gutterBottom>
Oops, Something Went Wrong
</Typography>
<Typography paragraph>
Unfortunately, the HubSpot scheduling widget isn&apos;t
available right now. But don&apos;t worry, you can still
schedule your appointment:
</Typography>
</CardContent>
<CardActions>
<Link
href={props.url}
target="scheduling"
passHref
legacyBehavior
>
<Button>Schedule Now</Button>
</Link>
</CardActions>
<CardContent>
<Typography variant="body3">
If you have any questions or need assistance, please feel
free to{' '}
<ButtonBase
onClick={handleClick}
sx={{
color: 'primary.main',
p: 0,
minWidth: 'auto',
fontSize: 'inherit',
}}
>
Contact Us.
</ButtonBase>
</Typography>
</CardContent>
</Card>
);
};
type IInlineWidgetProps = {
url: string;
};
export const InlineWidget = (props: IInlineWidgetProps) => {
const elementRef = React.useRef<null | HTMLDivElement>(null);
const [error, updateError] = React.useState<null | Error>(null);
const urlSrc = React.useMemo(() => {
const url = new URL(props.url);
url.searchParams.append('embed', 'true');
return url.href;
}, [props.url]);
const handleError = React.useCallback(
(e: Error) => {
Sentry.captureException(e);
updateError(e);
},
[updateError]
);
const handleReady = React.useCallback(() => {
if (window.hbspt === undefined) {
handleError(new Error('HubSpot Meetings Scheduler failed to load'));
}
if (
elementRef.current.childNodes.length === 0 &&
window.hbspt &&
typeof window.hbspt.meetings?.create === 'function'
) {
window.hbspt.meetings.create('.meetings-iframe-container');
}
}, [handleError]);
return (
<>
<Box
className="HubSpotInlineWidget-root meetings-iframe-container"
minHeight={500}
pt={2}
data-src={urlSrc}
ref={elementRef}
>
{error ? <WidgetFallback url={props.url} /> : null}
</Box>
<Script
src="https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js"
onReady={handleReady}
onError={handleError}
/>
</>
);
};
type IHubSpotMeetingsPayload = {
linkType: string;
offline: boolean;
userSlug: string;
formGuid: string;
bookingResponse: IHubSpotBookingResponse;
isPaidMeeting: boolean;
};
type IHubSpotBookingResponse = {
postResponse: PostResponse;
};
type PostResponse = {
timerange: {
start: number;
end: number;
};
organizer: IHubSpotContact;
bookedOffline: boolean;
contact: IHubSpotContact;
};
type IHubSpotContact = {
firstName: string;
lastName: string;
email: string;
fullName: string;
name: string;
userId: null;
};
export type IHubSpotMessageEvent = MessageEvent<
| string
| {
meetingBookSucceeded: boolean;
meetingsPayload: IHubSpotMeetingsPayload;
}
>;
import React from 'react';
import { IHubSpotMessageEvent } from './types';
export type IUseHubSpotMeetingsSchedulerArgs = {
onMeetingBookedSuccess?: (event: IHubSpotMessageEvent) => void;
onMeetingBookedFailed?: (event: IHubSpotMessageEvent) => void;
};
export const useHubSpotMeetingsSchedulerListener = (
args: IUseHubSpotMeetingsSchedulerArgs = {}
) => {
const eventListeners = React.useRef(args);
React.useEffect(() => {
eventListeners.current = args;
}, [args]);
React.useEffect(() => {
const handleMessage = (event: IHubSpotMessageEvent) => {
if (
Boolean(event.origin.match(/hubspot.com/)) === false ||
typeof event.data === 'string'
) {
return;
}
const data = event.data;
if (
data?.meetingBookSucceeded === true &&
typeof eventListeners.current?.onMeetingBookedSuccess ===
'function'
) {
eventListeners.current.onMeetingBookedSuccess(event);
}
if (
data?.meetingBookSucceeded === false &&
typeof eventListeners.current?.onMeetingBookedFailed ===
'function'
) {
eventListeners.current.onMeetingBookedFailed(event);
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment