[2024 Remix Edition] Official Shopify App Development Tutorial - Complete Explanation!

[2024年 Remix版] Shopifyアプリ開発公式チュートリアル - 徹底解説!

In August 2023, with Shopify Edition Summer'23 , the template for Shopify app development was changed to the Remix framework .

You might think that Express and Ruby would continue to coexist in the future, but Remix is ​​now the only option for templates that can be built from the Shopify CLI .

In November 2022, Remix and Shopify partnered, and Remix is ​​essentially becoming a framework exclusively for Shopify development.

In the future, Shopify app development engineers and Shopify custom storefront engineers may find themselves forced to develop with Remix.

So, this time I would like to explain the official Shopify documentation on the Remix framework.

💡Estimated completion time

3 hr

What you will learn

In this tutorial you will learn:

  • Shopify CLI How to build a Remix app that uses
  • How to install apps in the test store
  • How to create a product through the app

Prerequisites

  • Created a Shopify part-time account and test store
  • Node.js 16 or later installed.
  • A Node.js package manager (npm, yarn) is installed
  • Git 2.28 or later installed

Official Documentation

https://shopify.dev/docs/apps/getting-started/build-qr-code-app?framework=remix

So, let's get started with the explanation!

Welcome to the world of Shopify app development 🌍🌎!!!

What are Shopify apps?

Shopify apps allow you to build apps that extend the functionality of your store and create unique shopping experiences for your customers. You can also use data from your Shopify store in your apps and platforms.

Merchants use Shopify apps to fit their specific needs, help them build their business, integrate with third-party services, and add functionality to their Shopify admin.

A diagram illustrating the relationships between apps, merchants, developers, and Shopify

Quote: https://shopify.dev/apps/getting-started

The relationship between a Developer (app developer) and a Merchant (store manager) begins when the store manager installs an app provided by the app developer. The store manager provides additional functions to his/her store through the app and improves the customer's shopping experience.

Building the Shopify app environment

Setting up the Shopify app environment is very simple. This blog will explain it on the assumption that you are using Mac OS, so if you are using Windows, please refer to the Shopify documentation.

Install Shopify CLI

$ brew tap shopify/shopify
 $ brew install shopify-cli
 $ shopify version
 => 3 .24 .1

If you have already installed Shopify CLI, please make sure that it is version 3. If it is version 2, please update it!

 ※ バージョンが2系になっていない場合
$ brew update
 $ brew upgrade shopify-cli
 $ brew unlink shopify-cli && brew link --force --overwrite shopify-cli

What is the Remix Framework?

Remix is ​​a framework for developing modern web applications, mainly based on React. This framework is officially partnered with Shopify and can be considered the official Shopify framework. The framework adopts many modern best practices and patterns in web development.

Main features

  1. Server-Side Rendering (SSR) : Remix offers a wealth of tools and settings to facilitate server-side rendering, which improves performance and SEO.

  2. Nested Routing : Remix has file-based routing and supports nested routing, making large applications easier to manage.

  3. Data Prefetching : When a user hovers the mouse over a link, the required data is prefetched, making page transitions extremely fast.

  4. Powerful Data Fetching : Remix provides a mechanism to efficiently load the data required for each route.

  5. Code splitting : Allows efficient code splitting, which improves performance.

  6. TypeScript Support : For developers looking for type safety, there's support for TypeScript.

Usage scenarios

  • High performance websites and applications
  • SEO focused projects
  • Large, Complex Applications
  • Web applications that work closely with APIs

Remix has gained a lot of popularity in a short period of time and has become a powerful tool for efficiently building advanced web applications, especially for developers with experience with React.

Build a Shopify app with Remix

First, decide what kind of app you want to develop.

In this tutorial, we will develop an app that creates a QR code for a product. When a customer scans the QR code, they will be taken to the checkout screen or product detail page where the product is registered.

The app also logs each time a QR code is scanned and shares usage information with merchants.

Through this tutorial you will learn:

What you'll learn

Now that we have a good understanding of the basics, let's get started on development.

First, enter the following command in the terminal to install the Remix Shopify app template.

 $ npm init @shopify/app@latest
 ? Your project name?: プロジェクト名
 ? Get started building your app:
 ✔ Start with Remix (recommended)

The latest version of Shopify templates only allow you to choose the Remix framework.

In addition, access to the database (SQLite) has been changed from writing SQL to prisma.

First, check that the installed app runs.

 $ cd プロジェクト名
$ npm run dev
 ? Create this project as a new app on Shopify? 
> **(y) Yes, create it as a new app**
 (n) No , connect it to an existing app
 ? App name :
 > **remix-tutorial (app name)**
 ? Which store would you like to use to view your project ?
 > **※Please make sure to select the test store**
 ? Have Shopify automatically update your app 's URL in order to create a preview experience?

 ┃ Current app URL
 ┃ • <https://shopify.dev/apps/default-app-home>
 ┃
 ┃ Current redirect URLs 
┃ • <https://shopify.dev/apps/default-app-home/api/auth>

 > **(y) Yes, automatically update**
 (n) No, never

When you first launch the app, you will be asked which store to install, but be sure to select the test store. The store you select will not be available as a production environment.

This time, I created a new app called " remix-tutorial " in the Partner Dashboard and set it up to automatically update the app URL to the preview URL.

Shown in the terminal Preview URL Click here.

Preview URL

When you click, the app installation screen will be displayed in the test store admin.

Installing the Shopify app

Once the installation is complete, you will see the app look like this:

Initial screen

Once installed, the app has links to each documentation and a “Generate a Product” button to create a new demo product.

When you click the button, the GraphQL API code will be displayed and a new product will be registered.

Product registration completion screen

This completes the initial construction of the app! 🎉

thank you for your hard work.

Now, let's get straight into the main tutorial!

1. Add the QR Code data model to the database

💡 Attention!

The prisma and @prisma/client modules are installed with version 4. The latest version is version 5, but you must use version 4.

To store the QR codes, you need to add a table to the database included in the template.

Create this time QRCodeテーブル The specifications are as follows:

  • id : the table's primary key.
  • title : The name of the QR code specified by the user of your app.
  • shop : The store that owns the QR code.
  • productId : The product this QR code applies to.
  • productHandle : Used to create the destination URL for the QR code.
  • productVariantId : Used to create the destination URL for the QR code.
  • destination : The destination where the QR code will be sent.
  • scans : The number of times the QR code was read.
  • createdAt : The date and time the QR code was created.

QRCode model contains a key that the app uses to get Shopify product and variant data.

At runtime, additional product and variant properties are retrieved and used to set in the UI.

Normally, you would create tables directly on PostgresQL or MySQL, but this time we will use Prisma , an ORM (Object-relational mapping) .

prisma/schema.prisma Open QRCodeモデル Define

 //...省略
model QRCode { 
id Int @id @default (autoincrement())
 title String
 shop String
 productId String
 productHandle String
 productVariantId String
 destination String
 scans Int @default ( 0 )
 createdAt DateTime @default (now())
 }

With Prisma, you can create tables by defining models without having to write SQL directly.

To create a table, enter the following command:

 $ npm run prisma migrate dev -- --name add-qrcode-table

Now the table is created!

It's very simple, so if you want to check if it was really created, just enter the following command:

 $ npm run prisma studio

This will automatically open your browser. If it doesn't, http://localhost:5555/ Go to:

Prisuma Studio Model

You can see that two models are registered.

Please click on the QRCode model.

Prisuma Studio Table

You can see that the column names of the table are displayed.

In this way, using Prisma you can easily check what models have been registered, check the columns in a table, and save, update, and delete data.

It's very attractive that you can use it easily without having to install the tool!

2. Get the QR code and product data

After you create the database, you add code to retrieve data from the tables.

We retrieve QR code data from our database and supplement it with product information through a GraphQL API.

First, create a model.

However, here we say Model is slightly different from Prisma's schema-defined model.

The M in the Model-View-Controller (MVC) model defines the layer that manages the business logic and data of an application.

Database interaction, data processing, validation, etc. takes place in this layer.

A model for obtaining and verifying QR codes /app/models Create a folder and in it QRCode.server.js Create a file called .

 $ mkdir app/models
 $ touch app/models/QRCode.server.js

One thing to note here is the file name rules.

xxx.server.js The filename tells Remix not to include the code in the file in the browser.

In other words, it means " specify it as a file that only works on the server side ."

This is the file you need to write your database requests and any confidential information you don't want to share, such as your private key.

Be careful not to accidentally write your private key on the front end!!! (No matter how many times I tell people to do this, there's always someone who does it...)

Next, the QR code generation module qrcode and the loader can easily throw errors. tiny-invariant Install.

 $ npm i qrcode tiny-invariant

Created app/models/QRCode.server.js Write the code to get the QR code in the file.

 import db from "../db.server" ; 
import qrcode from "qrcode" ;
 import invariant from "tiny-invariant" ;

 // Get the QRCode data from the database and return the QRCode data, product information, and link URL.
 export async function getQRCode ( id, graphql ) {
 const qrCode = await db.qRCode.findFirst({ where : { id } });

 if (!qrCode) {
 return null ;
 }
 
return supplementQRCode(qrCode, graphql);
 }
 // Returns multiple getQRCodes
 export async function getQRCodes ( shop, graphql ) {
 const qrCodes = await db.qRCode.findMany({
 where : { shop },
 orderBy : { id : "desc" },
 });

 if (qrCodes.length === 0 ) return [];

 return Promise .all( 
qrCodes.map( ( qrCode ) => supplementQRCode(qrCode, graphql))
 );
 }
 // Generate QR code image data from the URL
 export function getQRCodeImage ( id ) {
 const url = new URL( `/qrcodes/ ${id} /scan` , process.env.SHOPIFY_APP_URL);
 return qrcode.toDataURL(url.href);
 }
 // Generate a product detail page URL or checkout URL 
export function getDestinationUrl ( qrCode ) {
 if (qrCode.destination === "product" ) {
 return `https:// ${qrCode.shop} /products/ ${qrCode.productHandle} ` ;
 }

 const match = /gid:\/\/shopify\/ProductVariant\/([0-9]+)/ .exec(qrCode.productVariantId);
 invariant(match, "Unrecognized product variant ID" );

 return `https:// ${qrCode.shop} /cart/ ${match[ 1 ]} :1` ;
 } 
// Get product information from the QRCode data via the Graphql API and return it including the link URL.
 async function supplementQRCode ( qrCode, graphql ) {
 const response = await graphql(
 `
 query supplementQRCode($id: ID!) {
 product(id: $id) {
 title
 images(first: 1) {
 nodes {
 altText
 URL
 }
 }
 }
 }
 ` ,
 {
 variables : {
 id : qrCode.productId,
 },
 }
 );

 const {
 data : { product },
 } = await response.json();
 
return {
 ...qrCode,
 productDeleted : !product?.title,
 productTitle : product?.title,
 productImage : product?.images?.nodes[ 0 ]?.url,
 productAlt : product?.images?.nodes[ 0 ]?.altText,
 destinationUrl : getDestinationUrl(qrCode),
 image : await getQRCodeImage(qrCode.id),
 };
 }
 // Implement QRCode validation
 export function validateQRCode ( data ) {
 const errors = {};

 if (!data.title) { 
errors.title = "Title is required" ;
 }

 if (!data.productId) {
 errors.productId = "Product is required" ;
 }

 if (!data.destination) {
 errors.destination = "Destination is required" ;
 }

 if ( Object .keys(errors).length) {
 return errors;
 }
 }

Let's take a look at the role of each function defined here.

  • getQRCode, getQRCodes : Functions to get a single QR code and functions to get multiple QR codes. The specific return value is supplementQRCode The return value will be:
  • getQRCodeImage : Returns a Base 64 encoded QR code image using the qrcode package.
  • getDestinationUrl When a customer scans a QR code, they will be directed to one of two places: a product detail page or checkout with the item in their cart. Create a function to conditionally build this URL depending on the destination the merchant chooses.
  • supplementQRCode : QRCodeテーブル does not contain product information. Therefore, we use the Shopify Admin GraphQL API to get product information using a function that queries the product title, the URL of the first featured product image, and the alternative text. We also use the getDestinationUrl and getQRCodeImage functions we created to get the QR code image of the destination URL, and return an object containing the QR code information and product information.
  • validateQRCode : Validation functions for title, product ID, and destination.

This completes the creation of the QRCode model!

3. Create a QR code form

Create a form where your app users can manage their QR codes.

To create this form Use Remix Routes, Polaris Components, and App Bridge.

First, the QR code form page app.qrcodes.$id.jsx Create a new one.

 $ touch app/routes/app.qrcodes.\ $id .jsx

The backslash before the $ sign is there to prevent the $id from disappearing from the file name .

By the way, there is a special reason why file names have "." (dot) and "$" in them.

The Remix framework uses file-based routing .

File-based routing means that the URL is /app/users in the case of, app.users.jsx It's how files are loaded and file names and routes are matched.

In other words, the "." (dot) serves to separate levels.

When rendering in the Remix framework, it does not just separate hierarchies with a "." (dot), but also loads the parent layout root.

In the previous example, If the URL is app/users , app.jsx Files too app.users.jsx It also loads files.

This means: app.jsx Now, implement the user authentication function. app.users.jsx Files can have different roles, such as implementing a profile page.

Whereas, "$" means a dynamic segment .

In this tutorial, the id parameter is new In this case, create a new QR code page. 1 For QR code IDs such as the above, use it as an editing page.

This means that Remix will match any value in the URL for that segment and serve it to your app.

First, app.qrcodes.$id.jsx The server side implementation will be done in the file.

 import { json } from "@remix-run/node" ;
 import { getQRCode } from "~/models/QRCode.server" ;
 import { authenticate } from "~/shopify.server" ;

 export async function loader ( { request, params } ) { 
const { admin } = await authenticate.admin(request);
 if (params.id === "new" ) {
 return json({
 destination : "product" ,
 title : "" ,
 });
 }
 return json( await getQRCode( Number (params.id), admin.graphql));
 }

The loader function is special to Remix.

When an HTTP request comes in, on the server side loader The function is executed.

In other words, the server side description is loader This means that it is done inside a function.

A very similar function, There is also an action function.

The action function is executed on the server side when the HTTP method is other than the GET method, and loader Runs before any functions, such as form submissions, that are used outside of page load.

loader Within the function, user authentication is performed, and the JSON returned to the browser is conditionally branched depending on the value of the id parameter.

To return JSON, json functions are used. json The function has the following capabilities:

 json (JSONデータ)
 ↓同じ意味
new Response (文字列化したJSONデータ, {
 headers : {
 "Content-Type" : "application/json; charset=utf-8" ,
 "status" : 200
 }
 });

Next, we will build the front end (View).

First, let's build the code to manage the form state.

 // loader関数の上に追加 
import { useActionData, useLoaderData, useNavigate, useNavigation, useSubmit } from "@remix-run/react" ;
 import { useState } from "react" ;

 // ...omission

 // Building the front end (state management only)
 export default function QRCodeForm ( ) {
 const errors = useActionData()?.errors || {};

 const qrCode = useLoaderData();
 const [formState, setFormState] = useState(qrCode); 
const [cleanFormState, setCleanFormState] = useState(qrCode);
 const isDirty = JSON .stringify(formState) !== JSON .stringify(cleanFormState);

 const nav = useNavigation();
 const isSaving =
 nav.state === "submitting" && nav.formData?.get( "action" ) !== "delete" ;
 const isDeleting =
 nav.state === "submitting" && nav.formData?.get( "action" ) === "delete" ;

 const navigate = useNavigate();
 
async function selectProduct ( ) {
 const products = await window .shopify.resourcePicker({
 type : "product" ,
 action : "select" , // customized action verb, either 'select' or 'add',
 });

 if (products) {
 const { images, id, variants, title, handle } = products[ 0 ];

 setFormState({
 ...formState,
 productId : id,
 productVariantId : variants[ 0 ].id, 
productTitle : title,
 productHandle : handle,
 productAlt : images[ 0 ]?.altText,
 productImage : images[ 0 ]?.originalSrc,
 });
 }
 }

 const submit = useSubmit();
 function handleSave ( ) {
 const data = {
 title : formState.title,
 productId : formState.productId || "" ,
 productVariantId : formState.productVariantId || "" , 
productHandle : formState.productHandle || "" ,
 destination : formState.destination,
 };

 setCleanFormState({ ...formState });
 submit(data, { method : "post" });
 }
 }

Here is a quick explanation of Remix's special functions.

Let's explain what this form state management code means.

  • errors If the user does not fill in all of the QR code form fields, validateQRCode Get the return value of the function.
  • formState : The state of the input form.
  • cleanFormState : The initial state of the form. useLoaderData Assign the return value of .
  • isDirty : Determines if the form has been modified.
  • isSaving , isDeleting : useNavigation Use functions to manage the state of the page.

selectProduct Take a look at the following code in the function:

 await window .shopify.resourcePicker

This uses AppBridge's resourcePicker function.

resourcePicker The function provides a search-based interface that enables users to search for and select one or more products, collections, or product variants, and returns the selected resources to the app.

ResourcePicker

This function window.shopify can be obtained from

This makes it easy to retrieve registered product information.

AppBrdige was previously managed by a component. In the Remix framework, component management has been abolished and it is defined as a function of window.shopify. window.shopify is imported at the initial template time.

Now we finally create the form layout.

We will be designing using Polaris components.

Polaris is a design system for the Shopify Admin. Using Polaris components ensures that your UI is accessible, responsive, and appears consistent with the Shopify Admin.

The form layout will look like this:

 //...省略

//loader関数の上に追加
import {
 Card,
 Bleed,
 Button,
 ChoiceList,
 Divider,
 EmptyState,
 HorizontalStack,
 InlineError,
 Layout,
 Page,
 Text,
 TextField,
 Thumbnail,
 VerticalStack,
 PageActions,
 } from "@shopify/polaris";

 //...省略

export default function QRCodeForm() {

 //...省略

 return (
 < Page > 
< ui-title-bar title = {qrCode.id ? " Edit QR code " : " Create new QR code "}>
 < button variant = "breadcrumb" onClick = {() => navigate("/app")}>
 QR codes
 </ button >
 </ ui-title-bar >
 < Layout > 
< Layout.Section >
 < VerticalStack gap = "5" >
 < Card >
 < VerticalStack gap = "5" >
 < Text as = { " h2 " } variant = " headingLg " >
 Title
 </ Text >
 < TextField 
id = "title"
 helpText = "Only store staff can see this title"
 label = "title"
 labelHidden
 autoComplete = "off"
 value = {formState.title}
 onChange = {(title) => setFormState({ ...formState, title })}
 error={errors.title}
 />
 </ VerticalStack >
 </ Card > 
< Card >
 < VerticalStack gap = "5" >
 < HorizontalStack align = "space-between" >
 < Text as = { " h2 " } variant = " headingLg " >
 Product
 </ Text >
 {formState.productId ? ( 
< Button plain onClick = {selectProduct} >
 Change product
 </Button>
 ) : null}
 </HorizontalStack>
 {formState.productId ? (
 < HorizontalStack blockAlign = "center" gap = { " 5 "}>
 < Thumbnail 
source = {formState.productImage || ImageMajor }
 alt = {formState.productAlt}
 />
 < Text as = "span" variant = "headingMd" fontWeight = "semibold" >
 {formState.productTitle}
 </ Text >
 </HorizontalStack>
 ) : ( 
< VerticalStack gap = "2" >
 < Button onClick = {selectProduct} id = "select-product" >
 Select product
 </Button>
 {errors.productId ? (
 < InlineError
 message = {errors.productId}
 fieldID = "myFieldID"
 />
 ) : null} 
</ VerticalStack >
 )}
 < Bleed marginInline = "20" >
 < Divider />
 </ Bleed >
 < HorizontalStack
 gap = "5"
 align = "space-between"
 blockAlign = "start"
 >
 < ChoiceList 
title = "Scan destination"
 choices = {[
 { label: " Link to product page ", value: " product " },
 {
 label: " Link to checkout page with product in the cart ",
 value: " cart ", 
},
 ]}
 selected = {[formState.destination]}
 onChange = {(destination) =>
 setFormState({
 ...formState,
 destination: destination[0],
 })
 }
 error={errors.destination}
 />
 {qrCode.destinationUrl ? (
 < Button plain url = {qrCode.destinationUrl} external >
 Go to destination URL
 </Button>
 ) : null} 
</HorizontalStack>
 </ VerticalStack >
 </ Card >
 </ VerticalStack >
 </ Layout.Section >
 < Layout.Section secondary >
 < Card >
 < Text as = { " h2 " } variant = " headingLg " >
 QR code 
</ Text >
 {qrCode ? (
 < EmptyState image = {qrCode.image} imageContained = {true} />
 ) : (
 < EmptyState image = " " >
 Your QR code will appear here after you save
 </ EmptyState >
 )}
 < VerticalStack gap = "3" >
 < Button 
disabled = {!qrCode?.image}
 url = {qrCode?.image}
 download
 primary
 >
 Download
 </Button>
 < Button
 disabled = {!qrCode.id}
 url = { `/ qrcodes /${ qrCode.id }`}
 external
 >
 Go to public URL
 </Button> 
</ VerticalStack >
 </ Card >
 </ Layout.Section >
 < Layout.Section >
 < PageActions
 secondaryActions = {[
 {
 content: " Delete ",
 loading: isDeleting ,
 disabled: ! qrCode.id || ! qrCode || isSaving || isDeleting , 
destructive: true ,
 outline: true ,
 onAction: () =>
 submit({ action: "delete" }, { method: "post" }),
 },
 ]}
 primaryAction= {{
 content: "Save",
 loading: isSaving,
 disabled: !isDirty || isSaving || isDeleting,
 onAction: handleSave,
 }}
 />
 </ Layout.Section >
 </ Layout >
 </ Page >
 );
 }

Let's look at what components it consists of from the top.

  1. Breadcrumbs
  2. Title Field
  3. ResourcePicker
  4. Destination options
  5. QR Code Preview
  6. Save and Delete buttons

Polaris makes it easy to build your designs.

We can also see that AppBridge is being used in conjunction with Polaris.

  • <ui-title-bar> : Use AppBridge to display a title bar that shows whether the user is creating or editing a QR code, and a breadcrumb trail to return to the QR code list.
  • <Button onClick={selectProduct} id="select-product"> : Button In the component, selectProduct Define a function and call the AppBridge resourcePicker . The retrieved product information is formState will be held in

The form layout is now complete.

Click the Save button, and hadleSave By executing submit The function is executed.

A POST method HTTP request is sent.

As I mentioned a while ago, server-side processing other than the GET method action It can be defined as a function!

action The function handles authentication, validation, saving to the database, and redirection.

Here's what happens:

 //...省略

// loader関数より前に追記
import { json, redirect } from "@remix-run/node" ; // redirectを追記
import { getQRCode, validateQRCode } from "~/models/QRCode.server" ; // validateQRCodeを追記
import db from "../db.server" ;

 // ...省略
 
export async function action ( { request, params } ) {
 const { session } = await authenticate.admin(request);
 const { shop } = session;

 /** @type {any} */
 const data = {
 ...Object.fromEntries( await request.formData()),
 shop,
 };

 if (data.action === "delete" ) {
 await db.qRCode.delete({ where : { id : Number (params.id) } }); 
return redirect( "/app" );
 }

 const errors = validateQRCode(data);

 if (errors) {
 return json({ errors }, { status : 422 });
 }

 const qrCode =
 params.id === "new"
 ? await db.qRCode.create({ data })
 : await db.qRCode.update({ where : { id : Number (params.id) }, data });

 return redirect( `/app/qrcodes/ ${qrCode.id} ` );
 }

 //...omission

The action function creates, updates, or deletes a QR code.

To save to DB, db.server.js File db Loading instance.

To use Prisma, db.server.js In the file I am instantiating PrismaClient and exporting it.

Your QR code form is now complete!

Start the server and check that it works.

app/qrcodes/new

app/qrcodes/new

app/qrcodes/1

app/qrcodes/1

I was able to confirm that the QR code generation and saving worked properly.

Next, we will display a list of QR codes.

4. Displaying a list of QR codes

To allow app users to navigate to a QR code, list the QR code on your app's home page.

To read the QR code, go to the app's index root app._index.jsx So, we use the loader function.

 //最初のコード
export const loader = async ({ request }) => {
 const { session } = await authenticate.admin(request);

 return json({ shop : session.shop.replace( ".myshopify.com" , "" ) });
 };

 //変更後のコード
import { getQRCodes } from "~/models/QRCode.server" ;
 
export async function loader ( { request } ) {
 const { admin, session } = await authenticate.admin(request);
 const qrCodes = await getQRCodes(session.shop, admin.graphql);

 return json({
 qrCodes,
 });
 }

In advance app/models/QRCode.server.js Defined in getQRCodes Use a function to return a list of all QR codes in JSON.

At this time, the display content will change depending on whether or not there is any QR code data.

If there is no QR code data, the display will be as follows:

 const EmptyQRCodeState = ( { onAction } ) => (
 < EmptyState
 heading = "Create unique QR codes for your product"
 action = {{
 content: " Create QR code ",
 onAction ,
 }}
 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 >
 ); 
Design when data is empty

If QR code data exists, the code is as follows:

 const QRTable = ({ qrCodes }) => (
 < IndexTable
 resourceName = {{
 singular: " QR code ",
 plural: " QR codes ",
 }}
 itemCount = {qrCodes.length} 
headings = {[
 { title: " Thumbnail ", hidden: true },
 { title: " Title " },
 { title: " Product " },
 { title: " Date created " },
 { title: " Scans " },
 ]}
 selectable = {false}
 >
 {qrCodes.map((qrCode) => (
 < QRTableRow key = {qrCode.id} qrCode = {qrCode} /> 
))}
 </ IndexTable >
 );

 const QRTableRow = ({ qrCode }) => (
 < IndexTable.Row id = {qrCode.id} position = {qrCode.id} >
 < IndexTable.Cell >
 < Thumbnail
 source = {qrCode.productImage || ImageMajor }
 alt = {qrCode.productTitle}
 size = "small"
 />
 </ IndexTable.Cell > 
< IndexTable.Cell >
 < Link to = { ` qrcodes /${ qrCode.id }`}> {truncate(qrCode.title)} </ Link >
 </ IndexTable.Cell >
 < IndexTable.Cell >
 {qrCode.productDeleted ? (
 < HorizontalStack align = "start" gap = "2" > 
< span style = {{ width: " 20px " }} >
 < Icon source = {DiamondAlertMajor} color = "critical" />
 </ span >
 < Text color = "critical" as = "span" >
 product has been deleted
 </ Text > 
</HorizontalStack>
 ) : (
 truncate(qrCode.productTitle)
 )}
 </ IndexTable.Cell >
 < IndexTable.Cell >
 {new Date(qrCode.createdAt).toDateString()}
 </ IndexTable.Cell >
 < IndexTable.Cell > {qrCode.scans} </ IndexTable.Cell >
 </ IndexTable.Row >
 ); 
QR Code List

Your QR code list page is now ready.

If you have QR code data QRTable Load the component, if not EmptyQRCodeState Load the.

Here is the code for the layout:

 
export default function Index() {
 const { qrCodes } = useLoaderData();
 const navigate = useNavigate();
 return (
 < Page >
 < ui-title-bar title = "QR codes" >
 < button variant = "primary" onClick = {() => navigate("/app/qrcodes/new")}>
 Create QR code
 </ button > 
</ ui-title-bar >
 < Layout >
 < Layout.Section >
 < Card padding = "0" >
 {qrCodes.length === 0 ? (
 < EmptyQRCodeState onAction = {() => navigate("qrcodes/new")} />
 ) : (
 < QRTable qrCodes = {qrCodes} />
 )}
 </ Card > 
</ Layout.Section >
 </ Layout >
 </ Page >
 );
 } 

Now, the QR code list display is complete!

5. Add a public QR code route

Up until now, all the pages we've developed have handled requests from the store admin page.

Here, you will expose the QR code using a public URL so that your customers can scan the QR code.

When a customer scans the QR code, the scan count is incremented and the customer is redirected to the destination URL.

Create a public page.

 $ touch app/routes/qrcodes.\ $id .jsx

Renders the title and image data of the QR code.

 import { json } from "@remix-run/node" ;
 import invariant from "tiny-invariant" ;
 import { useLoaderData } from "@remix-run/react" ;

 import db from "../db.server" ;
 import { getQRCodeImage } from "~/models/QRCode.server" ;

 export const loader = async ({ params }) => { 
invariant(params.id, "Could not find QR code destination" );

 const id = Number (params.id);
 const qrCode = await db.qRCode.findFirst({ where : { id } });

 invariant(qrCode, "Could not find QR code destination" );

 return json({
 title : qrCode.title,
 image : await getQRCodeImage(id),
 });
 };

 export default function QRCode ( ) {
 const { image, title } = useLoaderData();

 return ( 
<>
 <h1>{title}</h1>
 <img src={image} alt={`QR Code for product`} />
 </>
 );
 }

Where: invariant The function is a validation function loaded from the tiny-invariant module. It references the value of the first argument and throws an error if it does not exist.

If a matching ID is found, the Remix function will be used to return the QR code as json.

When the user scans the QR code, they will be redirected to the destination URL.

Next, implement the route to the destination URL.

Redirect the customer to a destination URL

Once the QR code is scanned, it redirects the customer to a destination URL. You can also increment the QR code scan count to reflect the number of times the QR code has been used.

First, to create a scanning route, we create a public route that handles scanning the QR code.

 $ touch app/routes/qrcodes.\ $id .scan.jsx

loader Use a function to count QR code scans on the server side and redirect to the destination URL if successful.

 import { redirect } from "@remix-run/node" ;
 import invariant from "tiny-invariant" ; 
import db from "../db.server" ;

 import { getDestinationUrl } from "../models/QRCode.server" ;

 export const loader = async ({ params }) => {
 invariant(params.id, "Could not find QR code destination" );

 const id = Number (params.id);
 const qrCode = await db.qRCode.findFirst({ where: { id } });

 invariant(qrCode, "Could not find QR code destination" );

 await db.qRCode.update({
 where: { id },
 data: { scans: { increment: 1 } },
 });
 
return redirect(getDestinationUrl(qrCode));
 };

If you can't find the QR code invariant The function throws an error, and if found, uses Prisma to find the QRCode table scans Increments a column.

That's it, your QRCode generation app is complete!

Let's check if it works without any problems!

Operation check

The operation check is as follows:

  • Generate a QR code
  • Download the QR code
  • Open the public URL of the QR code
  • Edit your QR code
  • Delete a QR code
  • Scan the QR code
  • Verify that the page redirects to the checkout screen
  • Make sure the scan count is set to 1

Deploy to Fly.io

Finally, this app Fly.io Deploy to.

If you do not have an account with fly.io, please register in advance.

Sign Up · Fly

Once you have logged into the Fly.io dashboard, install the flyctl command .

 $ brew install flyctl

flyctl Deploy the app using the command:

 $ flyctl launch
 ? Choose an app name (leave blank to generate one): アプリ名
 ? Select Organization : Fly.ioのアカウント名 (personal) 
? Choose a region for deployment: Tokyo, Japan (nrt)
 $ flyctl deploy

If the deployment is successful, a domain (https://app name.fly.dev) will be issued.

To register environment variables: flyctl This can be done by command or on the Fly.io dashboard.

 $ fly secrets set SHOPIFY_APP_URL=https://アプリ名.fly.dev SCOPES=write_products NODE_ENV=production SHOPIFY_API_KEY=xxxxxxxxx SHOPIFY_API_SECRET=xxxxxxxx

Go to your Shopify Partner Dashboard, go to "App Settings" for the target app, and switch the "App URL" and "Allowed Redirect URLs" domains to the Fly.io domain.

Setting the App URL

Now the deployment and configuration is complete!

Fly.io can use SQLite, however in production environments you should avoid using SQLite and use PostgreSQL, MySQL, DynamoDB, etc.

The current setting is still SQLite, so please try changing it to PostgreSQL as an assignment.

This completes the Shopify app development tutorial with Remix framework!

thank you for your hard work!

lastly

To develop a Shopify app, you will need at least the following understanding:

It really requires a lot of different learning.

This tutorial is just a general overview.

Here are some examples of common problems:

If you try to get customer information via the REST API, the SCOPE read_customers Even if you add it, you will not be able to retrieve it due to a permission error.

This is because, as stated in the Partner Terms and Conditions, prior application and approval is required to obtain customer information.

Even if you are able to retrieve customer information, you will then notice a problem: you can only retrieve a maximum of 50 items.

This is because the REST API has a rate limit of only 50 results by default, so pagination will be required.

To develop a Shopify app like this, you really need a wide range of knowledge.

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.