Node 環境(Vue)で Amazon Payの実装
May 26, 2020

DEVELOPMENT

web
js
aws
api-gateway
amplify
vue
lambda
python
amazon-pay

はじめに

オンライン決済で有名どころはStripeがありますが、

今回は、あまり有名でない(小さい会社等の)ショッピングサイトで、購入者がカード入力するハードルを考え、

大手ショッピングサイトの決済機能を使ってみようと言うことで、

を導入しようと考えました。

これらを導入することで、サイト自体でのログインを不要とし、 ブラウザ上で上記3社のどこかなら、よく使っているであろうというこで、

購入の手続きのハードルを下げたい

と考えました。その中で、

まずは最大手Amazonの決済代行サービスAmazonPayを組み込んでみました。

結構というかかなり苦労したので、内容を紹介したいと思います。

AmazonPayって?

引用します。▶ Amazon Payとは

Amazon Payは、Amazon.co.jpのアカウントに登録されている住所情報とクレジットカード情報を使用して、Amazon.co.jp以外のサイトで支払いができるサービスです。Amazon Payを使用すると、Amazon Payに対応しているサイト(Amazon.co.jp以外のサイト)で、これらの情報を再入力することなく、簡単に安心して購入できます。Amazon Payを使用するための追加費用はかかりません。

ということで、サイト内に「Amazonで購入ボタン」を設置し、クリックすると、Amazonで未ログインであれば、ログイン後、サイトに戻り、Amazonに登録済みのお支払い方法や、送付先住所を選択し、決済を完了できるという仕組みで、普段Amazonを使っているユーザーならすんなり外部サイトで購入が可能になるということです。

組み込む環境

  • Node環境にて、Vue-Cliを使用
  • AmazonPayのクライアント側(ボタン配置や住所、カード選択ページ)は、Vueのアプリへ組み込む
  • 決済処理はサーバーサイドですが、AWSAPI Gatewayと、Lambdaを使用で、API GatewayへのアクセスはCognitoのプールIDを気休めに使用
  • Lambda関数内での言語は、Pythonを使用。AmazonPayの決済処理のSDKは、他には、

残念ながら、Nodejsはない(2020年5月現在)

悩ましい点

  • まずクライアント側のボタンの表示をするために、weget.jsが用意されており、nodeパッケージがない
  • Amazon payのwidgetは、window functionとして実装されており、コレをVueやReactの、Componentライフサイクル内でどうやって呼べばいいかわからない

    • ▶ Vueはないが、ReactはComponentがあるみたいだった(amazon-pay-react
  • サーバーサイドはAWSだが、Amplifyを使い、APIを作成すべきかどうか悩んだが、AmplifyREST APIを作成すると、CognitoのPoolIDでの認証を追加できなそうなので、今回はAmplifyは使用せず、手動で作成した

決済フロー

  • 商品ページに購入ボタンを設置し、クリック
  • amazon側のページへ遷移し、ログイン認証後、コールバックURLへ

手順

Amazon Pay申し込み・登録

  • Amazon Payから利用申し込みを行います。

    • 審査がありますが、実際まだ未稼働のURLを指定しても通りました。
  • 審査完了後メールが来ますので手順に沿って、

クライアントサイドの実装(Vuejsに組み込む)

  • Widget.jsをロードする
  • src/main.js(vue-headを使うなら、)

    // load amazon pay wedget.js
    head.script.push({
    src: 'https://static-fe.payments-amazon.com/OffAmazonPayments/jp/sandbox/lpa/js/Widgets.js',
    head: true,
    async: true
    })
  • headタグ内で読ませればよい

amazon pay configファイルを用意

  • src/以下に、amazon-pay-config.js作成
const amazonPayConfig = {
  clientId: "xxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxx",
  marchantId: "xxxxxxx",
  redirectUri: "http://localhost:8080/xxxxxxxxx/",// 購入ボタン後リダイレクトしてくるURL
  scope: "profile postal_code payments:widget payments:shipping_address",
  endpoint: "xxxxxxxxxxxxxxxxx" // ここは後ほど購入処理のために設置するAPI Gatewwayのendpointです
}

export default amazonPayConfig

商品ページで購入ボタンを表示する

  • 購入ページのVueコンポーネント
  • 注意!!Routerなどでこのページに飛んで来た時(ページの再読み込みなしで)、window.onAmazonLoginReadyこれらのメソッドがコールされず、ボタンが表示されません。 とりあえず暫定として、vue routerなどで遷移の際、ページがロードされるようにしてます(Window loationとかで(あしからず))。

    • 他にいい方法やrouterでの遷移での解決策があれば教えて下さい。
<template>
  <div id="AmazonPayButton" />
</template>

<script>
import amazonPayConfig from '../amazon-pay-config'
import store from '../store'

export default {
  ~
  ~
  mounted() {
    this.loadAmazonPay()
  },
  methods: {
    loadAmazonPay() {
      // VueのComponentのロードが終わったら
      this.$nextTick(() => {
        try {
          let self = this
          window.onAmazonLoginReady = function(){
            amazon.Login.setClientId(amazonPayConfig.clientId);
          },
          window.onAmazonPaymentsReady = function() {
            self.amazonPayShowButton()
          }
        } catch (error) {
          console.error(error);
        }
      });
    },
    amazonPayShowButton: function() {
      const self = this
      var authRequest;
      // ボタンDesignなど
      OffAmazonPayments.Button("AmazonPayButton", amazonPayConfig.marchantId, {
        type: "PwA",
        color: "Gold",
        size: "large",
        language: "jp",
        authorization: function () {
          const loginOptions = {
            scope: amazonPayConfig.scope,
            popup: true, interactive: 'always'
          };
          // 決済に必要なデータはstoreのlocalstrageに入れた
          ....

          // ここでGetパラメータつけてもいい
          const url = amazonPayConfig.redirectUri + "xxxxxxxx"
          authRequest = amazon.Login.authorize(loginOptions, url);
        },
        onError: function(error) {
          // error handling
          console.log(error)
        }
      })
    }
  }
}
</script>

決済確認ページ

  • AmazonPayで購入ボタンを押されログイン後、Amazon側から返ってくるページです。
  • このページはボタン表示時にamazon.Login.authorizeで指定しているURLとなります
  • ここでは以下の内容をAmazonから取得し(widgetで)表示し、選択後、購入決定を押すことにより、サーバーサイドで実際の購入処理がされ決済完了となります

    • 送信先の住所
    • 決済するクレジットカード
  • 例 confirm.vue
<template>
~
~
// class等で、widthやheightを必ず指定する
<div id="walletWidgetDiv" class="amazonPayWidget"></div>

<div id="addressBookWidgetDiv" class="amazonPayWidget"></div>
~
~
</templete>
<script>
~
export default {
  name: "Confirm",
  data () {
    return {
      ~
      orderReferenceId: null,
      loading: false
      ~
    }
  },
  mounted() {
    this.loadAmazonPay()
  },
  methods: {
    goBack() {
      // これで戻って再ロードさせる
      this.$router.go(-1)
    },
    loadAmazonPay() {
      this.$nextTick(() => {
        try {
          // selfにいれておく 下でthisは使えなくなるため
          let self = this
          window.onAmazonLoginReady = function(){
            amazon.Login.setClientId(amazonPayConfig.clientId);
          },
          window.onAmazonPaymentsReady = function() {
            self.showAddressBookWidget()
          }
        } catch (error) {
          console.error(error);
        }
      });
    },
    showAddressBookWidget() {
      // selfにいれておく 下でthisは使えなくなるため
      let self = this
      new OffAmazonPayments.Widgets.AddressBook({
        sellerId: amazonPayConfig.marchantId,

        onReady: function (orderReference) {
          self.orderReferenceId = orderReference.getAmazonOrderReferenceId();

          //アドレス帳ウィジェット表示確認後にお支払いウィジェットを表示する
          self.showWalletWidget(self.orderReferenceId);
        },
        onAddressSelect: function (orderReference) {
        },
        design: {
          designMode: 'responsive'
        },
        onError: function (error) {
          // エラー処理 セッション切れ
          // @see https://payments.amazon.com/documentation/lpwa/201954960
          console.log('OffAmazonPayments.Widgets.AddressBook', error.getErrorCode(), error.getErrorMessage());
          self.$router.push("/")
        }
      }).bind("addressBookWidgetDiv");
    },
    showWalletWidget(orderReferenceId) {
      new OffAmazonPayments.Widgets.Wallet({
        sellerId: amazonPayConfig.marchantId,
        amazonOrderReferenceId: orderReferenceId,
        onReady: function(orderReference) {
          console.log(orderReference.getAmazonOrderReferenceId());
        },
        onPaymentSelect: function() {
          console.log(arguments);
        },
        design: {
          designMode: 'responsive'
        },
        onError: function(error) {
          // エラー処理 セッション切れ
          // @see https://payments.amazon.com/documentation/lpwa/201954960
          console.log('OffAmazonPayments.Widgets.Wallet', error.getErrorCode(), error.getErrorMessage());
        }
      }).bind("walletWidgetDiv");
    },
    async payment() {
      this.loading = true
      try {
        // ここでサーバーサイドへ決済処理をリクエスト
        const response = await fetch(amazonPayConfig.endpoint + "amazon", {
          method: 'POST',
          body: JSON.stringify({
            orderReferenceId: this.orderReferenceId,
            ~
            // 決済処理や決済メールに使うデータを送る
            // storeのlocalstrageから取ったり getで送ったならgetパラメータから取ったり
            total: this.total,
          }),
          //headers: {}
        })
        console.log(response)
        if (response.ok) {
          console.log("OK")
          const data = await response.json();
          console.log(data)
        }
      }
      catch (e) {
        console.log("error!!!")
        console.log(e)
      }
    }
  }
}
~
~
</script>

サーバーサイド

mkdir lambda_prj
cd lambda_prj
  • 必要なライブラリを入れる

    $ pip install install amazon_pay -t .
  • -t . で、ここに入れるという意味
  • localで開発ように環境変数を扱えるようにする

    $ pip install python-dotenv -t .
  • .envファイル作成
IS_LOCAL=true
MARCHANT_ID=xxxxxxx
ACCESS_KEY=xxxxxxxxxxx
ACCESS_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
  • lambda_function.pyを作成
import json
from datetime import datetime
from amazon_pay.client import AmazonPayClient

import os
import random, string

# ちゃんとやればいいけど、取り急ぎ。ここはローカルで試すときにコメントイン
#from dotenv import load_dotenv
#load_dotenv(verbose=True)

def lambda_handler(event, context):

    if "pathParameters" in event and event['pathParameters']['name'] == 'amazon':
        return proccess_amazonpay(event)
    else:
        raise Exception('400')

def proccess_amazonpay(event):

    body = json.loads(event['body'])

    if "orderReferenceId" in body and body['orderReferenceId'] == True:
        raise Exception('401')

    referenceId = body['orderReferenceId']

    # (1) Clientインスタンスを作成
    client = AmazonPayClient(
        mws_access_key=os.environ.get("ACCESS_KEY"),
        mws_secret_key=os.environ.get("ACCESS_SECRET"),
        merchant_id=os.environ.get("MARCHANT_ID"),
        region='jp',
        currency_code='JPY',
        sandbox=True)

    # (2) 注文情報をセット
    total = body['total']

    if total == False or item == False:
        raise Exception('401')

    orderId = create_order_id() # 任意のIDを生成
    note = "購入者へのメールで表示される文章"

    ret = client.set_order_reference_details(
        amazon_order_reference_id=referenceId,
        order_total=total,
        seller_note=note,
        seller_order_id=orderId,
        store_name="なんとかショッピング",
        custom_information="なんでも",
        merchant_id=os.environ.get("MARCHANT_ID"))

    json_response = json.loads(ret.to_json())
    print("set_order_reference_details")
    print(json_response)

    if ret.success == False:
        raise Exception('500')

    # (3) 注文情報を確定
    ret = client.confirm_order_reference(
        amazon_order_reference_id=referenceId)

    print("confirm_order_reference")
    print(ret.to_json()) # to_xml and to_dict are also valid

    if ret.success == False:
        raise Exception('500')

    # ここは運用次第だが、商品を発送後、管理画面より、注文を確定するのでもいいかもしれない
    # (4) オーソリをリクエスト
    orderId = create_order_id() + "01"
    ret = client.authorize(
        amazon_order_reference_id=referenceId,
        authorization_reference_id=orderId,
        authorization_amount=total,
        seller_authorization_note=note,
        transaction_timeout=0,
        capture_now=False)
    json_response = ret.to_json()

    print("authorize")
    print(json.loads(json_response))

    if "ErrorResponse" in json.loads(json_response):
        raise Exception('500')

    # authorization ID returned from 'Authorize' call.
    authorization_id = json.loads(json_response)['AuthorizeResponse']['AuthorizeResult']['AuthorizationDetails']['AmazonAuthorizationId']

    print("authorization_id")
    print(authorization_id)

    if authorization_id == False:
        raise Exception('500')

    # (5) 注文を確定
    orderId = create_order_id() + "02"
    ret = client.capture(
        amazon_authorization_id=authorization_id,
        capture_reference_id=orderId,
        capture_amount=total,
        seller_capture_note=note)

    print("capture")
    print(ret.to_json())

    json_response = ret.to_json()

    # 注文の確定に失敗したらオーソリを取り消して、注文をクローズする
    if ret.success == False:
        ret = client.cancel_order_reference(
            amazon_order_reference_id=referenceId,
            merchant_id=os.environ.get("MARCHANT_ID"))

        print(ret.to_json()) # to_xml and to_dict are also valid

        ret = client.close_authorization(
            amazon_order_reference_id=referenceId,
            merchant_id=os.environ.get("MARCHANT_ID"))

        print(ret.to_json()) # to_xml and to_dict are also valid

        raise Exception('500')

    return success_response()

def success_response():
    return {
        'headers': {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'application/json'
        },
        'statusCode': 200,
        'body': json.dumps("購入処理が完了しました。準備が整い次第、発送のご連絡を致します。")
    }

def create_order_id():
    randlst = [random.choice(string.ascii_letters + string.digits) for i in range(10)]
    return ''.join(randlst) + datetime.now().strftime("%Y%m%d%H%M%S")

if os.environ.get("IS_LOCAL") == True:
    event = {
        "pathParameters": {
            'name': 'amazon'
        },
        "body": {
            "orderReferenceId": "コールバックで返ってきた際に取得できるID",
            "total": "1000",
        }
    }
    # lambda上では、json loadする
    lambda_handler(event, "")
  • dockerとかでlocalで環境をつくればいいが、とりあえずのlocalデバッグコード入りです
  • オーソリや、請求のタイミングは運用方法で変更すべきです。
  • こちらのコードだと、購入リクエストから、オーソリ、請求まで一気に行っていますが、もし、すぐに発送できなかったりするのであれば、オーソリ以下は、商品発送時に行うべきだと考えます。
  • zipして、lambdaにUPロードする

    $ zip -r upload.zip *
  • Lambda上にも環境変数をセットする
  • 上記をupload

まとめ

いかがでしたか?Vueなどのnode環境でのAmazon Payの記事はとても少なく、クライアント側の実装は苦労しましたが、何とか実装できました。 是非VuejsにAmazonPayを組み込みたいかたのお役に立てればと思います! ツッコミどころやこうしたがいいなど、下のform、Slackもしくは、Twitter等でご指摘いただけると嬉しいです。

参考

うかい / 株式会社UKAI
うかい@代表取締役兼エンジニア株式会社UKAI
Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから

🙏よかったらシェアお願いします🙏
WRITTEN BY
うかい / 株式会社UKAI
うかい@代表取締役兼エンジニア株式会社UKAI

Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから

CONACT
入力して下さい
Slack からもどうぞ

※お気軽にどうぞ!(6月20日まで有効!お早めに)

COPYRIGHT © 2020 UKAI CO., LTD. ALL RIGHTS RESERVED.