Shopify app fails when outside iframe (Failed to execute 'postMessage' on 'DOMWindow')

basw1
Visitor
2 0 1

Ok guys im banging my head against the wall on this. I just cant figure out why my app fails outside an iframe (forceRedirect = false) but works fine inside the shopify iframe. I want to be able to work without the iframe for testing.

I'm using rails, react, apollo. I used a bunch of examples but clearly I dont understand what's happening where.

I start at the installation page, where you can enter the shop url. I hit install and go to https://<dev_url>?shop=<shop_url> The page loads, packs are loaded, and then it tries to do a Graphql query to my shopify app and it shows the following error:

Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('<URL>') does not match the recipient window's origin ('<URL>').

And a screenshot, you can see the navigation already rendered and now tries to get data to show (which fails):

error1.png

When I'm opening the samen page within the iframe there are no errors. All good.

I'll just share some code which I think is relevant.

embedded_app.html.erb:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <% application_name = ShopifyApp.configuration.application_name %>
    <title><%= application_name %></title>
    <% if ShopifyApp.use_webpacker? %>
      <%= stylesheet_pack_tag 'application' %>
      <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
    <% else %>
      <%= stylesheet_link_tag 'application' %>
      <%= javascript_include_tag 'application', "data-turbolinks-track" => true %>
    <% end %>
    <%= csrf_meta_tags %>
  </head>

  <body>
    <div class="app-wrapper">
      <div class="app-content">
        <main role="main">
          <%= yield %>
        </main>
      </div>
    </div>

    <%= render 'layouts/flash_messages' %>

    <script src="https://unpkg.com/@shopify/app-bridge"></script>

    <%= content_tag(:div, nil, id: 'shopify-app-init', data: {
      api_key: ShopifyApp.configuration.api_key,
      shop_origin: @shop_origin || (@current_shopify_session.domain if @current_shopify_session),
      debug: Rails.env.development?,
      forceRedirect: !(Rails.env.development? || Rails.env.test?)
    } ) %>

    <% if content_for?(:javascript) %>
      <div id="ContentForJavascript" data-turbolinks-temporary>
        <%= yield :javascript %>
      </div>
    <% end %>
  </body>
</html>

So here forceRedirect is really false. I've checked it a dozen of times.

Then I have app/javascript/shopify_app file:

document.addEventListener('DOMContentLoaded', () => {
    var data       = document.getElementById('shopify-app-init').dataset;
    var AppBridge  = window['app-bridge'];
    var createApp  = AppBridge.default;
        window.app = createApp({
        apiKey       : data.apiKey,
        shopOrigin   : data.shopOrigin,
        forceRedirect: data.forceRedirect == 'true',
    });

    var actions  = AppBridge.actions;
    var TitleBar = actions.TitleBar;
    TitleBar.create(app, {
        title: data.page,
    });
});

 And of course, my App.js:

import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache } from '@apollo/client';
import { AppProvider } from '@shopify/polaris';
import { createApp } from '@shopify/app-bridge';
import { authenticatedFetch } from '@shopify/app-bridge-utils';
import { getSessionToken } from '@shopify/app-bridge-utils';
import { Provider as AppBridgeProvider } from '@shopify/app-bridge-react';
import enTranslations from '@shopify/polaris/locales/en.json';
import '@shopify/polaris/dist/styles.css';
import React from 'react';
import { BrowserRouter, Link } from 'react-router-dom';
import Routes from './Routes';
import AppRouter from './AppRouter';
import AppTabs from './elements/AppTabs';

export const ConditionContext = React.createContext();

export default function App({ shopOrigin, apiKey, conditions, forceRedirect }) {
    console.log('shopOrigin', shopOrigin);
    console.log('window.location.host', window.location.host);
    console.log('window.location', window.location);

    const client = new ApolloClient({
        link: new createHttpLink({
            credentials: 'same-origin',
            fetch      : authenticatedFetch(window.app),
            uri        : '/graphql',
        }),
        cache: new InMemoryCache(),
    });

    const CustomLinkComponent = ({ children, url, ...rest }) => {
        return (
            <Link to={url} {...rest}>
                {children}
            </Link>
        );
    };

    return (
        <BrowserRouter>
            <AppProvider i18n={enTranslations} linkComponent={CustomLinkComponent}>
                <AppBridgeProvider
                    config={{
                        apiKey       : apiKey,
                        shopOrigin   : shopOrigin,
                        forceRedirect: forceRedirect,
                    }}
                >
                    <ApolloProvider client={client}>
                        <ConditionContext.Provider value={conditions}>
                            <AppRouter />
                            <AppTabs>
                                <Routes />
                            </AppTabs>
                        </ConditionContext.Provider>
                    </ApolloProvider>
                </AppBridgeProvider>
            </AppProvider>
        </BrowserRouter>
    );
}

 

This is my assumption: but for some reason the authenticatedFetch thinks it has a different url than it should. Which works fine from inside an iframe but not if it is rendered outside of it.

Hope you guys can help me out!

Replies 4 (4)

basw1
Visitor
2 0 1

Ok, this is probably not the way to go but I found a solution that works.

I check if the window is in an iframe or not. If not I use the csrf token to make requests.

import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache, HttpLink } from '@apollo/client';
import { TitleBar } from '@shopify/app-bridge/actions';
import { getSessionToken } from '@shopify/app-bridge-utils';
import { Redirect } from '@shopify/app-bridge/actions';
import { AppProvider } from '@shopify/polaris';
import { createApp } from '@shopify/app-bridge';
import { authenticatedFetch } from '@shopify/app-bridge-utils';
import { Provider as AppBridgeProvider } from '@shopify/app-bridge-react';
import enTranslations from '@shopify/polaris/locales/en.json';
import '@shopify/polaris/dist/styles.css';
import React from 'react';
import { BrowserRouter, Link } from 'react-router-dom';
import Routes from './Routes';
import AppRouter from './AppRouter';
import AppTabs from './elements/AppTabs';

export const ConditionContext = React.createContext();

export default function App({ shopOrigin, apiKey, conditions, forceRedirect }) {
    let link, origin;
    if (window.top == window.self) {
        // Not in an iframe
        origin = window.location.host;
        link   = new HttpLink({
            uri        : '/graphql',
            credentials: 'include',
            headers    : {
                'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').getAttribute('content'),
            },
        });
    } else {
        // Inside the iframe
        origin = shopOrigin;
        link   = createHttpLink({
            credentials: 'same-origin',
            fetch      : authenticatedFetch(app),
            uri        : '/graphql',
        });
    }
    const app = createApp({
        apiKey    : apiKey,
        shopOrigin: origin,
    });
    const client = new ApolloClient({
        link : link,
        cache: new InMemoryCache(),
    });

    const CustomLinkComponent = ({ children, url, ...rest }) => {
        return (
            <Link to={url} {...rest}>
                {children}
            </Link>
        );
    };

    return (
        <BrowserRouter>
            <AppProvider i18n={enTranslations} linkComponent={CustomLinkComponent}>
                <AppBridgeProvider
                    config={{
                        apiKey       : apiKey,
                        shopOrigin   : origin,
                        forceRedirect: forceRedirect,
                    }}
                >
                    <ApolloProvider client={client}>
                        <ConditionContext.Provider value={conditions}>
                            <AppRouter />
                            <AppTabs>
                                <Routes />
                            </AppTabs>
                        </ConditionContext.Provider>
                    </ApolloProvider>
                </AppBridgeProvider>
            </AppProvider>
        </BrowserRouter>
    );
}

 

hannachen
Shopify Staff
54 8 17

Hi @basw1,

The purpose of App Bridge is to provide a seamless experience in the Shopify admin in the form of an Embedded App. When your app is outside of the iframe, it is not embedded. That is the reason why you are seeing the PostMessage errors.

If you are looking to build a non-embedded app, remove App Bridge from your app and use Polaris to build your UI, and communicate with the Shopify Admin API to get the store data from your app's backend.

Hanna | Shopify 
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution
 - To learn more visit the Shopify Help Center or the Shopify Blog

VladimirC
Shopify Partner
14 0 6

Hi @hannachen,

Well, here (see the "What's next?" section) it says you should allow App Bridge to redirect you to the app if it's not loaded within the iframe:


At the end of the authentication flow, the user will end up at the redirectUri you provided. It's highly recommended that you let App Bridge redirect the user back to Shopify. As part of the initialization process, App Bridge redirects the user if necessary to ensure your app is embedded in the Shopify admin.

Note

Do not construct a Shopify admin URL manually (such as /admin/apps/\${apiKey}) since it might not be compatible in the future.

I see these errors at the last step OAuth on installation callback. Yes, it redirects back to Shopify but these errors in the console still don't seem right.

 

tunisoft
Shopify Partner
18 4 1

I experienced the same issue
I rely on App Bridge for making the redirect using `forceRedirect: true` but it fails

The issue is that it tried to redirect using the postMessage and not using window.location

I think it should detect when the app is not embedded and use `location.assign` instead of `postMessage`