9

I am new to Shopify App Devlopment, especially the Shopify API.

I create a working app with the Shopify CLI and now want to communicate with the API.

I try to access following endpoint: https://{my_shop]/admin/api/2021-07/shop.json

I learned that I need some access token and the shop name to access this endpoint.

I created an access token under my private apps section.

But I dont know how to get the currently logged in store.

For example, when clicking a button in my frontend, I would like to call my endpoint, which in turn calls the Shopify API endpoint and retrieves the information. How do I do this the right way? And how do I get the currently logged in shop?

This is my code so far:

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";
import axios from 'axios';

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;
        const host = ctx.query.host;
        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}&host=${host}`);
      },
    })
  );

  router.get("/test2", verifyRequest(), async(ctx, res) => {
    const {shop, accessToken } = ctx.session;
    console.log(shop);
    console.log(accessToken);
  })

  router.get("/test", async (ctx) => {

    const config = {
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Access-Token': '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'
      }
    }

    await axios.get('${the_store_name_belongs_here}/admin/api/2021-07/shop.json', config).then(res => {
      ctx.body = res.data;
    });
  });

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

  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("(.*)", 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);
    }
  });


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

Please have a look at my attempts - endpoint /test and endpoint /test2. test2 is not working. ctx.session is null. ctx itself is null. Why?

test1 is working when I hard code my shops name into the url, then I get the desired data. But how do I put a shop variable inside? That's my struggle.

IonicMan
  • 743
  • 1
  • 12
  • 31
  • Considering you're accessing the `Admin API`, you should be using `shopify-admin-api` https://www.npmjs.com/package/shopify-admin-api And after the auth, you'd do `const shop = await shops.get();` – Ovi Sep 17 '21 at 12:36
  • @Ovi could you show an example in an answer? That would be very helpful! – IonicMan Sep 21 '21 at 10:44
  • Do you call the URLs from inside or outside the app? – Antoine Sep 23 '21 at 14:00
  • @AntoineAndrieu for now, only in the backend – IonicMan Sep 23 '21 at 14:16
  • From what I understand, only the request coming from Shopify has the shop as a query params. What I usually do is that I manually pass the shop as a query params, in the body or the headers. – Antoine Sep 24 '21 at 05:14
  • @AntoineAndrieu could you try to make an answer with an example? that would be very nice – IonicMan Sep 24 '21 at 06:38
  • https://stackoverflow.com/a/70084201/5950360 You can check this for a similar issue and it's solution – Surbhit Rao Nov 24 '21 at 05:03

3 Answers3

3

First of all, it is not good practice to use MemorySessionStorage in production environment due to its limitations, you can find a good explanation here

MemorySessionStorage exists as an option to help you get started developing your apps as quickly as possible...

So, implement a CustomSessionStorage (refer to the doc above), you will have access to the session that stores data such as shop, accessToken, scope among others. As long as authenticated requests are made, providing the JWT in the header, you can get the context properly working.

e.g. (react-koa):

//client.js
import { useAppBridge } from "@shopify/app-bridge-react";
import { getSessionToken } from "@shopify/app-bridge-utils";

function Index() {
   const app = useAppBridge();

   async function getProducts() {
       const token = await getSessionToken(app);

       const response = await fetch("/api/products", {
           headers: { "Authorization": `Bearer ${token}` }
       });

       const result = await response.json();
       console.log(result);
   }

   return (<></>);
}

and then...

// server.js

router.get("/api/products", verifyRequest({ returnHeader: true }), async (ctx) => {
    // Load the current session to get the `accessToken`.
    // pass a third parameter clarifying the accessMode (isOnline = true by default)
    const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);

    // Create a new client for the specified shop.
    const client = new Shopify.Clients.Rest(session.shop, session.accessToken);

    // Use `client.get` to request the specified Shopify REST API endpoint, in this case `products`.
    const products = await client.get({
      path: 'products',
    });

    ctx.body = results.body;
    ctx.res.status = 200;
  });

more details here.

Using axios, you can define it as a hook (working example using TypeScript):

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

function useAxios() {
  const app = useAppBridge();
  const instance = axios.create();
  instance.interceptors.request.use(async function (config) {
    const token = await getSessionToken(app);
    config.headers['Authorization'] = `Bearer ${token}`;
    return config;
  });
  return [instance];
}

export default useAxios;

// index.js

// ...
const [axios] = useAxios();

// ...
const result = await axios.get('/api/products');
console.log(result.data);
// ...

Hope this helps anyone who is still looking for help.

hnakao11
  • 785
  • 2
  • 9
  • 30
2

I faced this problem and resolved it by passing the shop as a query parameter.

I call the endpoint with:

axios.get('/test', {
  params: {
    shop: 'fo.myshopify.com'
  }
});

And get the shop with:

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

Of course, you have to know the shop where you call the endpoint.

Antoine
  • 130
  • 2
  • 12
0

There is no reference to any ctx.session in koa-shopify-auth documentation. What about this:

router.get("/test2", verifyRequest(), async(ctx) => {
  const { shop, accessToken } = ctx.state.shopify;
  console.log(shop, accessToken);
})

Other solutions

You can store a Cookie after authentication

afterAuth(ctx) {
    const { shop, accessToken } = ctx.session;
    ctx.cookies.set("shop", shop, { httpOnly: false, secure: true, sameSite: "none" });
    ctx.redirect("/");
},

And then read it in future requests:

router.get("/test2", verifyRequest(), async(ctx) => {
  const shop = ctx.cookies.get("shop");
  console.log(shop);
})
Newbie
  • 4,462
  • 11
  • 23
  • 1
    From the question: "ctx itself is null". So it's probably not the right way to get the shop. – Antoine Sep 24 '21 at 05:15
  • yeah, sadly ctx itself is null so no – IonicMan Sep 24 '21 at 06:37
  • 1
    I've reproduced your very same example as close as possible and have the context properly working. In `koa` there is no request without context so it is very unlikely the context to be missing. Furthermore the middleware (https://github.com/Shopify/koa-shopify-auth/blob/976e95a1d8fa3628aff4611493c83eb636753c20/src/verify-request/verify-request.ts) has nothing that can mess up the context. Can you provide us a complete example, including `package.json` and anything else required to reproduce the error? – Newbie Sep 24 '21 at 07:58
  • @AntoineAndrieu from the original question: "ctx.session is null. ctx itself is null". If ctx was null then ctx.session can not be null (being it an invalid accessor). This is why I did not take this sentence as completely true. – Newbie Sep 24 '21 at 08:03