🛍️

Shopifyアプリの「Post-Purchase」拡張機能を実装してみる #shopify

2021/12/16に公開1

この記事はShopifyアドベントカレンダーの16日目の記事です。

はじめに

Shopifyでは、Shopifyアプリに購入してからウェブサイトを離脱する前にお客さまに対してアプローチを行う「Post-Purchase」拡張機能を実装することができます(記事、現時点ではベータバージョン)

「Post-Purchase」拡張機能を利用することで例えば下記のようなことが実現できます。

  • 商品に関連したおすすめの商品の紹介
  • 定期購入の提案
  • アンケートや寄付の依頼
  • 次回注文時の割引特定の表示など

今回は、この「Post-Purchase」拡張機能を使ったShopifyアプリの実装を行いたいと思います。

「Post-Purchase」拡張機能の実装

実装する前に

「Post-Purchase」拡張機能は、あくまでShopifyアプリの拡張機能となるので、まずはShopifyアプリを実装します。

今回は「mkazutaka-post-purchase-app」という名前でアプリケーションを作成しています。

> shopify app create node
? App name
mkazutaka-post-purchase-app
? What type of app are you building? (You chose: Public: An app built for a wide merchant audience.)
? Select a development store (You chose: **.myshopify.com)

> cd mkazutaka-post-purchase-app

作成したShopifyアプリを起動し、オンラインストアにインストールします。

> shopify app serve

...
⭑ To install and start using your app, open this URL in your browser:
https://....ngrok.io/auth?shop=<YOUR_ONLINE_STORE>.myshopify.com

出力されたURL(ここでは、https://....ngrok.io/auth?shop=<YOUR_ONLINE_STORE>.myshopify.com)にアクセスし、アプリのインストールをします。

これで準備は終わりです。次から拡張機能を作っていきます。

1. 「Post-Purchase」拡張機能の作成

shopifyコマンドを通して拡張機能のパッケージを作ります。
フレームワークは vanilla react vanilla-typescript react-typescript の中から選んでいますが、本記事ではreactを選択しています。「mkazutaka-post-purchase-extension」という名前で拡張機能を作成しています。

> shopify extension create
Loading your apps…
? Which app would you like to register this extension with? (You chose: mkazutaka-post-purchase-app by AnyReach)
? What type of extension are you creating? (You chose: Checkout Post Purchase )
? Extension name
mkazutaka-post-purchase-extension

2. Chrome拡張機能のインストール

「Post-Purchase」拡張機能は、Shopify側でホストされ提供されます。開発のたびにShopify側にホストするのは手間なのか、公式がホストせずにローカルでの開発をオンラインストア上で試せるブラウザ拡張機能を提供しています。

こちらから該当の拡張機能をダウンロードして、ブラウザにインストールしてください。

インストール後、インストールしたChrome拡張機能を開くと下記のようなページが表示されます。

3. Chrome拡張機能の設定

下記コマンドを実行し、「Post-Purchase]拡張機能を動かします。

> shopify extension serve

✓ ngrok tunnel running at ...
...
┃ 🔭 > Your extension is available at https://****.ngrok.io/assets/extension.js
┃ 🔭 > You can append this query string: script_url=https%3A%2F%2F****.ngrok.io%2Fassets%2Fextension.js&api_key=****

上記のように動かすとScript URLとAPI keyが表示されるのでそれぞれを先程開いたChrome拡張機能のページに入力し、保存します。

4. 「Post-Purchase」拡張機能を動かす

実際に動かします。オンラインストアで商品を購入し、下記のような表示が出ます。

以上で実装は完了です。引き続きこれをもとにアップセルを実装していきます。

「Post-Purchase」拡張機能を用いたアップセルの実装

本セクションでは、前セクションで作成した「Post-Purchase」拡張機能をベースにアップセルを実装していきます。
ここでは、商品購入後に特定の商品を表示させ、そこからさらに追加購入できる機能になります。

事前準備

アップセルを実現するにあたって、商品情報を取得する必要があります。商品情報の取得には、Shopifyが提供しているAdmin APIを使用するのですがその際にアクセストークンが必要となってきます。

前セクションで作成したShopifyアプリは、オンメモリ上にアクセストークンを保存しているため、これをまず特定のファイルに保存するように変更します。

diff --git a/mkazutaka-post-purchase-app/server/server.js b/mkazutaka-post-purchase-app/server/server.js
index e2cf88a..a1e8c54 100644
--- a/mkazutaka-post-purchase-app/server/server.js
+++ b/mkazutaka-post-purchase-app/server/server.js
@@ -6,6 +6,7 @@ import Shopify, { ApiVersion } from "@shopify/shopify-api";
 import Router from "koa-router";
+import * as fs from 'fs';

@@ -30,6 +31,13 @@ Shopify.Context.initialize({
 const ACTIVE_SHOPIFY_SHOPS = {};
 
+const FILE_NAME = "shopify_shops.json"
+const saveActiveShopifyShops = () => {
+  fs.writeFile(FILE_NAME, JSON.stringify(ACTIVE_SHOPIFY_SHOPS, null, '  '), (err) => {
+    console.log(err)
+  });
+}
+
 app.prepare().then(async () => {
@@ -40,7 +48,8 @@ app.prepare().then(async () => {
         const host = ctx.query.host;
-        ACTIVE_SHOPIFY_SHOPS[shop] = scope;
+        ACTIVE_SHOPIFY_SHOPS[shop] = accessToken;
+        saveActiveShopifyShops()
 
         const response = await Shopify.Webhooks.Registry.register({

変更後、shopify app serveでアプリケーションを動かし、オンラインストアにインストール用のURLにアクセスします。

これでShopifyアプリディレクトリ直下に、shopify_shops.jsonというファイルが作成され、その中にアクセストークンが保存されるようになりました。

1. 製品の追加

取得したアクセストークンを使って、製品の追加をします。この製品のproduct idは後で使用するためどこかに保存しておいてください。

> export DEV_STORE=****
> export ACCESS_TOKEN=****
> curl -X POST \
    https://$DEV_STORE.myshopify.com/admin/api/2021-04/products.json \
    -H 'Content-Type: application/json' \
    -H "X-Shopify-Access-Token: $ACCESS_TOKEN" \
    --data '{
        "product": {
            "title": "Fertilizer",
            "body_html" : "Fertilizer will help your plant flourish all year round. <br/> Show your botanical friends some love.",
            "variants": [{"price" : "19.99", "compare_at_price" : "22.00"}],
            "images": [{"src" : "https://cdn.shopify.com/s/files/1/0595/3237/8273/products/plant-fertilizer.jpg"}]
       }
   }'

2. アップセルで表示する商品を返すサーバ実装

アップセル時に表示する商品を返すサーバを実装します。
まずnpm initを使用してパッケージを作成します。APIは「mkazutaka-post-purchase-api」という名前で作成します。

> mkdir mkazutaka-post-purchase-api
> cd mkazutaka-post-purchase-api/
> npm init

# 必要なパッケージのインストール
npm install express cors jsonwebtoken uuid node-fetch@2.6.2 graphql graphql-request dotenv

環境変数設定のために、.envファイルも用意します。SHOPIFY_API_KEY``SHOPIFY_API_SECRETは、mkazutaka-post-purchase-appの.envファイルに書いています。SHOPIFY_ACCESS_TOKENは事前準備の節の中で取得したものが使用できます。PRODUCT_IDは製品の追加の節で取得したものが使用できます。

.env
SHOPIFY_API_KEY=****
SHOPIFY_API_SECRET=****
SHOPIFY_ACCESS_TOKEN=****
DEV_STORE=****
PRODUCT_ID=****

商品を返すエンドポイントとして、/offerを実装します。/offer内では、先程保存したPRODUCT_IDの情報を取得し、それをレスポンスとして返す処理を行います
すべてのコードは公式のドキュメントを見てください。

server/index.js
...
app.get("/offer", async (req, res) => {
  const query = gql`
    query ($productId: ID!) {
      product(id: $productId) {
        id
        title
        featuredImage {
          originalSrc
        }
        descriptionHtml
        variants(first: 1) {
          edges {
            node {
              id
              price
              compareAtPrice
            }
          }
        }
      }
    }
  `;

  const result = await graphQLClient.request(query, {
    productId: `gid://shopify/Product/${process.env.PRODUCT_ID}`,
  });

  const product = result.product;
  const variant = result.product.variants.edges[0].node;

  const initialData = {
    variantId: variant.id.split("gid://shopify/ProductVariant/")[1],
    productTitle: product.title,
    productImageURL: product.featuredImage.originalSrc,
    productDescription: product.descriptionHtml.split(/<br.*?>/),
    originalPrice: variant.compareAtPrice,
    discountedPrice: variant.price
  };

  res.send(initialData);
});
...

またリクエストがShopifyから来たものか確認するためのエンドポイントも実装しています。

server/index.js
...
app.post("/sign-changeset", (req, res) => {
  const decodedToken = jwt.verify(
    req.body.token,
    process.env.SHOPIFY_API_SECRET
  );
  const decodedReferenceId =
    decodedToken.input_data.initialPurchase.referenceId;

  if (decodedReferenceId !== req.body.referenceId) {
    res.status(400).render();
  }

  const payload = {
    iss: process.env.SHOPIFY_API_KEY,
    jti: uuidv4(),
    iat: Date.now(),
    sub: req.body.referenceId,
    changes: req.body.changes,
  };

  const token = jwt.sign(payload, process.env.SHOPIFY_API_SECRET);
  res.json({ token });
});
...

3. 「Post-Purchase」拡張機能の実装

引き続き「Post-Purchase」拡張機能を変更し実装していきます。
ShouldRender内は、localhostにリクエストを投げ製品の情報を取得しstorageに保存します

extension/src/index.jsx
extend('Checkout::PostPurchase::ShouldRender', async ({storage}) => {
  const postPurchaseOffer = await fetch(
    'http://localhost:8077/offer'
  ).then((res) => res.json());

  await storage.update(postPurchaseOffer);

  return {render: true};
});

Render側のコードは長いのでここでは省略します。すべてのコードは公式ドキュメントをみてください。

コード内では以下のことをやっています

  • 送料と税金の計算
  • 注文の更新
送料と税金の計算

送料と税金の計算には、calculateChangesetを使用します

const {done, storage, calculateChangeset, applyChangeset, inputData} = useExtensionInput();

const changes = [{type: 'add_variant', variantId, quantity: 1}];
// ...
useEffect(() => {
  async function calculatePurchase() {
    // Request Shopify to calculate shipping costs and taxes for the cross-sell
    const result = await calculateChangeset({changes});

    setCalculatedPurchase(result.calculatedPurchase);
    setLoading(false);
  }

  calculatePurchase();
}, []);

applyChangesetを使用して、注文を更新します。

async function acceptOffer() {
  setLoading(true);

  // Make a request to your app server to sign the changeset
  const token = await fetch('http://localhost:8077/sign-changeset', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
      referenceId: inputData.initialPurchase.referenceId,
      changes: changes,
      token: inputData.token,
    }),
  })
    .then((response) => response.json())
    .then((response) => response.token);

  // Make a request to Shopify servers to apply the changeset
  await applyChangeset(token);

  // Redirect to the thank-you page
  done();
}

4. 「Post-Purchase」拡張機能を動かす

実際に動かします。オンラインストアで商品を購入し、下記のような表示が出ます。

またこのままPayNowをクリックすると表示された商品も同時に購入することができます

まとめ

Shopifyアプリの「Post-Purchase」機能の実装を紹介しました。
https://shopify.dev/apps/checkout/post-purchase

その他

Shopify EC制作やアプリの受託開発やってます!相談等々気軽にTwitterまでどうぞ!

Discussion

たまごやきプログラマーたまごやきプログラマー

「Post-Purchase」拡張機能は、Shopify側でホストされ提供されます。
公式がホストせずにローカルでの開発をオンラインストア上で試せるブラウザ拡張機能を提供しています。

→私はこの部分を理解できていないです。。。
 Shopifyのpost-purchaseチュートリアルには、拡張機能としてデプロイすることが紹介されています。
 これは、Google Chrome拡張機能の代わりになるものかと思いました。

図のようにデプロイしています。

しかし、Google Chrome拡張機能を使わないとpost-purchaseが動きません。

理由には以下の2つが考えられます。
・公開申請をしなければならない
・私のコード(または考え方)が間違っている

mkazutakaさんの意見をもらえるととても助かります><