Shopifyアプリ開発 ~ スクリプトAPIでストアフロントに会員情報と連携するプラグインを設置する方法 ~

#簡単自己紹介
はじめまして、熱狂的ファンを作る接客チャット[チャネルトーク](https://channel.io/ja)の[Shopifyアプリ](https://channel.io/ja/blog/shopify-case)をリリース中のチャネルトークCCO[あやーそん](https://twitter.com/ayaaason)と申します。

かなり特殊な経歴ですが、アクセンチュアでデジタル技術を使ったPoC専門チームに新卒で入り、エンジニア(ソフトウェア・ハードウェア)とUXデザインなど開発とビズを兼業していました。

また、ソフトウェアエンジニアとして再ジョインした[チャネルトーク](https://channel.io/ja)でもCRE(Customer Reliablity Engineer)とCSやPMMを兼務するなど常に開発とビズを両方やるおかしなキャリアを歩み、極めていきたいと思っています。

#チャネルトークとは？
熱狂的ファンを作るCSチャットのチャネルトークは、「[答えは顧客にある](https://twitter.com/search?q=%23%E7%AD%94%E3%81%88%E3%81%AF%E9%A1%A7%E5%AE%A2%E3%81%AB%E3%81%82%E3%82%8B&src=typed_query&f=live)」をモットーに、気軽に声かけができるチャット窓口から顧客の声をきける機能と顧客中心文化を提供しています。

顧客の声を社内に共有してサービスや商品に反映させたり、素晴らしいサポートや接客を実施することでファンを作りや売上やLTVの向上など、Shopifyマーチャントをはじめ、SMBの持続的な成長を支援するのが私たちのミッションです。

##Shopifyの導入事例が急増中
コロナの影響もあり、チャット接客をはじめたり、利便性向上のためにサポート窓口をメール・電話からチャネルトークに移行するマーチャントさんが増えています。
[▶︎ Shopify導入事例まとめ](https://channel.io/ja/blog/shopify-case)

▶︎[KURAND](https://kurand.jp/)：[「チャット導入で購入前の相談件数、約600%UPを実現！コロナ禍でオンライン接客に注力！」](https://channel.io/ja/blog/case-kurand)
▶︎[ジョンマスターオーガニック](https://www.johnmasters-select.jp/)：[「ジョンマスターオーガニックがチャット接客で、オンラインストアの購入に繋げる」](https://channel.io/ja/blog/case-johnmasters)


#チャネルトークのShopifyアプリのリリース
顧客に商品を直接販売できる自社ECを提供するShopifyに共感をするとともに、EC・D2Cに顧客中心文化に提供することで持続的な成長を支援したく、Shopifyアプリの開発に臨みました。すでに提供しているスクリプトの手動追加による[Shopifyへの導入](https://channel.io/ja/blog/shopify-channel-talk)が多かったことや、マーチャントさんや支援事業社さんからの期待の声もあり優先度をあげて開発をしてきました！

##Shopify設置後のイメージと機能
[▶︎ Shopifyのデモサイト](https://channel-talks-shop.myshopify.com/)で動作確認も可能です
[▶︎ Shopifyへの設置方法(theme.liquidのbody一番下に手動でコード設置)](https://channel.io/ja/blog/shopify-channel-talk)
※Shopifyアプリストアへの掲載が決まったら更新します！

###5分でサイト設置して顧客と会話が始められる[「接客チャット」](https://channel.io/ja/user-chat)
![接客チャット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/cac42d92-2a2f-aab2-787c-278a015dd478.png)
###FAQチャットボットや問い合わせ統計で効率化や改善ができる[「サポート強化機能」](https://channel.io/ja/user-chat)
![サポート強化機能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/0c388306-da15-d93c-5286-6138e7a7a066.png)
###サイト内ポップアップやオフラインの顧客にもSMSやメールで一斉・自動で話しかけてスケールさせられる[「マーケティング機能」](https://channel.io/ja/marketing)
![マーケティング機能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/0d88531c-88ea-6f01-4b10-268d8e1ca263.png)

##Shopifyの会員情報やカート内情報との連携
![Shopify会員情報連携](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/a0959d77-f066-3f93-4268-a0e65adb5ca6.png)

- Shopifyの会員情報やカート内情報との連携
- クリックひとつでShopifyの顧客ページに飛んで注文履歴を確認できるCRMリンク
- イベント欄にて閲覧ページなどからお客様の興味を把握した上での接客・サポートが可能


#チャネルトーク アプリの開発方法
さて、長くなりましたが開発方法はざっくりと以下の通りです。

1. OAuth2.0による認証処理の実装(https://shopify.dev/tutorials/authenticate-with-oauth#step-3-confirm-installation)
2. [ScriptTag API](https://shopify.dev/docs/admin-api/rest/reference/online-store/scripttag)を使ってアプリのインストール時に[チャネルトークの連携スクリプト](https://channel.io/ja/blog/shopify-channel-talk)挿入
3. Liquid objectsにて取得できる会員情報([customerオブジェクト](https://shopify.dev/docs/themes/liquid/reference/objects/customer))やカート情報([cartオブジェクト](https://shopify.dev/docs/themes/liquid/reference/objects/cart))を取得するために[App proxy](https://shopify.dev/tutorials/display-data-on-an-online-store-with-an-application-proxy-app-extension?shpxid=812406be-BBAF-4C31-A796-278D456169F5)を実装
4. Shopのlocaleに合わせたアプリの[ローカライズ](https://shopify.dev/tutorials/update-your-app-to-support-localization)
4. [billing API](https://shopify.dev/docs/admin-api/rest/reference/billing)による有料アプリの課金連携
5. [GDPR準拠のための必須Webhook](https://shopify.dev/tutorials/add-gdpr-webhooks-to-your-app#mandatory-webhooks)

## 引っかかった点
###1. App Proxyの実装!?
③の手順にあるApp Proxyの実装しなくてはならない点で引っかかりました。ScriptTag APIが挿入される時点ではすでにjsコードになっており、liquid objectを直接呼び出すことができません。

App Proxyでliquid objectを取得しておいて、jsの変数に保存、Script APIで挿入されるコードに受け渡す必要があります！

###2. Billing API
チャネルトークの課金システムが前払いかつ従量課金制であるため、billing APIの実装にもっとも時間がかかりました。他社アプリなどの事例なども参考にして、どうにかbilling APIなしでもまずはリリースできないか考えました。。。が、billing APIを連携しない有料アプリは過去は審査通過が可能だったけど今は無理で、今後停止になる可能性もあるとのこと。アプリストアの掲載には必須だという結論になりました。有料アプリは素直に実装するのがオススメです！

###3. OAuth時のローカライズ実装
チャネルトークは完全日本語対応のアプリでありながら、グローバルに展開するため(自動翻訳機能や韓国語・英語にも対応しているため[越境EC](https://channel.io/ja/blog/case-tricera)にもオススメです^^)、OAuth処理のレスポンスで送られてくるShopのlocale情報に従ってアプリのUIや設定をローカライズしました。

ただし、OAuthのレスポンスでlocale情報をくれるのは、["online access mode"](https://shopify.dev/concepts/about-apis/authentication#api-access-modes)でOAuthを実装したときのみ。チャネルトークでは["online access mode"](https://shopify.dev/concepts/about-apis/authentication#api-access-modes)のように、Webセッションに紐付く処理は不必要かつ開発コストが高いため、["Offline access"](https://shopify.dev/concepts/about-apis/authentication#api-access-modes)を採用していました。

代わりに、OAuth直後の処理で[Shop情報](https://shopify.dev/docs/admin-api/rest/reference/store-properties/shop)をAPIで取得してローカライズしました。


## App ProxyでScript Tagsに顧客情報を受け渡す方法
OAuth2.0やScriptTag APIなどは基本的な使い方だったのでサンプルコードとして、App ProxyからScript TagsにLiquid Objectで提供する顧客情報を受け渡す部分を共有します。
※あくまでもサンプルコードで実際のアプリ開発したコードはより詳細な処理があるため異なります！

- App Proxyの設定
![App Proxyの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/ec99e9a1-207b-b035-c161-cae20c45c97b.png)
![App Proxyの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324313/895ba3f0-c3ee-e0cd-7f19-7c9d77d499a7.png)

- Script Tagsで登録されたurlによりアクセスされるスクリプト

```app.js
/* plugin */
router.get('/plugin/:pluginKey',  async (ctx, next) => {
  let pluginKey = ctx.params.pluginKey;
  ctx.body = `var myAppJavaScript = function(a) {
    a.get("/apps/integrate-liquid", function(e) {
      var t = function(e) {
        var t = document.createElement("div");
        t.innerHTML = e;
        var a = document.createDocumentFragment();
        return a.appendChild(t), a.querySelector("#channel-widget")
      }(e);
          a("body").append(t);
          ChannelIO('boot', {
              "pluginKey": "${pluginKey}",
              "memberId": window.channelMemberId,
              "profile": window.channelParameters
          });
    })
  };
  var loadScript = function(e, t) {
    var a = document.createElement("script");
    a.type = "text/javascript", a.readyState ? a.onreadystatechange = function() {
      "loaded" != a.readyState && "complete" != a.readyState || (a.onreadystatechange =
        null, t())
    } : a.onload = function() {
      t()
    }, a.src = e, document.getElementsByTagName("head")[0].appendChild(a)
  };
  "undefined" == typeof jQuery || parseFloat(jQuery.fn.jquery) < 1.7 ? loadScript(
    "//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js",
    function() {
      jQuery191 = jQuery.noConflict(!0), myAppJavaScript(jQuery191)
    }) : myAppJavaScript(jQuery);
  !function() {
    var t = window;
    if (t.ChannelIO) return (window.console.error || window.console.log ||
      function() {})("ChannelIO script included twice.");
    window.document;
    var e = function() {
      e.c(arguments)
    };
  
    function n() {
      if (!t.ChannelIOInitialized) {
        t.ChannelIOInitialized = !0;
        var n = document.createElement("script");
        n.type = "text/javascript", n.async = !0, n.src =
          "https://cdn.channel.io/plugin/ch-plugin-web.js", n.charset = "UTF-8";
        var e = document.getElementsByTagName("script")[0];
        e.parentNode.insertBefore(n, e)
      }
    }
    e.q = [], e.c = function(n) {
        e.q.push(n)
      }, t.ChannelIO = e, "complete" === document.readyState ? n() : window.attachEvent ?
      window.attachEvent("onload", n) : (window.addEventListener("DOMContentLoaded",
        n, !1), window.addEventListener("load", n, !1))
  }()`
});
```

- 上記のScript Tagsに会員情報を受け渡せるように、
App Proxyにアクセスされた際に、Liquid Objectを取得してjs変数に入れる処理

```app.js
/* integrate-liquid */
router.get('/integrate-liquid',  async (ctx, next) => {
  ctx.set('Content-Type', 'application/liquid');
  ctx.body = `<script id="channel-widget">
      window.channelMemberId = "{{customer.id}}"
      window.channelParameters = {
        "name": "{{ customer.name }}",
        "email": "{{ customer.email }}",
        "defaultAddressCity": "{{ customer.default_address.city}}",
        "cartTotalPrice": "{{ cart.total_price | money }}",
        "totalSpent": "{{ customer.total_spent | money }}",
        "ordersCount": "{{customer.orders_count}}",
        "lastOrderCreatedAt": "{{ customer.last_order.created_at | date: %s*1000}}",
        "lastOrderNumber": "{{ customer.last_order.order_number }}",
        "fulfillmentUrl": "{{ fulfillment.tracking_url }}"
      }
    </script>`
});
```

最後まで読んでいただきありがとうございました！

チャネルトークのShopifyアプリは、リリース後もさらにアップデートをして強化していく予定です！まずはリリースを楽しみにしていてください！

▶︎ 疑問点は[チャネルトークホームページ](https://channel.io/ja)からお問い合わせください！
▶︎ アプリのリリース前にすぐに導入したい場合は、[「Shopifyにチャネルトークを10分で設置する方法」](https://channel.io/ja/blog/shopify-channel-talk)から可能です！😁