Embedding without sdk
Implmenting the postmessage embed interface requires managing complex scenarios. We've already done the hard work for you in our Embedding sdk. We strongly suggest using the sdk instead of implmenting it yourself.
Melio requires implementors to pass a certification before enabling embedding without and sdk in the production environment.
If you are setting up a single-scroll hosted or modular experience, you'll need to handle the following scenarios in your frontend code:
- Add iframe
- Establish postMessge communication
- Synchronize frame dimensions and position
- Handle Exception events
There are additional scenarios you can support for your application:
Add iframe
The host application should embed the following html:
<iframe
id="<your iframe id>"
src="<embedUrl>"
height="100%"
width="100%"
style="display:block;border:0;"
allow="clipboard-write"
/>
If using a single scroll experience, instead embed the following html:
<iframe
id="<your iframe id>"
src="<embedUrl>"
width="100%"
style="display:block;border:0;"
scrolling="no"
allow="clipboard-write"
/>
Establishing postMessage Communication
In order to provide the best user experience while embedding the Melio web application in an iframe, the host application and the Melio web application communicate by postMessage.
Receiving Messages
Right after rendering the iframe the host application should listen to message events from the iframe web application.
function listenToMelioMessages() {
window.addEventListener("message", (event) => {
const frameWindow = document.getElementById(
"<iframe element id>",
).contentWindow;
const payload = JSON.parse(event.data);
if (event.source === frameWindow) {
// react to messages (insert sample reactions here)
}
});
}
listenToMelioMessages();
Sending Messages
In certain situations the host application might need to send messages to the Melio web application, it should do so by sending a postMessage:
function sendMessage(type, payload) {
const { contentWindow } = document.getElementById("<iframe element id>");
contentWindow.postMessage(
JSON.stringify({
type,
messageId: crypto.randomUUID(),
origin: "<partner name>",
...payload,
}),
"*",
);
}
Message Structure
Each message from the Melio web application to the host application or vice versa has a json stringified payload with the following structure:
{
"type": "<MessageType>",
"messageId": "<uuid>",
...type based payload
}
The additional payload is based on the message type, a list of different message types and their relevant additional payload can be found in the Resources section.
The list of possible messages is available in the Resources section.
Limiting postMessage Origins
To prevent errors relating to changing domains and quirks resulting from browser compatibility this guide opts to use a permissive domain policy for post messages origin.
If the host application contains additional iframes from less trusted sources or untrusted third party code, then it should check the source/origin of messages it receives and send the originUrl's domain instead of * when sending messages to the iframe.
Synchronizing Frame Dimensions and Position
To support a smooth and modern web experience with a single scroll between the host and the Melio application, the position and dimensions of the frame needs to be synchronized via a two way communication between the host and the frame.
The host application needs to listen and handle the following messages:
- CONTENT_SIZE_CHANGE - the host application needs to update the frame size based on the properties sent in this message.
- SCROLL_TO - the host application needs to scroll the window based on this message
The host application needs to send these messages for the following scenarios:
- DIMENSIONS_CHANGED - needs to be sent as a response to the frame LOADED event, when a window resize event occurs, and whenever the frame moves within the document (such as for example, if the header of the host changes height).
- USER_SCROLL - needs to be sent when the user scrolls the host application.
Melio suggests having a single scrollbar that scrolls the entire screen. When embedding an iframe there is no way to automatically fit the viewport to the size of the iframe content, so the default browser behavior is to add a scrollbar just on the iframe window. To work around that we require the host application to change the height of the iframe based on messages the Melio application sends. To allow for floating elements to appear in the correct position inside an iframe when partial iframe integration is enabled the iframe needs to know the scroll position at all times.
Message Structure
{
"messageId": "uuid",
"type": "CONTENT_SIZE_CHANGE",
"height": "number",
"width?": "number",
};
{
"messageId": "uuid",
"type": "SCROLL_TO",
"relativeScrollX?": "number",
"relativeScrollY?": "number",
"preventScroll?": "boolean",
};
{
"messageId": "uuid",
"type": "DIMENSIONS_CHANGED",
"windowHeight": "number",
"windowWidth": "number",
"elementDistanceFromTop": "boolean",
"elementDistanceFromLeft": "boolean",
};
{
"messageId": "uuid",
"type": "USER_SCROLL",
"scrollX": "number",
"scrolLY": "number",
};
Snippet
This should run when the iframe is rendered:
function listenToMessages() {
window.addEventListener("message", (event) => {
if (event.source === getIframeElement().contentWindow) {
const payload = event.data;
// react to messages (insert sample reactions here)
if (payload.type === "CONTENT_SIZE_CHANGE") {
updateOnContentChange(payload);
} else if (payload.type === "SCROLL_TO") {
updateOnScrollTo(payload);
} else if (payload.type === "LOADED") {
sendPositionChangedMessage();
}
}
});
}
function getIframeElement() {
return document.getElementById("<iframe element id>");
}
function updateOnContentChange(payload) {
const frame = getIframeElement();
const headerHeight = frame.getBoundingClientRect().top + window.scrollY;
const minHeight = window.innerHeight - headerHeight;
frame.height = `${Math.max(minHeight, payload.height || 0)}px`;
if (payload.width) {
frame.width = `${payload.width}px`;
}
}
function updateOnScrollTo(payload) {
const frame = getIframeElement();
const headerHeight = frame.getBoundingClientRect().top + window.scrollY;
if (payload.relativeScrollY != null || payload.relativeScrollX != null) {
window.scrollTo({
top: headerHeight + (payload.relativeScrollY || 0),
left: payload.relativeScrollX || 0,
});
}
if (payload.preventScroll) {
document.documentElement.style.overflow = "hidden";
} else {
document.documentElement.style.overflow = "auto";
}
}
function sendPositionChangedMessage() {
const elementBounds = getIframeElement().getBoundingClientRect();
sendMessage("DIMENSIONS_CHANGED", {
windowHeight: window.innerHeight,
windowWidth: window.innerWidth,
elementDistanceFromTop: elementBounds.top + window.scrollY,
elementDistanceFromLeft: elementBounds.left + window.scrollX,
});
}
function sendUserScroll() {
sendMessage("USER_SCROLL", {
scrollX: window.scrollX,
scrollY: window.scrollY,
});
}
function initListeners() {
window.addEventListener("resize", () => {
sendPositionChangedMessage();
});
window.addEventListener("scroll", () => {
sendUserScroll();
});
listenToMessages();
}
initListeners();
Handling Exception Events
Melio web application sends various messages to the host application when an exception occurs.
The host application is not expected to do anything with these messages.
Authentication and Syncing Errors
If the user encounters an error during the initial authentication and syncing, Melio will show an error page and will send a message to the host application.
{
"messageId": "uuid",
"type": "AUTHENTICATION_ERROR",
"reason": "<reason code>",
"message": "<error message>"
}
Session Expired Message
{
"messageId": "uuid",
"type": "SESSION_EXPIRED"
}
Synchronizing URLs
In order to synchronize the browser url in the host application, when the Melio web application navigates to a targetable page it'll send a NAVIGATED_TO_TARGET message to the host application.
The host application should change the url to match the target url in its own application.
When a user navigates inside an iframe the parent url does not change by default. As such users cannot bookmark or share specific pages in the iframe.
Message Structure
{
"messageId": "uuid",
"type": "NAVIGATED_TO_TARGET",
"target": "string"
}
Expected Reaction
if (payload.type === "NAVIGATED_TO_TARGET") {
if (path !== null) {
history.replaceState(
history.state,
"",
`${partnerBasePath}?target=${payload.target}`,
);
}
}
When Melio navigates to another screen it uses history.pushState, which creates another item in the browser's history stack. If the host application will then use history.pushState itself then the stack would have two history items which will mean that the user will have to click back twice before to get to the previous state he'll expect.
Requesting Navigation
For partial iframe integration if a user clicks on a link in the host application that should navigate the Melio web application to a different target the host application can send a message to navigate to a different target.
Another way to accomplish this is for the host application to change the iframe element src to the new desired location, however this will cause the Melio web application to reload and reauthenticate, which can take an additional amount of time that isn't required if the iframe is already rendered.
Snippet
document.getElementById("mylink").onclick = function (event) {
if (
event.ctrlKey ||
event.shiftKey ||
event.metaKey || // apple
(event.button && event.button == 1)
) {
// user click with the intent to open a new window, let him do it.
return;
}
event.preventDefault();
const { contentWindow } = document.getElementById("<iframe element id>");
contentWindow.postMessage(
JSON.stringify({
type: "NAVIGATE_REQUEST",
messageId: crypto.randomUUID(),
target: "",
}),
"*",
);
};
The host application should not navigate on its own and then notify the iframe as that will cause a duplicated history stack and the back button would not work as expected.
Handling Exit Request
In a fullscreen iframe implementation the user might indicate to the Melio web application that it would like to get back to the host application.
Message Structure
{
"messageId": "uuid",
"type": "EXIT_REQUESTED",
"originator": "<the button the user clicked on or their intent>"
}
Expected Reaction
if (payload.type === "EXIT_REQUESTED") {
history.pushState({}, "", "/");
}
Session Extension
The Melio web application will stay active as long as the user interacts with it.
Periodically as long as the user is active, the Melio web application will notify the host that the user is still active.
Message Structure
{
"messageId": "uuid",
"type": "USER_ACTIVE_PING"
}
When hosted in an iframe the user might interact with the Melio web application for a while, but the browser will not inform the host application that the user is still active. To prevent a situation where the host application closes the session while the user is still active inside the iframe, the host application should extend the session as long as the Melio web application sends the USER_ACTIVE_PING message.
Reusing Token in Session
When loading the Melio web application on the browser if the same token is used in the authUrl that was last used in the same browser session, melio will not initiate a token exchange again.
In such cases the Melio web application might call the Account Information endpoint or refresh endpoints depending on internal heuristics.
When using such a mechanism the host application should deal with Authentication Error and Session Expired messages by issuing a new code and loading the browser application again.
The oauth handshake and account information loading can be a lengthy process. If we know the user data is up to date and the token is still valid, usually by the fact the user loaded the session recently, we can skip this so the page can load faster. Examples of this might be when refreshing the page in an iFrame integration, or when the user moves back and forth between the Melio web application and the host in the same session.