A space to discuss GraphQL queries, mutations, troubleshooting, throttling, and best practices.
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!
Solved! Go to the solution
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
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
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
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
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?
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.
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.
This is the best solution I found for this which allows appBridge inside a hook:
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