勇者(さかもっちゃん) 弊社若手エンジニアがShopifyでRPG風サイトを制作中!遊んで行ってね!
MORE

BLOG

Shopifyアプリ開発チュートリアルを実践しよう!①

  • Shopify
  • アプリ開発
  • カスタムアプリ
  • コピペ
  • チュートリアル
  • 実践
  • 開発
25.07.18

はじめに

1.1 記事について

テックディレクションではプロジェクトマネジメント業務に加えて、SaaS型ECプラットフォームShopifyを中心にECストアの構築および標準機能では対応できない要件を満たすアプリケーション開発も行っております。

「Shopifyアプリ開発チュートリアルを実践しよう!」シリーズではShopify公式アプリ開発チュートリアルに沿ってアプリ構築手順を解説します。

本記事は「Shopifyアプリのチュートリアルを実践しよう!」シリーズの第一弾です。

1.2 記事の対象

・Shopify公式チュートリアルを実践したい方

・TypeScriptで実装したい方

1.3 記事

この記事を読み終えるまでに、約20分です。実践しながら進める場合は1時間です。

2. 実践内容

商品ページURLのQRコード画像を作成するアプリを開発します。チュートリアルではJavaScriptで実装されていますが、今回はTypescriptで実装します!

画面一覧

・QRコード一覧画面

・QRコード作成画面

・QRコード編集画面

機能

・商品ページURLのQRコードを作成する

・作成したQRコードを一覧で表示する

・作成したQRコードを編集する

・作成したQRコードを削除する

・作成したQRコード画像をダウンロードする

・作成したQRコードの公開URLを発行する

・QRコードが読み取られた回数を表示する

Shopify公式アプリ開発チュートリアル

3. 環境構築

3.1 Shopify CLIを使えるようにする

こちらの記事が参考になります!

3.2 ベースソースの準備をする

$ shopify app init

上記コマンドでアプリソースを作成する準備を始めます。「dev-watanabe-test3」ディレクトリが作成されます。

?  Get started building your app:

>  Build a Remix app (recommended)
   Build an extension-only app

今回はバックエンドも調整ので上を選択します。

?  For your Remix template, which language do you want?

   JavaScript
>  TypeScript

今回はTypescriptで作成するためTypescriptを選択します。公式チュートリアルはJavaScriptなのでそちらのソースを使う場合はJavaScriptを選択します。

?  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:
✔  dev-watanabe-test3

アプリ名(そのままディレクトリ名になる)を入力します。

╭─ info ────────────────────────────────────────────────────────────────────────────╮
│                                                                                   │
│  Initializing project with `npm`                                                  │
│  Use the `--package-manager` flag to select a different package manager.          │
│                                                                                   │
╰───────────────────────────────────────────────────────────────────────────────────╯

▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
Installing dependencies with npm ...

作成中…

╭─ success ─────────────────────────────────────────────────────────────────────────╮
│                                                                                   │
│  dev-watanabe-test3 is ready for you to build!                                    │
│                                                                                   │
│  Next steps                                                                       │
│    • Run `cd dev-watanabe-test3`                                                  │
│    • For extensions, run `shopify app generate extension`                         │
│    • To see your app, run `shopify app dev`                                       │
│                                                                                   │
│  Reference                                                                        │
│    • Shopify docs [1]                                                             │
│    • For an overview of commands, run `shopify app --help`                        │
│                                                                                   │
╰───────────────────────────────────────────────────────────────────────────────────╯
[1] <https://shopify.dev>

ソース作成が成功すると上記のように表示されます。

cd dev-watanabe-test3

ディレクトリを移動します。

3.3 利用モジュールをインストールする

$ npm install qrcode

Base64エンコードされたQRコード画像を生成できます。

qrcode

$ npm install tiny-invariant

例外条件を完結に記載できます。

tiny-invariant

4. アプリソース編集

schema.prisma

QRコードのデータを入れるテーブルを追加します。

// This is your Prisma schema file,
// learn more about it in the docs: <https://pris.ly/d/prisma-schema>

generator client {
  provider = "prisma-client-js"
}

// Note that some adapters may set a maximum length for the String type by default, please ensure your strings are long
// enough when changing adapters.
// See <https://www.prisma.io/docs/orm/reference/prisma-schema-reference#string> for more information
datasource db {
  provider = "sqlite"
  url      = "file:dev.sqlite"
}

model Session {
  id            String    @id
  shop          String
  state         String
  isOnline      Boolean   @default(false)
  scope         String?
  expires       DateTime?
  accessToken   String
  userId        BigInt?
  firstName     String?
  lastName      String?
  email         String?
  accountOwner  Boolean   @default(false)
  locale        String?
  collaborator  Boolean?  @default(false)
  emailVerified Boolean?  @default(false)
}

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())
}

上記で上書きします。

💡チュートリアルのソースを使うとアプリ起動時にエラーが発生するので上記ソースを使ってください!!!

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

上記コマンドを実行してPrismaにテーブルを作成します。

$ npm run prisma studio

上記コマンドでGUIでローカルテーブルを確認できます。

※この時点でまだデータは入っていないです。

QRCode.server.ts

データベースからQRコード情報を取得する処理を追加します。

$ cd app
$ mkdir models

modelsディレクトリを作成します。

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

interface QRCode {
  id: number;
  title: string;
  shop: string;
  productId: string;
  productHandle?: string;
  productVariantId?: string;
  destination: string;
  scans: number;
  createdAt: Date;
}

interface SupplementedQRCode extends QRCode {
  productDeleted: boolean;
  productTitle?: string;
  productImage?: string;
  productAlt?: string;
  destinationUrl: string;
  image: string;
}

export async function getQRCode(id: number, graphql: (query: string, variables: Record<string, any>) => Promise<Response>): Promise<SupplementedQRCode | null> {
  const qrCode = await db.qRCode.findUnique({
    where: { id },
  });

  if (!qrCode) {
    return null;
  }

  return supplementQRCode(qrCode, graphql);
}

export async function getQRCodes(shop: string, graphql: (query: string, variables: Record<string, any>) => Promise<Response>) {
  const qrCodes = await db.qRCode.findMany({
    where: { shop },
    orderBy: { id: "desc" },
  });

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

  return Promise.all(
    qrCodes.map((qrCode: QRCode) => supplementQRCode(qrCode, graphql))
  );
}

export function getQRCodeImage(id: number) {
  const url = new URL(`/qrcodes/${id}/scan`, process.env.SHOPIFY_APP_URL);
  return qrcode.toDataURL(url.href);
}

export function getDestinationUrl(qrCode: QRCode) {
  if (qrCode.destination === "product" || !qrCode.productVariantId) {
    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`;
}

async function supplementQRCode(qrCode: QRCode, graphql: (query: string, variables: Record<string, any>) => Promise<Response>) {
  const qrCodeImagePromise = getQRCodeImage(qrCode.id);

  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 qrCodeImagePromise,
  };
}

export function validateQRCode(data: any) {
  const errors: Record<string, string> = {};

  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;
  }
}

modelsディレクトリの中にタイトルのファイルを作成します。

app.qrcodes.$id.tsx

QRコードを管理するフォームを作成します。

$ cd ../routes
$ touch [app.qrcodes.$id.tsx](<https://shopify.dev/docs/apps/build/build?framework=remix#create-a-qr-code-form>)

routesディレクトリにタイトルのファイルを作成します。

import { useState } from "react";
import { json, redirect, LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
import {
  useActionData,
  useLoaderData,
  useNavigation,
  useSubmit,
  useNavigate,
} from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
  Card,
  Bleed,
  Button,
  ChoiceList,
  Divider,
  EmptyState,
  InlineStack,
  InlineError,
  Layout,
  Page,
  Text,
  TextField,
  Thumbnail,
  BlockStack,
  PageActions,
} from "@shopify/polaris";
import { ImageIcon } from "@shopify/polaris-icons";

import db from "../db.server";
import { getQRCode, validateQRCode } from "../models/QRCode.server";

interface QRCode {
  id: number;
  title: string;
  shop: string;
  productId: string;
  productHandle?: string;
  productVariantId?: string;
  destination: string;
  scans: number;
  createdAt: Date;
}

interface SupplementedQRCode extends QRCode {
  productDeleted: boolean;
  productTitle?: string;
  productImage?: string;
  productAlt?: string;
  destinationUrl: string;
  image: string;
}

interface Errors {
  [key: string]: string;
}

interface ActionData {
  errors?: Errors; // errors はオプション(存在する場合のみ)
}

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
  const { admin } = await authenticate.admin(request);

  if (params.id === "new") {
    return json({
      destination: "product",
      title: "",
    });
  }

  return json(await getQRCode(Number(params.id), admin.graphql));
}

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const { session } = await authenticate.admin(request);
  const { shop } = session;

  const data: any = {
    ...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: 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}`);
}

export default function QRCodeForm() {
  const errors = useActionData<ActionData>()?.errors || {};

  const qrCode = useLoaderData<SupplementedQRCode>();
  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: any = await window.shopify.resourcePicker({
      type: "product",
      action: "select", // customized action verb, either 'select' or 'add',
    });

    if (products && products.length > 0) {
      const selected = products[0];

      const product = {
        id: selected.id,
        title: selected.title,
        handle: selected.handle,
        variants: selected.variants.map((variant: any) => ({
          id: variant.id ?? "", // undefined の場合、空文字を代入
        })),
        images: selected.images.map((image: any) => ({
          altText: image.altText ?? "",
          originalSrc: image.originalSrc,
        })),
      };

      setFormState({
        ...formState,
        productId: product.id,
        productVariantId: product.variants[0].id,
        productTitle: product.title,
        productHandle: product.handle,
        productAlt: product.images[0]?.altText,
        productImage: product.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" });
  }

  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>
          <BlockStack gap="500">
            <Card>
              <BlockStack gap="500">
                <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}
                />
              </BlockStack>
            </Card>
            <Card>
              <BlockStack gap="500">
                <InlineStack align="space-between">
                  <Text as={"h2"} variant="headingLg">
                    Product
                  </Text>
                  {formState.productId ? (
                    <Button variant="plain" onClick={selectProduct}>
                      Change product
                    </Button>
                  ) : null}
                </InlineStack>
                {formState.productId ? (
                  <InlineStack blockAlign="center" gap="500">
                    <Thumbnail
                      source={formState.productImage || ImageIcon}
                      alt={formState.productAlt || ""}
                    />
                    <Text as="span" variant="headingMd" fontWeight="semibold">
                      {formState.productTitle}
                    </Text>
                  </InlineStack>
                ) : (
                  <BlockStack gap="200">
                    <Button onClick={selectProduct} id="select-product">
                      Select product
                    </Button>
                    {errors.productId ? (
                      <InlineError
                        message={errors.productId}
                        fieldID="myFieldID"
                      />
                    ) : null}
                  </BlockStack>
                )}
                <Bleed marginInlineStart="200" marginInlineEnd="200">
                  <Divider />
                </Bleed>
                <InlineStack gap="500" 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
                      variant="plain"
                      url={qrCode.destinationUrl}
                      target="_blank"
                    >
                      Go to destination URL
                    </Button>
                  ) : null}
                </InlineStack>
              </BlockStack>
            </Card>
          </BlockStack>
        </Layout.Section>
        <Layout.Section variant="oneThird">
          <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>
            )}
            <BlockStack gap="300">
              <Button
                disabled={!qrCode?.image}
                url={qrCode?.image}
                download
                variant="primary"
              >
                Download
              </Button>
              <Button
                disabled={!qrCode.id}
                url={`/qrcodes/${qrCode.id}`}
                target="_blank"
              >
                Go to public URL
              </Button>
            </BlockStack>
          </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>
  );
}

💡Remixはファイルベースのルーティングをしている。

.を/で置き換えたURLとなる$の後ろが変数となり、パラメータとして渡す頃ができる

app._index.tsx

アプリを開いた時に最初に表示します。

今回は作成したQRコード一覧を表示します。

import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, Link, useNavigate } from "@remix-run/react";
import { authenticate } from "../shopify.server";
import {
  Card,
  EmptyState,
  Layout,
  Page,
  IndexTable,
  Thumbnail,
  Text,
  Icon,
  InlineStack,
} from "@shopify/polaris";

import { getQRCodes } from "../models/QRCode.server";
import { AlertDiamondIcon, ImageIcon } from "@shopify/polaris-icons";

interface EmptyQRCodeStateProps {
  onAction: () => void; // onAction は引数なしで void を返す関数
}

interface QRCode {
  productDeleted: boolean;
  productTitle: any;
  productImage: any;
  productAlt: any;
  destinationUrl: string;
  image: string;
  id: number;
  title: string;
  shop: string;
  productId: string;
  productHandle?: string;
  productVariantId?: string;
  destination: string;
  scans: number;
  createdAt: Date;
}

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin, session } = await authenticate.admin(request);
  const qrCodes = await getQRCodes(session.shop, admin.graphql);

  return json({
    qrCodes,
  });
}

const EmptyQRCodeState: React.FC<EmptyQRCodeStateProps> = ({ 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>
);

const truncate = (str: string, { length = 25 } = {}) => {
  if (!str) return "";
  if (str.length <= length) return str;
  return str.slice(0, length) + "…";
}

const QRTable: React.FC<any> = ({ 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: QRCode) => (
      <QRTableRow key={qrCode.id} qrCode={qrCode} />
    ))}
  </IndexTable>
);

const QRTableRow: React.FC<any> = ({ qrCode }) => (
  <IndexTable.Row id={qrCode.id} position={qrCode.id}>
    <IndexTable.Cell>
      <Thumbnail
        source={qrCode.productImage || ImageIcon}
        alt={qrCode.productTitle}
        size="small"
      />
    </IndexTable.Cell>
    <IndexTable.Cell>
      <Link to={`qrcodes/${qrCode.id}`}>{truncate(qrCode.title)}</Link>
    </IndexTable.Cell>
    <IndexTable.Cell>
      {qrCode.productDeleted ? (
        <InlineStack align="start" gap="200">
          <span style={{ width: "20px" }}>
            <Icon source={AlertDiamondIcon} tone="critical" />
          </span>
          <Text tone="critical" as="span">
            product has been deleted
          </Text>
        </InlineStack>
      ) : (
        truncate(qrCode.productTitle)
      )}
    </IndexTable.Cell>
    <IndexTable.Cell>
      {new Date(qrCode.createdAt).toDateString()}
    </IndexTable.Cell>
    <IndexTable.Cell>{qrCode.scans}</IndexTable.Cell>
  </IndexTable.Row>
);

export default function Index() {
  const { qrCodes } = useLoaderData<typeof loader>();
  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>
  );
}

qrcodes.$id.tsx

URLパラメータで指定された QRコードを DB から探して、画像付きでページに表示します。このURLはアプリを立ち上げている間は公開できます。

$ cd ../routes
$ touch app.qrcodes.$id.tsx
import { json, LoaderFunctionArgs } 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 }: LoaderFunctionArgs) => {
  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<typeof loader>();

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

qrcodes.$id.scan.tsx

QRコードが読み込まれた回数をカウントします。

$ touch qrcodes.$id.scan.tsx
import { redirect, LoaderFunctionArgs } 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 }: LoaderFunctionArgs) => {
  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));
};

5. アプリ起動

$ npm run dev

> dev
> shopify app dev

?  Which store would you like to use to view your project?   Type to search...
✔  td-watanabe

自分のテストストアを選択してください。

?  Have Shopify automatically update your app's URL in order to create a preview experience?

   ┃  Current app URL
   ┃  • https://example.com/
   ┃
   ┃  Current redirect URLs
   ┃  • https://example.com/api/auth

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

テストアプリであれば自動更新をおすすめします。自動更新設定をすれば都度アプリURLを更新する手間が省けます!

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

インストールします。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

アプリのトップページ(QRコード一覧)が表示されました!!!「Create QR code」をクリックしてみましょう。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

QRコード作成画面が表示されました!!!(画像内のQRコード部分はサイト公開時点で読み込めないので隠していますが、QRコード自体は表示されています)

ブラウザバーに注目するとapp/qrcodes/newとなっています。新規作成の場合、qrcodes/の後ろは「new」が表示されます。

テストデータを作成してみましょう。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

タイトルを入力して商品を選択したら保存ボタンをクリックしてみましょう。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

QRコードが作成されました!!!

ブラウザバーに注目するとapp/qrcodes/1となっています。qrcodes/の後ろはQRコードテーブルに保存されたidが表示されます。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

一覧ページを再度表示すると先ほど作成したQRコードが表示されました!!!試しにスマホでQRコードを読み込んでみましょう。

Shopifyアプリ開発チュートリアルを実践しよう!① - 株式会社テックディレクション

スキャン回数が増えています!!!(画像は2回読み込んだ後です

参考

Shopify公式チュートリアル

Remix公式ドキュメント

qrcode

tiny-invariant

まとめ

今回はShopfiy公式アプリ開発チュートリアルを実行してみました。案外簡単にアプリを立ち上げることができると感じていただければ幸いです。このほかにもCheckoutアプリやCustomer Accountアプリもあるので、そちらもやっていきたいと思います。

著者:とんとろ

弊社では、ECサイトのリプレース案件から、Shopifyカスタムアプリ開発、保守案件に至るまで、EC中心にプロジェクトの質にこだわり、お客様に笑顔になってもらえるよう日々邁進しております。

皆様からのお問い合わせ・ご相談をお待ちしております。

コメントはこちら