Webサービスを作っていてAmazonPayを導入することになり、フロントはReact で FirebaseのCloudFunctions(Node.js)で実装しました。
AmazonPayのSDKはPHPやPythonなどは用意されているのですが、Node.jsだけなかったので自前実装することになりました。Amazonさんの公式ドキュメントを読んだだけではつまづきやすい点が多かったので備忘録として残しておきます。
今回実装したのはワンタイムペイメント(通常購入)のみです。
この記事の目次
事前準備
AmazonPayを利用するには公式サイトから利用登録、審査が必要です。とくに審査は厳しくありません。
実装
登録が完了したら、セルラーセントラルにログインしてSellerID(出品者ID)、アクセスキー、シークレットキー、クライアントIDを取得することができます。
課金テスト用アカウントの作成も忘れずに行ってください。
フロント側(React)
index.htmlのheadにAmazonPayのボタンウィジェトを表示するための以下のコードを追加する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
<!-- Amazon pay --> <meta name="viewport" content="width=device-width,initial-scale=1.0, maximum-scale=1.0" /> <script async="async" src="https://static-fe.payments-amazon.com/OffAmazonPayments/jp/sandbox/lpa/js/Widgets.js" ></script> <script type="text/javascript"> //オーダーID window.orderReferenceId; //エラー window.amazonPayError; window.onAmazonLoginReady = function () { amazon.Login.setClientId( "<You're Client ID>" ); }; window.onAmazonPaymentsReady = function () { self.showAddressBookWidget(); }; window.onAmazonPaymentsReady = function () { window.showButton = function (onError) { var authRequest; OffAmazonPayments.Button("AmazonPayButton", "<You're SellerId>", { type: "PwA", color: "Gold", size: "large", language: "jp", authorization: function () { loginOptions = { scope: "profile postal_code payments:widget payments:shipping_address", popup: true, interactive: "always", }; authRequest = amazon.Login.authorize( loginOptions, "%PUBLIC_URL%/pay-with-amazon/" ); }, onError: function (error) { onError(error_message); }, }); }; window.logout = function () { amazon.Login.logout(); }; window.showWalletWidget = function () { new OffAmazonPayments.Widgets.Wallet({ sellerId: "<You're SellerId>", onReady: function (billingAgreement) { }, onOrderReferenceCreate: function (orderReference) { window.orderReferenceId = orderReference.getAmazonOrderReferenceId(); }, design: { designMode: "responsive", }, onPaymentSelect: function (billingAgreement) { }, onError: function (error) { window.amazonPayError = getErrorCode(); }, }).bind("walletWidgetDiv"); }; }; </script> <!-- Amazon pay --> |
次にAmazonPayのボタンを表示するコンポーネントを作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React, { useEffect } from "react"; declare const window: any; export const AmazonPayButton: React.FC = () => { const onError = (error_message: string) => { // this.setState({ error: true, error_message }); console.log(error_message); }; useEffect(() => { window["showButton"](onError); }, []); return <div id="AmazonPayButton"></div>; }; |
このコンポーネント使えば、AmazonPayボタンが表示される。
バックエンド側(CloudFunctions [Node.js + TypeScritpt])
リクエストを送信するクラスを作成する。
AmazonPayにリクエストを送る際には署名が必要になるので、シグネチャーを作成して付与する必要があります。シグネチャーの作成部分は、ドキュメントを見てもかなり分かりにくいので、参考になればと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
const rp = require("request-promise"); const parser = require("xml2json"); const crypto = require("crypto"); import * as admin from "firebase-admin"; export class AmazonPayRequest { static post(data: { values: Map<string, any>; }): Promise<{ [index: string]: any }> { const postValues: { [index: string]: any; } = {}; //タイムスタンプ const timestamp: string = admin.firestore.Timestamp.now() .toDate() .toISOString(); //共通データ data.values.set("AWSAccessKeyId", "<You're Access Key>"); data.values.set("SellerId", "<You're SellerId>"); data.values.set("SignatureMethod", "HmacSHA256"); data.values.set("SignatureVersion", "2"); data.values.set("Timestamp", timestamp); data.values.set("Version", "2013-01-01"); //シグネチャーの作成 const signature = AmazonPayRequest.createSignature({ action: data.values.get("Action"), orderReferenceId: data.values.get("AmazonOrderReferenceId"), timestamp: timestamp, values: data.values, }); data.values.set("Signature", signature); data.values.forEach((value: any, key: string) => { postValues[key] = value; }); const options = { method: "POST", uri: "https://mws.amazonservices.jp/OffAmazonPayments_Sandbox/2013-01-01/", timeout: 30 * 1000, headers: { Host: "mws.amazonservices.jp", "User-Agent": `node.js/${process.versions.node}; ${process.platform}`, "Content-Type": "application/x-www-form-urlencoded", }, form: postValues, }; const xmlOptions = { object: false, }; return rp(options) .then((response: any) => { console.log(response); //xml形式で返ってくるのでJsonでParseする const jsonData = parser.toJson(response, xmlOptions); const _response: { [index: string]: any } = JSON.parse(jsonData); console.log(_response); return _response; }) .catch((error: any) => { console.log(error); const jsonData = parser.toJson(error.error, xmlOptions); const errorResponse: { [index: string]: any } = JSON.parse(jsonData); const _error = { statusCode: error.statusCode, errorCode: errorResponse.ErrorResponse.Error.Code, detail: error.message, }; console.log(_error); throw _error; }); } /** * シグネチャーの作成 * 詳細は以下 * https://m.media-amazon.com/images/G/09/AmazonPayments/Signature.pdf * @param data */ static createSignature(data: { action: string; orderReferenceId: string; timestamp: string; values: Map<string, any>; }) { const text: string = "POST\nmws.amazonservices.jp\n/OffAmazonPayments_Sandbox/2013-01-01\n".replace(/¥n/g, "\n"); const keys: string[] = []; data.values.forEach((value: any, key: string) => { keys.push(key); }); //ソートして連結 const sortedValue = keys .sort((a, b) => { return a == b ? 0 : a < b ? -1 : 1; }) .map((key) => { return key + "=" + encodeURIComponent(data.values.get(key)); }) .join("&"); //ベースとなる文字列を作成 const str: string = text + sortedValue; //sha256、amazonのシークレットキーでハッシュ化 const signature = crypto .createHmac("sha256", "<You're Secret Key>") .update(str) .digest("base64"); return signature; } } |
ワンタイムペイメントで必要なAPIのUsecaseを作成する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
import { AmazonPayRequest } from "../amazon-pay-request"; export interface ResultOfCapture { amazonCaptureId: string; } export interface ResultOfAuthorize { amazonAuthorizationId: string; } export class AmazonPayPaymentUseCase { /** * 1.注文をセット * https://developer.amazon.com/ja/docs/amazon-pay-api/setorderreferencedetails.html * @param data */ static setOrderReferenceDetails(data: { amazonOrderReferenceId: string; amount: number; }): Promise<void> { const values: Map<string, any> = new Map<string, any>(); values.set("Action", "SetOrderReferenceDetails"); values.set("AmazonOrderReferenceId", data.amazonOrderReferenceId); values.set("OrderReferenceAttributes.OrderTotal.Amount", data.amount); values.set("OrderReferenceAttributes.OrderTotal.CurrencyCode", "JPY"); console.log("post amazonpay SetOrderReferenceDetails"); return AmazonPayRequest.post({ values: values, }) .then((valueDict: { [index: string]: any }) => { console.log(valueDict); }) .catch((error) => { console.log("failed to amazon pay setOrderReferenceDetails"); throw error; }); } /** * 2.注文情報を確定 * https://developer.amazon.com/ja/docs/amazon-pay-api/confirmorderreference.html * @param data */ static confirmOrderReference(data: { amazonOrderReferenceId: string; }): Promise<void> { const values: Map<string, any> = new Map<string, any>(); values.set("Action", "ConfirmOrderReference"); values.set("AmazonOrderReferenceId", data.amazonOrderReferenceId); return AmazonPayRequest.post({ values: values, }) .then((valueDict: { [index: string]: any }) => { console.log(valueDict); }) .catch((error) => { console.log("failed to amazon pay confirmOrderReference"); throw error; }); } /** * 3.オーソリをリクエスト * https://developer.amazon.com/ja/docs/amazon-pay-api/authorize.html * @param data */ static authorize(data: { amazonOrderReferenceId: string; amount: number; authorizationReferenceId: string; }): Promise<ResultOfAuthorize> { const values: Map<string, any> = new Map<string, any>(); values.set("Action", "Authorize"); values.set("AmazonOrderReferenceId", data.amazonOrderReferenceId); values.set("AuthorizationAmount.Amount", data.amount); values.set("AuthorizationAmount.CurrencyCode", "JPY"); values.set("AuthorizationReferenceId", data.authorizationReferenceId); values.set("TransactionTimeout", 0); console.log("post amazonpay Authorize"); return AmazonPayRequest.post({ values: values, }) .then((valueDict: { [index: string]: any }) => { const result: ResultOfAuthorize = { amazonAuthorizationId: valueDict.AuthorizeResponse.AuthorizeResult.AuthorizationDetails .AmazonAuthorizationId, }; return result; }) .catch((error) => { console.log("failed to amazon pay authorize"); throw error; }); } /** * 4.売上請求 * https://developer.amazon.com/ja/docs/amazon-pay-api/capture.html */ static capture(data: { amazonAuthorizationId: string; amount: number; captureReferenceId: string; }): Promise<ResultOfCapture> { const values: Map<string, any> = new Map<string, any>(); values.set("Action", "Capture"); values.set("CaptureAmount.Amount", data.amount); values.set("CaptureAmount.CurrencyCode", "JPY"); values.set("AmazonAuthorizationId", data.amazonAuthorizationId); values.set("CaptureReferenceId", data.captureReferenceId); console.log("post amazonpay Capture"); return AmazonPayRequest.post({ values: values, }) .then((valueDict: { [index: string]: any }) => { console.log(valueDict); const result: ResultOfCapture = { amazonCaptureId: valueDict.CaptureResponse.CaptureResult.CaptureDetails .AmazonCaptureId, }; return result; }) .catch((error) => { console.log("failed to amazon pay capture"); throw error; }); } /** * 購入確定の失敗時に注文のクローズ * https://developer.amazon.com/ja/docs/amazon-pay-api/closeauthorization.html * @param data */ static closeAuthorization(data: { amazonAuthorizationId: string; }): Promise<void> { const values: Map<string, any> = new Map<string, any>(); values.set("Action", "CloseAuthorization"); values.set("AmazonAuthorizationId", data.amazonAuthorizationId); console.log("post amazonpay CloseAuthorization"); return AmazonPayRequest.post({ values: values, }) .then((valueDict: { [index: string]: any }) => { console.log(valueDict); }) .catch((error) => { console.log("failed to amazon pay authorize"); throw error; }); } } |
あとは順番にAPIを叩いていけば、支払いの確定まですることが可能です。
支払い確定(capture)時に失敗をした場合は、オーソリをクローズしてあげてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
export const amazonPayApi = async (data: { oderReferenceId: string; amount: number; }) => { try { //注文のセット try { await AmazonPayPaymentUseCase.setOrderReferenceDetails({ amazonOrderReferenceId: data.oderReferenceId, amount: data.amount, }); } catch (error) { console.log("failed to amazon pay setOrderReferenceDetails"); throw error; } //注文の確定 try { await AmazonPayPaymentUseCase.confirmOrderReference({ amazonOrderReferenceId: data.oderReferenceId, }); } catch (error) { console.log("failed to amazon pay confirmOrderReference"); throw error; } //オーソリのリクエスト let authorize: ResultOfAuthorize; try { //32文字以下にしか対応しない為、UUIDのハイフンは削除する const authorizationReferenceId: string = uuidv4().replace(/-/g, ""); authorize = await AmazonPayPaymentUseCase.authorize({ amazonOrderReferenceId: data.oderReferenceId, amount: data.amount, authorizationReferenceId: authorizationReferenceId, }); } catch (error) { console.log("failed to amazon pay authorize"); throw error; } //支払い確定 try { //32文字以下にしか対応しない為、UUIDのハイフンは削除する const captureReferenceId: string = uuidv4().replace(/-/g, ""); const capture: ResultOfCapture = await AmazonPayPaymentUseCase.capture({ amazonAuthorizationId: authorize.amazonAuthorizationId, amount: data.amount, captureReferenceId: captureReferenceId, }); console.log(capture); } catch (error) { console.log("failed to amazon pay capture"); //支払い失敗時にオーソリをクローズ await AmazonPayPaymentUseCase.closeAuthorization({ amazonAuthorizationId: authorize.amazonAuthorizationId, }); throw error; } return Promise.resolve(); } catch (error) { throw error; } }; |
本番環境と開発環境でURIやシグネチャー作成時のテキストが違うのでConfigファイルを作って切り替えてあげてください。
コメント