入店したらLINE内のボタン1つで即決済できるシステムを作ってみた

## 作ったもの

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">obnizを使って、LINE Beaconで仮想的にお店に入ったら即LINE Payでチョコ購入できるデモ <a href="https://t.co/Fv6cUJPPIa">pic.twitter.com/Fv6cUJPPIa</a></p>&mdash; るしわん/おの (@tkyko13) <a href="https://twitter.com/tkyko13/status/1342042009121345536?ref_src=twsrc%5Etfw">December 24, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 何が起きてる？

obnizで作ったLINE Beaconに反応してLINE Botから通知が来て、そのLINE Botのボタンを押すと即物が買えちゃうデモです。

即といってもLINE Payの確認画面には遷移するんですが…

## obnizでLINE Beacon

こちらの(自分の)記事を参考にしました！

[IFTTT有料化したので前回作ったなんちゃってポケモンGOをLINE Beaconで作ってみた - Qiita](https://qiita.com/tkyko13/items/88963b5eb313470b7a6c)

## line-pay-v3が動かない

https://github.com/tmitsuoka0423/line-pay-v3

こちらの方で動かせましたので、こちらを利用させていただいてます

```sh
npm i line-pay-v3 https://github.com/tmitsuoka0423/line-pay-v3
```

## いざ開発していく

### その他のライブラリのインストール

```sh
npm i dotenv obniz @line/bot-sdk express uuid
```

### obniz側のソースコード

```js
// おまじない
"use strict";

// .envファイルを読み込みます
require('dotenv').config();

const Obniz = require('obniz');
const obniz = new Obniz(process.env.OBNIZ_ID);
const LINE_BEACON_HID = process.env.LINE_BEACON_HID; //払い出されたID
const hardwareId = [];
// 16進数のByteArray型に変換
for (let i = 0; i < LINE_BEACON_HID.length / 2; i++) {
  hardwareId.push(parseInt(LINE_BEACON_HID.substr(i * 2, 2), 16));
}

obniz.onconnect = async function () {
  console.log('on connect!');

  const deviceMessage = [0x01, 0x02, 0x10]; //サーバーに送るメッセージ　1~13byte

  const UUID_FOR_LINECORP = [0x6F, 0xFE];
  // flag
  let adv = [];
  adv = adv.concat([0x02, 0x01, 0x06]);  //LE General Discoverable Mode & BR/EDR Not Supported

  //16bit uuid
  adv = adv.concat([0x03, 0x03]);
  adv = adv.concat(UUID_FOR_LINECORP);

  //simple beacon
  adv = adv.concat([1 + 9 + deviceMessage.length, 0x16]);
  adv = adv.concat(UUID_FOR_LINECORP);
  adv = adv.concat([0x02]);
  adv = adv.concat(hardwareId);
  adv = adv.concat([0x7F]);
  adv = adv.concat(deviceMessage);

  await obniz.ble.initWait()
  obniz.ble.advertisement.setAdvDataRaw(adv);
  obniz.ble.advertisement.start();
  console.log('start beacon!');
}
```

## LINE Bot側のソースコード

```js
'use strict';

// .envファイルを読み込みます
require('dotenv').config();

// パッケージを使用します
const { v4: uuidv4 } = require('uuid');
const LinePay = require('line-pay-v3');
const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

// 注文のデータを一時的に保管します
const orderData = {};

// line-pay-v3を使用する準備
const pay = new LinePay({
  channelId: process.env.LINE_PAY_CHANNEL_ID,
  channelSecret: process.env.LINE_PAY_CHANNEL_SECRET,
  uri: process.env.ENV === 'production' ? 'https://api-pay.line.me' : 'https://sandbox-api-pay.line.me'
});

// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
  channelSecret: process.env.LINE_BOT_CHANNEL_SECRET,
  channelAccessToken: process.env.LINE_BOT_ACCESS_TOKEN
};

const app = express();

// publicフォルダのファイルを公開する
app.use(express.static(__dirname + "/public"));

const client = new line.Client(config);

async function handleEvent(event) {
  if (event.type === 'message' && event.message.type === 'text') {
    if (event.message.text === 'チョコレート') {
      orderChocolate(event);
    }
    else {
      // テキストがきたらオウムがえし
      return client.replyMessage(event.replyToken, {
        type: 'text',
        text: event.message.text
      });
    }
  } else if (event.type === 'beacon' && event.beacon) {
    // ビーコン用
    console.log(`beacon device(${event.beacon.hwid})から${event.beacon.dm}が届いたよ`)

    orderChocolate(event);

    // return client.replyMessage(event.replyToken, {
    //   type: 'text',
    //   text: '入店ありがとうございます！'
    // });
  }
  // テキストでもビーコンでもなければ無視
  return Promise.resolve(null);
}

// チョコレートを買う予約処理
async function orderChocolate(event) {
  console.log('決済予約処理を実行します。');

  // 商品名や値段を設定する場合はここを変更します。
  const order = {
    amount: 100, // packages[].amountの合計金額を記入する
    currency: 'JPY',
    orderId: uuidv4(),
    packages: [
      {
        id: 'Item001',
        amount: 100, // products[].priceの合計金額を記入する
        name: '買い物かご',
        products: [
          {
            name: 'チョコレート', // 商品名
            imageUrl: 'https://2.bp.blogspot.com/-zEtBQS9hTfI/UZRBlbbtP8I/AAAAAAAASqE/vbK1D7YCNyU/s800/valentinesday_itachoco2.png', // 商品画像
            quantity: 1, // 購入数
            price: 100 // 商品金額
          }
        ]
      }
    ],
    redirectUrls: {
      confirmUrl: `${process.env.NGROK_URL}/pay/confirm`,
    }
  };
  console.log('以下のオプションで決済予約を行います。');
  console.log('order', order);

  try {
    // LINE Pay APIを使って、決済予約を行う。
    const response = await pay.request(order);
    console.log('response', response);

    // 決済確認処理に必要な情報を保存しておく。
    order.userId = event.source.userId;
    orderData[order.orderId] = order;

    const message = {
      type: "template",
      altText: `チョコレートを購入するには下記のボタンで決済に進んでください`,
      template: {
        type: "buttons",
        text: `チョコレートを購入するには下記のボタンで決済に進んでください`,
        actions: [
          { type: "uri", label: "LINE Payで決済", uri: response.info.paymentUrl.web },
        ]
      }
    }
    await client.replyMessage(event.replyToken, message);
    console.log('決済予約が完了しました。');
  } catch (e) {
    console.log('決済予約でエラーが発生しました。');
    console.log(e);
  };
}

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)

// 以下replay message用
app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);

  if (req.body.events.length == 0) {
    res.send('Hello LINE BOT!(POST)');
    console.log('疎通確認用');
    return;
  }

  Promise.all(req.body.events.map(handleEvent)).then((result) =>
    res.json(result)
  );
});

// 決済確認処理
app.use('/pay/confirm', async (req, res) => {
  console.log('/pay/confirmの処理を実行します。');

  // 決済予約時に保存した情報を取り出す。
  const orderId = req.query.orderId;
  if (!orderId) {
    throw new Error('Order ID is not found');
  }
  const order = orderData[req.query.orderId];
  if (!order) {
    throw new Error('Order is not found');
  }

  // 決済確認処理に必要なオプションを用意する。
  const option = {
    amount: order.amount,
    currency: order.currency
  }
  console.log('以下のオプションで決済確認を行います。');
  console.log(option);

  try {
    // LINE Pay APIを使って、決済確認を行う。
    await pay.confirm(option, req.query.transactionId)

    await client.pushMessage(order.userId, {
      type: 'text',
      text: '決済が完了しました。'
    });

    res.send('決済が完了しました。');

    console.log('決済が完了しました。');
  } catch (e) {
    console.log('決済確認処理でエラーが発生しました。');
    console.log(e);
  };
});

app.listen(PORT);
console.log(`Server running at ${PORT}`);
```

## .envファイルの作成

```
LINE_PAY_CHANNEL_ID=
LINE_PAY_CHANNEL_SECRET=
LINE_BOT_CHANNEL_SECRET=
LINE_BOT_ACCESS_TOKEN=
NGROK_URL=
OBNIZ_ID=
LINE_BEACON_HID=
```

それぞれの値の取得方法は参考サイトからお願いします

## まとめ

obnizに挿しているセンサーからのデータや、今日の天気や気温などからおすすめの商品を渡せたりしたらもっと便利ですね！