🏁

【Shopify.dev和訳】Apps/Getting started/Add app features

2021/09/04に公開

この記事について

この記事は、Apps/Getting started/Add app featuresの記事を和訳したものです。

記事内で使用する画像は、公式ドキュメント内の画像を引用して使用させていただいております。

Shopify アプリのご紹介

Shopify アプリである、「商品ページ発売予告アプリ | リテリア Coming Soon」は、商品ページを買えない状態のまま、発売日時の予告をすることができるアプリです。Shopify で Coming Soon 機能を実現することができます。

https://apps.shopify.com/shopify-application-314?locale=ja&from=daniel

Shopify アプリである、「らくらく日本語フォント設定|リテリア Font Picker」は、ノーコードで日本語フォントを使用できるアプリです。日本語フォントを導入することでブランドを演出することができます。

https://apps.shopify.com/font-picker-1?locale=ja&from=daniel

アプリ構築を始めよう| Start building your app

Quickstart で、ShopifyCLI を使用して新しいアプリを作成しました。これでアプリの作成を開始する準備が整いました。

このチュートリアルでは、アプリに特定の機能を追加するための一連のタスクを実行します。最終的なアプリはシンプルになりますが、より複雑な機能を自分で構築するためのリソースを見つける場所を学びます。

シナリオ| Scenario

販売者が商品の在庫を管理するのに役立つアプリを作成したいと考えています。販売者はアプリを使用して、商品名や価格など、商品に関する重要な詳細をドキュメント化します。

在庫管理は、サプライチェーン全体で在庫を整理および管理するプロセスです。効果的な在庫管理は、企業が顧客の需要を満たすのに十分な在庫を確保するために重要です。

あなたが学ぶこと| What you'll learn

このチュートリアルを終了すると、次のことが完了します。

  • 開発ストアで、アプリをテストするための製品を配置します
  • Polaris を使用してユーザーインターフェイスの始まりを構築します
  • 製品を取得するための GraphQL クエリを設定します
  • GraphQL ミューテーションを設定して、製品の価格を更新します

前提条件| Requipments

  • アプリのクイックスタートの作成が完了している
  • VSCodeAtom などのコードエディターがコンピューターにインストールされている

Step 1: 商品データを入力する

Shopify CLI は、アプリの動作をテストするためのサンプルデータを追加するプロセスを手助けします。Shopify CLI を使用して、製品、顧客、およびドラフト注文のレコードを作成できます。

アプリは商品データとやり取りする必要があるため、開発ストアに商品を入力することから始めます。

  1. 新しいターミナルウィンドウを開きます。
  2. プロジェクトディレクトリに移動します。
  3. shopify populateコマンドで製品を実行します。

Step 2: 空の状態を追加する

Shopify でアプリを実行できるようになったので、フロントエンドコンポーネントをビルドしながら表示およびテストできます。 Shopify の React コンポーネントライブラリおよびデザインシステムである Polaris を使用して、ユーザーインターフェイスを構築できます。

Polaris を使用して、アプリに空の状態を追加します。 Polaris Empty state componentは、マーチャントが最初にアプリを Shopify 管理者に追加したときに、アプリの価値とその主要なアクションを伝えるのに役立ちます。

  1. コードエディタで、pages /index.js ファイルに移動します。
  2. ファイルの内容を EmptyState コンポーネントに置き換えます。
pages/index.js
import { Heading, Page, TextStyle, Layout, EmptyState} from "@shopify/polaris";

const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg';

const Index = () => (
  <Page>
    <Layout>
      <EmptyState // Empty state component
        heading="Discount your products temporarily"
        action={{
          content: 'Select products',
          onAction: () => this.setState({ open: true }),
        }}
        image={img}
      >
        <p>Select products to change their price temporarily.</p>
      </EmptyState>
    </Layout>
  </Page>
);

export default Index;

埋め込みアプリをプレビューすると、空の状態が表示されます。

Step 3: リソースピッカーを追加する

次に、リソースピッカーを追加して、アプリから商品を選択できるようにします。 Shopify の standalone vanilla JavaScript ライブラリであるAppBridge を使用して、アプリにリソースピッカーを追加できます。

App Bridge ResourcePicker アクションセットは、1 つ以上の製品を見つけて選択し、選択したリソースをアプリに返すのに役立つ検索ベースのインターフェイスを提供します。

  1. pages/index.jsファイルに、リソースピッカーの状態を設定するクラスを追加します。次に、ResourcePickerコンポーネントをEmptyStateコンポーネントのプライマリアクションボタンに追加します。
pages/index.js
import React from 'react';
import { Heading, Page, TextStyle, Layout, EmptyState} from "@shopify/polaris";
import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react';

const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg';

// Sets the state for the resource picker
class Index extends React.Component {
  state = { open: false };
  render() {
    return (
      <Page>
        <TitleBar
          primaryAction={{
            content: 'Select products',
            onAction: () => this.setState({ open: true }),
          }}
        />
        <ResourcePicker // Resource picker component
          resourceType="Product"
          showVariants={false}
          open={this.state.open}
          onSelection={(resources) => this.handleSelection(resources)}
          onCancel={() => this.setState({ open: false })}
        />
        <Layout>
          <EmptyState
            heading="Discount your products temporarily"
            action={{
              content: 'Select products',
              onAction: () => this.setState({ open: true }),
            }}
            image={img}
          >
            <p>Select products to change their price temporarily.</p>
          </EmptyState>
        </Layout>
      </Page>
    );
  }
  handleSelection = (resources) => {
    this.setState({ open: false });
    console.log(resources);
  };
}

export default Index;

埋め込みアプリで、製品の選択をクリックすると、製品の追加モーダルが開きます。

Step 4: リソースリストを追加する

リソースピッカーを設定したので、商品を取得する方法が必要です。 GraphQL Admin API を使用して製品を取得できます。最終的には、これらの製品をリソースリストに表示する必要があります。

アプリが GraphQL を使用してデータをクエリできるようにするには、新しいResourceList.jsファイルを作成し、ファイルにgraphql-tagreact-apolloのインポートを含めます。

次に、getProductsという GraphQL クエリを設定して、製品とその価格のリストを取得します。

  1. npm install store-jsを実行します。
    store-jsは、ローカルストレージを管理するためのクロスブラウザ JavaScript ライブラリです。 store-jsを使用して、アプリでテスト目的でデータを永続化します。

  2. ロジェクトのpagesフォルダーに新しいcomponentsフォルダーを作成し、そのフォルダーに新しいResourceList.jsファイルを作成します。

    Tips
    コンポーネントを整理するためのベストプラクティスは、pages/components ディレクトリでファイルを配置することです。

  3. ResourceList.jsファイルにインポートを追加し、GraphQL クエリを設定して、製品とその価格を取得します。

pages/components/ResourceList.js
import React from 'react';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
import {
  Card,
  ResourceList,
  Stack,
  TextStyle,
  Thumbnail,
} from '@shopify/polaris';
import store from 'store-js';
import { Redirect } from '@shopify/app-bridge/actions';
import { Context } from '@shopify/app-bridge-react';

// Dで商品を検索するGraphQLクエリです。
// 商品のバリエーションによって価格が異なる場合があるため、
// priceフィールドはvariantsオブジェクトに属します。
const GET_PRODUCTS_BY_ID = gql`
  query getProducts($ids: [ID!]!) {
    nodes(ids: $ids) {
      ... on Product {
        title
        handle
        descriptionHtml
        id
        images(first: 1) {
          edges {
            node {
              originalSrc
              altText
            }
          }
        }
        variants(first: 1) {
          edges {
            node {
              price
              id
            }
          }
        }
      }
    }
  }
`;
  1. ResourceList.jsファイルで、GraphQL クエリの後に、ResourceListコンポーネントを拡張し、製品と価格を返すResourceListWithProductsというクラスを設定します。次に、ResourceListコンポーネントを定義します。
pages/components/ResourceList.js
class ResourceListWithProducts extends React.Component {
  static contextType = Context;

  render() {
    const app = this.context;

    return (
      // 商品とその価格を取得するGraphQLクエリ
      <Query query={GET_PRODUCTS_BY_ID} variables={{ ids: store.get('ids') }}>
        {({ data, loading, error }) => {
          if (loading) return <div>Loading…</div>;
          if (error) return <div>{error.message}</div>;

          return (
            <Card>
              <ResourceList // リソースリストコンポーネントの定義
                showHeader
                resourceName={{ singular: 'Product', plural: 'Products' }}
                items={data.nodes}
                renderItem={item => {
                  const media = (
                    <Thumbnail
                      source={
                        item.images.edges[0]
                          ? item.images.edges[0].node.originalSrc
                          : ''
                      }
                      alt={
                        item.images.edges[0]
                          ? item.images.edges[0].node.altText
                          : ''
                      }
                    />
                  );
                  const price = item.variants.edges[0].node.price;
                  return (
                    <ResourceList.Item
                      id={item.id}
                      media={media}
                      accessibilityLabel={`View details for ${item.title}`}
                      onClick={() => {
                        store.set('item', item);
                      }}
                    >
                      <Stack>
                        <Stack.Item fill>
                          <h3>
                            <TextStyle variation="strong">
                              {item.title}
                            </TextStyle>
                          </h3>
                        </Stack.Item>
                        <Stack.Item>
                          <p>${price}</p>
                        </Stack.Item>
                      </Stack>
                    </ResourceList.Item>
                    );
                  }}
                />
              </Card>
            );
          }}
        </Query>
      );
    }
  }

export default ResourceListWithProducts;

pages/index.jsファイルで、インポートを追加し、アプリの空の状態の定数を定義します。次に、空の状態のレイアウトを制御するコードを更新し、製品で新しいリソースリストを使用して指定します。

pages/index.js
import React from 'react';
import { Page, Layout, EmptyState} from "@shopify/polaris";
import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react';
import store from 'store-js';
import ResourceListWithProducts from './components/ResourceList';

const img = 'https://cdn.shopify.com/s/files/1/0757/9955/files/empty-state.svg';

class Index extends React.Component {
  state = { open: false };
  render() {
    //  アプリの空の状態を定義する定数
    const emptyState = !store.get('ids');
    return (
      <Page>
        <TitleBar
          primaryAction={{
            content: 'Select products',
            onAction: () => this.setState({ open: true }),
          }}
        />
        <ResourcePicker
          resourceType="Product"
          showVariants={false}
          open={this.state.open}
          onSelection={(resources) => this.handleSelection(resources)}
          onCancel={() => this.setState({ open: false })}
        />
        {emptyState ? ( // アプリの空の状態のレイアウトを制御する
          <Layout>
            <EmptyState
              heading="Discount your products temporarily"
              action={{
                content: 'Select products',
                onAction: () => this.setState({ open: true }),
              }}
              image={img}
            >
              <p>Select products to change their price temporarily.</p>
            </EmptyState>
          </Layout>
        ) : (
          // IDで商品を検索する新リソースリストを使用
          <ResourceListWithProducts />
        )}
      </Page>
    );
  }
  handleSelection = (resources) => {
    const idsFromResources = resources.selection.map((product) => product.id);
    this.setState({ open: false });
    store.set('ids', idsFromResources);
  };
}

export default Index;

これで、製品の選択をクリックし、製品の追加モーダルから製品を追加すると、製品のリストが表示されます。

Step 5: 製品価格を更新する

製品データを読み取るための GraphQL クエリを実装し、取得した製品をリソースリストに表示する機能を追加しました。次に、GraphQL を使用して製品データを変更します。

ProductVariantUpdateと呼ばれる GraphQL ミューテーションを設定して、アプリ内の製品の価格を更新します。

  1. コンポーネントフォルダに新しいApplyRandomPrices.jsファイルを作成します。
  2. ApplyRandomPrices.jsファイルにインポートを追加し、アプリが製品の価格を更新できるようにする GraphQL ミューテーションを設定します。
pages/components/ApplyRandomPrices.js
import React, { useState } from 'react';
import gql from 'graphql-tag';
import { Mutation } from 'react-apollo';
import { Layout, Button, Banner, Toast, Stack, Frame } from '@shopify/polaris';
import { Context } from '@shopify/app-bridge-react';

// 商品の価格を更新するGraphQLのミューテーション
const UPDATE_PRICE = gql`
  mutation productVariantUpdate($input: ProductVariantInput!) {
    productVariantUpdate(input: $input) {
      product {
        title
      }
      productVariant {
        id
        price
      }
    }
  }
`;
  1. ApplyRandomPrices.jsでミューテーションを行った後、ApplyRandomPricesというクラスを設定します。このクラスは、ミューテーションの入力を受け取り、選択した製品にランダムな価格を適用します。
pages/components/ApplyRandomPrices.js
class ApplyRandomPrices extends React.Component {
  static contextType = Context;

  render() {
    return ( // mutationの入力を利用して製品価格を更新
      <Mutation mutation={UPDATE_PRICE}>
        {(handleSubmit, {error, data}) => {
          const [hasResults, setHasResults] = useState(false);

          const showError = error && (
            <Banner status="critical">{error.message}</Banner>
          );

          const showToast = hasResults && (
            <Toast
              content="Successfully updated"
              onDismiss={() => setHasResults(false)}
            />
          );

          return (
            <Frame>
              {showToast}
              <Layout.Section>
                {showError}
              </Layout.Section>

              <Layout.Section>
                <Stack distribution={"center"}>
                  <Button
                    primary
                    textAlign={"center"}
                    onClick={() => {
                      let promise = new Promise((resolve) => resolve());
                      for (const variantId in this.props.selectedItems) {
                        const price = Math.random().toPrecision(3) * 10;
                        const productVariableInput = {
                          id: this.props.selectedItems[variantId].variants.edges[0].node.id,
                          price: price,
                        };

                        promise = promise.then(() => handleSubmit({ variables: { input: productVariableInput }}));
                      }

                      if (promise) {
                        promise.then(() => this.props.onUpdate().then(() => setHasResults(true)));
                    }}
                  }
                  >
                    Randomize prices
                  </Button>
                </Stack>
              </Layout.Section>
            </Frame>
          );
        }}
      </Mutation>
    );
  }
}

export default ApplyRandomPrices;
  1. pages/index.jsファイルを更新して、次のインポートを含めます。
pages/index.js
import React from 'react';
import gql from 'graphql-tag';
import { Mutation } from 'react-apollo';
import { Page, Layout, EmptyState, Button, Card } from "@shopify/polaris";
import { ResourcePicker, TitleBar } from '@shopify/app-bridge-react';
import store from 'store-js';
import ResourceListWithProducts from './components/ResourceList';
  1. ResourceList.jsで、ApplyRandomPricesインポートを追加します。 ResourceListWithProductsクラスにコンストラクターを実装し、GraphQL クエリを更新して、ID による製品の再フェッチを有効にします。最後に、ResourceListコンポーネントを更新します。
pages/components/ResourceList.js
import React from 'react';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
import {
  Card,
  ResourceList,
  Stack,
  TextStyle,
  Thumbnail,
} from '@shopify/polaris';
import store from 'store-js';
import { Redirect } from '@shopify/app-bridge/actions';
import { Context } from '@shopify/app-bridge-react';
import ApplyRandomPrices from './ApplyRandomPrices';

// IDで商品を検索するGraphQLクエリ
const GET_PRODUCTS_BY_ID = gql`
  query getProducts($ids: [ID!]!) {
    nodes(ids: $ids) {
      ... on Product {
        title
        handle
        descriptionHtml
        id
        images(first: 1) {
          edges {
            node {
              originalSrc
              altText
            }
          }
        }
        variants(first: 1) {
          edges {
            node {
              price
              id
            }
          }
        }
      }
    }
  }
`;

class ResourceListWithProducts extends React.Component {
  static contextType = Context;

  // 選択された items と nodes を定義するコンストラクタ
  constructor(props) {
    super(props);
    this.state = {
      selectedItems: [],
      selectedNodes: {},
    };
  }

  render() {
    const app = this.context;

    // ID によって商品を返す
    return (
        <Query query={GET_PRODUCTS_BY_ID} variables={{ ids: store.get('ids') }}>
          {({ data, loading, error, refetch }) => { // ID で商品を再フェッチ
            if (loading) return <div>Loading…</div>;
            if (error) return <div>{error.message}</div>;

            const nodesById = {};
            data.nodes.forEach(node => nodesById[node.id] = node);

            return (
              <>
                <Card>
                  <ResourceList
                    showHeader
                    resourceName={{ singular: 'Product', plural: 'Products' }}
                    items={data.nodes}
                    selectable
                    selectedItems={this.state.selectedItems}
                    onSelectionChange={selectedItems => {
                      const selectedNodes = {};
                      selectedItems.forEach(item => selectedNodes[item] = nodesById[item]);

                      return this.setState({
                        selectedItems: selectedItems,
                        selectedNodes: selectedNodes,
                      });
                    }}
                    renderItem={item => {
                      const media = (
                        <Thumbnail
                          source={
                            item.images.edges[0]
                              ? item.images.edges[0].node.originalSrc
                              : ''
                          }
                          alt={
                            item.images.edges[0]
                              ? item.images.edges[0].node.altText
                              : ''
                          }
                        />
                      );
                      const price = item.variants.edges[0].node.price;
                      return (
                        <ResourceList.Item
                          id={item.id}
                          media={media}
                          accessibilityLabel={`View details for ${item.title}`}
                          verticalAlignment="center"
                          onClick={() => {
                            let index = this.state.selectedItems.indexOf(item.id);
                            const node = nodesById[item.id];
                            if (index === -1) {
                                this.state.selectedItems.push(item.id);
                                this.state.selectedNodes[item.id] = node;
                            } else {
                              this.state.selectedItems.splice(index, 1);
                                delete this.state.selectedNodes[item.id];
                            }

                            this.setState({
                              selectedItems: this.state.selectedItems,
                              selectedNodes: this.state.selectedNodes,
                              });
                          }}
                        >
                          <Stack alignment="center">
                            <Stack.Item fill>
                              <h3>
                                <TextStyle variation="strong">
                                  {item.title}
                                </TextStyle>
                              </h3>
                            </Stack.Item>
                            <Stack.Item>
                              <p>${price}</p>
                            </Stack.Item>
                          </Stack>
                        </ResourceList.Item>
                      );
                    }}
                  />
                </Card>

              <ApplyRandomPrices selectedItems={this.state.selectedNodes} onUpdate={refetch} />
            </>
          );
        }}
      </Query>
    );
  }
}

export default ResourceListWithProducts;

アプリで、商品の価格を更新できるようになりました。

次のステップ| Next steps

  • Webhookを使用して、Shopify との同期を維持するか、ショップで特定のイベントが発生した後にコードを実行します。
  • アプリのビジネスモデルを特定し、Billing APIを使用して、毎月の定期的な請求または 1 回限りの購入で顧客に請求する方法を学びます。
  • App extensions(アプリ拡張機能)を使用して Shopify 管理者または POS に機能を追加する方法を学びます。
  • GraphQL Admin API および REST Admin API リファレンスを調べてください。

Shopify アプリのご紹介

Shopify アプリである、「商品ページ発売予告アプリ | リテリア Coming Soon」は、商品ページを買えない状態のまま、発売日時の予告をすることができるアプリです。Shopify で Coming Soon 機能を実現することができます。

https://apps.shopify.com/shopify-application-314?locale=ja&from=daniel

Shopify アプリである、「らくらく日本語フォント設定|リテリア Font Picker」は、ノーコードで日本語フォントを使用できるアプリです。日本語フォントを導入することでブランドを演出することができます。

https://apps.shopify.com/font-picker-1?locale=ja&from=daniel

Discussion

ログインするとコメントできます