reactjsnext.jsshopifyshopify-app-bridge

Shopify embedded App - Loading different pages does not work using nextjs


I have the following setup, when loading a new page using the nextjs router it does not work as the new page is blank. There seems to be no client-side or iframe-based navigation redirection occurring at all.

I have been successful using the Polaris Link components to navigate from page to page but that seems to completely reload my app in the iframe. I would like to use client-side routing and have even followed this example with no luck https://stackoverflow.com/a/63481122/671095

I am using a custom hook called useAppRoute to hook into the history of shopify-app-bridge but I don't think that's the best approach for what I would like to achieve.

_app.js

import {
  ApolloClient,
  ApolloProvider,
  ApolloLink,
  HttpLink,
  InMemoryCache,
} from "@apollo/client";
import App from "next/app";
import { AppProvider } from "@shopify/polaris";
import { Provider, useAppBridge } from "@shopify/app-bridge-react";
import { authenticatedFetch } from "@shopify/app-bridge-utils";
import { Redirect } from "@shopify/app-bridge/actions";
import "@shopify/polaris/build/esm/styles.css";
import translations from "@shopify/polaris/locales/en.json";
import RoutePropagator from "../components/RoutePropagator";
import { useAppRoute } from "src/hooks/useAppRoute";
import { ShopifySettingsProvider } from "src/contexts/ShopifySettings";

function userLoggedInFetch(app) {
  const fetchFunction = authenticatedFetch(app);

  return async (uri, options) => {
    const response = await fetchFunction(uri, options);

    if (
      response.headers.get("X-Shopify-API-Request-Failure-Reauthorize") === "1"
    ) {
      const authUrlHeader = response.headers.get(
        "X-Shopify-API-Request-Failure-Reauthorize-Url"
      );

      const redirect = Redirect.create(app);
      redirect.dispatch(Redirect.Action.APP, authUrlHeader || `/auth`);
      return null;
    }

    return response;
  };
}

function MyProvider(props) {
  const app = useAppBridge();

  const client = new ApolloClient({
    cache: new InMemoryCache(),
    link: ApolloLink.split(
      (operation) => operation.getContext().clientName === "shopify",
      new HttpLink({
        uri: "/graphql-shopify",
        fetch: userLoggedInFetch(app),
        fetchOptions: {
          credentials: "include",
        },
      }),
      new HttpLink({ uri: "/graphql" })
    ),
  });

  const { shop } = props;

  return (
    <ApolloProvider client={client}>
      <ShopifySettingsProvider shop={shop}>
        {props.children}
      </ShopifySettingsProvider>
    </ApolloProvider>
  );
}

function PolarisLink({ url, children, external, ...rest }) {
  if (external) {
    return (
      <a href={url} {...rest}>
        {children}
      </a>
    );
  }
  const redirect = useAppRoute();
  return (
    <span
      onClick={(e) => {
        console.log("redirected");
        e.preventDefault();
        e.stopPropagation();
        redirect(url);
      }}
    >
      <a {...rest}>{children}</a>
    </span>
  );
}

class MyApp extends App {
  render() {
    const { Component, pageProps, host, shop } = this.props;
    console.log(host);
    console.log(shop);
    return (
      <AppProvider i18n={translations} linkComponent={PolarisLink}>
        <Provider
          config={{
            apiKey: API_KEY,
            host: host,
            forceRedirect: true,
          }}
        >
          {/* <ClientRouter /> */}
          <RoutePropagator />
          <MyProvider Component={Component}>
            <Component {...pageProps} />
          </MyProvider>
        </Provider>
      </AppProvider>
    );
  }
}

MyApp.getInitialProps = async ({ ctx }) => {
  console.log(ctx);
  return {
    host: ctx.query.host,
  };
};

export default MyApp;

useAppRoute.js

import { useRouter } from "next/router";
import { useAppBridge } from "@shopify/app-bridge-react";
import { History } from "@shopify/app-bridge/actions";

export function useAppRoute() {
  const app = useAppBridge();
  const router = useRouter();
  const history = History.create(app);
  return (path) => {
    const [, asPath] = router.asPath.split("?");
    const pagePath = path.replace(/\/\d+/g, "/[id]");

    router.push(pagePath, `${path}?${asPath}`).then(() => {
      history.dispatch(History.Action.REPLACE, path);
    });
  };
}

RoutePropigator.js

import React, {useEffect, useContext} from 'react';
import Router, { useRouter } from "next/router";
import { Context as AppBridgeContext } from "@shopify/app-bridge-react";
import { Redirect } from "@shopify/app-bridge/actions";
import { RoutePropagator as ShopifyRoutePropagator } from "@shopify/app-bridge-react";

const RoutePropagator = () => {
  const router = useRouter();
  const { asPath } = router;
  const appBridge = React.useContext(AppBridgeContext);

  // Subscribe to appBridge changes - captures appBridge urls
  // and sends them to Next.js router. Use useEffect hook to
  // load once when component mounted
  useEffect(() => {
    appBridge.subscribe(Redirect.Action.APP, ({ path }) => {
      Router.push(path);
    });
  }, []);

  return appBridge && asPath ? (
    <ShopifyRoutePropagator location={asPath} app={appBridge} />
  ) : null;
}

export default RoutePropagator;

index.js - router.push example

import React, { useState } from "react";
import Link from "next/link";
import {
  Frame,
  Page,
  Layout,
  EmptyState,
  Button,
  Card,
} from "@shopify/polaris";
import { ResourcePicker, TitleBar } from "@shopify/app-bridge-react";
import store from "store-js";
import ResourceListWithProducts from "../components/elements/ResourceList";
import Sidebar from "../components/Sidebar";
import { useRouter } from 'next/router'

const img = "https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg";

const Index = () => {
  const router = useRouter()
  const [open, setOpen] = useState(false);

  // A constant that defines your app's empty state
  const emptyState = !store.get("ids");
  const handleSelection = (resources) => {
    const idsFromResources = resources.selection.map((product) => product.id);
    setOpen(false);
    store.set("ids", idsFromResources);
  };

  return (
    <Frame navigation={<Sidebar />}>
      <Page>
        <TitleBar />
        <ResourcePicker
          resourceType="Product"
          showVariants={false}
          open={open}
          onSelection={(resources) => handleSelection(resources)}
          onCancel={() => setOpen(false)}
        />
        {emptyState ? ( // Controls the layout of your app's empty state
          <Layout>
            <EmptyState heading="Customise your product" image={img}>
              <p>Add options to customise your product.<button onClick={() => router.push('/colours')}>Go to colours</button></p>
            </EmptyState>
          </Layout>
        ) : (
          // Uses the new resource list that retrieves products by IDs
          <ResourceListWithProducts />
        )}
      </Page>
    </Frame>
  );
};

export default Index;

Solution

  • So turns out the solution to resolving this was first to basically impliment the _app.js, RoutePropigater code example laid out here https://github.com/carstenlebek/shopify-node-app-starter

    Also in particular, I had to also update my node packages to the same versions as this starter pack example. Hope this helps other people