Solved

Example REST call in new node library

Sascha5
Shopify Partner
8 2 7

So this is the server after you create a new embedded app with the Shopify CLI:

 

import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import Shopify, { ApiVersion } from "@shopify/shopify-api";
import Koa from "koa";
import next from "next";
import Router from "koa-router";

dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
  dev,
});
const handle = app.getRequestHandler();

Shopify.Context.initialize({
  API_KEY: process.env.SHOPIFY_API_KEY,
  API_SECRET_KEY: process.env.SHOPIFY_API_SECRET,
  SCOPES: process.env.SCOPES.split(","),
  HOST_NAME: process.env.HOST.replace(/https:\/\//, ""),
  API_VERSION: ApiVersion.October20,
  IS_EMBEDDED_APP: true,
  // This should be replaced with your preferred storage strategy
  SESSION_STORAGE: new Shopify.Session.MemorySessionStorage(),
});

// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};

app.prepare().then(async () => {
  const server = new Koa();
  const router = new Router();
  server.keys = [Shopify.Context.API_SECRET_KEY];
  server.use(
    createShopifyAuth({
      async afterAuth(ctx) {
        // Access token and shop available in ctx.state.shopify
        const { shop, accessToken, scope } = ctx.state.shopify;
        ACTIVE_SHOPIFY_SHOPS[shop] = scope;

        const response = await Shopify.Webhooks.Registry.register({
          shop,
          accessToken,
          path: "/webhooks",
          topic: "APP_UNINSTALLED",
          webhookHandler: async (topic, shop, body) =>
            delete ACTIVE_SHOPIFY_SHOPS[shop],
        });

        if (!response.success) {
          console.log(
            `Failed to register APP_UNINSTALLED webhook: ${response.result}`
          );
        }

        // Redirect to app with shop parameter upon auth
        ctx.redirect(`/?shop=${shop}`);
      },
    })
  );

  const handleRequest = async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  };

  router.get("/", async (ctx) => {
    const shop = ctx.query.shop;

    // This shop hasn't been seen yet, go through OAuth to create a session
    if (ACTIVE_SHOPIFY_SHOPS[shop] === undefined) {
      ctx.redirect(`/auth?shop=${shop}`);
    } else {
      await handleRequest(ctx);
    }
  });

  router.post("/webhooks", async (ctx) => {
    try {
      await Shopify.Webhooks.Registry.process(ctx.req, ctx.res);
      console.log(`Webhook processed, returned status code 200`);
    } catch (error) {
      console.log(`Failed to process webhook: ${error}`);
    }
  });

  router.post(
    "/graphql",
    verifyRequest({ returnHeader: true }),
    async (ctx, next) => {
      await Shopify.Utils.graphqlProxy(ctx.req, ctx.res);
    }
  );

  router.get("(/_next/static/.*)", handleRequest); // Static content is clear
  router.get("/_next/webpack-hmr", handleRequest); // Webpack content is clear
  router.get("(.*)", verifyRequest(), handleRequest); // Everything else must have sessions

  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

 

To call the REST API, I can use

new Shopify.Clients.Rest()

as we can read here: https://github.com/Shopify/shopify-node-api/blob/main/docs/usage/rest.md

However, I don't understand where exactly I can put that code. So given in my React app there is a button that says 'All products', and the Next app fires a request to my app, which then calls the product endpoint of the API and returns the data to react, how do I do that in the new library? In the old node library I just added a route, but I didn't quite understand yet how this works  here.

 

Thanks for your help!

Accepted Solution (1)
Sascha5
Shopify Partner
8 2 7

This is an accepted solution.

Yes, you can just use router.get. For example, to have an endpoint that respond with all scripttags from the corresponding REST endpoint, you write:

 

 

  router.get("/getScriptTags", verifyRequest(),  async (ctx) => {
    const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
    const client = new Shopify.Clients.Rest(session.shop, session.accessToken);
    ctx.body = await client.get({
       path: 'script_tags',
     });
     ctx.status = 200;
  });

 

 

Now, the tricky part is that from within your nextJS app, you cannot just use native fetch anymore, instead you have to use 

authenticatedFetch
 
By default, you have this part in your _app.js:

 

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;
  };
}

 

 

I brought this into the page component like this:

 

 

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

  const fetchFunction = userLoggedInFetch(app);
  const client = new ApolloClient({
    fetch: fetchFunction,
    fetchOptions: {
      credentials: "include",
    },
  });

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} fetch={fetchFunction} />
    </ApolloProvider>
  );
}

 

 

So inside the page I can now use

 

  const getScriptTags = async () => {
    const res = await props.fetch('/getScriptTags'); 
...

 

 

I hope this helps

 

View solution in original post

Replies 8 (8)

buerohuegel
Shopify Expert
7 0 2

I'm stuck with the very same problem. @Sascha5  could you figure it out?

Sascha5
Shopify Partner
8 2 7

This is an accepted solution.

Yes, you can just use router.get. For example, to have an endpoint that respond with all scripttags from the corresponding REST endpoint, you write:

 

 

  router.get("/getScriptTags", verifyRequest(),  async (ctx) => {
    const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
    const client = new Shopify.Clients.Rest(session.shop, session.accessToken);
    ctx.body = await client.get({
       path: 'script_tags',
     });
     ctx.status = 200;
  });

 

 

Now, the tricky part is that from within your nextJS app, you cannot just use native fetch anymore, instead you have to use 

authenticatedFetch
 
By default, you have this part in your _app.js:

 

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;
  };
}

 

 

I brought this into the page component like this:

 

 

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

  const fetchFunction = userLoggedInFetch(app);
  const client = new ApolloClient({
    fetch: fetchFunction,
    fetchOptions: {
      credentials: "include",
    },
  });

  const Component = props.Component;

  return (
    <ApolloProvider client={client}>
      <Component {...props} fetch={fetchFunction} />
    </ApolloProvider>
  );
}

 

 

So inside the page I can now use

 

  const getScriptTags = async () => {
    const res = await props.fetch('/getScriptTags'); 
...

 

 

I hope this helps

 

Sebastian_H
Shopify Partner
17 3 4

Dear @Sascha5, I've implemented your code but still get errors, when i call

await this.props.fetch(`/getScriptTags`).then((res) => {
              console.log("my response", res);
              // res.json();
            });

i get the response:

my response 
Response { type: "basic", url: "https://my-app/auth?shop=undefined", redirected: true, status: 200, ok: true, statusText: "OK", headers: Headers, body: ReadableStream, bodyUsed: false }

this is not the behaviour i expected. Do you have any hints?

Sascha5
Shopify Partner
8 2 7

To me this looks like your shop variable is undefined, which comes from ctx.state.shopify during the initial authentication of the app. This part is completely handled by the code generated by the CLI - maybe spin up a fresh app and try it there, and then double check all the parts in your server code that handle that shop variable.

There might also be a problem with the shop property in the router endpoint (session.shop), maybe something went wrong there (for example an old version of @Shopify/koa-shopify-auth, I use 4.1.2).

Shop should not be undefined, like we see it in your response.

Sebastian_H
Shopify Partner
17 3 4

@Sascha5 wrote:

Shop should not be undefined, like we see it in your response.


yes indeed 🙂🙂

@Sascha5thanks for your hint. Yes ctx.state.shopify is undefined.

Although it was a newly set up app, I will double check it again.

My version of koa-shopify-auth is 4.1.2. so for now not the cause.

Sebastian_H
Shopify Partner
17 3 4

After a few tests, I seem to have found the root cause. I need offline accessTokens and also work with them. When I switch everything back to online accessTokens, verifyRequest() works as expected and also the REST API call goes through.

{ accessMode: offline} is apparently not used by verifyRequest (by design!). This in turn causes the session to not load properly and then the call does not go through.

So I will switch back to online session for all browser to server communications and get the offline access token differently. Found an explanation here: https://github.com/Shopify/shopify-node-api/issues/140#issuecomment-801053492 It's GraphQL related, but still fits.

sagar_at_avyya
Shopify Partner
31 1 16

This is the best solution I found for this which allows appBridge inside a hook:

https://community.shopify.com/c/shopify-apis-and-sdks/getting-session-token-in-axios-intercept/m-p/1...

Here is my version of it, which works:

 

import axios from "axios";
import { useAppBridge } from '@shopify/app-bridge-react';
import { getSessionToken } from '@shopify/app-bridge-utils';

const config = {
	headers: {
		'Content-Type': 'application/json',
		Accept: 'application/json',
	},
};
/*
this is custom hook that fetch data from my backend
*/
export const useBackendApiClient = () => {
	const app = useAppBridge();
  const instance = axios.create();
  instance.interceptors.request.use(function (config) {

    return getSessionToken(app) // requires a Shopify App Bridge instance
      .then((token) => {
        // Append your request headers with an authenticated token
        config.headers["Authorization"] = `Bearer ${token}`;
        return config;
      });
  });
	return instance;
};

 

 

Usage in a component:

 

import { useBackendApiClient } from "../../hooks/useBackendApi";

const MyComponent = (props) => {
  const BackEndApiClient = useBackendApiClient();

  const handleSave = async () => {
    const res = await BackEndApiClient.get('/xxx',);
  }
}

 


Sagar @ Avyya - Modern Sales Booster
 - Was my reply helpful? Click Like to let me know! 
 - Was your question answered? Mark it as an Accepted Solution

muhammadfraz98
Shopify Partner
2 0 0

@sagar_at_avyya  can you also give example of post api?