Last active
October 12, 2023 16:44
-
-
Save alettieri/f382458694b8c73a321f3ccad36f69f2 to your computer and use it in GitHub Desktop.
HubSpot React/Next Scheduler Widget Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]" />) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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't | |
available right now. But don'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} | |
/> | |
</> | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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