简体   繁体   中英

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;

I've tried a similar approach with the RoutePropagator but the subscription doesn't actually fire reliably on page changes.

I just don't think Shopify will support NextJS - especially with the change in the CLI tool now scaffolding a custom build instead of using NextJS + Koa.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM