これは「関連記事表示 by Vector Search 2.0」のデモサイトです。記事コンテンツはソリューションブログから引用しています。
実店舗で利用できる「LINEデジタル会員証」をKARTE Craftで実装する

実店舗で利用できる「LINEデジタル会員証」をKARTE Craftで実装する

こんにちは、Customer Engineerのgamiです!最近は喉が痛くて、e-maのど飴を箱買いして舐め続けています。

さて、実店舗を運営している事業において、ユーザーがポイント利用などで利用できる「デジタル会員証」機能が欲しくなることがあります。特に自社アプリではなくLINE上でデジタル会員証を実現できると、ユーザーの利用ハードルを下げることができます。

LINE公式でも、LINEミニアプリを使ったデジタル会員証導入のためのノウハウやパッケージが紹介されています。

デジタル会員証とは?移行・管理のメリットと導入方法|LINEヤフー for Business

こうした「LINEデジタル会員証」を提供するSaaSもたくさんありますが、外部の会員管理システムと連携させて独自に開発したいケースもあります。

そこで今回はKARTE Craftを使って「LINEデジタル会員証」を構築する手順について、サンプルをご紹介します。

※ 会員情報やポイント情報を管理するための「会員・ポイント管理システム」については、別で用意する必要があります。この記事で紹介するコードでは、そのシステムとの連携部分はダミーの処理を記載しています。

アウトプットイメージ

この記事では、LIFFアプリを作った簡単な「LINEデジタル会員証」を作ってみます。

最終的なアウトプットイメージは次の通りです。

まずLINE公式アカウントとのトーク画面でリッチメニュー内のLIFF URLをタップします。

するとLIFFアプリ上にデジタル会員証が表示されます。

※ LIFFアプリ上のトラブルシューティングのため「デバッグメッセージ」を表示していますが、本リリース時は非表示にします。

構成図は次の通り。

デジタル会員証としてLIFFブラウザから開かれるWebページをCraft Sitesで、そのバックエンドをCraft Functionsのファンクションで実装します。

  1. Craft Sitesで構築したWebページをLIFFアプリとして表示
  2. LIFFアプリ上で取得したLINE IDをCraft Functionsに連携
  3. Craft KVS上で管理する「LINE IDと会員IDの対応表」から、対応する会員IDを取得
  4. その会員IDを外部の「会員・ポイント管理システム」に連携し、ポイント情報などを取得してフロントエンドに返す

なお、対象ユーザーのLINE IDがCraft KVSの「LINE IDと会員IDの対応表」上に無い場合は、新規会員登録ボタンを表示し、新規会員登録フローに入ります。

なお、構成図にもある「会員・ポイント管理システム」は、次のような機能を持っている想定です。

  • 「会員・ポイント管理システム」の機能
    • 新規会員登録する機能
    • 会員IDを指定して、その最新の会員情報やポイント数を取得する機能
    • バーコードリーダーを含むPOSシステムと連携して、ポイントの消費や付与をする機能

この「会員・ポイント管理システム」が提供する「新規会員登録API」や「会員情報取得API」をCraft Functions経由で利用することで、LINE上でのデジタル会員証を実現します。ただし今回の記事では、そのシステムとの連携部分はダミーの処理を実装しています。

注意点

前提として、次の点に注意をしてください。

  • KARTE CraftのGrowthプラン以上を利用できる環境が必要です
  • 検証用のLINE公式アカウントおよびLINE Messaging APIチャネルが用意されていることを前提としています
    • 無い場合は新規に作成してください
  • 実際にエンドユーザー向けにサービス提供するためには、前述した「会員・ポイント管理システム」や、バーコードリーダーを含むPOSシステムなどが別途必要になります

設定手順

設定の手順は次の通りです。

  1. Craft Functionsのファンクションを作成する
  2. Craft SitesでLIFFアプリ用のWebページを作成する
  3. LINEログインチャネルを作成する
  4. LINEログインチャネルにLIFFアプリを追加する
  5. リッチメニューのリンク先にLIFF URLを設定する

順番に見てみましょう。

1. Craft Functionsのファンクションを作成する

Craft Functionsのファンクションを作成します。作成手順は次の通りです。

  • [Craft > ファンクション > 新規作成 > コードを書いて作成] を選択

  • [設定 > ファンクションのタイプ] で「HTTPタイプ」を選択

  • 適当なファンクション名を付けて、 [コード] タブに後述するコードを貼り付ける

  • [デプロイ] する

  • [設定 > エンドポイント] に表示されるURLをメモしておく

コードサンプルは、次の通りです。

const LOG_LEVEL = 'DEBUG';
const KVS_PREFIX = 'line_membership_card'
const KVS_MINUTES_TO_EXPIRE = 1440; // Craft KVSに保存する会員IDの生存期間(分)。本番リリース時は、十分に長い値に設定してください

// TODO: 外部APIから会員情報を取得する処理を実装してください
async function fetchMemberInfo(memberId) {
  return {
    memberId,
    point: getRandomPoint(),
  };
  function getRandomPoint() {
    const ps = [300, 500, 800]; // 300pt, 500pt, 800pt からランダムに1つを返す
    const i = Math.floor(Math.random() * ps.length);
    return ps[i];
  }
}

// TODO: 外部API経由で新規会員登録をする処理を実装してください
async function registerMember() {
  const randomNumber = Math.floor(Math.random() * 10000000000);
  const memberId = randomNumber.toString().padStart(10, '0');
  return memberId;
}

// 'read'時処理。受け取ったLINE IDに対応する会員IDをkvsから取得し、その会員IDに対応する会員情報を外部API経由で取得する
async function handleReadAction(lineId, { kvs, logger }) {
  const key = `${KVS_PREFIX}-${lineId}`;
  const kvsResult = await kvs.get({ key });
  const memberId = kvsResult[key]?.value?.member_id;
  logger.debug(`[read] kvs.get() finished. member_id: ${memberId}`);
  if (!memberId) return {};
  const info = await fetchMemberInfo(memberId);
  logger.debug(`[read] member info fetched. member_id: ${info.memberId}, point: ${info.point}`);
  return info;
}

// 'create'時処理。フロントエンドからLINE IDを受け取り、外部API経由で新規会員登録を実施して発行された会員IDとLINE IDとの対応をkvsに保存する
async function handleCreateAction(lineId, { kvs, logger }) {
  const memberId = await registerMember();
  const key = `${KVS_PREFIX}-${lineId}`;
  await kvs.write({ key, value: { member_id: memberId }, minutesToExpire: KVS_MINUTES_TO_EXPIRE });
  logger.debug(`[create] member created. member_id: ${memberId}`);
  return memberId;
}

export default async function (data, { MODULES }) {
  const { req, res } = data;
  const { initLogger, kvs } = MODULES;
  const logger = initLogger({ logLevel: LOG_LEVEL });

  // CORSを許可
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.method === 'OPTIONS') {
    res.status(204).end();
    return;
  }

  const action = req.body.action;
  if (!action) {
    res.status(400).send({ error: 'action field is required' });
    return;
  }
  const lineId = req.body.line_id;
  if (!lineId) {
    res.status(400).send({ error: 'line_id is required' });
    return;
  }

  if (action === 'read') {
    // 会員情報取得
    const { memberId, point } = await handleReadAction(lineId, { kvs, logger });
    if (!memberId) {
      res.status(404).send({ error: 'member not found' });
      return;
    }
    res.status(200).send({ member_id: memberId, point });
    return;
  } else if (action === 'create') {
    // 新規会員登録
    const memberId = await handleCreateAction(lineId, { kvs, logger });
    res.status(200).send({ member_id: memberId, point: 0 });
    return;
  } else {
    res.status(400).send({ error: 'Invalid action' });
    return;
  }
}

このコードでは、主に次の処理が実装されています。

  • read処理
    • フロントエンドから受け取ったLINE IDをもとに、対応する会員IDと現在のポイント数を返す
    • もし対応する会員IDが存在しなければ、404エラーを返す
  • create処理
    • フロントエンドから受け取ったLINE IDを使って新規会員登録を実行する

前述の通り、「会員・ポイント管理システム」との連携部分についてはダミーの処理を実装しています。実際に本番利用する場合は、連携対象の「会員・ポイント管理システム」のAPI仕様に合わせて連携のための実装をしてください。

2. Craft SitesでLIFFアプリ用のWebページを作成する

Craft SitesでLIFFアプリ用のWebページを作成します。作成手順は次の通りです。

  • [Craft > サイト] を開く

  • 適切なフォルダに index.html ファイルを作成し、後述するコードを貼り付ける

  • index.htmlファイルのscriptタブ内にある定数の値を設定

    • CRAFT_FUNCTIONS_END_POINT
      • 作成したファンクションのエンドポイントURL
    • LIFF_ID
      • LINE developersコンソールから取得するLIFF ID
      • ※後続の手順で発行されます
  • 変更を保存し、対象ファイルを公開

  • index.htmlの[サイトで見る] から表示用のサイトURLをコピーしておく

コードサンプルは、次の通りです。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LINEデジタル会員証</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@3.3.4"></script>
    <script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
    <script charset="utf-8" src="https://static.line-scdn.net/liff/edge/versions/2.22.3/sdk.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>

<body>
    <div id="app" class="container mx-auto p-4 bg-white rounded-lg shadow-md max-w-md">
        <h1 class="text-2xl font-bold text-center mb-4">会員証</h1>
        <template v-if="isLoading">
            <div class="flex justify-center items-center h-48">
                <div class="w-16 h-16 border-4 border-b-8 border-gray-200 rounded-full animate-spin"></div>
            </div>
        </template>

        <template v-else-if="memberId">
            <div class="text-center mb-4">
                <svg id="barcode" class="mx-auto"></svg>
                <p class="text-gray-600 text-sm mt-2">会員番号: {{ memberId }}</p>
            </div>

            <div class="text-center mb-4">
                <p class="text-3xl font-bold text-green-500"> {{ point }} ポイント</p>
            </div>

            <div class="text-left mb-4 text-gray-600">
                <p>注意事項</p>
                <ul class="list-disc list-inside">
                    <li>ポイントの付与や利用の際は、店舗でバーコードをスタッフにご提示ください。</li>
                </ul>
            </div>
        </template>

        <template v-else>
            <div class="text-center mb-4">
                <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                    @click="registerButtonClicked">
                    新規会員登録
                </button>
            </div>
        </template>
        <hr>

        <!-- LIFF上でエラーメッセージなどを確認するための領域。本番リリース時は非表示にする -->
        <div class="debug-console">
            <p class="mt-4">デバッグメッセージ:</p>
            <p class="text-gray-500">{{debugMessage}}</p>
        </div>
    </div>

    <script>
        // [変更必須] Craft FunctionsのエンドポイントURL
        const CRAFT_FUNCTIONS_END_POINT = 'https://xxxxx.cev2.karte.io/functions/xxxxxxxxxxxxxxxxxxxxxx';
        // [変更必須] LINE developersコンソールから取得したLIFF ID
        const LIFF_ID = '12345678-xxxxxxxx';
        // 検証用にダミーのLINE IDを利用する場合に設定
        const DUMMY_LINE_ID = '';

        const app = Vue.createApp({
            data() {
                return {
                    lineId: null,
                    memberId: null,
                    point: null,
                    isLoading: true,
                    debugMessage: 'デバッグコンソールです',
                };
            },
            // ページ読み込み時に、LIFFからLINE IDを受け取り対応する会員情報を取得
            async mounted() {
                try {
                    this.lineId = await this.getLineId();
                    const { member_id, point } = await this.fetchMemberInfo(this.lineId);
                    this.memberId = member_id;
                    this.point = point;
                    this.isLoading = false;
                    if (this.memberId) {
                        // 会員情報が取得できたらバーコードを表示
                        this.generateBarcode(this.memberId);
                    }
                } catch (error) {
                    this.debugMessage = `[mounted] error: ${error}`;
                }
            },
            methods: {
                // LIFFからlINE IDを取得
                async getLineId() {
                    if (DUMMY_LINE_ID) return DUMMY_LINE_ID;
                    await liff.init({ liffId: LIFF_ID });
                    try {
                        const profile = await liff.getProfile();
                        return profile.userId;
                    } catch (error) {
                        console.error('Failed to get LINE ID:', error);
                        this.debugMessage = `[getLineId] Failed to get LINE ID. error: ${error}`;
                        return null;
                    }
                },
                // 「新規会員登録」ボタン押下時、新規会員登録処理を実行してバーコードを表示
                async registerButtonClicked() {
                    try {
                        this.isLoading = true;
                        const { member_id, point } = await this.registerMember(this.lineId);
                        this.memberId = member_id;
                        this.point = point;
                        this.isLoading = false;
                        if (this.memberId) this.generateBarcode(this.memberId);
                    } catch (error) {
                        this.debugMessage = `[registerButtonClicked] error: ${error}`;
                    }
                },
                // Craft Functions経由でユーザー情報を取得
                async fetchMemberInfo(lineId) {
                    try {
                        const data = await this.fetchCraftFunctions(lineId, 'read');
                        this.debugMessage = `[fetchMemberInfo] fetch member info succeeded. member_id: ${data.member_id}`;
                        return data;
                    } catch (error) {
                        this.debugMessage = `[fetchMemberInfo] Failed to fetch member info. error: ${error}`;
                        return {};
                    }
                },
                // Craft Functions経由で新規会員登録処理を実行
                async registerMember(lineId) {
                    try {
                        const data = await this.fetchCraftFunctions(lineId, 'create');
                        this.debugMessage = `[registerMember] register member succeeded. member_id: ${data.member_id}`;
                        return data;
                    } catch (error) {
                        this.debugMessage = `[registerMember] Failed to register member. error: ${error}`;
                        return {};
                    }
                },
                async fetchCraftFunctions(lineId, action) {
                    const response = await fetch(CRAFT_FUNCTIONS_END_POINT, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify({
                            action,
                            line_id: lineId,
                        }),
                    });
                    const data = await response.json();
                    if (!response.ok) {
                        throw new Error(`error: ${data.error}. status: ${response.status}`);
                    }
                    return data;
                },
                // バーコードの表示処理を実行
                generateBarcode(memberId) {
                    this.$nextTick(() => {
                        try {
                            JsBarcode('#barcode', memberId, {
                                format: 'CODE128',
                                displayValue: true,
                                lineColor: '#000',
                                width: 2,
                                height: 80,
                                textAlign: 'center',
                                fontSize: 18
                            });
                        } catch(error) {
                            this.debugMessage = `[generateBarcode] error: ${error}`;
                        }
                    });
                },
            }
        }).mount('#app');
    </script>


</body>

</html>

このコードでは、主に次の処理が実装されています。

  • LIFFアプリの機能でユーザーのLINE IDを取得
  • そのLINE IDをCraft Functionsに送信し、対応する会員IDとポイント数を取得(read処理)
  • 会員IDが取得できた場合
    • 会員ID(+そのバーコード)とポイント数を画面に表示
  • 会員IDが取得できなかった場合(=404エラーの場合)
    • 「新規会員登録」ボタンを表示
    • 「新規会員登録」ボタンが押されたら、LINE IDをCraft Functionsに送信し、新規会員登録を実行(create処理)
    • 新規発行された会員ID(+そのバーコード)を画面に表示

要するに、LINEのLIFFアプリとして表示するWebサイトの機能として、LINE側とCraft Functions側の間をつなぐ部分の処理が実装されています。

3. LINEログインチャネルを作成する

LIFFアプリを登録するためのLINEログインチャネルを作成します。

  • LINE Developersコンソールにアクセス
  • 対象LINE公式アカウントに対応するLINE Messaging APIチャネルが属するプロバイダー配下に、LINEログインチャネルを作成
    • 各種設定は、検証用であれば適当に入力してください

4. LINEログインチャネルにLIFFアプリを追加する

作成したLINEログインチャネルにLIFFアプリを追加します。

  • LINE Developersコンソールで、作成したLINEログインチャネルの設定を開く

  • [LIFF > LIFFアプリ > 追加] をクリック

  • アプリの設定をして保存

    • エンドポイントURLに、Craft Sitesで作成したWebページのURLを設定します
    • Scopeに profile を必ず追加します
    • その他の設定は、検証用であれば適当に入力してください
  • 発行された LIFF ID をコピーし、もう一度Craft Sitesの管理画面を開いて、先ほど作成したサイトのindex.htmlにある定数の値に登録

5. リッチメニューのリンク先にLIFF URLを設定する

LINE公式アカウントとのトーク画面からLIFFアプリを開くために、リッチメニューのリンク先にLIFF URLを設定します。

検証で利用するLINE公式アカウントの LINE Offical Account Manager を開いて、リッチメニュー内の適当な箇所にLIFF URLへの導線を表示できるようにしてください。

詳細の手順は公式ドキュメントをご覧ください。

LINE公式アカウント (LINE Official Account Manager) リッチメニューを作成するマニュアル|LINEヤフー for Business

実際に動かしてみる

実際に動作を確認してみましょう。

  • LINEアプリから、検証で利用するLINE公式アカウントとのトーク画面を開く
  • リッチメニュー内のLIFF URLをタップして、LIFFブラウザを開く
  • 少し待つと、「新規会員登録ボタン」が表示される
  • 「新規会員登録ボタン」を押して少し待つと、会員登録が完了し会員IDが発行される
  • もう一度同じ手順でLIFFアプリを開き直すと、最新のポイント数(サンプルコードではランダム表示)の表示が確認できる

※ 画面上にある「デバッグメッセージ」は開発時のデバッグ目的でLIFFアプリ上に表示しています。実際にリリースする場合は、エンドユーザーに見えないように非表示にしてください

これで、簡単な「LINEデジタル会員証」アプリを実装することができました。

なお、上手く動かない場合は次の点を確認してみてください。

  • Craft Sitesで作成したindex.html内の定数の設定がすべて完了していることを確認
  • 画面上に表示された「デバッグメッセージ」を確認し、エラー原因の当たりを付ける
  • Craft Functionsのファンクション編集画面からログを確認し、エラー原因の当たりを付ける

おわりに

今回はKARTE Craftを使ってLIFFアプリを作成し、「LINEデジタル会員証」を実現する方法について紹介しました。

外部の「会員・ポイント管理システム」と連携するのが前提にはなりますが、KARTE Craftの管理画面から「LINEデジタル会員証」の画面表示を簡単に改修することができるようになります。

自由度の高い「LINEデジタル会員証」の構築を検討の際は、ぜひ試してみてください!