The Shopify app development tutorial was revamped in June 2022, so we will provide a thorough explanation of the content.
Shopify app development templates and tutorials updated in December 2022.
Therefore, there are differences between this article and the latest version (2023 version).
In August 2023, with Shopify Summer Edition '23, the template for Shopify app development was changed to the Remix framework.
If you would like to learn the latest version, please check the link below.
Prerequisites
- Partner account and development store set up
- Node.js 14.13.1 or later installed
- npm or yarn installed
- Git installed
- I have registered an ngrok account
- Ruby 3.0 or later and bundler 2.0 or later installed (Added 10/20/2022)
Prerequisites
- Basic knowledge of React.js, Vite, Next.js
- Node.js, Express
- Knowledge of databases and SQL
- Basic knowledge of APIs (authentication, etc.)
∟ Includes GraphQL API and REST API
Official website
Let's get started and enjoy developing a Shopify app!
- Build the app
- Start a local development server
- Let's make a QR code generating app
- Building the front end
- QR code generation page
- About page routing
- QR Code Generation Page – Create New
- QR Code Generation Page – Edit
- Add a List Component to the Home Page
- Building the backend
- Configure DB
- Adding an API Layer
- Middleware that handles API requests from the frontend
- Connect DB and API endpoints to your server
- Fetching Data
- Save the QR code
- Save QR code
- Delete QR code
- Build discount processing logic
- Connect your front-end and back-end code to display and select discounts
- Connect a list view to display saved QR codes
- Add a code for the QR code image
- Operational test
Build the app
First, choose a directory to work in and enter the following command in your terminal:
Until now, you had to install Shopify CLI v2 using Homebrew or similar, but with Shopify CLI 3, you no longer need to install it in advance, and it's also possible to change the CLI for each app.
$ yarn create @shopify/app --template node
=> アプリ名を登録してください
As you can see from the installed template, most initial settings are completed with just this one command.
Under the template's web directory,
A React.js frontend that lets you add products to your development store A Node.js backend ready to interact with the Shopify REST and GraphQL admin APIs
is being constructed.
Start a local development server
Use ngrok to build a local server.
$ yarn dev
This time, we will create a new app in the Partner Dashboard.
You can set the app name as you like, but this time I named it “new-tutorial-app”.
Once you select the development store you want to install to, the server will start with nodemon.
Next, install this app in your development store.
Look at the URL in the red box in the terminal.
When you access the App URL in the red frame, it will access the store you selected as the installation destination earlier, and the installation screen will be displayed.
Once the installation is complete, an app like this will be created.
That's all it takes to create and install the app!
I think it's really amazing.
This app has a feature that allows you to register five demo products by clicking "Populate 5 products." By the way, the registered products do not have sales channels set, so if you want to actually display them, you need to set them from "Sales Channels and Apps" > "Manage" on the product management page.
Let's make a QR code generating app
I wish I could say that this is the end of the tutorial explanation, but how should I actually proceed with development from here?
rest assured!
Now I will explain the steps to actually develop an app.
Developing a Shopify app requires a solid understanding of common design patterns, components, and functionality.
To do this, let's create a QR code generation app as a demo app.
This QR code generator app has the ability to generate a URL (QR code) with a discount code attached for a specific product.
https://shopify.dev/apps/getting-started/build-app-example
In this app,
We will not go into the details of these two libraries, so if you want to know more, please refer to the official documentation.
Also, this sample app GitHub It is published above.
Building the front end
The contents to be built are:
- Displaying the QR code
- Create a QR code
- Edit QR Code
This is the page for .
We will build these using Polaris components.
The frontend is built under the /web/frontend directory.
Directory structure is very important, so don't forget to work with the frontend under the frontend directory.
Let's edit the code now.
Please delete all the existing code and overwrite it with the new code.
/web/frontend/pages/index.jsx
import { useNavigate, TitleBar, Loading } from '@shopify/app-bridge-react'
import { Card, EmptyState, Layout, Page, SkeletonBodyText } from '@shopify/polaris'
export default function HomePage ( ) {
/*
Add an App Bridge useNavigate hook to set up the navigate function.
This function modifies the top-level browser URL so that you can
navigate within the embedded app and keep the browser in sync on reload.
*/
const navigate = useNavigate()
/*
These are mock values. Setting these values lets you preview the loading markup and the empty state.
*/
const isLoading = true
const isRefetching = false
const QRCodes = []
/* loadingMarkup uses the loading component from AppBridge and components from Polaris */
const loadingMarkup = isLoading ? (
< Card sectioned >
< Loading />
< SkeletonBodyText />
</ Card >
) : null
/* Use Polaris Card and EmptyState components to define the contents of the empty state */
const emptyStateMarkup =
!isLoading && !QRCodes?.length ? (
< Card sectioned >
< EmptyState
heading = "Create unique QR codes for your product"
/* This button will take the user to a Create a QR code page */
action = {{
content: ' Create QR code ',
onAction: () => navigate('/qrcodes/new'),
}}
image="https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
>
<p>
Allow customers to scan codes and buy products using their phones.
</ p >
</ EmptyState >
</ Card >
) : null
/*
Use Polaris Page and TitleBar components to create the page layout,
and include the empty state contents set above.
*/
return (
< Page >
< TitleBar
title = "QR codes"
primaryAction = {{
content: ' Create QR code ',
onAction: () => navigate('/qrcodes/new'),
}}
/>
< Layout >
< Layout.Section >
{loadingMarkup}
{emptyStateMarkup}
</ Layout.Section >
</ Layout >
</ Page >
)
}
Once you have edited the code, start the server.
$ yarn dev
If you see the loading message like this, it is working properly.
Now you're ready to build.
Next, we will create a page to generate the QR code.
QR code generation page
To generate the QR code, we use the "@shopify/react-form" library.
Forms have a very complex and varied functionality.
And every form needs similar functionality.
Unless you have a special reason, using a library to build your form makes sense.
There are several well-known form libraries, but here we use @shopify/react-form, which is optimized for Polaris.
Let's install the library now.
Kill the server with Ctrl + C and make sure to install it in the /web/frontend directory.
$ cd web/frontend
$ yarn add @shopify/react-form
Next, we'll create the form component.
To understand the concept of React.js components, please refer to the React documentation.
/ / web/frontendディレクトリで入力
$ touch components/QRCodeForm.jsx
web/frontend/components/QRCodeForm.jsx
Put the following code in:
import { useState, useCallback } from 'react'
import {
Banner,
Card,
Form,
FormLayout,
TextField,
Button,
ChoiceList,
Select,
Thumbnail,
Icon,
Stack,
TextStyle,
Layout,
EmptyState,
} from '@shopify/polaris'
import {
ContextualSaveBar,
ResourcePicker,
useAppBridge,
useNavigate,
} from '@shopify/app-bridge-react'
import { ImageMajor, AlertMinor } from '@shopify/polaris-icons'
/* Import the useAuthenticatedFetch hook included in the Node app template */
import { useAuthenticatedFetch, useShopifyQuery } from '../hooks'
/* Import custom hooks for forms */
import { useForm, useField, notEmptyString } from '@shopify/react-form'
const NO_DISCOUNT_OPTION = { label: 'No discount' , value : '' }
/*
The discount codes available in the store.
This variable will only have a value after retrieving discount codes from the API.
*/
const DISCOUNT_CODES = {}
export function QRCodeForm ( { QRCode: InitialQRCode } ) {
const [QRCode, setQRCode] = useState(InitialQRCode)
const [showResourcePicker, setShowResourcePicker] = useState( false )
const [selectedProduct, setSelectedProduct] = useState(QRCode?.product)
const navigate = useNavigate()
const appBridge = useAppBridge()
const fetch = useAuthenticatedFetch()
const deletedProduct = QRCode?.product?.title === 'Deleted product'
/*
This is a placeholder function that is triggered when the user hits the "Save" button.
It will be replaced by a different function when the frontend is connected to the backend.
*/
const onSubmit = (body) => console.log( 'submit' , body)
/*
Sets up the form state with the useForm hook.
Accepts a "fields" object that sets up each individual field with a default value and validation rules.
Returns a "fields" object that is destructured to access each of the fields individually, so they can be used in other parts of the component.
Returns helpers to manage form state, as well as component state that is based on form state.
*/
const {
fields: {
title,
productId,
variantId,
handle,
discountId,
discountCode,
destination,
},
dirty,
reset,
submitting,
submit,
makeClean,
} = useForm({
fields: {
title: useField({
value : QRCode?.title || '' ,
validates: [notEmptyString( 'Please name your QR code' )],
}),
productId: useField({
value : deletedProduct ? 'Deleted product' : (QRCode?.product?.id || '' ),
validates: [notEmptyString( 'Please select a product' )],
}),
variantId: useField(QRCode?.variantId || '' ),
handle: useField(QRCode?.handle || '' ),
destination: useField(
QRCode?.destination ? [QRCode.destination] : [ 'product' ]
),
discountId: useField(QRCode?.discountId || NO_DISCOUNT_OPTION. value ),
discountCode: useField(QRCode?.discountCode || '' ),
},
onSubmit,
})
const QRCodeURL = QRCode ? new URL(
`/qrcodes/${QRCode.id}/image`,
location.toString()
).toString() : null
/*
This function is called with the selected product whenever the user clicks "Add" in the ResourcePicker.
It takes the first item in the selection array and sets the selected product to an object with the properties from the "selection" argument.
It updates the form state using the "onChange" methods attached to the form fields.
Finally, closes the ResourcePicker.
*/
const handleProductChange = useCallback(({ selection }) => {
setSelectedProduct({
title: selection[ 0 ].title,
images: selection[ 0 ].images,
handle: selection[ 0 ].handle,
})
productId.onChange(selection[ 0 ].id)
variantId.onChange(selection[ 0 ].variants[ 0 ].id)
handle.onChange(selection[ 0 ].handle)
setShowResourcePicker( false )
}, [])
/*
This function updates the form state whenever a user selects a new discount option.
*/
const handleDiscountChange = useCallback((id) => {
discountId.onChange(id)
discountCode.onChange(DISCOUNT_CODES[id] || '' )
}, [])
/*
This function is called when a user clicks "Select product" or cancels the ProductPicker.
It switches between a show and hide state.
*/
const toggleResourcePicker = useCallback(
() => setShowResourcePicker(!showResourcePicker),
[ showResourcePicker ]
)
/*
This is a placeholder function that is triggered when the user hits the "Delete" button.
It will be replaced by a different function when the frontend is connected to the backend.
*/
const isDeleting = false
const deleteQRCode = () => console.log( 'delete' )
/*
This function runs when a user clicks the "Go to destination" button.
It uses data from the App Bridge context as well as form state to construct destination URLs using the URL helpers you created.
*/
const goToDestination = useCallback(() => {
if (!selectedProduct) return
const data = {
host: appBridge.hostOrigin,
productHandle: handle. value || selectedProduct.handle,
discountCode: discountCode. value || undefined,
variantId: variantId. value ,
}
const targetURL = deletedProduct || destination. value [ 0 ] === 'product'
? productViewURL(data)
: productCheckoutURL(data)
window.open(targetURL, '_blank' , 'noreferrer,noopener' )
}, [QRCode, selectedProduct, destination, discountCode, handle, variantId])
/*
This array is used in a select field in the form to manage discount options.
It will be extended when the frontend is connected to the backend and the array is populated with discount data from the store.
For now, it contains only the default value.
*/
const isLoadingDiscounts = true ;
const discountOptions = [NO_DISCOUNT_OPTION]
/*
These variables are used to display product images, and will be populated when image URLs can be retrieved from the Admin.
*/
const imageSrc = selectedProduct?.images?.edges?.[ 0 ]?.node?.url
const originalImageSrc = selectedProduct?.images?.[ 0 ]?.originalSrc
const altText =
selectedProduct?.images?.[ 0 ]?.altText || selectedProduct?.title
/* The form layout, created using Polaris and App Bridge components. */
return (
<Stack vertical>
{deletedProduct && <Banner
title= 'The product for this QR code no longer exists.'
status= 'critical'
>
<p>
Scans will be directed to a 404 page, or you can choose another product for this QR code.
</p>
</Banner>}
<Layout>
<Layout.Section>
<Form>
<ContextualSaveBar
saveAction = {{
label: 'Save' ,
onAction: submit,
loading: submitting,
disabled: submitting,
}}
discardAction= {{
label: 'Discard' ,
onAction: reset,
loading: submitting,
disabled: submitting,
}}
visible={dirty}
fullWidth
/>
<FormLayout>
<Card sectioned title= 'Title' >
<TextField
{...title}
label= 'Title'
labelHidden
helpText= 'Only store staff can see this title'
/>
</Card>
<Card
title= 'Product'
actions={[
{
content: productId. value
? 'Change product'
: 'Select product' ,
onAction: toggleResourcePicker,
},
]}
>
<Card.Section>
{showResourcePicker && (
<ResourcePicker
resourceType= 'Product'
showVariants={ false }
selectMultiple={ false }
onCancel={toggleResourcePicker}
onSelection={handleProductChange}
open
/>
)}
{productId. value ? (
<Stack alignment= 'center' >
{(imageSrc || originalImageSrc) ? (
<Thumbnail
source={imageSrc || originalImageSrc}
alt={altText}
/>
) : (
<Thumbnail source={ImageMajor} color= 'base' size= 'small' />
)}
<TextStyle variation= 'strong' >
{selectedProduct.title}
</TextStyle>
</Stack>
) : (
<Stack vertical spacing= 'extraTight' >
<Button onClick={toggleResourcePicker}>
Select product
</Button>
{productId.error && (
<Stack spacing= 'tight' >
<Icon source={AlertMinor} color= 'critical' />
<TextStyle variation= 'negative' >
{productId.error}
</TextStyle>
</Stack>
)}
</Stack>
)}
</Card.Section>
<Card.Section title= 'Scan Destination' >
<ChoiceList
title= 'Scan destination'
titleHidden
choices={[
{ label: 'Link to product page' , value : 'product' },
{
label: 'Link to checkout page with product in the cart' ,
value : 'checkout' ,
},
]}
selected={destination. value }
onChange={destination.onChange}
/>
</Card.Section>
</Card>
<Card
Sectioned
title= 'Discount'
actions={[
{
content: 'Create discount' ,
onAction: () =>
navigate(
{
name: 'Discount' ,
resource: {
create: true ,
},
},
{ target: 'new' }
),
},
]}
>
<Select
label= 'discount code'
options={discountOptions}
onChange={handleDiscountChange}
value = {discountId. value }
disabled={isLoadingDiscounts || discountsError}
labelHidden
/>
</Card>
</FormLayout>
</Form>
</Layout.Section>
<Layout.Section secondary>
<Card sectioned title= 'QR Code' >
{QRCode ? (
<EmptyState
imageContained={ true }
image={QRCodeURL}
/>
) : (
<EmptyState>
<p>Your QR code will appear here after you save.</p>
</EmptyState>
)}
<Stack vertical>
<Button fullWidth primary download url={QRCodeURL} disabled={!QRCode || isDeleting}>
Download
</Button>
<Button
fullWidth
onClick={goToDestination}
disabled={!selectedProduct}
>
Go to destination
</Button>
</Stack>
</Card>
</Layout.Section>
<Layout.Section>
{QRCode?.id && (
<Button
Outline
destructive
onClick={deleteQRCode}
loading={isDeleting}
>
Delete QR code
</Button>
)}
</Layout.Section>
</Layout>
</Stack>
)
}
/* Builds a URL to the selected product */
function productViewURL ( { host, productHandle, discountCode } ) {
const url = new URL(host)
const productPath = `/products/${productHandle}`
/*
If a discount is selected, then build a URL to the selected discount that redirects to the selected product: /discount/{code}?redirect=/products/{product}
*/
if (discountCode) {
url.pathname = `/discount/${discountCode}`
url.searchParams.append( 'redirect' , productPath)
} else {
url.pathname = productPath
}
return url.toString()
}
/* Builds a URL to a checkout that contains the selected product */
function productCheckoutURL ( {
host,
variantId,
quantity = 1 ,
discountCode,
} ) {
const url = new URL(host)
const id = variantId.replace(
/gid:\/\/shopify\/ProductVariant\/([ 0 -9 ]+)/,
'$1'
)
url.pathname = `/cart/${id}:${quantity}`
/*
Builds a URL to a checkout that contains the selected product with a discount code applied
*/
if (discountCode) {
url.searchParams.append( 'discount' , discountCode)
}
return url.toString()
}
Now that the QRCodeForm component is complete, we will include it in the component index.
/web/frontend/components/index.js
export { ProductsCard } from "./ProductsCard";
export * from "./providers";
export { QRCodeForm } from './QRCodeForm' // Addition
Next we'll create the page itself.
The page that displays the form.
The pages you will create are a New page and an Edit page.
-
/web/frontend/pages/qrcodes/new.jsx
: Newly created page -
/web/frontend/pages/qrcodes/[id].jsx
: edit page
/ / web/frontendディレクトリで入力
$ mkdir pages/qrcodes
$ touch pages/qrcodes/new.jsx
$ touch pages/qrcodes/\[id\].jsx
About page routing
This sample app uses file-based routing.
This is clearly a Next.js take on the idea.
Next.js is a great full-stack framework, but it presents some challenges when used for a Shopify app.
I don't know what the engineers at Shopify's headquarters think, but by building a similar function independently without using Next.js as a framework, I can say that this sample code has great advantages while avoiding disadvantages (although this is my subjective opinion).
This article will only provide an overview of file-based routing, so if you are unfamiliar with it, please refer to the official Next.js tutorial.
File-based routing is web/frontend/Routes.jsx
It is designed with.
This will make the .jsx file name under the /web/frontend/pages directory the URL.
For example, if the URL is “domain/qrcodes/new”, it will be rendered at /web/frontend/pages/qrcodes/new.jsx.
If the input URL does not match the filename, it will be rendered to /pages/NotFound.jsx.
By the way, the [id].jsx filename is a bit special because when it's enclosed in brackets, it maps to a unique URL.
For details, I will refer you to the Next.js tutorial.
QR Code Generation Page – Create New
Add the following code to /web/frontend/pages/qrcodes/new.jsx.
import { Page } from '@shopify/polaris'
import { TitleBar } from '@shopify/app-bridge-react'
import { QRCodeForm } from '../../components'
export default function ManageCode ( ) {
const breadcrumbs = [{ content : 'QR codes' , url : '/' }]
return (
< Page >
< TitleBar
title = "Create new QR code"
breadcrumbs = {breadcrumbs}
primaryAction = {null}
/>
< QRCodeForm />
</ Page >
)
}
Let's change the routing for the page.
Open web/frontend/App.jsx and modify the navigationLinks.
The whole code looks like this:
import { BrowserRouter } from "react-router-dom" ;
import { NavigationMenu } from "@shopify/app-bridge-react" ;
import Routes from "./Routes" ;
import {
AppBridgeProvider,
GraphQLProvider,
PolarisProvider,
} from "./components" ;
export default function App ( ) {
// Any .tsx or .jsx files in /pages will become a route
// See documentation for <Routes /> for more info
const pages = import .meta.globEager( "./pages/**/!(*.test.[jt]sx)*.([jt]sx)" );
return (
<PolarisProvider>
<BrowserRouter>
<AppBridgeProvider>
<GraphQLProvider>
<Navigation Menu
navigationLinks={[
{
label: "Generate QR code",
destination: "/qrcodes/new",
},
]}
/>
<Routes pages={pages} />
</GraphQLProvider>
</AppBridgeProvider>
</BrowserRouter>
</PolarisProvider>
);
}
This completes the new page.
Let's start the server and check it out.
To start the server, run it in the root directory of the application.
// アプリのルートディレクトリで実行してください
$ yarn dev
Once you have entered a title and selected a product, your new page is complete.
Since we haven’t created the backend processing yet, we can’t actually generate or save QR codes.
QR Code Generation Page – Edit
When editing, the same components as when creating a new file are used, and editing information is retrieved from the database (hereafter referred to as DB).
Since the backend processing has not yet been built, we will use mock data to check whether the page is displayed as expected.
Let's start by adding the following code to the /web/frontend/pages/qrcodes/[id].jsx file we just created.
import { Card, Page, Layout, SkeletonBodyText } from '@shopify/polaris'
import { Loading, TitleBar } from '@shopify/app-bridge-react'
import { QRCodeForm } from '../../components'
export default function QRCodeEdit() {
const breadcrumbs = [{content: 'QR codes', url: '/' }]
/*
These are mock values.
Set isLoading to false to preview the page without loading markup.
*/
const isLoading = false
const isRefetching = false
const QRCode = {
createdAt: '2022-06-13',
destination: 'checkout',
title: 'My first QR code',
product: {}
}
/* Loading action and markup that uses App Bridge and Polaris components */
if (isLoading || isRefetching) {
return (
< Page >
< TitleBar title = "Edit QR code" breadcrumbs = {breadcrumbs} primaryAction = {null} />
< Loading />
< Layout >
< Layout.Section >
< Card sectioned title = "Title" >
< SkeletonBodyText />
</ Card >
< Card title = "Product" >
< Card.Section >
< SkeletonBodyText lines = {1} />
< /Card.Section >
< Card.Section >
< SkeletonBodyText lines = {3} />
< /Card.Section >
</ Card >
< Card sectioned title = "Discount" >
< SkeletonBodyText lines = {2} />
</ Card >
</ Layout.Section >
< Layout.Section secondary >
< Card sectioned title = "QR code" />
</ Layout.Section >
</ Layout >
</ Page >
)
}
return (
< Page >
< TitleBar title = "Edit QR code" breadcrumbs = {breadcrumbs} primaryAction = {null} />
< QRCodeForm QRCode = {QRCode} />
</ Page >
)
}
Refresh the URL in your browser from the app's homepage to see the QR code editing page.
Please enter the following:
https://{shop}.myshopify.com/apps/{app-name}/qrcodes/1
Please change {shop} and {app-name} as appropriate for your environment.
If it is written like this, it is OK.
Add a List Component to the Home Page
The last part of the frontend build is to create an index page that lists the QR codes you have created.
As before, create a component and load it on the index page.
Now we will load some additional libraries to create our list component.
/ / web/frontendディレクトリで入力してください。
$ cd web/frontend
$ yarn add @shopify/react-hooks dayjs
Create a new component, QRCodeIndex.jsx.
$ touch components/QRCodeIndex.jsx
Insert the following code into QRCodeIndex.jsx.
import { useNavigate } from '@shopify/app-bridge-react'
import { Card, Icon, IndexTable, Stack, TextStyle, Thumbnail, UnstyledLink } from '@shopify/polaris'
import { DiamondAlertMajor, ImageMajor } from '@shopify/polaris-icons'
/* useMedia is used to support multiple screen sizes */
import { useMedia } from '@shopify/react-hooks'
/* dayjs is used to capture and format the date a QR code was created or modified */
import dayjs from 'dayjs'
/* Markup for small screen sizes (mobile) */
function SmallScreenCard({ id, title, product, discountCode, scans, createdAt, navigate }) {
return (
< UnstyledLink onClick = {() => navigate(`/qrcodes/${id}`)}>
< div style = {{ padding: ' 0.75rem 1rem ', borderBottom: ' 1px solid # E1E3E5 ' }} >
< Stack >
< Stack.Item >
< Thumbnail
source = {product?.images?.edges[0]?.node?.url || ImageMajor }
alt = "placeholder"
color = "base"
size = "small"
/>
</ Stack.Item >
< Stack.Item fill >
< Stack vertical = {true} >
< Stack.Item >
<p>
< TextStyle variation = "strong" > {truncate(title, 35)} </ TextStyle >
</ p >
< p > {truncate(product?.title, 35)} </ p >
< p > {dayjs(createdAt).format('MMMM D, YYYY')} </ p >
</ Stack.Item >
< div style = {{display: ' flex '}} >
< div style = {{flex: ' 3 '}} >
< TextStyle variation = "subdued" > Discount </ TextStyle >
< p > {discountCode || '-'} </ p >
</ div >
< div style = {{flex: ' 2 '}} >
< TextStyle variation = "subdued" > Scans </ TextStyle >
<p> {scans} </p>
</ div >
</ div >
</ Stack >
</ Stack.Item >
</ Stack >
</ div >
</ UnstyledLink >
)
}
export function QRCodeIndex({ QRCodes, loading }) {
const navigate = useNavigate()
/* Check if screen is small */
const isSmallScreen = useMedia('(max-width: 640px)')
/* Map over QRCodes for small screen */
const smallScreenMarkup = QRCodes.map((QRCode) => (
< SmallScreenCard key = {QRCode.id} navigate = {navigate} { ...QRCode }/>
))
const resourceName = {
singular: 'QR code',
plural: 'QR codes',
}
const rowMarkup = QRCodes.map(
({ id, title, product, discountCode, scans, createdAt }, index) => {
const deletedProduct = product.title.includes('Deleted product')
/* The form layout, created using Polaris components. Includes QR code data set above. */
return (
< IndexTable.Row
id = {id}
key = {id}
position = {index}
onClick = {() => {
navigate(`/qrcodes/${id}`)
}}
>
< IndexTable.Cell >
< Thumbnail
source = {product?.images?.edges[0]?.node?.url || ImageMajor }
alt = "placeholder"
color = "base"
size = "small"
/>
</ IndexTable.Cell >
< IndexTable.Cell >
< UnstyledLink data-primary-link url = { `/ qrcodes /${ id }`}>
{truncate(title, 30)}
</ UnstyledLink >
</ IndexTable.Cell >
< IndexTable.Cell >
< Stack >
{deletedProduct && < Icon source = {DiamondAlertMajor} color = "critical" /> }
< TextStyle variation = {deletedProduct ? " negative " : null }>
{truncate(product?.title, 30)}
</ TextStyle >
</ Stack >
</ IndexTable.Cell >
< IndexTable.Cell > {discountCode} </ IndexTable.Cell >
< IndexTable.Cell >
{dayjs(createdAt).format('MMMM D, YYYY')}
</ IndexTable.Cell >
< IndexTable.Cell > {scans} </ IndexTable.Cell >
</ IndexTable.Row >
)
}
)
/* A layout for small screens, built using Polaris components */
return (
< Card >
{isSmallScreen ? smallScreenMarkup : (
< IndexTable
resourceName = {resourceName}
itemCount = {QRCodes.length}
headings = {[
{ title: ' Thumbnail ', hidden: true },
{ title: ' Title ' },
{ title: ' Product ' },
{ title: ' Discount ' },
{ title: ' Date created ' },
{ title: ' Scans ' },
]}
selectable = {false}
loading = {loading}
>
{rowMarkup}
</ IndexTable >
)}
</ Card >
)
}
/* A function to truncate long strings */
function truncate(str, n) {
return str.length > n ? str.substr(0, n - 1) + '…' : str
}
Include the list component in the index component.
/web/frontend/components/index.js
export { ProductsCard } from "./ProductsCard" ;
export * from "./providers" ;
export { QRCodeForm } from './QRCodeForm'
export { QRCodeIndex } from './QRCodeIndex' // Addition
Change your index page to the following:
/web/frontend/pages/index.jsx
import { useNavigate, TitleBar, Loading } from '@shopify/app-bridge-react'
import { Card, EmptyState, Layout, Page, SkeletonBodyText } from '@shopify/polaris'
import { QRCodeIndex } from '../components'
export default function HomePage() {
const navigate = useNavigate()
const isLoading = false
const isRefetching = false
const QRCodes = []
const qrCodesMarkup = QRCodes?.length ? (
<QRCodeIndex QRCodes={QRCodes} loading={isRefetching} />
) : null
const loadingMarkup = isLoading ? (
<Card sectioned>
<Loading />
<SkeletonBodyText />
</Card>
) : null
const emptyStateMarkup =
!isLoading && !QRCodes?.length ? (
<Card sectioned>
<EmptyState
heading= "Create unique QR codes for your product"
action= {{
content: 'Create QR code' ,
onAction: () => navigate( '/qrcodes/new' ),
}}
image= "https://cdn.shopify.com/s/files/1/0262/4071/2726/files/emptystate-files.png"
>
<p>
Allow customers to scan codes and buy products using their phones.
</p>
</EmptyState>
</Card>
) : null
return (
<Page>
<TitleBar
title= "QR codes"
primaryAction= {{
content: 'Create QR code' ,
onAction: () => navigate( '/qrcodes/new' ),
}}
/>
<Layout>
<Layout.Section>
{loadingMarkup}
{emptyStateMarkup}
{qrCodesMarkup}
</Layout.Section>
</Layout>
</Page>
)
}
That's it, we've completed setting up the list component on the index page!
Since the app does not yet have backend processing, we will check its operation using mock data.
Update the value of the constant QRCodes as follows:
const QRCodes = [
{
createdAt: '2022-06-13' ,
destination: 'checkout' ,
title: 'My first QR code' ,
id: 1 ,
discountCode: 'SUMMERDISCOUNT' ,
product: {
title: 'Faded t-shirt' ,
}
},
{
createdAt: '2022-06-13' ,
destination: 'product' ,
title: 'My second QR code' ,
id: 2 ,
discountCode: 'WINTERDISCOUNT' ,
product: {
title: 'Cozy parka' ,
}
},
{
createdAt: '2022-06-13' ,
destination: 'product' ,
title: 'QR code for deleted product' ,
id: 3 ,
product: {
title: 'Deleted product' ,
}
},
]
Start the server and if you can see the following display, then you're good to go!
At this point, it's only half done! 🎉
There's still a long way to go, so let's do our best!
Building the backend
I'm going to start building the backend that I've been putting off!
What you need this time is:
- Obtaining the QR code
- keep
- edit
- delete
We will create what is known as CRUD.
You will also need an API layer to connect your DB and frontend to your backend.
Here,
- Configure a database to store QR codes
- Adding an API layer to your app
We will learn:
Configure DB
In this tutorial, we will use SQLite as the database.
In actual development, you will probably use MySQL, PostgresQL, etc., so please substitute appropriately.
By the way, I use DynamoDB (NoSQL).
Create a qr-codes-db.js file under the /web directory and add the following code:
// webディレクトリで実行してください
$ touch qr-codes-db.js
web/qr-codes-db.js
/*
This file interacts with the app's database and is used by the app's REST APIs.
*/
import sqlite3 from "sqlite3" ;
import path from "path" ;
import { Shopify } from "@shopify/shopify-api" ;
const DEFAULT_DB_FILE = path.join(process.cwd(), "qr_codes_db.sqlite" );
const DEFAULT_PURCHASE_QUANTITY = 1 ;
export const QRCodesDB = {
qrCodesTableName : "qr_codes" ,
db : null ,
ready : null ,
create : async function ( {
shopDomain,
title,
productId,
variantId,
handle,
discountId,
discountCode,
destination,
} ) {
await this .ready;
const query = `
INSERT INTO ${ this .qrCodesTableName}
(shopDomain, title, productId, variantId, handle, discountId, discountCode, destination, scans)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
RETURNING id;
` ;
const rawResults = await this .__query(query, [
shopDomain,
title,
productId,
variantId,
handle,
discountId,
discountCode,
destination,
]);
return rawResults[ 0 ].id;
},
update : async function (
id,
{
title,
productId,
variantId,
handle,
discountId,
discountCode,
destination,
}
) {
await this .ready;
const query = `
UPDATE ${ this .qrCodesTableName}
SET
title = ?,
productId = ?,
variantId = ?,
handle = ?,
discountId = ?,
discountCode = ?,
destination = ?
WHERE
id = ?;
` ;
await this .__query(query, [
title,
productId,
variantId,
handle,
discountId,
discountCode,
destination,
id,
]);
return true ;
},
list : async function ( shopDomain ) {
await this .ready;
const query = `
SELECT * FROM ${ this .qrCodesTableName}
WHERE shopDomain = ?;
` ;
const results = await this .__query(query, [shopDomain]);
return results.map( ( qrcode ) => this .__addImageUrl(qrcode));
},
read : async function ( id ) {
await this .ready;
const query = `
SELECT * FROM ${ this .qrCodesTableName}
WHERE id = ?;
` ;
const rows = await this .__query(query, [id]);
if (! Array .isArray(rows) || rows?.length !== 1 ) return undefined ;
return this .__addImageUrl(rows[ 0 ]);
},
delete : async function ( id ) {
await this .ready;
const query = `
DELETE FROM ${ this .qrCodesTableName}
WHERE id = ?;
` ;
await this .__query(query, [id]);
return true ;
},
/* The destination URL for a QR code is generated at query time */
generateQrcodeDestinationUrl : function ( qrcode ) {
return ` ${Shopify.Context.HOST_SCHEME} :// ${Shopify.Context.HOST_NAME} /qrcodes/ ${qrcode.id} /scan` ;
},
/* The behavior when a QR code is scanned */
handleCodeScan : async function ( qrcode ) {
/* Log the scan in the database */
await this .__increaseScanCount(qrcode);
const url = new URL(qrcode.shopDomain);
switch (qrcode.destination) {
/* The QR code redirects to the product view */
case "product" :
return this .__goToProductView(url, qrcode);
/* The QR code redirects to checkout */
case "checkout" :
return this .__goToProductCheckout(url, qrcode);
default :
throw `Unrecognized destination " ${qrcode.destination} "` ;
}
},
// Private
/*
Used to check whether to create the database.
Also used to make sure the database and table are set up before the server starts.
*/
__hasQrCodesTable : async function ( ) {
const query = `
SELECT name FROM sqlite_schema
WHERE
type = 'table' AND
name = ?;
` ;
const rows = await this .__query(query, [ this .qrCodesTableName]);
return rows.length === 1 ;
},
/* Initializes the connection with the app's sqlite3 database */
init : async function ( ) {
/* Initializes the connection to the database */
this .db = this .db ?? new sqlite3.Database(DEFAULT_DB_FILE);
const hasQrCodesTable = await this .__hasQrCodesTable();
if (hasQrCodesTable) {
this .ready = Promise .resolve();
/* Create the QR code table if it hasn't been created */
} else {
const query = `
CREATE TABLE ${ this .qrCodesTableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
shopDomain VARCHAR(511) NOT NULL,
title VARCHAR(511) NOT NULL,
productId VARCHAR(255) NOT NULL,
variantId VARCHAR(255) NOT NULL,
handle VARCHAR(255) NOT NULL,
discountId VARCHAR(255) NOT NULL,
discountCode VARCHAR(255) NOT NULL,
destination VARCHAR(255) NOT NULL,
scans INTEGER,
createdAt DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))
)
` ;
/* Tell the various CRUD methods that they can execute */
this .ready = this .__query(query);
}
},
/* Perform a query on the database. Used by the various CRUD methods. */
__query : function ( sql, params = [] ) {
return new Promise ( ( resolve, reject ) => {
this .db.all(sql, params, (err, result) => {
if (err) {
reject(err);
return ;
}
resolve(result);
});
});
},
__addImageUrl : function ( qrcode ) {
try {
qrcode.imageUrl = this .__generateQrcodeImageUrl(qrcode);
} catch (err) {
console .error(err);
}
return qrcode;
},
__generateQrcodeImageUrl : function ( qrcode ) {
return ` ${Shopify.Context.HOST_SCHEME} :// ${Shopify.Context.HOST_NAME} /qrcodes/ ${qrcode.id} /image` ;
},
__increaseScanCount : async function ( qrcode ) {
const query = `
UPDATE ${ this .qrCodesTableName}
SET scans = scans + 1
WHERE id = ?
` ;
await this .__query(query, [qrcode.id]);
},
__goToProductView : function ( url, qrcode ) {
return productViewURL({
discountCode : qrcode.discountCode,
host : url.toString(),
productHandle : qrcode.handle,
});
},
__goToProductCheckout : function ( url, qrcode ) {
return productCheckoutURL({
discountCode : qrcode.discountCode,
host : url.toString(),
variantId : qrcode.variantId,
quantity : DEFAULT_PURCHASE_QUANTITY,
});
},
};
/* Generate the URL to a product page */
function productViewURL ( { host, productHandle, discountCode } ) {
const url = new URL(host);
const productPath = `/products/ ${productHandle} ` ;
/* If this QR Code has a discount code, then add it to the URL */
if (discountCode) {
url.pathname = `/discount/ ${discountCode} ` ;
url.searchParams.append( "redirect" , productPath);
} else {
url.pathname = productPath;
}
return url.toString();
}
/* Generate the URL to checkout with the product in the cart */
function productCheckoutURL ( {
host,
variantId,
quantity = 1 ,
discountCode,
} ) {
const url = new URL(host);
const id = variantId.replace(
/gid:\/\/shopify\/ProductVariant\/([0-9]+)/ ,
"$1"
);
/* The cart URL resolves to a checkout URL */
url.pathname = `/cart/ ${id} : ${quantity} ` ;
if (discountCode) {
url.searchParams.append( "discount" , discountCode);
}
return url.toString();
}
If you understand SQL, you should be able to easily understand the processing that is written.
Adding an API Layer
Now that we have the DB configured, we'll add an API layer so that the frontend can access the data.
// webディレクトリで実行してください
$ touch helpers/qr-codes.js
Please add the following code to qr-codes.js.
import { Shopify } from "@shopify/shopify-api" ;
import { QRCodesDB } from "../qr-codes-db.js" ;
const QR_CODE_ADMIN_QUERY = `
query nodes($ids: [ID!]!) {
nodes(ids: $ids) {
... on Product {
id
handle
title
images(first: 1) {
edges {
node {
URL
}
}
}
}
... on ProductVariant {
id
}
... on DiscountCodeNode {
id
}
}
}
` ;
export async function getQrCodeOr404 ( req, res, checkDomain = true ) {
try {
const response = await QRCodesDB.read(req.params.id);
if (
response === undefined ||
(checkDomain &&
( await getShopUrlFromSession(req, res)) !== response.shopDomain)
) {
res.status( 404 ).send();
} else {
return response;
}
} catch (error) {
res.status( 500 ).send(error.message);
}
return undefined ;
}
export async function getShopUrlFromSession ( req, res ) {
const session = await Shopify.Utils.loadCurrentSession(req, res, true );
return `https:// ${session.shop} ` ;
}
/*
Expect body to contain
title: string
productId: string
variantId: string
handle: string
discountId: string
discountCode: string
destination: string
*/
export async function parseQrCodeBody ( req, res ) {
return {
title : req.body.title,
productId : req.body.productId,
variantId : req.body.variantId,
handle : req.body.handle,
discountId : req.body.discountId,
discountCode : req.body.discountCode,
destination : req.body.destination,
};
}
/*
Replaces the productId with product data queried from the Shopify GraphQL Admin API
*/
export async function formatQrCodeResponse ( req, res, rawCodeData ) {
const ids = [];
/* Get every product, variant and discountID that was queried from the database */
rawCodeData.forEach( ( { productId, discountId, variantId } ) => {
ids.push(productId);
ids.push(variantId);
if (discountId) {
ids.push(discountId);
}
});
/* Instantiate a new GraphQL client to query the Shopify GraphQL Admin API */
const session = await Shopify.Utils.loadCurrentSession(req, res, true );
const client = new Shopify.Clients.Graphql(session.shop, session.accessToken);
/* Query the Shopify GraphQL Admin API */
const adminData = await client.query({
data : {
query : QR_CODE_ADMIN_QUERY,
/* The IDs that are pulled from the app's database are used to query product, variant and discount information */
variables : { ids },
},
});
/*
Replace the product, discount and variant IDs with the data fetched using the Shopify GraphQL Admin API.
*/
const formattedData = rawCodeData.map( ( qrCode ) => {
const product = adminData.body.data.nodes.find(
( node ) => qrCode.productId === node?.id
) || {
title : "Deleted product" ,
};
const discountDeleted =
qrCode.discountId &&
!adminData.body.data.nodes.find( ( node ) => qrCode.discountId === node?.id);
/*
A user might create a QR code with a discount code and then later delete that discount code.
For optimal UX it's important to handle that edge case.
Use mock data so that the frontend knows how to interpret this QR Code.
*/
if (discountDeleted) {
QRCodesDB.update(qrCode.id, {
...qrCode,
discountId : "" ,
discountCode : "" ,
});
}
/*
Merge the data from the app's database with the data queried from the Shopify GraphQL Admin API
*/
const formattedQRCode = {
...qrCode,
product,
discountCode : discountDeleted ? "" : qrCode.discountCode,
};
/* Since product.id already exists, productId isn't required */
delete formattedQRCode.productId;
return formattedQRCode;
});
return formattedData;
}
We use GraphQL to retrieve product information, product variant information (option information), and discount code information.
As for how to retrieve this data, if you understand GraphQL, you should be able to understand it.
I will provide some additional explanation.
Middleware that handles API requests from the frontend
(If you don't know what middleware is, I recommend googling something like "nodejs middleware")
// webディレクトリで実行してください。
$ touch middleware/qr-code-api.js
Please add the following code to qr-code-api.js.
/*
The custom REST API to support the app frontend.
Handlers combine application data from qr-codes-db.js with helpers to merge the Shopify GraphQL Admin API data.
The Shop is the Shop that the current user belongs to. For example, the shop that is using the app.
This information is retrieved from the Authorization header, which is decoded from the request.
The authorization header is added by App Bridge in the frontend code.
*/
import { QRCodesDB } from "../qr-codes-db.js" ;
import {
getQrCodeOr404,
getShopUrlFromSession,
parseQrCodeBody,
formatQrCodeResponse,
} from "../helpers/qr-codes.js" ;
export default function applyQrCodeApiEndpoints ( app ) {
app.post( "/api/qrcodes" , async (req, res) => {
try {
const id = await QRCodesDB.create({
...( await parseQrCodeBody(req)),
/* Get the shop from the authorization header to prevent users from spoofing the data */
shopDomain : await getShopUrlFromSession(req, res),
});
const response = await formatQrCodeResponse(req, res, [
await QRCodesDB.read(id),
]);
res.status( 201 ).send(response[ 0 ]);
} catch (error) {
res.status( 500 ).send(error.message);
}
});
app.patch( "/api/qrcodes/:id" , async (req, res) => {
const qrcode = await getQrCodeOr404(req, res);
if (qrcode) {
try {
await QRCodesDB.update(req.params.id, await parseQrCodeBody(req));
const response = await formatQrCodeResponse(req, res, [
await QRCodesDB.read(req.params.id),
]);
res.status( 200 ).send(response[ 0 ]);
} catch (error) {
res.status( 500 ).send(error.message);
}
}
});
app.get( "/api/qrcodes" , async (req, res) => {
try {
const rawCodeData = await QRCodesDB.list(
await getShopUrlFromSession(req, res)
);
const response = await formatQrCodeResponse(req, res, rawCodeData);
res.status( 200 ).send(response);
} catch (error) {
res.status( 500 ).send(error.message);
}
});
app.get( "/api/qrcodes/:id" , async (req, res) => {
const qrcode = await getQrCodeOr404(req, res);
if (qrcode) {
const formattedQrCode = await formatQrCodeResponse(req, res, [qrcode]);
res.status( 200 ).send(formattedQrCode[ 0 ]);
}
});
app.delete( "/api/qrcodes/:id" , async (req, res) => {
const qrcode = await getQrCodeOr404(req, res);
if (qrcode) {
await QRCodesDB.delete(req.params.id);
res.status( 200 ).send();
}
});
}
It sets up the backend endpoints and handles routing and controller-like processing.
The actual process of connecting to the database will be written in a function built in the helpers directory.
Connect DB and API endpoints to your server
Update your server configuration to use the new database configuration and API endpoints.
Change web/index.js to the following:
// @ts-check
import { join } from "path";
import fs from "fs";
import express from "express" ;
import cookieParser from "cookie-parser" ;
import { Shopify, ApiVersion } from "@shopify/shopify-api" ;
import applyAuthMiddleware from "./middleware/auth.js" ;
import verifyRequest from "./middleware/verify-request.js" ;
import { setupGDPRWebHooks } from "./gdpr.js" ;
import { BillingInterval } from "./helpers/ensure-billing.js" ;
import applyQrCodeApiEndpoints from "./middleware/qr-code-api.js" ;
import { QRCodesDB } from "./qr-codes-db.js" ;
const USE_ONLINE_TOKENS = true ;
const TOP_LEVEL_OAUTH_COOKIE = "shopify_top_level_oauth" ;
const PORT = parseInt (process.env.BACKEND_PORT || process.env.PORT, 10 );
const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD;
const versionFilePath = "./version.txt" ;
let templateVersion = "unknown" ;
if (fs.existsSync(versionFilePath)) {
templateVersion = fs.readFileSync(versionFilePath, "utf8" ).trim();
}
// TODO: There should be provided by env vars
const DEV_INDEX_PATH = ` ${process.cwd()} /frontend/` ;
const PROD_INDEX_PATH = ` ${process.cwd()} /frontend/dist/` ;
const dbFile = join(process.cwd(), "database.sqlite" );
const sessionDb = new Shopify.Session.SQLiteSessionStorage(dbFile);
// Initialize SQLite DB
QRCodesDB.db = sessionDb.db;
QRCodesDB.init();
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?:\/\// , "" ),
HOST_SCHEME: process.env.HOST.split( "://" )[ 0 ],
API_VERSION: ApiVersion.April22,
IS_EMBEDDED_APP: true ,
SESSION_STORAGE: sessionDb,
});
// Storing the currently active shops in memory will force them to re-login when your server restarts. You should
// persist this object in your app.
const ACTIVE_SHOPIFY_SHOPS = {};
Shopify.Webhooks.Registry.addHandler( "APP_UNINSTALLED" , {
path: "/api/webhooks" ,
webhookHandler: async (topic, shop, body) =>
delete ACTIVE_SHOPIFY_SHOPS[shop],
});
// The transactions with Shopify will always be marked as test transactions, unless NODE_ENV is production.
// See the ensureBilling helper to learn more about billing in this template.
const BILLING_SETTINGS = {
required: false ,
// This is an example configuration that would do a one-time charge for $5 (only USD is currently supported)
// chargeName: "My Shopify One-Time Charge",
// amount: 5.0,
// currencyCode: "USD",
// interval: BillingInterval.OneTime,
};
// This sets up the mandatory GDPR webhooks. You'll need to fill in the endpoint
// in the “GDPR mandatory webhooks” section in the “App setup” tab, and customize
// the code when you store customer data.
//
// More details can be found on shopify.dev:
// https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks
setupGDPRWebHooks( "/api/webhooks" );
// export for test use only
export async function createServer (
root = process.cwd(),
isProd = process.env.NODE_ENV === "production",
billingSettings = BILLING_SETTINGS
) {
const app = express();
app.set( "top-level-oauth-cookie" , TOP_LEVEL_OAUTH_COOKIE);
app.set( "active-shopify-shops" , ACTIVE_SHOPIFY_SHOPS);
app.set( "use-online-tokens" , USE_ONLINE_TOKENS);
app.use(cookieParser(Shopify.Context.API_SECRET_KEY));
applyAuthMiddleware(app, {
billing: billingSettings,
});
app.post( "/api/webhooks" , async (req, res) => {
try {
await Shopify.Webhooks.Registry.process(req, res);
console .log( `Webhook processed, returned status code 200` );
} catch (error) {
console .log( `Failed to process webhook: ${error} ` );
if (!res.headersSent) {
res.status( 500 ).send(error.message);
}
}
});
// All endpoints after this point will require an active session
app.use(
"/api/*" ,
verifyRequest(app, {
billing: billingSettings,
})
);
app.get( "/api/products-count" , async (req, res) => {
const session = await Shopify.Utils.loadCurrentSession(req, res, true );
const { Product } = await import (
`@shopify/shopify-api/dist/rest-resources/ ${Shopify.Context.API_VERSION} /index.js`
);
const countData = await Product.count({ session });
res.status( 200 ).send(countData);
});
app.post( "/api/graphql" , async (req, res) => {
try {
const response = await Shopify.Utils.graphqlProxy(req, res);
res.status( 200 ).send(response.body);
} catch (error) {
res.status( 500 ).send(error.message);
}
});
app.use(express.json());
applyQrCodeApiEndpoints(app);
app.use( ( req, res, next ) => {
const shop = req.query.shop;
if (Shopify.Context.IS_EMBEDDED_APP && shop) {
res.setHeader(
"Content-Security-Policy" ,
`frame-ancestors https:// ${shop} https://admin.shopify.com;`
);
} else {
res.setHeader( "Content-Security-Policy" , `frame-ancestors 'none';` );
}
console .log( `Content-Security-Policy: ${res.getHeader("Content-Security-Policy")} ` );
next();
});
if (isProd) {
const compression = await import ( "compression" ).then(
( { default : fn } ) => fn
);
const serveStatic = await import ( "serve-static" ).then(
( { default : fn } ) => fn
);
app.use(compression());
app.use(serveStatic(PROD_INDEX_PATH));
}
app.use( "/*" , async (req, res, next) => {
const shop = req.query.shop;
// Detect whether we need to reinstall the app, any request from Shopify will
// include a shop in the query parameters.
if (app.get( "active-shopify-shops" )[shop] === undefined && shop) {
res.redirect( `/api/auth?shop= ${shop} ` );
} else {
// res.set('X-Shopify-App-Nothing-To-See-Here', '1');
const fs = await import ( "fs" );
const fallbackFile = join(
isProd ? PROD_INDEX_PATH : DEV_INDEX_PATH,
"index.html"
);
res
.status( 200 )
.set( "Content-Type" , "text/html" )
.send(fs.readFileSync(fallbackFile));
}
});
return { app };
}
if (!isTest) {
createServer().then( ( { app } ) => app.listen(PORT));
}
In fact, there are a lot of very important points such as OAuth, Webhooks, and session tokens, and it would probably take an hour just to explain them all.
I will leave the explanation of OAuth and session tokens to the official documentation.
More about Webhooks here.