Embed Profile Portal (Shopify)

The Omneo Profile Portal is a great way for customers to quickly interact with their profile in any situation. For many brands, it acts as a significant upgrade to the typical 'My Account' page used on their ecommerce websites.

This guide explains how to embed the Omneo Profile Portal experience into a typical Shopify Plus website.

Prerequisites

Shopify Plus with multiPass enabled

You will need

  • Shopify domain
  • Shopify api token
  • Shopify multiPass secret
  • Omneo Profile Portal URL (e.g. my.yourbrandomain.com)

Embed as iframe

Create a iFrame in your Shopify storefront, and link to the Omneo Profile Portal URL. You can customise the iFrame position and width, but most implementations see it replicate the standard cart configuration.

You need to give a unique ID to iframe and use that ID in event listeners - we recommend setting the ID to omneo_pp_iframe

Register Event Listener

Storefront needs to communicate with embed profile portal through postMessages.
Below are events used by Omneo Profile Portal

getUrl: get current URL request from iframe, you need to send current URL to Profile portal so it can redirect to current URL after login

const handleReceiveMessage = React.useCallback(event => {
        if (typeof event.data === 'object' && event.data.message === 'getUrl') {
            const childFrameObj = document.getElementById({iframe_id}); /** iframe_id from last step **/
            if (childFrameObj) {
                childFrameObj.contentWindow.postMessage(
                    {
                        url: window.location.href, /** Current url**/
                        action: event.data.action, 
                    },
                    '*'
                );
            }
        }
    });

 window.addEventListener('message', handleReceiveMessage);

login and logout: get Omneo Profile Portal login status so you can update storefront UI

const handleProfilePortalMessage = useCallback(event => {
        if (event.data === 'login') {
           /** handleLoginEvent **/;
        }

        if (event.data === 'logout') {
           /** handleLogoutEvent **/;
        }
    }, []);

window.addEventListener('message', handleProfilePortalMessage);

customer, get user details after login, includes

  • email
  • first_name
  • last_name
  • region
const handleReceiveMessage = React.useCallback(event => {
        if (typeof event.data === 'object' && event.data.message === 'customer') {
           /** get user data by event.data.data; **/
          
        }
    });

 window.addEventListener('message', handleReceiveMessage);

If the user does not leave the landing page on login from magic link and tries to log out, the token can remain in the URL and must be removed, using something similar to

function RemoveParameterFromUrl(url, parameter) {
      return url
      .replace(new RegExp('[?&]' + parameter + '=[^&#]*(#.*)?$'), '$1')
      .replace(new RegExp('([?&])' + parameter + '=[^&]*&'), '$1');
      }

This function should then be called within your event.data === 'logout' function above, for example:

if (event.data === 'logout') {
        var refreshUrl = RemoveParameterFromUrl(window.location.href,'token');
        if($('.profileportal').hasClass('logged-in')){
          $.ajax('your logout url').done(function() {
            $('.profileportal').removeClass('logged-in');
            $('.account-mobile').removeClass('logged-in');
            $('.profileportal.headerItem').removeClass('profile-sidebar-view--active');
            $('#global-page-wrap').removeClass('darken');
            window.location.href = refreshUrl;
          });
        }
        $('.profileportal').removeClass('logged-in');
        $('.account-mobile').removeClass('logged-in');
      }

Next JS/React Example

This example shows a basic Next JS/React component implementation of profile portal.
Unauthenticated: The frame will load embedded profile portal on it's Login/Register screen.

Authenticating: During the authentication process, when a login is triggered via either an email magic link, or username and password, the ID service will redirect users to your application with a token param in the URL query string. This component should store and use that token to direct the iFrame to the authenticated profile portal view for the user.

If the redirect param in the URL is also present, it will go directly to the designated page.

Authenticated: If the customer is already authenticated and the Omneo ID Service handoff token has been stored in your application, the component will retrieve it from storage and load the authenticated view.
You will need to add concrete implementations for the stubbed functions getIDSToken, setIDSToken, getOmneoRegionCode, useWindowMessage and ProfilePortalSkeleton

'use client';

import React, { useEffect, useRef, useState } from 'react';

// For Next JS app router - use next/navigation.
// For Pages Router - use next/router.
// For JS use new URL(document.location).searchParams; or similar.
import { useParams, useSearchParams } from 'next/navigation';

import { Language } from 'types';

// You can store your Omneo ID Service Token (IDS) in a cookie.
// This token can be provided to your profile portal URL as a query parameter
// to load the portal with the user authenticated.
import getIDSToken from '@app/lib/profile/get-ids-token';
import setIDSToken from '@app/lib/profile/set-ids-token';

// This is a custom hook that handles window messages via window events.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/message_event
// You may use simple functions instead of a hook if you prefer.
import useWindowMessage from '@lib/hooks/useWindowMessage';

// Your skeleton implementation
import ProfilePortalSkeleton from '@app/components/ProfilePortalSkeleton';

import s from './profile-portal.module.css';

export const ProfilePortal = () => {
    const { lang } = useParams(); // Your Language context.
    const portalFrameRef = useRef<any>(null); // A React ref to the iframe element.

    // const searchParams = new URL(document.location).searchParams;
    const searchParams = useSearchParams();

    // Used to track whether the iFrame has finished loading or not, and
    // display a loading state.
    const [frameLoaded, setFrameLoaded] = useState(false);

    const getOmneoRegionCode = (lang: Language) => {
        // Return your region code based on the application lang if required.
        return 'au';
    };

    const [portalURL, setPortalUrl] = useState<string>(
        `https://my.profileportal.domain`
    );

    const queryToken = searchParams.get('token');
    const queryRedirect = searchParams.get('redirect');

    // Just changing regions.
    useEffect(() => {
        setPortalUrl(
            `${portalURL}?region=${getOmneoRegionCode(lang as Language)}`
        );
    }, [lang]);

    // Already authenticated and reopening the sidebar.
    useEffect(() => {
        const idServiceToken = getIDSToken();
        if (idServiceToken) {
            setPortalUrl(
                `${portalURL}/login?token=${idServiceToken}&region=${getOmneoRegionCode(
                    lang as Language
                )}`
            );
        }
        const handleProfilePortalMessage = (event: any) => {
            if (event.data === 'login') {
                // TODO: Event tracking
                // TODO: Store session information if used by your application.
            }
            if (event.data === 'logout') {
                // Clear the Omneo ID Service token on logout.
                setIDSToken(null);
            }
        };

        window.addEventListener('message', handleProfilePortalMessage);
        return () => {
            window.removeEventListener('message', handleProfilePortalMessage);
        };
    }, []);

    // The customer is logging in (starting a new session).
    useEffect(() => {
        if (queryToken && queryToken.length) {
            let url = `${portalURL}/login?token=${queryToken}&region=${getOmneoRegionCode(
                lang as Language
            )}`;
            if (queryRedirect) {
                url.concat(`&redirect=${queryRedirect}`);
            }
            setPortalUrl(url);
            setIDSToken(queryToken as string);
        }
    }, [queryToken, queryRedirect, lang]);

    useWindowMessage({
        customer: (event) => {
            // trackEvent({
            //     event: 'customer',
            //     data: event.data,
            // });
        },
        login: () => {
            // trackEvent({
            //     event: 'login',
            //     data: 'login',
            // });
        },
        getUrl: (data: any) => {
            // trackEvent({
            //     event: 'getUrl',
            //     data,
            // });
            portalFrameRef?.current?.contentWindow?.postMessage(
                {
                    url: window.location.href,
                    action: data.action,
                },
                '*'
            );
        },
    });

    // Return either a loading state, or the profile portal iFrame.
    return (
        <>
            {!frameLoaded && <ProfilePortalSkeleton />}
            <iframe
                id="profile_portal_frame"
                ref={portalFrameRef}
                src={portalURL}
                onLoad={() => setFrameLoaded(true)}
                className={s.iframe} // Add your iFrame styles here.
            />
        </>
    );
};

export default ProfilePortal;

Here is a test HTML file you can use to test embedded profile portal locally.

<html>
<head>
    <title>window.postMessage Test</title>
    <script>
        window.send = function () {
            const iframe = document.getElementById('pp-frame').contentWindow;

            iframe.postMessage({
                url: window.location.href,
                action: 'link',
            }, '*');
        }

        window.logout = function () {
            const iframe = document.getElementById('pp-frame').contentWindow;

            iframe.postMessage({
                action: 'logout',
            }, '*');
        }

        const handleProfilePortalMessage = (event) => {
            if (event.data.message === 'getUrl') {
                const iframe = document.getElementById('pp-frame').contentWindow;
                iframe.postMessage({
                    url: window.location.href
                }, '*');
            }
        };

        window.addEventListener('message', handleProfilePortalMessage);

    </script>
</head>

<body>
<div>
    <div class="output">
        <button onclick="send()">
            Send PostMessage
        </button>

        <button onclick="logout()">
            Logout
        </button>
    </div>

</div>
<hr>
<h3>
    Embed Profile Portal
</h3>
<iframe width="100%" height="800" id="pp-frame" src="http://localhost:3000" frameborder="0"></iframe>


</body>

</html>