Closed wsdt closed 1 month ago
They are basically just not fired.
Hey @wsdt,
Would you be able to provide the calendly URL you are testing? I'm not able to reproduce the issue; when using my link I'm seeing all events fired.
https://calendly.com/wavect/safety-call
We were able to capture it ourselves by listening to onSubmit
but not the Calendly Events.
Thanks for sending this over, I tested the URL in this project - https://stackblitz.com/edit/stackblitz-starters-7cdiye?file=src%2FApp.tsx - and I was able to see the events in the console. Can you confirm that this example project works for you as well?
window.addEventListener('message', function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify(e.data));
});
We are capturing the events through this right now as we are using it through a WebView/IFrame.
The PageHeight works, but the others don't. Are they emitted differently?
Yes, Calendly only sends the other events if the embed_domain
query parameter is defined. If you are embedding this in a mobile app it's possible that the domain is undefined.
Could you try adding this query param to your url, for example:
Also, I believe they require an embed_type as well, so the full url would be:
https://calendly.com/wavect/safety-call?embed_domain=1&embed_type=Inline
Looking at Calendly's client code I do see a difference in how the events are emitted.
Page height event:
window.parent.postMessage(data, '*')
Other events:
if (window.parent !== window) {
window.parent.postMessage(data, '*')
}
I'm not too familiar with react native, but I'm wondering if the window.parent and window are the same object in this context?
Interesting!
Tried it now this way, but it also didn't work interestingly.
function captureMessage(event) {
window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
}
// Capture messages sent to window
window.addEventListener('message', captureMessage);
// Capture messages sent to window.parent
var originalPostMessage = window.parent.postMessage;
window.parent.postMessage = function(message, targetOrigin, transfer) {
captureMessage({data: message});
originalPostMessage.call(this, message, targetOrigin, transfer);
};
@wsdt - how are you rendering the iframe in your mobile app? Are you using react-native-webview (https://docs.expo.dev/versions/latest/sdk/webview/)?
If so, are you passing in the html or a uri?
This is what I'm doing, yes:
import { TR, i18n } from "@/localization/translate.service";
import React, { FC, useRef, useState } from "react";
import { DimensionValue } from "react-native";
import { WebView, WebViewMessageEvent } from "react-native-webview";
import {
CalendlyEvent,
IframeTitle,
LoadingSpinner,
PageSettings,
Prefill,
Utm,
formatCalendlyUrl,
} from "./OCalendlyBase";
import styles from "./OCalendlyInline.styles";
import CalendlyLoadingSpinner from "./OCalendlyLoadingSpinner";
export interface Props {
url: string;
prefill?: Prefill;
utm?: Utm;
styles?: React.CSSProperties | undefined;
pageSettings?: PageSettings;
iframeTitle?: IframeTitle;
LoadingSpinner?: LoadingSpinner;
onEventScheduled?: (event: any) => void;
onDateAndTimeSelected?: (event: any) => void;
onEventTypeViewed?: (event: any) => void;
onProfilePageViewed?: (event: any) => void;
onPageHeight?: (event: any) => void;
}
const OCalendlyInline: FC<Props> = ({
url,
prefill,
utm,
pageSettings,
iframeTitle,
LoadingSpinner = CalendlyLoadingSpinner,
onEventScheduled,
onDateAndTimeSelected,
onEventTypeViewed,
onProfilePageViewed,
onPageHeight,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [webViewHeight, setWebViewHeight] = useState<DimensionValue>();
const webViewRef = useRef<WebView>(null);
// @dev This one might be more native/cleaner, but doesn't work for the Calendly events (except of PageHeight somehow)
const injectedJavaScript = `
(function() {
window.addEventListener('message', function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify(e.data));
});
window.addEventListener('submit', event => {
window.ReactNativeWebView.postMessage(JSON.stringify({event: event.type,payload:event}));
});
true;
})();
`;
const handleMessage = (event: WebViewMessageEvent) => {
try {
const data = JSON.parse(event.nativeEvent.data);
switch (data.event) {
/** @dev Calendly is not correctly emitting the events except of PageHeight, for that reason we also just listen to "submit" */
case "submit" || CalendlyEvent.EVENT_SCHEDULED:
onEventScheduled?.(data.payload);
break;
case CalendlyEvent.DATE_AND_TIME_SELECTED:
onDateAndTimeSelected?.(data.payload);
break;
case CalendlyEvent.EVENT_TYPE_VIEWED:
onEventTypeViewed?.(data.payload);
break;
case CalendlyEvent.PROFILE_PAGE_VIEWED:
onProfilePageViewed?.(data.payload);
break;
case CalendlyEvent.PAGE_HEIGHT:
setWebViewHeight(
parseInt(data.payload.height.replace("px", "")),
);
onPageHeight?.(data.payload);
break;
}
} catch (error) {
console.error("Failed to parse WebView message:", error);
}
};
const src = formatCalendlyUrl({
url,
pageSettings,
prefill,
utm,
embedType: "Inline",
});
return (
<>
{isLoading && <LoadingSpinner />}
<WebView
ref={webViewRef}
style={[styles.webView, { height: webViewHeight }]}
source={{ uri: src }}
originWhitelist={["*"]}
onLoadEnd={() => setIsLoading(false)}
injectedJavaScript={injectedJavaScript}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView error: ", nativeEvent);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView HTTP error: ", nativeEvent);
}}
title={iframeTitle || i18n.t(TR.calendlySchedulingPageDefault)}
/>
</>
);
};
export default OCalendlyInline;
@wsdt - would you be able to use the html
prop instead of injectedJavaScript
? I'm not sure why this is the case, but I was able to get the events to fire using the following code:
const html = `
<script>
window.addEventListener('message', function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify(e.data));
});
</script>
<iframe width="100%" height="100%" src="https://calendly.com/calforce-support?embed_domain=1&embed_type=Inline"
`
<Webview source={{ html }} onMessage={(event) => alert(event.nativeEvent.data)} />
const src = formatCalendlyUrl({
url,
pageSettings,
prefill,
utm,
embedType: "Inline",
});
const html = `
<script>
window.addEventListener('message', function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify(e.data));
});
</script>
<iframe width="100%" height="100%" src={src} />
`
return (
<>
{isLoading && <LoadingSpinner />}
<WebView
ref={webViewRef}
style={[styles.webView, { height: webViewHeight }]}
source={{ html }}
originWhitelist={["*"]}
onLoadEnd={() => setIsLoading(false)}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView error: ", nativeEvent);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView HTTP error: ", nativeEvent);
}}
title={iframeTitle || i18n.t(TR.calendlySchedulingPageDefault)}
/>
</>
);
Unfortunately I just receive a blank screen with this approach.
@wsdt - from the code I see above I'm not able to reproduce the issue. Do you happen to have a public github repository that I could pull down to recreate the issue? I'm using the latest version of RN and webview, not sure if that changes anything.
@tcampb unfortunately not. But here otherwise the full code:
import { TR, i18n } from "@/localization/translate.service";
import React, { FC, useRef, useState } from "react";
import { DimensionValue } from "react-native";
import { WebView, WebViewMessageEvent } from "react-native-webview";
import {
CalendlyEvent,
IframeTitle,
LoadingSpinner,
PageSettings,
Prefill,
Utm,
formatCalendlyUrl,
} from "./OCalendlyBase";
import styles from "./OCalendlyInline.styles";
import CalendlyLoadingSpinner from "./OCalendlyLoadingSpinner";
export interface Props {
url: string;
prefill?: Prefill;
utm?: Utm;
styles?: React.CSSProperties | undefined;
pageSettings?: PageSettings;
iframeTitle?: IframeTitle;
LoadingSpinner?: LoadingSpinner;
onEventScheduled?: (event: any) => void;
onDateAndTimeSelected?: (event: any) => void;
onEventTypeViewed?: (event: any) => void;
onProfilePageViewed?: (event: any) => void;
onPageHeight?: (event: any) => void;
}
const OCalendlyInline: FC<Props> = ({
url,
prefill,
utm,
pageSettings,
iframeTitle,
LoadingSpinner = CalendlyLoadingSpinner,
onEventScheduled,
onDateAndTimeSelected,
onEventTypeViewed,
onProfilePageViewed,
onPageHeight,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [webViewHeight, setWebViewHeight] = useState<DimensionValue>();
const webViewRef = useRef<WebView>(null);
// @dev This one might be more native/cleaner, but doesn't work for the Calendly events (except of PageHeight somehow)
const injectedJavaScript = `
(function() {
window.addEventListener('message', function(e) {
window.ReactNativeWebView.postMessage(JSON.stringify(e.data));
});
window.addEventListener('submit', event => {
window.ReactNativeWebView.postMessage(JSON.stringify({event: event.type,payload:event}));
});
true;
})();
`;
const handleMessage = (event: WebViewMessageEvent) => {
try {
const data = JSON.parse(event.nativeEvent.data);
switch (data.event) {
/** @dev Calendly is not correctly emitting the events except of PageHeight, for that reason we also just listen to "submit" */
case "submit" || CalendlyEvent.EVENT_SCHEDULED:
onEventScheduled?.(data.payload);
break;
case CalendlyEvent.DATE_AND_TIME_SELECTED:
onDateAndTimeSelected?.(data.payload);
break;
case CalendlyEvent.EVENT_TYPE_VIEWED:
onEventTypeViewed?.(data.payload);
break;
case CalendlyEvent.PROFILE_PAGE_VIEWED:
onProfilePageViewed?.(data.payload);
break;
case CalendlyEvent.PAGE_HEIGHT:
setWebViewHeight(
parseInt(data.payload.height.replace("px", "")),
);
onPageHeight?.(data.payload);
break;
}
} catch (error) {
console.error("Failed to parse WebView message:", error);
}
};
const src = formatCalendlyUrl({
url,
pageSettings,
prefill,
utm,
embedType: "Inline",
});
return (
<>
{isLoading && <LoadingSpinner />}
<WebView
ref={webViewRef}
style={[styles.webView, { height: webViewHeight }]}
source={{ uri: src }}
originWhitelist={["*"]}
onLoadEnd={() => setIsLoading(false)}
injectedJavaScript={injectedJavaScript}
onMessage={handleMessage}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView error: ", nativeEvent);
}}
onHttpError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.error("WebView HTTP error: ", nativeEvent);
}}
title={iframeTitle || i18n.t(TR.calendlySchedulingPageDefault)}
/>
</>
);
};
export default OCalendlyInline;
import { sanitizePageSettingsProps } from "./OPropHelpers";
type Optional<T extends object> = {
[P in keyof T]?: T[P];
};
export type Prefill = Optional<{
name: string;
email: string;
firstName: string;
lastName: string;
smsReminderNumber: string;
location: string;
guests: string[];
customAnswers: Optional<{
a1: string;
a2: string;
a3: string;
a4: string;
a5: string;
a6: string;
a7: string;
a8: string;
a9: string;
a10: string;
}>;
date: Date;
}>;
export enum CalendlyEvent {
PROFILE_PAGE_VIEWED = "calendly.profile_page_viewed",
EVENT_TYPE_VIEWED = "calendly.event_type_viewed",
DATE_AND_TIME_SELECTED = "calendly.date_and_time_selected",
EVENT_SCHEDULED = "calendly.event_scheduled",
PAGE_HEIGHT = "calendly.page_height",
}
export type Utm = Optional<{
utmCampaign: string;
utmSource: string;
utmMedium: string;
utmContent: string;
utmTerm: string;
salesforce_uuid: string;
}>;
/**
* @description The default title is Calendly Scheduling Page
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title}
*/
export type IframeTitle = string;
/**
* @description LoadingSpinner is a React component that will be displayed while the Calendly iframe is loading. If no component is provided, the default Calendly loading spinner will be used.
*/
export type LoadingSpinner = React.FunctionComponent;
export type PageSettings = Optional<{
/**
* @description Use this setting to hide your profile picture, name, event duration, location, and description when Calendly is embedded. This will help reduce duplicate information that you may already have on your web page.
* @see {@link https://help.calendly.com/hc/en-us/articles/360020052833-Advanced-embed-options#2} for further information.
*/
hideLandingPageDetails: boolean;
/**
* @description Use this setting to hide your profile picture, name, event duration, location, and description when Calendly is embedded. This will help reduce duplicate information that you may already have on your web page.
* @see {@link https://help.calendly.com/hc/en-us/articles/360020052833-Advanced-embed-options#2} for further information.
*/
hideEventTypeDetails: boolean;
/**
* @description This setting is only available for Calendly users on the Pro plan. Use this setting to change your Calendly scheduling page's background color.
* @example 00a2ff
* @see {@link https://help.calendly.com/hc/en-us/articles/223147027-Embed-options-overview#3} for further information.
*/
backgroundColor: string;
/**
* @description This setting is only available for Calendly users on the Pro plan. Use this setting to change your Calendly scheduling page's text color.
* @example ffffff
* @see {@link https://help.calendly.com/hc/en-us/articles/223147027-Embed-options-overview#3} for further information.
*/
textColor: string;
/**
* @description This setting is only available for Calendly users on the Pro plan. Use this setting to change your Calendly scheduling page's primary color.
* @example 4d5055
* @see {@link https://help.calendly.com/hc/en-us/articles/223147027-Embed-options-overview#3} for further information.
*/
primaryColor: string;
/**
* @description The General Data Protection Regulation governs data protection in the EU and EEA. Certain Calendly integrations require access to cookies with user information. If you do not embed the GDPR banner, users in those areas will not have the ability to give their consent in order to access integrations such as Google Analytics, Facebook Pixel, PayPal, and Stripe.
* @see {@link https://help.calendly.com/hc/en-us/articles/360007385493-Cookie-FAQs} for further information.
*/
hideGdprBanner: boolean;
}>;
export const formatCalendlyUrl = ({
url,
prefill = {},
pageSettings = {},
utm = {},
embedType,
}: {
url: string;
prefill?: Prefill;
pageSettings?: PageSettings;
utm?: Utm;
embedType?: "Inline" | "PopupWidget" | "PopupButton";
}) => {
const sanitizedPageSettings = sanitizePageSettingsProps(pageSettings);
const {
backgroundColor,
hideEventTypeDetails,
hideLandingPageDetails,
primaryColor,
textColor,
hideGdprBanner,
} = sanitizedPageSettings;
const {
customAnswers,
date,
email,
firstName,
guests,
lastName,
location,
smsReminderNumber,
name,
} = prefill;
const {
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
salesforce_uuid,
} = utm;
const queryStringIndex = url.indexOf("?");
const hasQueryString = queryStringIndex > -1;
const queryString = url.slice(queryStringIndex + 1);
const baseUrl = hasQueryString ? url.slice(0, queryStringIndex) : url;
const updatedQueryString = [
hasQueryString ? queryString : null,
backgroundColor ? `background_color=${backgroundColor}` : null,
hideEventTypeDetails ? `hide_event_type_details=1` : null,
hideLandingPageDetails ? `hide_landing_page_details=1` : null,
primaryColor ? `primary_color=${primaryColor}` : null,
textColor ? `text_color=${textColor}` : null,
hideGdprBanner ? `hide_gdpr_banner=1` : null,
name ? `name=${encodeURIComponent(name)}` : null,
smsReminderNumber
? `phone_number=${encodeURIComponent(smsReminderNumber)}`
: null,
location ? `location=${encodeURIComponent(location)}` : null,
firstName ? `first_name=${encodeURIComponent(firstName)}` : null,
lastName ? `last_name=${encodeURIComponent(lastName)}` : null,
guests ? `guests=${guests.map(encodeURIComponent).join(",")}` : null,
email ? `email=${encodeURIComponent(email)}` : null,
date && date instanceof Date ? `date=${formatDate(date)}` : null,
utmCampaign ? `utm_campaign=${encodeURIComponent(utmCampaign)}` : null,
utmContent ? `utm_content=${encodeURIComponent(utmContent)}` : null,
utmMedium ? `utm_medium=${encodeURIComponent(utmMedium)}` : null,
utmSource ? `utm_source=${encodeURIComponent(utmSource)}` : null,
utmTerm ? `utm_term=${encodeURIComponent(utmTerm)}` : null,
salesforce_uuid
? `salesforce_uuid=${encodeURIComponent(salesforce_uuid)}`
: null,
embedType ? `embed_type=${embedType}` : null,
/*
* https://github.com/tcampb/react-calendly/pull/31
* embed_domain must be defined to receive messages from the Calendly iframe.
*/
`embed_domain=1`,
]
.concat(customAnswers ? formatCustomAnswers(customAnswers) : [])
.filter((item) => item !== null)
.join("&");
return `${baseUrl}?${updatedQueryString}`;
};
const formatDate = (d: Date) => {
const month = d.getMonth() + 1;
const day = d.getDate();
const year = d.getFullYear();
return [
year,
month < 10 ? `0${month}` : month,
day < 10 ? `0${day}` : day,
].join("-");
};
const CUSTOM_ANSWER_PATTERN = /^a\d{1,2}$/;
const formatCustomAnswers = (customAnswers: object) => {
const customAnswersFiltered = Object.keys(customAnswers).filter((key) =>
key.match(CUSTOM_ANSWER_PATTERN),
);
if (!customAnswersFiltered.length) return [];
return customAnswersFiltered.map(
(key) => `${key}=${encodeURIComponent(customAnswers[key])}`,
);
};
package.json:
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.5",
"react-native-web": "~0.19.10",
"react-native-webview": "13.8.6"
btw if we fully get it to work, happy to create an open source npm package for react-native
@wsdt - what are you passing in for these props?
onEventScheduled
onDateAndTimeSelected
onEventTypeViewed
onProfilePageViewed
When I changed source={{ uri: src }}
to
source={{ html: `<iframe src="${src}" width="100%" height="100%"></iframe>` }}
the code worked; so the only other difference I can think of is the props you are passing in.
source={{ html: `<iframe src="${src}" width="100%" height="100%"></iframe>` }}
oh nice!! Worked thank you @tcampb !
This is the code I have now to keep the exact same styling as by default from WebView:
source={{ html: `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
iframe {
border: none;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<iframe src="${src}" width="100%" height="100%"></iframe>
</body>
</html>
` }}
Created a ticket on our end, to create an official react-native-calendly package. Will add you as a significant contributor etc :-)
Thanks again!
This is great, thanks for publishing a react native version of the Calendly package 🎉 !
When using this we consistently only receive the onPageHeight event such as
On page height {"height": "602px"}
when logging to the console.Other than that we only receive events of type "undefined" such as:
{"canGoBack": false, "canGoForward": false, "data": "\"{\\\"originatingScript\\\":\\\"m2\\\",\\\"payload\\\":{\\\"guid\\\":\\\"5fed1a94-6532-49f5-81bc-fa79318799530 f36f3\\\",\\\"muid\\\":\\\"0e990be4-3f1f-4e35-a502-e5f66a32da67a2e3a0\\\",\\\"sid\\\":\\\"e5b8189a-0928-4837-be2f-31357f50385a692760\\\"}}\"", "loading": false, "target": 89, "title": "Select a Date & Time - Calendly", "url": "https://calendly.com/.............?hide_event_type_details=1&hide_landing_page_details=1&primary_color=36797d&hide_gdpr_banner=1&utm_source=MobileApp&embed_type=Inline&embed_domain=1&month=2024-09"}
Are these CalendlyEvent selectors still up-to-date? Thank you in advance!