404 error calling GET API from a shopify theme-extension - express

I'm new to shopify development, and can't figure out how to call an authenticated API from a shopify theme-extension. Essentially, I'm trying to make a theme extension, where one of the functionalities is that when a checkbox is clicked, an API that counts the number of products is called.
I have a working api that gets the product count, and in web>index.js, I have set-up the end-point:
app.get("/api/products/count", async (_req, res) => {
const countData = await shopify.api.rest.Product.count({
session: res.locals.shopify.session,
});
res.status(200).send(countData);
});
Under web>frontend>hooks, I have the authenticated hooks set-up as shown below. I've tested that if I call the "api/products/count" API from one of the web pages using useAppQuery, it works as expected, and returns the product count.
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useMemo } from "react";
import { useQuery } from "react-query";
export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => {
const authenticatedFetch = useAuthenticatedFetch();
const fetch = useMemo(() => {
return async () => {
const response = await authenticatedFetch(url, fetchInit);
return response.json();
};
}, [url, JSON.stringify(fetchInit)]);
return useQuery(url, fetch, {
...reactQueryOptions,
refetchOnWindowFocus: false,
});
};
In my theme extension code, I've added an event listener to the checkbox which calls getProductCount. In getProductCount, I want to call /api/products/count:
import { useAppQuery } from "../../../web/frontend/hooks";
export const getProductCount = (product) => {
const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: "/api/products/count",
reactQueryOptions: {
onSuccess: () => {
setIsLoading(false);
},
},
});
}
However, when I run locally and click the checkbox, it returns a 404 error trying to find useAppQuery. The request URL is https://cdn.shopify.com/web/frontend/hooks. It seems like the authentication isn't working because that URL looks incorrect.
Am I missing a step that I need to do in order to call an authenticated API from a theme-extension?
I thought the issue was just the import path for useAppQuery but I've tried different paths, and they all return the same 404 issue.

If you want a hot tip here. In your theme App extension, you do not actually need to make an API call to get a product count. In your theme app extension, you can just use Liquid, and dump the product count out to a variable of your choice, and use the count, display the count, do whatever.
{{ shop.product_count }}
Of course, this does not help you if you need other storefront API calls in your theme App extension, but whatever. In my experience, I render the API Access Token I need in my theme app extension, and then making my Storefront API calls is just a fetch().
The only time I would use authenticated fetch, is when I am doing embedded App API calls, but that is a different beast from a theme app extension. In there, you do not get to make authenticated calls as the front-end is verboten for those of course. Instead you'd use App Proxy for security.
TL:DR; Storefront API calls with a token should not fail with a 404 if you call the right endpoint. You can use Storefront API inside a theme app extension. Inside a theme app extension, if you need backend Admin API access, you can use App Proxy calls.

Related

How do I mock server-side API calls in a Nextjs app?

I'm trying to figure out how to mock calls to the auth0 authentication backend when testing a next js app with React Testing Library. I'm using auth0/nextjs-auth0 to handle authentication. My intention is to use MSW to provide mocks for all API calls.
I followed this example in the nextjs docs next.js/examples/with-msw to set up mocks for both client and server API calls. All API calls generated by the auth0/nextjs-auth0 package ( /api/auth/login , /api/auth/callback , /api/auth/logout and /api/auth/me) received mock responses.
A mock response for /api/auth/me is shown below
import { rest } from 'msw';
export const handlers = [
// /api/auth/me
rest.get(/.*\/api\/auth\/me$/, (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
user: { name: 'test', email: 'email#domain.com' },
}),
);
}),
];
The example setup works fine when I run the app in my browser. But when I run my test the mocks are not getting picked up.
An example test block looks like this
import React from 'react';
import {render , screen } from '#testing-library/react';
import Home from 'pages/index';
import App from 'pages/_app';
describe('Home', () => {
it('should render the loading screen', async () => {
render(<App Component={Home} />);
const loader = screen.getByTestId('loading-screen');
expect(loader).toBeInTheDocument();
});
});
I render the page inside the App component like this <App Component={Home} /> so that I will have access to the various contexts wrapping the pages.
I have spent about 2 days on this trying out various configurations and I still don't know what I might be doing wrong. Any and every help is appreciated.
This is probably resolved already for the author, but since I ran into the same issue and could not find useful documentation, this is how I solved it for end to end tests:
Overriding/configuring the API host.
The plan is to have the test runner start next.js as custom server and then having it respond to both the next.js, as API routes.
A requirements for this to work is to be able to specify the backend (host) the API is calling (via environment variables). Howerver, access to environment variables in Next.js is limited, I made this work using the publicRuntimeConfig setting in next.config.mjs. Within that file you can use runtime environment variables which then bind to the publicRuntimeConfig section of the configuration object.
/** #type {import('next').NextConfig} */
const nextConfig = {
(...)
publicRuntimeConfig: {
API_BASE_URL: process.env.API_BASE_URL,
API_BASE_PATH: process.env.API_BASE_PATH,
},
(...)
};
export default nextConfig;
Everywhere I reference the API, I use the publicRuntimeConfig to obtain these values, which gives me control over what exactly the (backend) is calling.
Allowing to control the hostname of the API at runtime allows me to change it to the local machines host and then intercept, and respond to the call with a fixture.
Configuring Playwright as the test runner.
My e2e test stack is based on Playwright, which has a playwright.config.ts file:
import type { PlaywrightTestConfig } from '#playwright/test';
const config: PlaywrightTestConfig = {
globalSetup: './playwright.setup.js',
testMatch: /.*\.e2e\.ts/,
};
export default config;
This calls another file playwright.setup.js which configures the actual tests and backend API mocks:
import {createServer} from 'http';
import {parse} from 'url';
import next from 'next';
import EndpointFixture from "./fixtures/endpoint.json";
// Config
const dev = process.env.NODE_ENV !== 'production';
const baseUrl = process?.env?.API_BASE_URL || 'localhost:3000';
// Context
const hostname = String(baseUrl.split(/:(?=\d)/)[0]).replace(/.+:\/\//, '');
const port = baseUrl.split(/:(?=\d)/)[1];
const app = next({dev, hostname, port});
const handle = app.getRequestHandler();
// Setup
export default async function playwrightSetup() {
const server = await createServer(async (request, response) => {
// Mock for a specific endpoint, responds with a fixture.
if(request.url.includes(`path/to/api/endpoint/${EndpointFixture[0].slug}`)) {
response.write(JSON.stringify(EndpointFixture[0]));
response.end();
return;
}
// Fallback for pai, notifies about missing mock.
else if(request.url.includes('path/to/api/')) {
console.log('(Backend) mock not implementeded', request.url);
return;
}
// Regular Next.js behaviour.
const parsedUrl = parse(request.url, true);
await handle(request, response, parsedUrl);
});
// Start listening on the configured port.
server.listen(port, (error) => {
console.error(error);
});
// Inject the hostname and port into the applications publicRuntimeConfig.
process.env.API_BASE_URL = `http://${hostname}:${port}`;
await app.prepare();
}
Using this kind of setup, the test runner should start a server which responds to both the routes defined by/in Next.js as well as the routes intentionally mocked (for the backend) allowing you to specify a fixture to respond with.
Final notes
Using the publicRuntimeConfig in combination with a custom Next.js servers allows you to have a relatively large amount of control about the calls that are being made on de backend, however, it does not necessarily intercept calls from the frontend, the existing frontend mocks might stil be necessary.

Shopify node backend- frontend communication

I am really new in shopify app development.
I have an allready a working app what i have created with next.JS (I have worked with node/express too)
I just would like to create a connection between my frontend and backend with a simple endpoint.
It means i send a get request and i receive something nonsense. The main goal would be that is the backend can communicate with the frontend.
I have created a git repo too.: https://github.com/akospaska/shopify-outofthebox
The app has been created with shopify-cli
In my pages folder there is an index.js file, where my frontend "lives". 
I have created (or i think ) 2 differend endpoints.
pages/api/test   endpoint: "/test"
server/server.js  endpoint: "/test2"
When i call the endpoints i get an error. 
I have read the documentation but it just makes me confused.
How should i authenticate between my backend and frontend exactly?
Thank you for your help Guys in advance.
The endpoints aren't pages, they are routes on your express app.
Here is a related question with answer:
Node backend communication between react frontend and node backend | Shopify related
Here is a checklist for you how to set up an endpoint (POST):
1.) Navigate to your index.js file in the /web directory
2.) Insert this code:
app.post("/api/test", async (req, res) => {
try {
res.status(201).send(response);
} catch (error) {
res.status(500).send(error.message);
}
});
}
app.post() sets up a route in your project.
3.) Navigate to your index.jsx file in /pages directory and insert this code (I set up a callback when a form submit button is clicked):
const handleSubmit = useCallback(
(body) => {
(async () => {
const parsedBody = body;
const response = await fetch("/api/test?shop=YOUR_SHOP_URL, {
method: "POST",
body: parsedBody
});
if (response.ok) {
console.log("Success");
}
})();
return { status: "success" };
},
[]
);
<Form onSubmit={handleSubmit}>
</Form>
it should call this API endpoint. So now you communicate with an API endpoint.
Maybe I could help you with my answer!

How to use a private API key with Nuxt (on the client)?

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

Running Nuxt middleware client side after static rendering

We're switching from SPA to statically generated, and are running into a problem with middleware.
Basically, when Nuxt is statically rendered, middleware is run on the build server first, and then is run after each page navigation client side. The important point is that middleware is not run client side on first page load. This is discussed here
We work around this for some use cases by creating a plugin that uses the same code, since plugins are run on the first client load.
However, this pattern doesn't work well for this use case. The following is an example of the middleware that we want to use:
// middleware/authenticated.js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
// Inside a component
<template>
<h1>Secret page</h1>
</template>
<script>
export default {
middleware: 'authenticated'
}
</script>
This example is taken directly from the Nuxt docs.
When rendered statically, this middleware is not called on first page load, so a user might end up hitting their dashboard before they've logged in, which causes problems.
To add this to a plugin, the only way I can think to do this is by adding a list of authenticated_routes, which the plugin could compare to and see if the user needs to be authed.
The problem with that solution though is that we'd then need to maintain a relatively complex list of authed pages, and it's made worse by having dynamic routes, which you'd need to match a regex to.
So my question is: How can we run our authenticated middleware, which is page specific, without needing to maintain some list of routes that need to be authenticated? Is there a way to actually get the middleware associated to a route inside a plugin?
To me it is not clear how to solve it the right way. We are just using the static site generation approach. We are not able to run a nuxt middleware for the moment. If we detect further issues with the following approach we have to switch.
One challenge is to login the user on hot reload for protected and unprotected routes. As well as checking the login state when the user switches the tabs. Maybe session has expired while he was on another tab.
We are using two plugins for that. Please, let me know what you think.
authRouteBeforeEnter.js
The plugin handles the initial page load for protected routes and checks if the user can access a specific route while navigating around.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes"
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
if(to.name === 'logout'){
await store.dispatch('app/shutdown', {userLogout:true})
return next('/')
}
if(PROTECTED_ROUTES.includes(to.name)){
if(document.cookie.indexOf('PHPSESSID') === -1){
await store.dispatch('app/shutdown')
}
if(!store.getters['user/isLoggedIn']){
await store.dispatch('user/isAuthenticated', {msg: 'from before enter plugin'})
console.log('user is logged 2nd try: ' + store.getters['user/isLoggedIn'])
return next()
}
else {
/**
* All fine, let him enter
*/
return next()
}
}
return next()
})
}
authRouterReady.js
This plugin ment for auto login the user on unprotected routes on initial page load dnd check if there is another authRequest required to the backend.
import { PROTECTED_ROUTES } from "~/constants/protectedRoutes";
export default function ({ app, store }) {
app.router.onReady(async (route) => {
if(PROTECTED_ROUTES.includes(route.name)){
// Let authRouterBeforeEnter.js do the job
// to avoid two isAuthorized requests to the backend
await store.dispatch('app/createVisibilityChangedEvent')
}
else {
// If this route is public do the full init process
await store.dispatch('app/init')
}
})
}
Additionally i have added an app module to the store. It does a full init process with auth request and adding a visibility changed event or just adds the event.
export default {
async init({ dispatch }) {
dispatch('user/isAuthenticated', {}, {root:true})
dispatch('createVisibilityChangedEvent')
},
async shutdown({ dispatch }, {userLogout}) {
dispatch('user/logout', {userLogout}, {root:true})
},
async createVisibilityChangedEvent({ dispatch }) {
window.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible') {
console.log('visible changed');
await dispatch('user/isAuthenticated', {}, {root:true})
}
})
},
}

React Native App Authentication with Instagram API

I've been trying to build a react native app that requires users to authenticate with their Instagram account. The Instagram API has a authorisation link and perhaps the only way to display that in an app would be through 'WebView' and so I used that.
The authentication workflow runs smoothly and then my server even gets the access token and user-id. But the problem is how to send this access token back to the app? I've used express-js for the 'redirect-uri' and so the WebView makes request to app.get() handler. In order to send response to same client on which the connection is opened, we must use res.send(). This would send the response to WebView, let's say I capture that using 'injectedJavaScript' but this javascript runs within WebView and so its unable to access react-native variables. In the event of a correct access-token, how would I ever navigate away from the WebView?
Any solutions to the above problems would be greatly appreciated. I suspect that there might even be problems with this approach(in my choice of WebView for this purpose, etc.), so a change of approach even entirely would also be of help. All I want is to authenticate the app users with Instagram and get my project going. Thanks a lot.
If you are using Expo, you can use AuthSessions to accomplish this (https://docs.expo.io/versions/latest/sdk/auth-session). The exact way to do it depends on whether you are using a managed workflow or a bare workflow, etc., but for managed workflow you can do the following:
Go to the Facebook Developer's console, go to your app, and add the Instagram Basic Display product.
Under Instagram Basic Display, under Valid OAuth Redirect URIs, use https://auth.expo.io/#your-expo-username/your-project-slug (project slug is in your app.json file)
On the same FB Developer page, add an Instagram tester profile and then follow the steps to authenticate that user.
In your project, install expo install expo-auth-session and import it into your Component
Also install expo-web-browser
Code your component like so:
import React, { useEffect } from 'react';
import { Button, Platform, Text, TouchableOpacity, View } from 'react-native';
import * as WebBrowser from 'expo-web-browser';
import { useAuthRequest, makeRedirectUri } from 'expo-auth-session';
WebBrowser.maybeCompleteAuthSession(); // <-- will close web window after authentication
const useProxy = Platform.select({ web: false, default: true });
const client_id = 9999999999999;
const redirect_uri = "https://auth.expo.io/#your-expo-username/your-project-slug";
const scope = "user_profile,user_media";
const site = "https://api.instagram.com/oauth/authorize?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&scope=" + scope + "&response_type=code&state=1";
const discovery = { authorizationEndpoint: site }
const GetInstagram = () => {
const [request, response, promptAsync] = useAuthRequest({
redirectUri: makeRedirectUri({
useProxy,
native: redirect_uri
}),
scopes: [scope],
clientId: client_id
}, discovery);
useEffect(() => {
if (response?.type === 'success') {
const { code } = response.params; <--- the IG code will be returned here
console.log("code : ", code);
}
}, [response]);
return (
<View>
<TouchableOpacity onPress={ () => promptAsync({useProxy,windowFeatures: { width: 700, height: 600 }}) }>
<Text>Connect Your Instagram</Text>
</TouchableOpacity>
</View>
)
}
export default GetInstagram;
One way to accomplish this is via using deeplink. I don't think it's the best practice though.
The response from the WebView will be sent to the redirect URL you've previously setup after successful authentication. Please set the redirect URL to your app. For example, on iOS if you have URL Scheme as "myapp123" then, anytime you open your browser and type myapp123://.. it will open your app and your app should be able to get response sent from the instagram.