Build a Shopify App with Node.js and React

Fetch data with Apollo

Now that you can query and mutate data with GraphQL, you need a way to build queries and mutations into your app. To do this you’ll use Apollo. The Apollo client and its React components were designed to let you quickly build a React UI that fetches data with GraphQL. Apollo’s components handle complexities like storing low-level networking details and maintaining a local cache when working with an API.

Set up GraphQL

You’ll need to install the JavaScript implementation of GraphQL, as well as the apollo-boost and react-apollo packages, which set up Apollo and provide a view layer integration for React. You’ll also need the Shopify koa middleware to securely proxy graphQL requests from Shopify.

  1. Stop your server from the terminal window that’s running Next.js
  2. Install the graphQL, apollo-boost, and react-apollo package:
    Terminal
    Copy
    npm install --save graphql apollo-boost react-apollo
    
  3. Install koa-shopify-graphql-proxy:
    Terminal
    Copy
    npm install --save @shopify/koa-shopify-graphql-proxy
    
  4. In your server.js file, import koa-shopify-graphql-proxy and add the GraphQL proxy to your middleware chain:

    /server.js

    code contained in /server.js
    require('isomorphic-fetch'); const dotenv = require('dotenv'); const Koa = require('koa'); const next = require('next'); const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth'); const {verifyRequest} = require('@shopify/koa-shopify-auth'); const session = require('koa-session'); dotenv.config(); Add:const { default: graphQLProxy } = require('@shopify/koa-shopify-graphql-proxy'); const port = parseInt(process.env.PORT, 10) || 3000; const dev = process.env.NODE_ENV !== 'production'; const app = next({dev}); const handle = app.getRequestHandler(); const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY } = process.env; app.prepare().then(() => { const server = new Koa(); server.use(session(server)); server.keys = [SHOPIFY_API_SECRET_KEY]; server.use( createShopifyAuth({ apiKey: SHOPIFY_API_KEY, secret: SHOPIFY_API_SECRET_KEY, afterAuth(ctx) { ctx.cookies.set('shopOrigin', shop, { httpOnly: false }) ctx.redirect('/'); }, }), ); Add: server.use(graphQLProxy()); server.use(verifyRequest()); server.use(async (ctx) => { await handle(ctx.req, ctx.res); ctx.respond = false; ctx.res.statusCode = 200; return }); server.listen(port, () => { console.log(`> Ready on http://localhost:${port}`); }); });
  5. Import the ApiVersion from koa-shopify-graphql-proxy, add the version of the API to the proxy. In most cases, you'll always want to use the most recent version of the API.
    Each stable version is supported for a minimum of 12 months. This means that there are at least 9 months of overlap between two consecutive stable versions. When a new stable version is introduced and contains changes that affect your app, you have 9 months to test and migrate your app to the new version before support for the previous version is removed.

    /server.js

    code contained in /server.js
    require('isomorphic-fetch'); const dotenv = require('dotenv'); const Koa = require('koa'); const next = require('next'); const { default: createShopifyAuth } = require('@shopify/koa-shopify-auth'); const { verifyRequest } = require('@shopify/koa-shopify-auth'); const session = require('koa-session'); dotenv.config(); const { default: graphQLProxy } = require('@shopify/koa-shopify-graphql-proxy'); Add:const { ApiVersion } = require('@shopify/koa-shopify-graphql-proxy'); const port = parseInt(process.env.PORT, 10) || 3000; const dev = process.env.NODE_ENV !== 'production'; const app = next({dev}); const handle = app.getRequestHandler(); const { SHOPIFY_API_SECRET_KEY, SHOPIFY_API_KEY } = process.env; app.prepare().then(() => { const server = new Koa(); server.use(session(server)); server.keys = [SHOPIFY_API_SECRET_KEY]; server.use( createShopifyAuth({ apiKey: SHOPIFY_API_KEY, secret: SHOPIFY_API_SECRET_KEY, afterAuth(ctx) { ctx.cookies.set('shopOrigin', shop, { httpOnly: false }) ctx.redirect('/'); }, }), ); Remove: server.use(graphQLProxy()); Add: server.use(graphQLProxy({version: ApiVersion.October19})) server.use(verifyRequest()); server.use(async (ctx) => { await handle(ctx.req, ctx.res); ctx.respond = false; ctx.res.statusCode = 200; return }); server.listen(port, () => { console.log(`> Ready on http://localhost:${port}`); }); });

Add your access scopes

Access scopes determine which actions your app can perform on a store. The scopes that you’ll need will depend on the functions of your app. For example, the sample embedded app needs the read_products permission to read product prices so it can apply a discount and the write_products permission to write a new price back.

It’s important to request only the scopes that your app needs. Merchants trust apps to do only what its features describe.
  1. Add the write_products scope to your shopifyAuth function:

    /server.js

    code contained in /server.js
    server.use( createShopifyAuth({ apiKey: SHOPIFY_API_KEY, secret: SHOPIFY_API_SECRET_KEY, Remove: scopes: ['read_products'], Add: scopes: ['read_products', 'write_products'], afterAuth(ctx) { ctx.cookies.set('shopOrigin', shop, { httpOnly: false }); ctx.redirect('/'); }, }), );
  2. Stop your server and start it again.

Configure the Apollo client

The default configuration of Apollo enables browsers to easily authenticate by passing credentials with every request. Since your app is already using cookies for login and session management, you can simply reuse these credentials when making requests to the /graphql endpoint by passing the credentials option.

You’ll also need to wrap your app in the Apollo Provider component and pass it to the client you just created. This will give components further down the tree access to the Apollo client.

  1. In your pages/_app.js, import the Apollo client from apollo-boost and add a constant:

    /pages/_app.js

    code contained in /pages/_app.js
    import App from 'next/app'; import Head from 'next/head'; import { AppProvider } from '@shopify/polaris'; import { Provider } from '@shopify/app-bridge-react'; import '@shopify/polaris/styles.css'; import Cookies from 'js-cookie'; Add:import ApolloClient from 'apollo-boost'; Add:const client = new ApolloClient({ Add: fetchOptions: { Add: credentials: 'include' Add: }, Add:}); class MyApp extends App { render() { const { Component, pageProps } = this.props; const config = { apiKey: API_KEY, shopOrigin: Cookies.get("shopOrigin"), forceRedirect: true }; return ( <React.Fragment> <Head> <title>Sample App</title> <meta charSet="utf-8" /> </Head> <Provider config={config}> <AppProvider> <Component {...pageProps} /> </AppProvider> </Provider> </React.Fragment> ); } } export default MyApp;
  2. Import the Apollo Provider component from react-apollo and wrap it around the Component in your wrapper:

    Now that your server is configured, you can use the Apollo query component. This lets Apollo make a query request whenever the component is rendered. You’ll use it to query the Shopify Admin API and display product data.

    /pages/_app.js

    code contained in /pages/_app.js
    import App from 'next/app'; import Head from 'next/head'; import { AppProvider } from '@shopify/polaris'; import { Provider } from '@shopify/app-bridge-react'; import '@shopify/polaris/styles.css'; import Cookies from 'js-cookie'; import ApolloClient from 'apollo-boost'; Add:import { ApolloProvider } from 'react-apollo'; const client = new ApolloClient({ fetchOptions: { credentials: 'include' }, }); class MyApp extends App { render() { const { Component, pageProps } = this.props; const config = { apiKey: API_KEY, shopOrigin: Cookies.get("shopOrigin"), forceRedirect: true }; return ( <React.Fragment> <Head> <title>Sample App</title> <meta charSet="utf-8" /> </Head> <Provider config={config}> <AppProvider> Remove: <Component {...pageProps} /> Add: <ApolloProvider client={client}> Add: <Component {...pageProps} /> Add: </ApolloProvider> </AppProvider> </Provider> </React.Fragment> ); } } export default MyApp;

Build a resource list from a query

The sample embedded app uses the getProducts query to load the products selected in the resource picker or in a resource list. As you did with the resource picker, you’ll create a separate ResourceList.js file that will include the query.

  1. Create a components folder in your root project folder.
  2. Create a ResourceList.js file in the components folder.

    The getProducts query accepts an array of IDs and returns the product’s title, handle, description, and ID. You’ll also request the product image’s URL and alt text, as well as the product’s price. In this case, you want to assign the query to a constant so it can be used in the query component. You’ll also need to use graphql-tag to parse the query so the component can read it.

  3. In the components/ResourceList.js file, add a constant that includes the query:
    variants are instances of the same product based on its available options. For example, a sweatshirt could come in a variety of colors and sizes. Each combination of color and size would be considered a variant of the product.

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    const GET_PRODUCTS_BY_ID = gql` query getProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { title handle descriptionHtml id images(first: 1) { edges { node { originalSrc altText } } } variants(first: 1) { edges { node { price id } } } } } } `;
  4. In your components/ResourceList.js file, add imports for react-apollo and graphql-tag:

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    Add:import gql from 'graphql-tag'; Add:import { Query } from 'react-apollo'; const GET_PRODUCTS_BY_ID = gql` query getProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { title handle descriptionHtml id images(first: 1) { edges { node { originalSrc altText } } } variants(first: 1) { edges { node { price id } } } } } } `;
  5. Add an import for the Card component:

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    import gql from 'graphql-tag'; import { Query } from 'react-apollo'; Add:import { Card } from '@shopify/polaris'; const GET_PRODUCTS_BY_ID = gql` query getProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { title handle descriptionHtml id images(first: 1) { edges { node { originalSrc altText } } } variants(first: 1) { edges { node { price id } } } } } } `;
  6. Create a class component that returns the Apollo Query component with the GET_PRODUCTS_BY_ID query, as well as the Page and Card components:
    Apollo’s components use the Render Props pattern in React to show loading and error states. In this example, you’ve set loading and error states for when the Apollo query component is rendering or an error occurs.

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    import gql from 'graphql-tag'; import { Query } from 'react-apollo'; import { Card } from '@shopify/polaris'; const GET_PRODUCTS_BY_ID = gql` query getProducts($ids: [ID!]!) { nodes(ids: $ids) { ... on Product { title handle descriptionHtml id images(first: 1) { edges { node { originalSrc altText } } } variants(first: 1) { edges { node { price id } } } } } } `; Add:class ResourceListWithProducts extends React.Component { Add: render() { Add: return ( Add: <Query query={GET_PRODUCTS_BY_ID}> Add: {({ data, loading, error }) => { Add: if (loading) return <div>Loading…</div>; Add: if (error) return <div>{error.message}</div>; Add: console.log(data); Add: return ( Add: <Card> Add: <p>stuff here</p> Add: </Card> Add: ); Add: }} Add: </Query> Add: ); Add: } Add:} Add: Add: export default ResourceListWithProducts;

Using localStorage to persist data

This tutorial uses localStorage to persist data. You’ll use store.js, a cross-browser JavaScript library for managing localStorage, to set and receive data using the store_set and store_get methods. This works well for testing your development app. But if you were building this app in production, then you’d probably want to store these IDs in a database.

When you add new products using localStorage, you have to manually refresh your page to see your products show up in the resource list.
  1. Install store.js:
    Terminal
    Copy
    npm install --save store-js
    
  2. In your pages/index.js file, save the selected resource IDs to localStorage to use in your query as variables:

    /pages/index.js

    code contained in /pages/index.js
    import { EmptyState, Layout, Page } from '@shopify/polaris'; import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react'; Add:import store from 'store-js'; const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg'; class Index extends React.Component { state = { open: false }; render() { return ( <Page> <TitleBar primaryAction={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} /> <ResourcePicker resourceType="Product" showVariants={false} open={this.state.open} onSelection={(resources) => this.handleSelection(resources)} onCancel={() => this.setState({ open: false })} /> <Layout> <EmptyState heading="Select products to start" action={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} image={img} > <p>Select products and change their price temporarily</p> </EmptyState> </Layout> </Page > ); } handleSelection = (resources) => { const idsFromResources = resources.selection.map((product) => product.id); this.setState({ open: false }); Remove: console.log(idsFromResources); Add: store.set('ids', idsFromResources); }; } export default Index;
  3. In your components/ResourceList.js file, add the query variables by importing the selected products from the browser session with store-js, and then adding them to the query component:

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    import gql from 'graphql-tag'; import { Query } from 'react-apollo'; import { Card } from '@shopify/polaris'; Add:import store from 'store-js'; class ResourceListWithProducts extends React.Component { render() { return ( Remove: <Query query={GET_PRODUCTS_BY_ID}> Add: <Query query={GET_PRODUCTS_BY_ID} variables={{ ids: store.get('ids') }}> {({ data, loading, error }) => { if (loading) { return <div>Loading…</div>; } if (error) { return <div>{error.message}</div>; } console.log(data); return ( <Card> <p>stuff here</p> </Card> ); }} </Query> ); } }

Add the resource list to your app

The Polaris Empty state component is the first thing that the merchant sees on the main page of your app. After they’ve selected their first product, the page displays a list of discounted products. To do this, you’ll need to use the resource list query that you just built, and add logic to tell the index file when to show the empty state and when to show the resource list.

  1. In pages/index.js, import and add your ResourceListWithProducts component:

    /pages/index.js

    code contained in /pages/index.js
    import { EmptyState, Layout, Page } from '@shopify/polaris'; import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react'; import store from 'store-js'; Add:import ResourceListWithProducts from '../components/ResourceList'; const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg'; class Index extends React.Component { state = { open: false }; render() { return ( <Page> <TitleBar primaryAction={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} /> <ResourcePicker resourceType="Product" showVariants={false} open={this.state.open} onSelection={(resources) => this.handleSelection(resources)} onCancel={() => this.setState({ open: false })} /> <Layout> <EmptyState heading="Select products to start" action={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} image={img} > <p>Select products and change their price temporarily</p> </EmptyState> </Layout> Add: <ResourceListWithProducts /> </Page > ); } handleSelection = (resources) => { const idsFromResources = resources.selection.map((product) => product.id); this.setState({ open: false }); store.set('ids', idsFromResources); }; } export default Index;
  2. Make sure that your server is running.
  3. Make sure that you have products in localStorage by selecting products with your app’s resource picker.
  4. If you imported products by using the products-export.csv and selected the first three products from the resource picker, then your browser's console log should look like this: Browser’s console log of products selected from the resource picker

Build UI for your resource list

You’ll need to set up your ResourceListWithProducts component to display data in the ResourceList.js file that contains the original query component. To do this, you’ll use the Polaris Resource list component as well as the Stack, TextStyle, and Thumbnail components from Polaris. The sample embedded app discounts products for two weeks, so you’ll also need to define a variable for twoWeeksFromNow when a discount is created. Finally, like the other pages in the app, the resource list page will need the resource picker set on the title bar’s primary action.

This file will also destructure the queryResults so you can use shorter syntax. For example, you can use data in place of queryResults.data.

  1. In your components/ResourceList.js file, add imports for the ResourceList, Stack, Text Style, and Thumbnail components:

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    import gql from 'graphql-tag'; import { Query } from 'react-apollo'; import { Card, Add: ResourceList, Add: Stack, Add: TextStyle, Add: Thumbnail, } from '@shopify/polaris'; import store from 'store-js';
  2. Add the ResourceList and other nested components inside the existing Card component:

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    class ResourceListWithProducts extends React.Component { render() { Add: const twoWeeksFromNow = new Date(Date.now() + 12096e5).toDateString(); return ( <Query query={GET_PRODUCTS_BY_ID} variables={{ids: store.get('ids')}}> {({data, loading, error}) => { if (loading) return <div>Loading…</div>; if (error) return <div>{error.message}</div>; console.log(data); return ( <Card> Remove: <p>stuff here</p> Add: <ResourceList Add: showHeader Add: resourceName={{ singular: 'Product', plural: 'Products' }} Add: items={data.nodes} Add: renderItem={item => { Add: const media = ( Add: <Thumbnail Add: source={ Add: item.images.edges[0] Add: ? item.images.edges[0].node.originalSrc Add: : '' Add: } Add: alt={ Add: item.images.edges[0] Add: ? item.images.edges[0].node.altText Add: : '' Add: } Add: /> Add: ); Add: const price = item.variants.edges[0].node.price; Add: return ( Add: <ResourceList.Item Add: id={item.id} Add: media={media} Add: accessibilityLabel={`View details for ${item.title}`} Add: > Add: <Stack> Add: <Stack.Item fill> Add: <h3> Add: <TextStyle variation="strong"> Add: {item.title} Add: </TextStyle> Add: </h3> Add: </Stack.Item> Add: <Stack.Item> Add: <p>${price}</p> Add: </Stack.Item> Add: <Stack.Item> Add: <p>Expires on {twoWeeksFromNow} </p> Add: </Stack.Item> Add: </Stack> Add: </ResourceList.Item> Add: ); Add: }} Add: /> </Card> ); }} </Query> ); } } export default ResourceListWithProducts;

Import IDs from localStorage

You’ll need to use the IDs in store to check if a merchant has already selected products, and then return the empty state if they haven’t. You’ll do this by using a ternary statement to check against the store.get method.

  1. In your pages/index.js, add a constant for the store.get method:

    /pages/index.js

    code contained in /pages/index.js
    import { EmptyState, Layout, Page } from '@shopify/polaris'; import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react'; import store from 'store-js'; import ResourceListWithProducts from '../components/ResourceList'; const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg'; class Index extends React.Component { state = { open: false }; render() { Add: const emptyState = !store.get('ids'); return ( <Page> <TitleBar primaryAction={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} /> <ResourcePicker resourceType="Product" showVariants={false} open={this.state.open} onSelection={(resources) => this.handleSelection(resources)} onCancel={() => this.setState({ open: false })} /> <Layout> <EmptyState heading="Select products to start" action={{ content: 'Select products', onAction: () => this.setState({ open: true }) }} image={img} > <p>Select products and change their price temporarily</p> </EmptyState> </Layout> <ResourceListWithProducts /> </Page > ); } handleSelection = (resources) => { const idsFromResources = resources.selection.map((product) => product.id); this.setState({ open: false }); store.set('ids', idsFromResources); }; } export default Index;
  2. Wrap your page in a conditional that switches views based on whether there are IDs stored:

    /pages/index.js

    code contained in /pages/index.js
    import { EmptyState, Layout, Page } from '@shopify/polaris'; import { ResourcePicker, TitleBar} from '@shopify/app-bridge-react'; import store from 'store-js'; import ResourceListWithProducts from '../components/ResourceList'; const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg'; class Index extends React.Component { state = { open: false }; render() { const emptyState = !store.get('ids'); return ( <Page> <TitleBar primaryAction={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} /> <ResourcePicker resourceType="Product" showVariants={false} open={this.state.open} onSelection={(resources) => this.handleSelection(resources)} onCancel={() => this.setState({ open: false })} /> Add: {emptyState ? ( <Layout> <EmptyState heading="Select products to start" action={{ content: 'Select products', onAction: () => this.setState({ open: true }), }} image={img} > <p>Select products and change their price temporarily</p> </EmptyState> </Layout> Add: ) : ( <ResourceListWithProducts /> Add: )} </Page> ); } handleSelection = (resources) => { const idsFromResources = resources.selection.map((product) => product.id); this.setState({ open: false }); store.set('ids', idsFromResources); }; } export default Index;
  3. If you view your resource list at this point, then it should be full of the products that you’ve been using to test: Polaris resource list

Set up your mutation

In the sample embedded app, clicking products in the resource list leads merchants to an edit page where they can update the product price. To do this, you’ll need to perform a different type of CRUD operation with a mutation. You’ll also need to build the form to display the information from the Apollo Query component in your components/ResourceList.js file.

  1. Create an edit-products.js file in your `pages` folder.
  2. Add the following skeleton form to the pages/edit-products.js file:

    The form above has its state set with data from the store. When the component mounts, the itemToBeConsumed requests the clicked item from your store, and sets up state and variables. This form will be rendered on click using the redirect action from Shopify App Bridge.

    Because you’re accessing Shopify App Bridge through the App Bridge React library, you’ll need to access additional Shopify App Bridge methods through Context. To do this, you’ll add context from the React library and a redirect for Shopify App Bridge to the ResourceList component from the App Bridge library.

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    import { Card, DisplayText, Form, FormLayout, Layout, Page, PageActions, TextField } from '@shopify/polaris'; import store from 'store-js'; class EditProduct extends React.Component { state = { discount: '', price: '', variantId: '' }; componentDidMount() { this.setState({ discount: this.itemToBeConsumed() }); } render() { const { name, price, discount, variantId } = this.state; return ( <Page> <Layout> <Layout.Section> <DisplayText size="large">{name}</DisplayText> <Form> <Card sectioned> <FormLayout> <FormLayout.Group> <TextField prefix="$" value={price} disabled={true} label="Original price" type="price" /> <TextField prefix="$" value={discount} onChange={this.handleChange('discount')} label="Discounted price" type="discount" /> </FormLayout.Group> <p> This sale price will expire in two weeks </p> </FormLayout> </Card> <PageActions primaryAction={[ { content: 'Save', onAction: () => { console.log('submitted'); } } ]} secondaryActions={[ { content: 'Remove discount' } ]} /> </Form> </Layout.Section> </Layout> </Page> ); } handleChange = (field) => { return (value) => this.setState({ [field]: value }); }; itemToBeConsumed = () => { const item = store.get('item'); const price = item.variants.edges[0].node.price; const variantId = item.variants.edges[0].node.id; const discounter = price * 0.1; this.setState({ price, variantId }); return (price - discounter).toFixed(2); }; } export default EditProduct;
  3. In your components/ResourceList.js file, add the following:
    Next.js automatically routes your edit page and gives it a URL path. This lets merchants use the browser to navigate back to the resource list.
    When merchants click items, they’ll be set in the localStorage. The clicked item, which is the product in this case, will generate information in the edit product form.

    /components/ResourceList.js

    code contained in /components/ResourceList.js
    import store from 'store-js'; Add:import { Redirect } from '@shopify/app-bridge/actions'; Add:import { Context } from '@shopify/app-bridge-react'; const GET_PRODUCTS_BY_ID = gql` query getProducts($id: [ID!]!) { nodes(ids: $id) { ... on Product { title handle descriptionHtml id images(first: 1) { edges { node { originalSrc altText } } } variants(first: 1) { edges { node { price id } } } } } } `; class ResourceListWithProducts extends React.Component { Add: static contextType = Context; render() { Add: const app = this.context; Add: const redirectToProduct = () => { Add: const redirect = Redirect.create(app); Add: redirect.dispatch( Add: Redirect.Action.APP, Add: '/edit-products', Add: ); Add: }; const twoWeeksFromNow = new Date(Date.now() + 12096e5).toDateString(); return ( <Query query={GET_PRODUCTS_BY_ID} variables={{id: store.get('ids')}}> {({data, loading, error}) => { if (loading) { return <div>Loading…</div>; } if (error) { return <div>{error.message}</div>; } return ( <ResourceList.Item id={item.id} media={media} accessibilityLabel={`View details for ${item.title}`} Add: onClick={() => { Add: store.set('item', item); Add: redirectToProduct(); Add: }} > <Stack> <Stack.Item fill> <h3> <TextStyle variation="strong"> {item.title} </TextStyle> </h3> </Stack.Item> <Stack.Item> <p>${price}</p> </Stack.Item> <Stack.Item> <p>Expires on {twoWeeksFromNow}</p> </Stack.Item> </Stack> </ResourceList.Item> ); }} /> </Card> ); }} </Query> ); } } export default ResourceListWithProducts;

Add your mutations

Now that the UI of your edit page it set up, you’re ready to add the mutation that will write price changes back to Shopify. You’ll use the Apollo Mutation Component to wrap the form in the same way you did the Apollo Query component. Using the onAction prop in the Polaris page component, you’ll submit the product changes back to Shopify by using the Apollo Mutation component’s handleSubmit function. Finally, you’ll add success and error messaging by using the Toast and Banner components from Polaris, and the destructured results of the handleSubmit.

  1. Add the UPDATE_PRICE mutation to your pages/edit-product.js file:

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    import { Card, DisplayText, Form, FormLayout, Layout, Page, PageActions, TextField, } from '@shopify/polaris'; import store from 'store-js'; Add:import gql from 'graphql-tag'; Add:const UPDATE_PRICE = gql` Add: mutation productVariantUpdate($input: ProductVariantInput!) { Add: productVariantUpdate(input: $input) { Add: product { Add: title Add: } Add: productVariant { Add: id Add: price Add: } Add: } Add: } Add:`; class EditProduct extends React.Component {
  2. Add the Apollo Mutation component to your pages_edit_products_js file:
    The results of the mutation have been destructured to error, loading, and data.
    The UPDATE_PRODUCT mutation is expecting the productVariableInput, which includes the new price and the id of the product that’s being updated. The onAction prop will pass the price and id to the productVariableInput, which handleSubmit will then be able to pass to your mutation.

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    import { Card, DisplayText, Form, FormLayout, Layout, Page, PageActions, TextField, } from '@shopify/polaris'; import store from 'store-js'; import gql from 'graphql-tag'; Add:import { Mutation } from 'react-apollo'; const UPDATE_PRICE = gql` mutation productVariantUpdate($input: ProductVariantInput!) { productVariantUpdate(input: $input) { product { title } productVariant { id price } } } `; class EditProduct extends React.Component { state = { discount: '', price: '', variantId: '', }; componentDidMount() { this.setState({discount: this.itemToBeConsumed()}); } render() { const {name, price, discount, variantId} = this.state; Add: return ( Add: <Mutation Add: mutation={UPDATE_PRICE} Add: > Add: {(handleSubmit, {error, data}) => { return ( <Page> <Layout> <Layout.Section> </Layout.Section> </Layout> </Page> ); Add: }} Add: </Mutation> Add: ); } handleChange = (field) => { return (value) => this.setState({[field]: value}); }; itemToBeConsumed = () => { const item = store.get('item'); const price = item.variants.edges[0].node.price; const variantId = item.variants.edges[0].node.id; const discounter = price * 0.1; this.setState({price, variantId}); return (price - discounter).toFixed(2); }; } export default EditProduct;
  3. In the onAction prop of the PageActions component, add id and price variables to the productVariableInput constant:

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    class EditProduct extends React.Component { state = { discount: '', price: '', variantId: '', }; componentDidMount() { this.setState({ discount: this.itemToBeConsumed() }); } render() { const { name, price, discount, variantId } = this.state; return ( <Mutation mutation={UPDATE_PRICE}> {(handleSubmit, { error, data }) => { return ( <Page> <Layout> <Layout.Section> <DisplayText size="large">{name}</DisplayText> <Form> <Card sectioned> </Card> <PageActions primaryAction={[ { content: 'Save', onAction: () => { Remove: console.log('submitted'); Add: const productVariableInput = { Add: id: variantId, Add: price: discount, Add: }; }, }, ]} />
  4. Add the handleSubmit and pass it the productVariableInput:

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    class EditProduct extends React.Component { state = { id: this.props.id, price: this.props.product.variants.edges[0].node.price, discount: '', variantId: this.props.product.variants.edges[0].node.id }; <PageActions primaryAction={[ { content: 'Save', onAction: () => { const productVariableInput = { id: variantId, price: discount, }; Add: handleSubmit({ Add: variables: { input: productVariableInput }, Add: }); }, }, ]} secondaryActions={[ { content: 'Remove discount', }, ]} /> </Form> </Layout.Section> </React.Fragment> ); }} </Mutation> ); } handleChange = (field) => { return (value) => this.setState({ [field]: value }); }; createDiscount = () => { const discounter = this.state.price * 0.1; return (this.state.price - discounter).toFixed(2); }; } export default EditProduct;
  5. Add a Toast component for a success message and a Banner component for an error message:

    You’ve now written a query and mutation using Apollo components!

    /pages/edit-products.js

    code contained in /pages/edit-products.js
    import { Add: Banner, Card, DisplayText, Form, FormLayout, Layout, Page, PageActions, TextField, Add: Toast, } from '@shopify/polaris'; import gql from 'graphql-tag'; import { Mutation } from 'react-apollo'; import store from 'store-js'; const UPDATE_PRICE = gql` mutation productVariantUpdate($input: ProductVariantInput!) { productVariantUpdate(input: $input) { userErrors { field message } product { title } productVariant { id price } } } `; class EditProduct extends React.Component { state = { discount: '', price: '', variantId: '', Add: showToast: false, }; componentDidMount() { this.setState({ discount: this.itemToBeConsumed() }); } render() { const { name, price, discount, variantId } = this.state; return ( <Mutation mutation={UPDATE_PRICE} > {(handleSubmit, { error, data }) => { Add: const showError = error && ( Add: <Banner status="critical">{error.message}</Banner> Add: ); Add: const showToast = data && data.productVariantUpdate && ( Add: <Toast Add: content="Sucessfully updated" Add: onDismiss={() => this.setState({ showToast: false })} Add: /> Add: ); return ( <Page> <Layout> Add: {showToast} Add: <Layout.Section> Add: {showError} Add: </Layout.Section> <Layout.Section> <DisplayText size="large">{name}</DisplayText> <Form> <Card sectioned> <FormLayout> <FormLayout.Group> <TextField prefix="$" value={price} disabled={true} label="Original price" type="price" /> <TextField prefix="$" value={discount} onChange={this.handleChange('discount')} label="Discounted price" type="discount" /> </FormLayout.Group> <p> This sale price will expire in two weeks </p> </FormLayout> </Card> <PageActions primaryAction={[ { content: 'Save', onAction: () => { const productVariableInput = { id: variantId, price: discount }; handleSubmit({ variables: { input: productVariableInput }, }); }, }, ]} secondaryActions={[ { content: 'Remove discount' }, ]} /> </Form> </Layout.Section> </Layout> </Page> ); }} </Mutation> ); } handleChange = (field) => { return (value) => this.setState({ [field]: value }); }; itemToBeConsumed = () => { const item = store.get('item'); const price = item.variants.edges[0].node.price; const variantId = item.variants.edges[0].node.id; const discounter = price * 0.1; this.setState({ price, variantId }); return (price - discounter).toFixed(2); }; } export default EditProduct;
Continue to Charge a fee using the Billing API