Code Monkey home page Code Monkey logo

shopify-app-template-node's Introduction

Shopify App Template for Node

This is a template for building a Shopify app using Node and React. It contains the basics for building a Shopify app.

Rather than cloning this repo, you can use your preferred package manager and the Shopify CLI with these steps.

Benefits

Shopify apps are built on a variety of Shopify tools to create a great merchant experience. The create an app tutorial in our developer documentation will guide you through creating a Shopify app using this template.

The Node app template comes with the following out-of-the-box functionality:

  • OAuth: Installing the app and granting permissions
  • GraphQL Admin API: Querying or mutating Shopify admin data
  • REST Admin API: Resource classes to interact with the API
  • Shopify-specific tooling:
    • AppBridge
    • Polaris
    • Webhooks

Tech Stack

This template combines a number of third party open-source tools:

The following Shopify tools complement these third-party tools to ease app development:

  • Shopify API library adds OAuth to the Express backend. This lets users install the app and grant scope permissions.
  • App Bridge React adds authentication to API requests in the frontend and renders components outside of the App’s iFrame.
  • Polaris React is a powerful design system and component library that helps developers build high quality, consistent experiences for Shopify merchants.
  • Custom hooks make authenticated requests to the Admin API.
  • File-based routing makes creating new pages easier.
  • @shopify/i18next-shopify is a plugin for i18next that allows translation files to follow the same JSON schema used by Shopify app extensions and themes.

Getting started

Requirements

  1. You must download and install Node.js if you don't already have it.
  2. You must create a Shopify partner account if you don’t have one.
  3. You must create a store for testing if you don't have one, either a development store or a Shopify Plus sandbox store.

Installing the template

This template can be installed using your preferred package manager:

Using yarn:

yarn create @shopify/app --template=node

Using npm:

npm init @shopify/app@latest -- --template=node

Using pnpm:

pnpm create @shopify/app@latest --template=node

This will clone the template and install the required dependencies.

Local Development

The Shopify CLI connects to an app in your Partners dashboard. It provides environment variables, runs commands in parallel, and updates application URLs for easier development.

You can develop locally using your preferred package manager. Run one of the following commands from the root of your app.

Using yarn:

yarn dev

Using npm:

npm run dev

Using pnpm:

pnpm run dev

Open the URL generated in your console. Once you grant permission to the app, you can start development.

Deployment

Application Storage

This template uses SQLite to store session data. The database is a file called database.sqlite which is automatically created in the root. This use of SQLite works in production if your app runs as a single instance.

The database that works best for you depends on the data your app needs and how it is queried. You can run your database of choice on a server yourself or host it with a SaaS company. Here’s a short list of databases providers that provide a free tier to get started:

Database Type Hosters
MySQL SQL Digital Ocean, Planet Scale, Amazon Aurora, Google Cloud SQL
PostgreSQL SQL Digital Ocean, Amazon Aurora, Google Cloud SQL
Redis Key-value Digital Ocean, Amazon MemoryDB
MongoDB NoSQL / Document Digital Ocean, MongoDB Atlas

To use one of these, you need to change your session storage configuration. To help, here’s a list of SessionStorage adapter packages.

Build

The frontend is a single page app. It requires the SHOPIFY_API_KEY, which you can find on the page for your app in your partners dashboard. Paste your app’s key in the command for the package manager of your choice:

Using yarn:

cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME yarn build

Using npm:

cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME npm run build

Using pnpm:

cd web/frontend/ && SHOPIFY_API_KEY=REPLACE_ME pnpm run build

You do not need to build the backend.

Hosting

When you're ready to set up your app in production, you can follow our deployment documentation to host your app on a cloud provider like Heroku or Fly.io.

When you reach the step for setting up environment variables, you also need to set the variable NODE_ENV=production.

Known issues

Hot module replacement and Firefox

When running the app with the CLI in development mode on Firefox, you might see your app constantly reloading when you access it. That happened in previous versions of the CLI, because of the way HMR websocket requests work.

We fixed this issue with v3.4.0 of the CLI, so after updating it, you can make the following changes to your app's web/frontend/vite.config.js file:

  1. Change the definition hmrConfig object to be:

    const host = process.env.HOST
      ? process.env.HOST.replace(/https?:\/\//, "")
      : "localhost";
    
    let hmrConfig;
    if (host === "localhost") {
      hmrConfig = {
        protocol: "ws",
        host: "localhost",
        port: 64999,
        clientPort: 64999,
      };
    } else {
      hmrConfig = {
        protocol: "wss",
        host: host,
        port: process.env.FRONTEND_PORT,
        clientPort: 443,
      };
    }
  2. Change the server.host setting in the configs to "localhost":

    server: {
      host: "localhost",
      ...

I can't get past the ngrok "Visit site" page

When you’re previewing your app or extension, you might see an ngrok interstitial page with a warning:

You are about to visit <id>.ngrok.io: Visit Site

If you click the Visit Site button, but continue to see this page, then you should run dev using an alternate tunnel URL that you run using tunneling software. We've validated that Cloudflare Tunnel works with this template.

To do that, you can install the cloudflared CLI tool, and run:

# Note that you can also use a different port
cloudflared tunnel --url http://localhost:3000

Out of the logs produced by cloudflare you will notice a https URL where the domain ends with trycloudflare.com. This is your tunnel URL. You need to copy this URL as you will need it in the next step.

2022-11-11T19:57:55Z INF Requesting new quick Tunnel on trycloudflare.com...
2022-11-11T19:57:58Z INF +--------------------------------------------------------------------------------------------+
2022-11-11T19:57:58Z INF |  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |
2022-11-11T19:57:58Z INF |  https://randomly-generated-hostname.trycloudflare.com                                     |
2022-11-11T19:57:58Z INF +--------------------------------------------------------------------------------------------+

Below you would replace randomly-generated-hostname with what you have copied from the terminal. In a different terminal window, navigate to your app's root and with the URL from above you would call:

# Using yarn
yarn dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000
# or using npm
npm run dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000
# or using pnpm
pnpm dev --tunnel-url https://randomly-generated-hostname.trycloudflare.com:3000

Developer resources

shopify-app-template-node's People

Contributors

abalejr avatar alvaro-shopify avatar amcaplan avatar byrichardpowell avatar cquemin avatar dependabot[bot] avatar fionoble avatar gonzaloriestra avatar hheyhhay avatar isaacroldan avatar lizkenyon avatar local-administrator avatar manojonurnet avatar mathiasgr avatar mkevinosullivan avatar nhymxu avatar paulomarg avatar pepicrft avatar rachel-carvalho avatar sam-killgallon avatar seanyb avatar shoheikawano avatar surma avatar tiffanytse avatar trishrempel avatar yuyaohshimo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shopify-app-template-node's Issues

Add custom User Agent

Issue summary

We want to gain some more insight into how people are using our tools, including this repo. A custom User Agent will help with that (though obviously folks would be able to remove/change it when building their app).

Suggestion: ShopifyWebgenEmbeddedApp

Not working in Heroku

Issue summary

I've created a new project by the CLI and deploy it to heroku.
But the application does not work in Heroku.
I got the Internal Server Error in the Shopify application.

Expected behavior

When accessing the application in the Shopify store admin,
we can see the default content.

Actual behavior

I got Internal Server Error.
スクリーンショット 2021-02-08 0 00 35

Steps to reproduce the problem

$ shopify create node
$ shopify deploy heroku

And put the Heroku's application domain to the application detail page.
スクリーンショット 2021-02-08 0 02 54

And I got these error to run heroku logs.

2021-02-07T15:00:29.233915+00:00 app[web.1]: 
2021-02-07T15:00:29.233957+00:00 app[web.1]: TypeError [ERR_INVALID_ARG_TYPE]: The "key" argument must be of type string or an instance of Buffer, TypedArray, DataView, or KeyObject. Received undefined
2021-02-07T15:00:29.233959+00:00 app[web.1]: at prepareSecretKey (internal/crypto/keys.js:322:11)
2021-02-07T15:00:29.233959+00:00 app[web.1]: at new Hmac (internal/crypto/hash.js:111:9)
2021-02-07T15:00:29.233960+00:00 app[web.1]: at Object.createHmac (crypto.js:147:10)
2021-02-07T15:00:29.233960+00:00 app[web.1]: at sign (/app/node_modules/keygrip/index.js:23:8)
2021-02-07T15:00:29.233961+00:00 app[web.1]: at Keygrip.sign (/app/node_modules/keygrip/index.js:30:38)
2021-02-07T15:00:29.233961+00:00 app[web.1]: at Cookies.set (/app/node_modules/cookies/index.js:110:30)
2021-02-07T15:00:29.233962+00:00 app[web.1]: at topLevelOAuthRedirect (/app/node_modules/@shopify/koa-shopify-auth/dist/src/auth/create-top-level-oauth-redirect.js:10:21)
2021-02-07T15:00:29.233963+00:00 app[web.1]: at /app/node_modules/@shopify/koa-shopify-auth/dist/src/auth/index.js:61:46
2021-02-07T15:00:29.233963+00:00 app[web.1]: at step (/app/node_modules/tslib/tslib.js:133:27)
2021-02-07T15:00:29.233963+00:00 app[web.1]: at Object.next (/app/node_modules/tslib/tslib.js:114:57)
2021-02-07T15:00:29.233964+00:00 app[web.1]: 

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.

Specifications

  • Browser: Firefox
  • Device: macOS
  • Operating System:

Video tutorial on authentication with session-tokens

https://www.youtube.com/watch?v=Vq0aWTaJDAY

Reduce the risk of your embedded app failing due to 3rd party cookie browser restrictions by switching
to session token-based authentication and stop relying on cookies to verify identity.

Session token-based authentication is now a requirement for all embedded apps in the Shopify App Store.
New embedded apps submitted for an app review will be required to adopt sessions tokens to meet Shopify standards.

Learn more about how to authenticate your app using session tokens by watching this YouTube video or on Shopify.dev

Can't seem to add a proper install page like the Rails app.

Issue summary

Would like a little guidance on setting up a proper login/install page like the Rails app.
It seems like auth is run on every server call, and I can't seem to figure out a way around this.
I may not fully understand the middleware you have running.

Doesn't look like we have control over the /auth page or the ability to overwrite that route or managed redirects here.

Would love some insight or examples of how this is expected to be handled.

Update session to include samesite setting

Issue summary

With the updates to chrome's security policies the app's session will need to include the sameSite setting: https://help.shopify.com/en/api/guides/samesite-cookies

Expected behavior

What do you think should happen?

Samesite settings are included out of the box as apps built with this boilerplate are embedded out of the box as far as I can tell

Actual behavior

What actually happens?

Users have to have awareness of the issue and be able to dig through documentation to resolve

Potential proposed resolution:
Shopify/quilt@c71b7de#diff-bc1e64c7a3f21c973926784c63f6b22d

Implement that line of code within the shopify-node-app boilerplate as opposed to being buried in a comment in a dependency (and with the monorepo set up of quilt it is difficult to find, IMO)

I will have more time next week to attempt a PR for the issue, but given the Feb 4th deadline, it might be useful to prioritise this earlier. Any comments or thoughts would be appreciated - I've yet to test or implement this, so apologies for that but thought I'd flag it early regardless.

There's no page at this address

After reInstall app, I see this message
There's no page at this address
Check the URL and try again, or use the search bar to find what you need

All ok locally, with ngrok tunnel.

Steps to reproduce the problem

  1. Create Node App
  2. Do simple changes
  3. Deploy to hosting (I used Digitalocean)
  4. Make changes on App Settings page(change URL and redirect URL to hosting urls)
  5. Install app to Development store
  6. Delete the app from the Development store
  7. Repeat Install app to Development store
  8. Get this error.

I tried submitting this app. And QA engineer wrote:
Your app hasn't implemented authentication through OAuth correctly and can't be reinstalled. Review our OAuth documentation.

Node app use koa-shopify-auth - as I see the app have AccessToken, Oauth working.

What means hasn't implemented OAuth correctly?
I think Shopify doesn't delete some app data from shop on app deletion.

Expected a valid shop query parameter

I was trying to finish the tutorial with instructions. So after I redirect the user to the edit products page I am facing this error inside of admin UI.

Expected a valid shop query parameter

The main page of the app still works correctly.

Specifications

  • Browser: Chrome
  • Operating System: Windows 10

Add tutorial on getting accessToken and shop name and creating simple REST API call

In the current app version no information about how we can store accessToken, shop name, and use they.

server/server.js

!!! CustomSessionStorage !!!

import RedisStore from './redis-store';
const sessionStorage = new RedisStore();

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.January21,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
    sessionStorage.storeCallback,
    sessionStorage.loadCallback,
    sessionStorage.deleteCallback,
  )
});

------
!!! accessToken !!!

 createShopifyAuth({
  async afterAuth(ctx) {
  // Access token and shop available in ctx.state.shopify
  const {shop, accessToken, scope} = ctx.state.shopify;

For example, get themes (using isomorphic-fetch and koa-router):

server/routes.js

const baseURL =  `https://${SHOPIFY_API_KEY}:${accessToken}@${shopOrigin}/admin/api/${ApiVersion.January21}`;

router.get('/themeId', async (ctx) => {

  const res = await fetch(
    `${baseUrl}/themes.json`
  );

  try {
      ctx.body = await res.json();
      ctx.status = 200;
  } catch (e) {
    console.log('error:', e.message);
  }
});

SHOPIFY_API_KEY - is environment variable, OK
But how I can save shop and accessToken?

The official tutorial doesn't contain any information about this.

Best practice for using accessing the app bridge context

Issue summary

Feel a tad silly with this one as I feel it is actually probably quite simple.

I'm looking to use the Redirect from the app-bridge. (import {Redirect} from '@shopify/app-bridge/actions').

The issue I'm having is around the app part.

app.dispatch(
  Redirect.toRemote({
    url: 'http://example.com',
  }),
)

Above it mentions doing:

const app = createApp({
  apiKey: '12345',
  shopOrigin: shopOrigin,
});

But given we're already using the <Provider I'd imagine this is accessible in context somewhere like it used to be?

I've tried going down the createApp route but I'm not sure where I should be saving this to have it accessible globally. Given we're using next I can't use window for example.

Apologies if this is the wrong place to ask this but the app-bridge repos are internal.

Why using Redirect to re-authenticate ?

Hi Shopify team,

I have read new guide and see you implement user re-authenticate.

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 added above code, update verifyRequest follow the guide, but i see when re-authenticating (request to /grapqhql return 403 code), above code using Redirect component to redirect me to a page have url [authUrlHeader].js.

I don’t have that page so result for that redirecting request will be 404 ( Not found ) => leads app working not properly.

I also don’t know how Redirect component work. But i think re-authenticate might be like:

Authenticate => get 403 code => re-fetch authenticate with authUrlHeader => server will be automatically do online authenticate

I mean it must be Re-fecth not Redirect.

Let me know if I get something wrong.
Thanks and best regards,

Unexpected token '<' on NextJS generated files

Issue summary

After authenticating the app it shows a blank page with a console error showing unexpected token on the NextJS generated chunks.

Expected behavior

The app should load and render

Actual behavior

The app renders a blank page. When opening Developer Tools an error shows up on each chunk generated by NextJS
image
When clicking on any of the links it displays the chunk and an error on the first line, "<!DOCTYPE html>". All of the chunks show this same HTML document:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <style>
    html,
body {
  min-height: 100%;
  height: 100%;
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 2rem;
  text-transform: initial;
  letter-spacing: initial;
  font-weight: 400;
  color: #212b36;
  font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto,
    Segoe UI, Helvetica Neue, sans-serif;
}
 
@media (min-width: 40em) {
  html,
  body {
    font-size: 1.4rem;
  }
}
 
html {
  position: relative;
  font-size: 62.5%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  -ms-text-size-adjust: 100%;
  text-size-adjust: 100%;
  text-rendering: optimizeLegibility;
}
 
body {
  min-height: 100%;
  margin: 0;
  padding: 0;
  background-color: #f4f6f8;
}
 
*,
*::before,
*::after {
  box-sizing: border-box;
}
 
h1,
h2,
h3,
h4,
h5,
h6,
p {
  margin: 0;
  font-size: 1em;
  font-weight: 400;
}
 
#CookiePartitionPrompt, #RequestStorageAccess {
  display: none;
}
 
.Polaris-Page {
  margin: 0 auto;
  padding: 0;
  max-width: 99.8rem;
}
 
@media (min-width: 30.625em) {
  .Polaris-Page {
    padding: 0 2rem;
  }
}
@media (min-width: 46.5em) {
  .Polaris-Page {
    padding: 0 3.2rem;
  }
}
 
.Polaris-Page__Content {
  margin: 2rem 0;
}
 
@media (min-width: 46.5em) {
  .Polaris-Page__Content {
    margin-top: 2rem;
  }
}
 
@media (min-width: 46.5em) {
  .Polaris-Page {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
  }
}
 
.Polaris-Layout {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: start;
  -ms-flex-align: start;
  align-items: flex-start;
  margin-top: -2rem;
  margin-left: -2rem;
}
 
.Polaris-Layout__Section {
  -webkit-box-flex: 2;
  -ms-flex: 2 2 48rem;
  flex: 2 2 48rem;
  min-width: 51%;
}
 
.Polaris-Layout__Section--fullWidth {
  -webkit-box-flex: 1;
  -ms-flex: 1 1 100%;
  flex: 1 1 100%;
}
 
.Polaris-Layout__Section {
  max-width: calc(100% - 2rem);
  margin-top: 2rem;
  margin-left: 2rem;
}
 
.Polaris-Stack {
  margin-top: -1.6rem;
  margin-left: -1.6rem;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-wrap: wrap;
  flex-wrap: wrap;
  -webkit-box-align: stretch;
  -ms-flex-align: stretch;
  align-items: stretch;
}
 
.Polaris-Stack > .Polaris-Stack__Item {
  margin-top: 1.6rem;
  margin-left: 1.6rem;
  max-width: calc(100% - 1.6rem);
}
 
.Polaris-Stack__Item {
  -webkit-box-flex: 0;
  -ms-flex: 0 0 auto;
  flex: 0 0 auto;
  min-width: 0;
}
 
.Polaris-Heading {
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 2.4rem;
  margin: 0;
}
 
@media (min-width: 40em) {
  .Polaris-Heading {
    font-size: 1.6rem;
  }
}
 
.Polaris-Card {
  overflow: hidden;
  background-color: white;
  box-shadow: 0 0 0 1px rgba(63, 63, 68, 0.05),
    0 1px 3px 0 rgba(63, 63, 68, 0.15);
}
 
.Polaris-Card + .Polaris-Card {
  margin-top: 2rem;
}
 
@media (min-width: 30.625em) {
  .Polaris-Card {
    border-radius: 3px;
  }
}
 
.Polaris-Card__Header {
  padding: 2rem 2rem 0;
}
 
.Polaris-Card__Section {
  padding: 2rem;
}
 
.Polaris-Card__Section + .Polaris-Card__Section {
  border-top: 1px solid #dfe3e8;
}
 
.Polaris-Card__Section--subdued {
  background-color: #f9fafb;
}
 
.Polaris-Stack--distributionTrailing {
  -webkit-box-pack: end;
  -ms-flex-pack: end;
  justify-content: flex-end;
}
 
.Polaris-Stack--vertical {
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -ms-flex-direction: column;
  flex-direction: column;
}
 
.Polaris-Button {
  fill: #637381;
  position: relative;
  display: -webkit-inline-box;
  display: -ms-inline-flexbox;
  display: inline-flex;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  min-height: 3.6rem;
  min-width: 3.6rem;
  margin: 0;
  padding: 0.7rem 1.6rem;
  background: linear-gradient(to bottom, white, #f9fafb);
  border: 1px solid #c4cdd5;
  box-shadow: 0 1px 0 0 rgba(22, 29, 37, 0.05);
  border-radius: 3px;
  line-height: 1;
  color: #212b36;
  text-align: center;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  text-decoration: none;
  transition-property: background, border, box-shadow;
  transition-duration: 200ms;
  transition-timing-function: cubic-bezier(0.64, 0, 0.35, 1);
}
 
.Polaris-Button:hover {
  background: linear-gradient(to bottom, #f9fafb, #f4f6f8);
  border-color: #c4cdd5;
}
 
.Polaris-Button:focus {
  border-color: #5c6ac4;
  outline: 0;
  box-shadow: 0 0 0 1px #5c6ac4;
}
 
.Polaris-Button:active {
  background: linear-gradient(to bottom, #f4f6f8, #f4f6f8);
  border-color: #c4cdd5;
  box-shadow: 0 0 0 0 transparent, inset 0 1px 1px 0 rgba(99, 115, 129, 0.1),
    inset 0 1px 4px 0 rgba(99, 115, 129, 0.2);
}
 
.Polaris-Button.Polaris-Button--disabled {
  fill: #919eab;
  transition: none;
  background: linear-gradient(to bottom, #f4f6f8, #f4f6f8);
  color: #919eab;
}
 
.Polaris-Button__Content {
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1.6rem;
  text-transform: initial;
  letter-spacing: initial;
  position: relative;
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-pack: center;
  -ms-flex-pack: center;
  justify-content: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  min-width: 1px;
  min-height: 1px;
}
 
@media (min-width: 40em) {
  .Polaris-Button__Content {
    font-size: 1.4rem;
  }
}
 
.Polaris-Button--primary {
  background: linear-gradient(to bottom, #6371c7, #5563c1);
  border-color: #3f4eae;
  box-shadow: inset 0 1px 0 0 #6774c8, 0 1px 0 0 rgba(22, 29, 37, 0.05),
    0 0 0 0 transparent;
  color: white;
  fill: white;
}
 
.Polaris-Button--primary:hover {
  background: linear-gradient(to bottom, #5c6ac4, #4959bd);
  border-color: #3f4eae;
  color: white;
  text-decoration: none;
}
 
.Polaris-Button--primary:focus {
  border-color: #202e78;
  box-shadow: inset 0 1px 0 0 #6f7bcb, 0 1px 0 0 rgba(22, 29, 37, 0.05),
    0 0 0 1px #202e78;
}
 
.Polaris-Button--primary:active {
  background: linear-gradient(to bottom, #3f4eae, #3f4eae);
  border-color: #38469b;
  box-shadow: inset 0 0 0 0 transparent, 0 1px 0 0 rgba(22, 29, 37, 0.05),
    0 0 1px 0 #38469b;
}
 
.Polaris-Button--primary.Polaris-Button--disabled {
  fill: white;
  background: linear-gradient(to bottom, #bac0e6, #bac0e6);
  border-color: #a7aedf;
  box-shadow: none;
  color: white;
}
  </style>
  <base target="_top">
  <title>Redirecting…</title>
 
  <script>
    window.apiKey = "6c6c69ca331963b5e9986644f889e9ae";
    window.shopOrigin = "https://none";
    (function() {
      function ITPHelper(opts) {
        this.itpContent = document.getElementById('TopLevelInteractionContent');
        this.itpAction = document.getElementById('TopLevelInteractionButton');
        this.redirectUrl = opts.redirectUrl;
      }
 
      ITPHelper.prototype.redirect = function() {
        sessionStorage.setItem('shopify.top_level_interaction', true);
        window.location.href = this.redirectUrl;
      }
 
      ITPHelper.prototype.userAgentIsAffected = function() {
        return Boolean(document.hasStorageAccess);
      }
 
      ITPHelper.prototype.canPartitionCookies = function() {
        var versionRegEx = /Version\/12\.0\.?\d? Safari/;
        return versionRegEx.test(navigator.userAgent);
      }
 
      ITPHelper.prototype.setUpContent = function(onClick) {
        this.itpContent.style.display = 'block';
        this.itpAction.addEventListener('click', this.redirect.bind(this));
      }
 
      ITPHelper.prototype.execute = function() {
        if (!this.itpContent) {
          return;
        }
 
        if (this.userAgentIsAffected()) {
          this.setUpContent();
        } else {
          this.redirect();
        }
      }
 
      this.ITPHelper = ITPHelper;
    })(window);
    (function() {
      var ACCESS_GRANTED_STATUS = 'storage_access_granted';
      var ACCESS_DENIED_STATUS = 'storage_access_denied';
 
      function StorageAccessHelper(redirectData) {
        this.redirectData = redirectData;
      }
 
      StorageAccessHelper.prototype.setNormalizedLink = function(storageAccessStatus) {
        return storageAccessStatus === ACCESS_GRANTED_STATUS ? this.redirectData.hasStorageAccessUrl : this.redirectData.doesNotHaveStorageAccessUrl;
      }
 
      StorageAccessHelper.prototype.redirectToAppTLD = function(storageAccessStatus) {
        var normalizedLink = document.createElement('a');
 
        normalizedLink.href = this.setNormalizedLink(storageAccessStatus);
 
        data = JSON.stringify({
          message: 'Shopify.API.remoteRedirect',
          data: {
            location: normalizedLink.href,
          }
        });
        window.parent.postMessage(data, this.redirectData.myshopifyUrl);
      }
 
      StorageAccessHelper.prototype.redirectToAppsIndex = function() {
        window.parent.location.href = this.redirectData.myshopifyUrl + '/admin/apps';
      }
 
      StorageAccessHelper.prototype.redirectToAppTargetUrl = function() {
        window.location.href = this.redirectData.appTargetUrl;
      }
 
      StorageAccessHelper.prototype.secureIncompatible = function(ua) {
        return ua.includes("iPhone OS 12_") || ua.includes("iPad; CPU OS 12_") || //iOS 12
        (ua.includes("UCBrowser/")
            ? this.isOlderUcBrowser(ua) //UC Browser < 12.13.2
            : (ua.includes("Chrome/5") || ua.includes("Chrome/6"))) ||
        ua.includes("Chromium/5") || ua.includes("Chromium/6") ||
        (ua.includes(" OS X 10_14_") &&
            ((ua.includes("Version/") && ua.includes("Safari")) || //Safari on MacOS 10.14
            ua.endsWith("(KHTML, like Gecko)"))); //Web view on MacOS 10.14
      }
 
      StorageAccessHelper.prototype.isOlderUcBrowser = function(ua) {
        var match = ua.match(/UCBrowser\/(\d+)\.(\d+)\.(\d+)\./);
        if (!match) return false;
        var major = parseInt(match[1]);
        var minor = parseInt(match[2]);
        var build = parseInt(match[3]);
        if (major != 12) return major < 12;
        if (minor != 13) return minor < 13;
        return build < 2;
      }
 
      StorageAccessHelper.prototype.setCookie = function(value) {
        if(!this.secureIncompatible(navigator.userAgent)) {
          value += '; secure'
        }
        document.cookie = value;
      }
 
      StorageAccessHelper.prototype.grantedStorageAccess = function() {
        try {
          sessionStorage.setItem('shopify.granted_storage_access', true);
          this.setCookie('shopify.granted_storage_access=true');
          if (!document.cookie) {
            throw 'Cannot set third-party cookie.'
          }
          this.redirectToAppTargetUrl();
        } catch (error) {
          console.warn('Third party cookies may be blocked.', error);
          this.redirectToAppTLD(ACCESS_DENIED_STATUS);
        }
      }
 
      StorageAccessHelper.prototype.handleRequestStorageAccess = function() {
        return document.requestStorageAccess().then(this.grantedStorageAccess.bind(this), this.redirectToAppsIndex.bind(this, ACCESS_DENIED_STATUS));
      }
 
      StorageAccessHelper.prototype.setupRequestStorageAccess = function() {
        var requestContent = document.getElementById('RequestStorageAccess');
        var requestButton = document.getElementById('TriggerAllowCookiesPrompt');
 
        requestButton.addEventListener('click', this.handleRequestStorageAccess.bind(this));
        requestContent.style.display = 'block';
      }
 
      StorageAccessHelper.prototype.handleHasStorageAccess = function() {
        if (sessionStorage.getItem('shopify.granted_storage_access')) {
          // If app was classified by ITP and used Storage Access API to acquire access
          this.redirectToAppTargetUrl();
        } else {
          // If app has not been classified by ITP and still has storage access
          this.redirectToAppTLD(ACCESS_GRANTED_STATUS);
        }
      }
 
      StorageAccessHelper.prototype.handleGetStorageAccess = function() {
        if (sessionStorage.getItem('shopify.top_level_interaction')) {
          // If merchant has been redirected to interact with TLD (requirement for prompting request to gain storage access)
          this.setupRequestStorageAccess();
        } else {
          // If merchant has not been redirected to interact with TLD (requirement for prompting request to gain storage access)
          this.redirectToAppTLD(ACCESS_DENIED_STATUS);
        }
      }
 
      StorageAccessHelper.prototype.manageStorageAccess = function() {
        return document.hasStorageAccess().then(function(hasAccess) {
          if (hasAccess) {
            this.handleHasStorageAccess();
          } else {
            this.handleGetStorageAccess();
          }
        }.bind(this));
      }
 
      StorageAccessHelper.prototype.execute = function() {
        if (ITPHelper.prototype.userAgentIsAffected()) {
          this.manageStorageAccess();
        } else {
          this.grantedStorageAccess();
        }
      }
 
      /* ITP 2.0 solution: handles cookie partitioning */
      StorageAccessHelper.prototype.setUpHelper = function() {
        return new ITPHelper({redirectUrl: window.shopOrigin + "/admin/apps/" + window.apiKey + window.returnTo});
      }
 
      StorageAccessHelper.prototype.setCookieAndRedirect = function() {
        this.setCookie('shopify.cookies_persist=true');
        var helper = this.setUpHelper();
        helper.redirect();
      }
 
      StorageAccessHelper.prototype.setUpCookiePartitioning = function() {
        var itpContent = document.getElementById('CookiePartitionPrompt');
        itpContent.style.display = 'block';
 
        // var button = document.getElementById('AcceptCookies');
        // button.addEventListener('click', this.setCookieAndRedirect.bind(this));
      }
 
      this.StorageAccessHelper = StorageAccessHelper;
    })(window);
    (function() {
      function redirect() {
        var targetInfo = {
          myshopifyUrl: "https://none",
          hasStorageAccessUrl: "/auth/inline?shop=none",
          doesNotHaveStorageAccessUrl: "/auth/enable_cookies?shop=none",
          appTargetUrl: "/?shop=none"
        }
 
        if (window.top == window.self) {
          // If the current window is the 'parent', change the URL by setting location.href
          window.top.location.href = targetInfo.hasStorageAccessUrl;
        } else {
          var storageAccessHelper = new StorageAccessHelper(targetInfo);
          storageAccessHelper.execute();
        }
      }
 
      document.addEventListener("DOMContentLoaded", redirect);
    })();
  </script>
</head>
<body>
  <main id="RequestStorageAccess">
    <div class="Polaris-Page">
      <div class="Polaris-Page__Content">
        <div class="Polaris-Layout">
          <div class="Polaris-Layout__Section">
            <div class="Polaris-Stack Polaris-Stack--vertical">
              <div class="Polaris-Stack__Item">
                <div class="Polaris-Card">
                  <div class="Polaris-Card__Header">
                    <h1 class="Polaris-Heading">This app needs access to your browser data</h1>
                  </div>
                  <div class="Polaris-Card__Section">
                    <p>Your browser is blocking this app from accessing your data. To continue using this app, click Continue, then click Allow if the browser prompts you.</p>
                  </div>
                </div>
              </div>
              <div class="Polaris-Stack__Item">
                <div class="Polaris-Stack Polaris-Stack--distributionTrailing">
                  <div class="Polaris-Stack__Item">
                    <button type="button" class="Polaris-Button Polaris-Button--primary" id="TriggerAllowCookiesPrompt">
                      <span class="Polaris-Button__Content"><span>Continue</span></span>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </main>
</body>
</html>

I believe that it has something to do with webpack or nextjs, not really sure.

Steps to reproduce the problem

  1. Follow https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react
  2. Start with shopify serve and open app in {myshop.myshopify.com}/admin/apps/{app-name}

Specifications

  • Browser: Version 89.0.4389.128 (Official Build) (64-bit)
  • Device: Desktop
  • Operating System: Windows 10 20H2 build 19042.928

Not possible to add Next.js API routes due to shopify-auth setup

Issue summary

It is not possible to follow Next.js documentation for creating API routes in your pages/api directory. It's caused by the default setup and documentation how koa-shopify-auth is being used. As its documented right now, every single GET request to your server will be forced to authenticate, which forces any SSR request to fail, because the server is trying to verify its own API calls.

Expected behavior

The app template should support usage of Next.js API routes feature.

Actual behavior

requests to /api are being redirected to /auth.

Reduced test case

https://nextjs.org/docs#api-routes
Add the simplest of pages to pages/api/index.js that just returns 200.
Add an API call in pages/index.js using getInitialProps to that newly added /api route.

Access Token

Not an actual issue.

How do I store the accessToken so that I can use it to call shopify api from my public app?

server/server.js

32  server.use(
33    createShopifyAuth({
34      apiKey: SHOPIFY_API_KEY,
35      secret: SHOPIFY_API_SECRET,
36      scopes: [SCOPES],
37
38      async afterAuth(ctx) {
39        // Access token and shop available in ctx.state.shopify
40        const { shop } = ctx.state.shopify;
41
42        // Redirect to app with shop parameter upon auth
43        ctx.redirect(`/?shop=${shop}`);
44      },
45    })

Do we really need to overwrite babel config to transcode jest tests to es6?

Issue summary

Write a short description of the issue here ↓

When I create a brand new app using the tool, I notice I cannot use other than the CommonJS require to include my modules in a Jest test case.

Expected behavior

What do you think should happen?

I was hoping I could use ES6 import like in the rest of the app

Actual behavior

What actually happens?

Tip: include an error message (in a <details></details> tag) if your issue is related to an error

({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import createClient from '../db/client';
                                                                                             ^^^^^^

    SyntaxError: Cannot use import statement outside a module

Steps to reproduce the problem

  1. Create a brand new app
  2. Create a test case in server/__tests__/db.client.test.js and try to import a module using the ES6 import

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.

import { createClient } from '../db/client';

test('true', () => {
    expect(true).toBe(true);
});

Specifications

  • App type: NodeJS
  • Operating System: Darwin MacBook-Air 19.2.0 Darwin Kernel Version 19.2.0: xnu-6153.61.1~20/RELEASE_X86_64 x86_64
  • Shell: zsh 5.7.1 (x86_64-apple-darwin19.0)
  • Ruby version (ruby -v): ruby 2.6.3p62 (2019-04-16 revision 67580)

Solution

The way I fixed it is to add the following config files:
jest.config.js

module.exports = {
    transform: {
        "^.+\\.js$": "./jest.transform.js"
    }
}

jest.transform.js

module.exports = require('babel-jest').createTransformer({
    presets: ['@babel/preset-env'],
    ignore: ['node_modules']
});

Can not show a page which has `getServerSideProps `

Issue summary

A page which has getServerSideProps (example below) return 400 error after redirecting /auth.

current page

import { Heading, Page } from "@shopify/polaris";
import Link from 'next/link'

const Index = () => (
  <Page>
    <Heading>Shopify app with Node and React 🎉</Heading>
    <ul>
      <li><Link href={'/serversideprops'}><a>page with serverSideProps</a></Link></li>
    </ul>
  </Page>
);

export default Index;

destination page

export async function getServerSideProps(context) {
  return {
    // Usually, we will get user's data from our DB.
    // But I set data for now. because it's demo.
    props: {
      time: (new Date()).toLocaleTimeString()
    }
  }
}

const ServerSideProps = ({time}) => (
  <>
    <h1>page with serverSideProps.</h1>
    <p>{time}</p>
  </>

)

export default ServerSideProps

Do you have any solution to fix this issue ???

Expected behavior

The page with getServerSideProps should be shown correctly.

Actual behavior

After clicking the page link only Bad Request message was shown.

image

Network looks like below.

image

Steps to reproduce the problem

  1. clone this repo
  2. $ shopify serve
  3. click page with serverSideProps link in index page

Reduced test case

Specifications

  • Browser: Chrome 89.0.4389.90
  • Device: Mac mini (2018)
  • Operating System: MacOS Catalina 10.15.7

Example app do not cover expired sessions and multi user

Issue summary

This example app do not cover basic scenarios like expired session, or multiple users using the app.
For example - when there is staff member account in shop, and the app is already installed, verifyRequest will always fail, since shop is already stored in ACTIVE_SHOPIFY_SHOPS. Session will never be created, and any call to/graphq endpoint will result in redirect to /auth - which will obviously not work.

Expected behavior

This app example should cover both scenarios:

  • expired session
  • user that do not have session yet

Actual behavior

Example app do not cover given scenarios, verifyRequest middleware redirect to /auth even if it is apollo call to /graphql

Steps to reproduce the problem

  1. Setup example app as given in Readme
  2. Add some page that is using Apollo Client (for example as given in Shopify tutorial https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react/learn-the-graphql-admin-api)
  3. Authenticate app in your store
  4. Create staff member account in your store
  5. Log in as staff member and go to the app
  6. Try to use page that is making requests to /graphql

Reduced test case

Specifications

  • Browser: Any
  • Device: Any
  • Operating System: Any

Why use "ctx.res" (node response object) instead of "ctx.response" (koa Response object)

Issue summary

This shouldn't actually cause any issues but I found the use of ctx.res is not supported by Koa.
Based on the docs:

Bypassing Koa's response handling is not supported. Avoid using the following node properties:

https://github.com/koajs/koa/blob/master/docs/api/context.md#ctxres

This is the code in /server/server.js

  router.get("(.*)", verifyRequest(), async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });

Wondering if this is for a special purpose

Enable cookies popup

Issue summary

I'm build app using this new API but still getting Enable cookies popup error whenever app make auth call and reload whole app; I've checked so many times official doc and code but not able to find any trace at my end. Even if I'm hitting Enable cookies button then also it's keep asking whenever it's make auth call and reload whole app.

Then weird part is that it's appear for few time when whole app get reload then it's disappear and app start to behave normally.

image

Expected behavior

It should not ask every time at least once we hit Enable cookie button.

Actual behavior

What actually happens?

P.S: Every time I restarting my node server it's asking same as well

Please check this video which I recorded with voice Enable cookie popup issue video

Steps to reproduce the problem

      1. I have simply followed steps what we have on official documents like [Build a Shopify App with Node and React](https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react and) & Shopify App CLI

Specifications

  • Browser: Chrome 89.0.4389.114 (Official Build) (64-bit)
  • Device: Desktop
  • Operating System:
    Edition Windows 10 Pro
    Version 20H2
    OS build 19042.867
    Experience Windows Feature Experience Pack 120.2212.551.0

"Token must be service access."

Nodejs simple app.

Get "Token must be service access." error when run request: GET /admin/api/2021-01/storefront_access_tokens.json.
App in the sales channel.
scopes: unauthenticated_read_product_listings,unauthenticated_read_product_tags

I found this:
https://community.shopify.com/c/Shopify-APIs-SDKs/Error-quot-Token-must-be-service-access-quot-when-creating-a/td-p/571151

But I don't understand how I can use these cases in this Nodejs App boilerplate.
Shopify contains this error message in API response but doesn't have any reasonable information to resolve it.

Fresh app doesn't work on Safari 14

Issue summary

We've received complaints that a fresh app created with the App CLI is not working on Safari, even though it works on other browsers. The app first asks for cookie access, then for data access to read the cookies, but it ends up in an infinite loop after that point - it reloads and then asks for data access again.

Expected behavior

The app should ask for the permissions once and then work normally from that point.

Actual behavior

The app continuously asks for access to cookies, and when it reloads it actually shows the contents of the page for a second, before asking for access again.

Steps to reproduce the problem

  1. Create a new app with the CLI.
  2. shopify serve.
  3. Open the brand new app on Safari and accept the permissions as they are requested.

Specifications

  • Browser: Safari 14
  • Operating System: macOS 10.15.7

AppBridgeError INVALID_CONFIG: host must be provided

When I open the app I get this error:

Unhandled Runtime Error
AppBridgeError: APP::ERROR::INVALID_CONFIG: host must be provided

Sometimes I can't even install it because of this.

I have both .env and process.env files completed.
Sometimes I see for a second a message popup to enable cookies but I can't press Enable, the redirect is too fast.

Update README

There are probably more/better things to go into the README.

CLI default Eslint rules

The below .eslintrc.js is the default configuration for Shopify CLI. Where can I find those specific rules that this config extends? And also I found @shopify/eslint-plugin on npm and it seems different from the default one CLI provides. Which one should I use? Please provide some best practices for configuring Eslint for Shopify app development.

module.exports = {
  extends: [
    'plugin:shopify/react',
    'plugin:shopify/polaris',
    'plugin:shopify/jest',
    'plugin:shopify/webpack',
  ],
  rules: {
    'import/no-unresolved': 'off',
  },
  overrides: [
    {
      files: ['*.test.*'],
      rules: {
        'shopify/jsx-no-hardcoded-content': 'off',
      },
    },
  ],
};

App loads extremely slow due to Next.js

Issue summary

When requesting a page, it can take up to 10 seconds to return a rendered page. I experienced this in the past, and it was resolved as soon as I pulled Next.js out of the app. I installed the Shopify CLI on a new MacBook Pro, created a new Node app, and had the same experience.

After installing the app, the auth callback page takes forever to load and redirect back into the admin dashboard. The embedded app can then take anywhere from 5-10 seconds to load. This consistently happens when refreshing the page.

Do other people not have this experience? It is pretty unbearable, and removing Next.js seems to instantly solve it with instant page loads.

Expected behavior

Next.js resolves page requests quickly when navigating the app or performing a fresh page load. When refreshing the app in the admin panel, it should render immediately with no hanging.

Actual behavior

Page loads, such as after reloading the embedded app in the app panel, can take up to 10 seconds to return. Next.js seems to be hanging for some reason.

Here is a hang after refresh:

Screen.Capture.on.2021-03-21.at.03-08-29.mov

Steps to reproduce the problem

  1. Install the Shopify CLI
  2. Create a new Node app shopify create node
  3. Start the app with shopify serve
  4. Visit the url under To install and start using your app, open this URL in your browser to install the app.
  5. After instlaling, the app should hang on the auth callback page for a few seconds.
  6. After redirecting, the embedded app may take a few seconds to render.
  7. Refresh the browser.
  8. Notice the app can take 5-20 seconds to load.

Reduced test case

This occurs in a freshly created Node app from Shopify CLI.

Specifications

  • Browser: Chrome
  • Device: Macbook Pro
  • Operating System: MacOS 10.15.3 and 10.15.7

App Bridge React Link in README Leads to a 404

Issue summary

The project README contains a link to a repository on GitHub called App Bridge React which does not exist or is private which results in a 404 when followed.

The broken link is:

[App Bridge React](https://github.com/Shopify/app-bridge/tree/master/packages/app-bridge-react)

Expected behavior

The link should lead to a public repository, relevant documentation or be removed.

Possible Replacement URLs

Following links always triggers full page refresh (non-embedded)

Issue summary

Write a short description of the issue here ↓
When using this build for non-embedded apps, all urls trigger a full page refresh.

Expected behavior

What do you think should happen?
Expecting NextJS routing to dynamically load components using in-built /pages routing syste,

Actual behavior

What actually happens?
Following an internal link triggers a full-page refresh. I've also tried passing a custom linkComponent to AppProvider but refresh behavior doesn't change (or triggers a whole new suite of errors if trying to use a different router on top of the build)

Tip: include an error message (in a <details></details> tag) if your issue is related to an error

Steps to reproduce the problem

Create a new shopify-app-node app (using shopify-app-cli)
Configure app in partner dash as non-embedded
Remove components, leaving the rest intact
Create 2 new pages using shopify generate (foo1, foo2)
In foo1, add import { Link } from "@shopify/polaris";

Foo 2

shopify serve
Install the app, authorize then navigate to ngrok_address/foo1 directly
Follow the link to /foo2

Full-page refresh will occur

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.

Specifications

  • Browser: Google Chrome Version 81.0.4044.122 (Official Build) (64-bit)
  • Device: PC
  • Operating System: Win10 Pro 10.0.19041 WSL2 Ubuntu 18.04

Not possible to add Next.js API routes due to shopify-auth setup

Issue summary

It is not possible to follow Next.js documentation for creating API routes in your pages/api directory. It's caused by the default setup and documentation how koa-shopify-auth is being used. As its documented right now, every single GET request to your server will be forced to authenticate, which forces any SSR request to fail, because the server is trying to verify its own API calls.

Additionally, the solution offered by @tolgap here doesn't work - it only works for fetching data during intial load when the context is exposed via getInitialProps.

E.g.

	static async getInitialProps(ctx) {
		const shop = ctx.query.shop;
		const res = await http.get(ctx, `/api/stores/${shop}`);
                // do something with res
		return {
			shop: shop,
			// ctx: ctx    // can't actually save the context due to circular reference
		}
	}

This works, however there's no way to make subsequent web calls since the context is ephemeral and can't be saved

Is exposing ApolloClient/Admin GraphQL API to the frontend a bad idea?

Issue summary

Exposing ApolloClient in the frontend will allow a Shop of your paid app to edit their subscription lineitems or even give appcredits to themselves.

Expected behavior

A shop shouldn't be allowed to change their own subscription items, or give themselves appCredits.

Actual behavior

If I inspect the ApolloClient in my browser, I can notice that it does API calls to /graphql. If I start inspecting the queries that sent to /graphql, I can figure out that its using the GraphQL Admin API.

Through this, I can deduct that I will have full access to the current AppInstallation, all through my browser. Even just performing fetch('/graphql') in my JS console would allow me to alter my AppInstallation to be $0.01, or give myself credits through appCreditCreate mutation.

Steps to reproduce the problem

  1. Setup a new shopify node app with shopify-app-cli.
  2. Visit the URL and when you're greeted with the default welcome message
  3. Setup recurring billing using the docs
  4. Logout and log back in, you will be prompted to perform a test payment for your app
  5. Perform payment and you're redirected to the welcome page
  6. Open the JS console and run the following:

Reduced test case

let mutation = { query: "mutation appCreditCreate($description: String!, $amount: MoneyInput!) { appCreditCreate(description: $description, amount: $amount) { appCredit { id amount { currencyCode amount } } } }", variables: { description: "hackerman credit", amount: { amount: "14.99", currencyCode: "USD" }  } };
let response = await fetch('/graphql', {credentials: 'include', method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mutation) });

console.log(await response.json());

Specifications

  • Browser: Firefox 71.0
  • Device: Macbook Pro
  • Operating System: OSX 10.13.6

Remove Discount button in fetch-data-with-apollo tutorial does nothing

Issue summary

"Remove Discount" like the name means, should remove the selected item from the localStorage store.

Expected behavior

when button clicked it should call the removeItem() method, which removes that item from the store using key

Actual behavior

when I click "Remove discount" button nothing happens.

Steps to reproduce the problem

  1. select products primary button
  2. chose which products to select by left clicking on them from modal
  3. then click "add" button
  4. then we our selected products got displayed in a form
  5. when we left click any products from that list we got redirect to a another form called "edit-product" where we can update the amount of the desired discount percentage then by clicking on "save" button for that specified item so the discount tales effect.
  6. but if we chose to discard we can click on "Remove discount" button so the discount wont take effect for that desired item and preferably we should be redirected to index page '/'

Reduced test case

Specifications

  • Browser: firefox
  • Device: VM
  • Operating System: Linux

Incorrect return type for userLoggedInFetch in example app

Issue summary

Incorrect return type from userLoggedInFetch provided in example app when using Typescript.

Code in example _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;
  };
}

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

Expected behavior

null is not an accepted return type for the ApolloClient fetch. If the return null line is omitted, then when the app needs to re-auth it throws errors because it takes a second to redirect and tries to run the graphql query in the meantime. There should be an accepted return type of Response that does not allow the query to proceed, or another method to simply not allow the userLoggedInFetch function to complete, and fully execute the redirect.

Actual behavior

With return null line present, Typescript throws error: Type 'null' is not assignable to type 'Response'.

TypeError: Cannot read property 'split' of undefined

Issue summary

when I do npm run dev at terminal it returns a TypeError :

TypeError: Cannot read property 'split' of undefined
at Object. (C:\Users\daves\Project\sample-app\server.js:15:42)
at Module._compile (node:internal/modules/cjs/loader:1092:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1121:10)
at Module.load (node:internal/modules/cjs/loader:972:32)
at Function.Module._load (node:internal/modules/cjs/loader:813:14)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
at node:internal/main/run_main_module:17:47

  • Browser: Chrome
  • Device: PC
  • Operating System: Windows 10

documentation issue

Issue summary

As is, the app would not work when I push it to heroku with shopify deploy.
This is because heroku dose not (and should not) use development tools.
And so I am forced to go learn bable for a day.

Expected behavior

The readme file should contain a paragraph explaining how to compile this bable.

Apollo Client

I just started on a new app for Shopify and as I'm new to Apollo I was really surprise to see that react-apollo is not really supported. Apollo is asking to use @apollo-client instead.

Is this something you are planning to update?

Issues when running shopify start serve bash

Issue summary

Write a short description of the issue here ↓
As the title explains. When I run the 'Shopify start serve' bash command, I get the following error messages:

> [email protected] dev /Users/Tom/eagle ┃ > NODE_ENV=development nodemon ./server/index.js --watch ./server/index.js ┃ ┃ [nodemon] 2.0.2 ┃ [nodemon] to restart at any time, enter rs┃ [nodemon] watching dir(s): server/index.js ┃ [nodemon] watching extensions: js,mjs,json ┃ [nodemon] startingnode ./server/index.js┃ Browserslist: caniuse-lite is outdated. Please run next commandnpm updatenpm update ┃ events.js:288 ┃ throw er; // Unhandled 'error' event ┃ ^ ┃ ┃ Error: listen EADDRINUSE: address already in use :::8081 ┃ at Server.setupListenHandle [as _listen2] (net.js:1309:16) ┃ at listenInCluster (net.js:1357:12) ┃ at Server.listen (net.js:1445:7) ┃ at Application.listen (/Users/Tom/eagle/node_modules/koa/lib/application.js:80:19) ┃ at /Users/Tom/eagle/server/server.js:63:10 ┃ at runMicrotasks (<anonymous>) ┃ at processTicksAndRejections (internal/process/task_queues.js:97:5) ┃ Emitted 'error' event on Server instance at: ┃ at emitErrorNT (net.js:1336:8) ┃ at processTicksAndRejections (internal/process/task_queues.js:84:21) { ┃ code: 'EADDRINUSE', ┃ errno: 'EADDRINUSE', ┃ syscall: 'listen', ┃ address: '::', ┃ port: 8081 ┃ } ┃ [nodemon] app crashed - waiting for file changes before starting...

Expected behavior

What do you think should happen?
To proceed with the Shopify App Cli set-up and be asked for my Shopify API Key & Secret API Key

Actual behavior

What actually happens?
Error message: [nodemon] app crashed - waiting for file changes before starting...

Tip: include an error message (in a <details></details> tag) if your issue is related to an error

Steps to reproduce the problem

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.

Specifications

  • Browser: Chrome
  • Device:
  • Operating System: Mac High Sierra 10.13.6

InternalServerError: Cannot complete OAuth process. Could not find an OAuth cookie for shop url ...

Issue summary

Issues navigations after app been installed

Write a short description of the issue here ↓
Installation and billing app parts was successful, but attempt navigate to index route return
POST https://ngrok.XXXX/graphql 401 (Unauthorized)

Expected behavior

Navigation worked fine in the old api without any problem

What do you think?
My opinion: problem with session tokens, possible reason: SSR rendered page lost session token, but not regenerate new one

Actual behavior

What actually happens?
Click nav link results browser error: 'bad request'. If reload page: server error:

 InternalServerError: Cannot complete OAuth process. Could not find an OAuth cookie for shop url: XXX.myshopify.com
      at Object.throw (/home/~/node_modules/koa/lib/context.js:97:11)
      at /home/~/node_modules/@shopify/koa-shopify-auth/dist/src/auth/index.js:100:42
      at step (/home/~/node_modules/tslib/tslib.js:141:27)
      at Object.throw (/home/~/node_modules/tslib/tslib.js:122:57)
      at rejected (/home/~/node_modules/tslib/tslib.js:113:69)
      at processTicksAndRejections (internal/process/task_queues.js:97:5)

If i delete app from devStore, then install it back on, installation process and initial page works nice.

  • Browser: Chrome
  • Device: PC
  • Operating System: Ubuntu 20

In the docs, you mentioned that tokens need to be written to the database for later use. Could you show example of using saved Access and Session Token, please?

Add RoutePropagator Component?

Issue summary

Can I open a PR to add the RoutePropagator component given the experience is embedded by default?

Expected behavior

Embedded apps should update the url bar and embedded app toolbar links should not cause a full-page refresh. Would be nice to have this OOB by default.

Actual behavior

Currently, url bar does not update when navigating client-side and embedded app toolbar links cause full-page refresh.

Code

RoutePropagator.js

import { RoutePropagator as ShopifyRoutePropagator } from "@shopify/react-shopify-app-route-propagator";
import { Context as AppBridgeContext } from "@shopify/app-bridge-react";
import { Redirect } from "@shopify/app-bridge/actions";
import Router, { useRouter } from "next/router";

const RoutePropagator = () => {
  const router = useRouter();
  const { route } = router;
  const appBridge = React.useContext(AppBridgeContext);

  useEffect(() => {
    appBridge.subscribe(Redirect.ActionType.APP, ({ path }) => {
      Router.push(path);
    });
  }, []);

  return appBridge && route ? (
    <ShopifyRoutePropagator location={route} app={appBridge} />
  ) : null;
}

export default RoutePropagator;

_app.js

<Provider
  config={{
    apiKey: API_KEY,
    shopOrigin: shopOrigin,
    forceRedirect: true
  }}
>
  <RoutePropogator />
  <Component {...pageProps} />     
</Provider>

Switching from custom server.js to Next.js 9 API routes

Issue summary

Write a short description of the issue here ↓

Since we are using a custom server.js file, Now V2 cannot be used to host the Next app. With Next.js 9, API routes were introduced. When I try to migrate the graphQL proxy and the Shopify auth middleware like so:

// pages/api/gqlproxy.js

import Koa from 'koa';
import { graphQLProxy } from '@shopify/koa-shopify-graphql-proxy';

const server = new Koa();
const handler = (req, res) => {
     graphQLProxy({shop: "testshop", password:'testpass',});
};

export default server.use(handler);

It doesn't work.

Expected behavior

What do you think should happen?

Actual behavior

What actually happens?

Tip: include an error message (in a <details></details> tag) if your issue is related to an error

Steps to reproduce the problem

  1. Comment out:
// server/server.js

  server.use(
    graphQLProxy({
      version: ApiVersion.October19
    })
  );
  1. Add new folder to pages called api and create a new file gqlproxy.js.
// pages/api/gqlproxy.js

import Koa from 'koa';
import { graphQLProxy } from '@shopify/koa-shopify-graphql-proxy';

const server = new Koa();
const handler = (req, res) => {
     graphQLProxy({shop: "testshop", password:'testpass',});
};

export default server.use(handler);
  1. Hit the endpoint localhost:3000/api/gqlproxy and the function should mount the graphql proxy, but it does not.

Reduced test case

The best way to get your bug fixed is to provide a reduced test case.

Specifications

  • Browser:
  • Device:
  • Operating System:

New routes created using the navigation don't work

Issue summary

I create an app using the CLI, after that, I create a new page, so now I have on my page folder 3 files.

_app.js
index.js
new.js

On the Shopify app page, I create 2 navigation items index and new and pointed to

http://.../index
http://.../new

A sample application can be found here - https://github.com/icaroNZ/simple-shopify-app

Expected behavior

When I go back on the app, I should be able to see the index page and the new page.

Actual behavior

The index page works fine, and once I try to access the new page, I get redirected to the index page.

Steps to reproduce the problem

Create a new app using the CLI
Create a new file inside the page called new.js
Copy the content of the index.js page into new.js (renaming the function and export)
Create 2 navigation items redirecting to index and new
GO to the app and try to navigate to the new page

Where I think is the problem

The problem seems to be here,

  server.use(
    createShopifyAuth({
      async afterAuth(ctx) { // <- afterAuth seems to be always call
        // 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}`);  // <- always get redirect to the shop aka index page
      },
    })
  );

Should the afterAuth be called every time even when the app is already authenticated?
If so how can I avoid redirection or redirect to the correct page?

Things that I try

Check if there is a accessToken and if there is call next() and return (didn't work)

if ( accessToken ) {
  return next();
}

I also try to look on the ctx for the address we want to go without lucky.

Specifications

  • Browser: Chrome
  • Device: Macbook pro
  • Operating System: macOS

Cannot access session from getServerSideProps

Issue summary

Using the exact redis-session-storage.ts example in the tutorial. I am trying to use Shopify.Utils.loadCurrentSession in my NextJS page's getServerSideProps, but it is always undefined.

Expected behavior

I should be able to access the session on my server.

Actual behavior

Session is always undefined.

Steps to reproduce the problem

Use the files below in the standard server.js setup.

Reduced test case

// server.js
import { RedisSessionStorage } from "./handlers/auth";

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.April21,
  IS_EMBEDDED_APP: true,
  SESSION_STORAGE: new Shopify.Session.CustomSessionStorage(
    RedisSessionStorage.storeCallback,
    RedisSessionStorage.loadCallback,
    RedisSessionStorage.deleteCallback
  ),
});
// redis-session-store.ts
import { Session } from "@shopify/shopify-api/dist/auth/session";
import { redis } from "../redis";

const storeCallback = async (session: Session) => {
  try {
    return await redis.setAsync(session.id, JSON.stringify(session));
  } catch (e) {
    throw new Error(e);
  }
};

const loadCallback = async (id: string) => {
  try {
    const sessionString = await redis.getAsync(id);
    if (sessionString) {
      return JSON.parse(sessionString);
    }

    return null;
  } catch (e) {
    throw new Error(e);
  }
};

const deleteCallback = async (id: string) => {
  try {
    return await redis.delAsync(id);
  } catch (e) {
    throw new Error(e);
  }
};

export const RedisSessionStorage = {
  storeCallback,
  loadCallback,
  deleteCallback,
};

Using these files, when visting the initial Auth setup link when booting the server using shopify serve, I see the following Session being generated and stored into Redis:

┃ storeCallback: Session {
┃   id: '44851b2e-ef60-490a-9a22-d24c584e040b',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   isOnline: true
┃ }
┃ loadCallback: 44851b2e-ef60-490a-9a22-d24c584e040b {
┃   id: '44851b2e-ef60-490a-9a22-d24c584e040b',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   isOnline: true
┃ }
┃ storeCallback: Session {
┃   id: '<myshop>.myshopify.com_72564015311',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   scope: 'write_products,write_customers,write_draft_orders',
┃   expires: 2021-04-21T19:59:31.873Z,
┃   isOnline: true,
┃   accessToken: '<accessToken>',
┃   onlineAccessInfo: {
┃     expires_in: 86399,
┃     associated_user_scope: 'write_products,write_customers,write_draft_orders',
┃     session: 'e308338216354df5f9c84ca09601e657ee733ae47cf1b0ed84f30ded25bcc14c',
┃     account_number: 1,
┃     associated_user: {...}
┃   }
┃ }
┃ storeCallback: {
┃   id: '44851b2e-ef60-490a-9a22-d24c584e040b',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   isOnline: true,
┃   accessToken: '<accessToken>',
┃   expires: 2021-04-20T20:00:02.876Z,
┃   scope: 'write_products,write_customers,write_draft_orders',
┃   onlineAccessInfo: {
┃     expires_in: 86399,
┃     associated_user_scope: 'write_products,write_customers,write_draft_orders',
┃     session: 'e308338216354df5f9c84ca09601e657ee733ae47cf1b0ed84f30ded25bcc14c',
┃     account_number: 1,
┃     associated_user: {...}
┃   }
┃ }
┃ loadCallback: 44851b2e-ef60-490a-9a22-d24c584e040b {
┃   id: '44851b2e-ef60-490a-9a22-d24c584e040b',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   isOnline: true,
┃   accessToken: '<accessToken>',
┃   expires: '2021-04-20T20:00:02.876Z',
┃   scope: 'write_products,write_customers,write_draft_orders',
┃   onlineAccessInfo: {
┃     expires_in: 86399,
┃     associated_user_scope: 'write_products,write_customers,write_draft_orders',
┃     session: 'e308338216354df5f9c84ca09601e657ee733ae47cf1b0ed84f30ded25bcc14c',
┃     account_number: 1,
┃     associated_user: {...}
┃   }
┃ }
┃ API Deprecation Notice: {
┃   message: 'https://help.shopify.com/api/getting-started/api-deprecations',
┃   path: 'https://<myshop>.myshopify.com/admin/api/2021-04/graphql.json'
┃ }
┃ event - build page: /
┃ wait  - compiling...
┃ event - compiled successfully
┃ loadCallback: 44851b2e-ef60-490a-9a22-d24c584e040b {
┃   id: '44851b2e-ef60-490a-9a22-d24c584e040b',
┃   shop: '<myshop>.myshopify.com',
┃   state: '432733481933355',
┃   isOnline: true,
┃   accessToken: '<accessToken>',
┃   expires: '2021-04-20T20:00:02.876Z',
┃   scope: 'write_products,write_customers,write_draft_orders',
┃   onlineAccessInfo: {...}
┃ }

Now, so for so good. I have my session stored in Redis, as the latest loadCallback with my session ID actually resulted in the lookup of this session with the correct accessToken. Now when I try to use this session in my getServerSideProps, the session is always undefined.

// pages/index.tsx
import { Link } from "@shopify/polaris";

export default function Index() {
  return (<Link url="/page-2">Go</Link>
}

export async function getServerSideProps(ctx) {
  const handler = (await import("page-data/index")).default
  return await handler(ctx);
}
// page-data/index.ts
import Shopify from "@shopify/shopify-api";

export default async ({ req, res }) => {
  const session = await Shopify.Utils.loadCurrentSession(req, res, true);
  console.log(session); // undefined

  return { props: { shop: session?.shop } };
};

How can I access the current session in getServerSideProps?

Specifications

  • Browser: Firefox 88
  • Device: Macbook Pro 16, 2019
  • Operating System: OS X 11.2.1 Big Sur

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.