PayPal のREST APIで、ログインやEC決済を試してみる

PayPal のREST APIで、ログインやEC決済を試してみる:

PayPalは、EC向けにいろんなツール類を用意してくれているのですが、ドキュメントが散在してわかりにくかったり、古いものから新しいものまでごちゃまぜになっている印象があります。

そんな中で、Node.jsの勉強のためもあり、REST APIを使ってみたいと思います。

割と今風で、マルチプラットフォームに対応できそうなので、これを選びました。

(これに慣れたら、最新のBraintreeに移行したいと思っています。)

(情報源)

PayPal Developer

 https://developer.paypal.com/

PayPal API Reference

 https://developer.paypal.com/docs/api/overview/

node.js SDK for PayPal RESTful APIs

 https://github.com/paypal/PayPal-node-SDK

Connect With PayPal

 https://developer.paypal.com/docs/integration/direct/identity/

他の決済サーバと同じように、自由にいじれるSandboxがあるので、気兼ねなく試してみましょう。

毎度のことですが、Swaggerを使ったRESTfulサーバを立ち上げます。

以下ご参考まで。

SwaggerでRESTful環境を構築する


PayPal Developerアカウントの作成

まずは、以下のページからアカウントを作成しましょう。

PayPal Developer

 https://developer.paypal.com/



image.png


まず見ていただきたいのが、Sandbox Accountです。

左側のナビゲータから選択します。



image.png


あらかじめ2つのアカウントができているかと思います。

 XXXXX-facilitator@YYYYY.yyy BUSINESS

 XXXXX-buyer@YYYYY.yyy PERSONAL

これらアカウントは、PayPal Sandboxの中だけのアカウントですので、メールアドレスは実在しませんし、本番PayPalには使えません。
XXXXX-buyer@YYYYY.yyy は、あとでログインに使うので、パスワードを変えておきましょう。


REST APIを使うための準備

いくつかの種類がありますが、今回使うのは、REST API appsです。

「Create App」ボタンを押下します。

App Nameには、適当な名前を付けます。例えば、「TestApp」とします。

Sandbox developer account には、BUSINESSのアカウントを指定するのですが、今回は、あらかじめ作られていた XXXXX-facilitator@YYYYY.yyy を選択します。



image.png


そうすると、Client IDやSecretが表示されます。

後で使うので、覚えておきます。



image.png


また、右上のところで、SandboxとLiveを選択できるのですが、当然ながら試作中なので、Sandboxにしておきます。


RESTfulサーバを立ち上げる

では早速、サーバを立ち上げます。

2つのエンドポイントを使います。

Swagger定義を示します。

swagger.yaml
/paypal-create: 
    get: 
      x-swagger-router-controller: routing 
      operationId: paypal-create 
      responses: 
        200: 
          description: Success 
          schema: 
            type: object 
 
  /paypal-redirect: 
    get: 
      x-swagger-router-controller: routing 
      operationId: paypal-redirect 
      responses: 
        200: 
          description: Success 
          schema: 
            type: object 
/paypal-create

 決済を開始します。品目や金額を指定します。例として、100円のお買い物です。

 その後、PayPalがユーザからApproveをもらうために、画面を表示してくれます。

/paypal-redirect

 PayPalの画面でユーザが承認してくれると、このエンドポイントが呼び出されます。

 Approveをもらったので、これで決済を完了させます。

 完了後、ユーザにPaymentIdなど、もろもろ返してあげています。

利用するnpmモジュールは以下の通りです。

  • paypal-rest-sdk
  • node-fetch
  • uuid
PayPalから、paypal-rest-sdkというモジュールを提供していただいているので、だいぶ楽になりました。

(エンドポイント「/paypal-token」もありますが、これは後程説明します。)

index.js
var paypal = require('paypal-rest-sdk'); 
 
const base_url = 【RESTfulサーバを立ち上げているURL】; 
 
var Response = require('../../helpers/response'); 
var Redirect = require('../../helpers/redirect'); 
var fetch = require('node-fetch'); 
const { URLSearchParams } = require('url'); 
const uuidv4 = require('uuid/v4'); 
 
const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || 【REST API appsのClient ID】; 
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET || 【REST API appsのSecret】; 
 
paypal.configure({ 
    'mode': 'sandbox', //sandbox or live 
    'client_id': PAYPAL_CLIENT_ID, 
    'client_secret': PAYPAL_CLIENT_SECRET, 
    'headers' : { 
        'custom': 'header' 
    } 
}); 
 
exports.handler = (event, context, callback) => { 
    if( event.path == '/paypal-token'){ 
        var body = JSON.parse(event.body); 
        var code = body.code; 
 
        var url = 'https://api.sandbox.paypal.com/v1/oauth2/token'; 
        var params = { 
            grant_type: 'authorization_code', 
            code: code 
        }; 
 
        return do_post_form_basic(url, params, PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET ) 
        .then(json =>{ 
            console.log(json); 
            if( json.error ) 
                throw json.error; 
            return do_get_token('https://api.sandbox.paypal.com/v1/identity/oauth2/userinfo', { schema: 'paypalv1.1'}, json.access_token); 
        }) 
        .then(json =>{ 
            console.log(json); 
            if( json.error ) 
                throw json.error; 
            return new Response( json ); 
        }) 
        .catch(error =>{ 
            return new Response().set_error(error); 
        }) 
    }else if( event.path == '/paypal-create'){ 
        var orderid = uuidv4(); 
 
        var create_payment_json = { 
            "intent": "authorize", 
            "payer": { 
                "payment_method": "paypal" 
            }, 
            "redirect_urls": { 
                "return_url": base_url + "/paypal-redirect", 
                "cancel_url": base_url + "/paypal/index.html" 
            }, 
            "transactions": [{ 
                "custom": JSON.stringify({ orderid: orderid, amount: 100 } ), 
                "item_list": { 
                    "items": [{ 
                        "name": "item", 
                        "price": "100", 
                        "currency": "JPY", 
                        "quantity": 1 
                    }] 
                }, 
                "amount": { 
                    "currency": "JPY", 
                    "total": "100" 
                }, 
                "description": "This is the payment description." 
            }] 
        }; 
 
        return new Promise((resolve, reject) =>{ 
            paypal.payment.create(create_payment_json, (error, payment) => { 
                if (error) { 
                    console.log(error.response); 
                    return reject(error); 
                } 
                console.log(payment); 
 
                for (var index = 0; index < payment.links.length; index++) { 
                    if (payment.links[index].rel === 'approval_url') 
                        return resolve( new Redirect(payment.links[index].href) ); 
                } 
 
                return reject('approval_url not found'); 
            }); 
        }); 
    }else if( event.path == '/paypal-redirect'){ 
        var qs = event.queryStringParameters; 
        console.log(qs); 
 
        var payerid = event.queryStringParameters.PayerID; 
        var paymentid = event.queryStringParameters.paymentId; 
 
        var execute_payment_json = { 
            "payer_id": payerid, 
            "transactions": [{ 
                "amount": { 
                    "currency": "JPY", 
                    "total": "100" 
                } 
            }] 
        }; 
 
        return new Promise((resolve, reject) =>{ 
            paypal.payment.execute(paymentid, execute_payment_json, (error, payment) => { 
                if (error) { 
                    console.log(error.response); 
                    return reject(error); 
                } 
                console.log(payment); 
 
                for( var i = 0 ; i < payment.transactions.length ; i++ ){ 
                    for( var j = 0 ; j < payment.transactions[i].related_resources.length ; j++ ){ 
                        if( payment.transactions[i].related_resources[j].authorization ){ 
                            var authorization_id = payment.transactions[i].related_resources[j].authorization.id; 
                            console.log('authorization_id=' + authorization_id); 
                        } 
                    } 
                } 
 
                var params = new URLSearchParams(); 
                for( var key in qs ) 
                    params.set(key, qs[key] ); 
 
                return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() )); 
            }); 
        }); 
    } 
}; 
 
function do_get_token(url, qs, token){ 
    var params = new URLSearchParams(); 
    for( var key in qs ) 
        params.set(key, qs[key] ); 
 
    return fetch(url + '?' + params.toString(), { 
        method : 'GET', 
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token } 
    }) 
    .then((response) => { 
        return response.json(); 
    }); 
} 
 
function do_post_form_basic(url, qs, client_id, client_secret){ 
    var params = new URLSearchParams(); 
    for( var key in qs ) 
        params.set(key, qs[key] ); 
 
    const headers = {  
        "Content-Type" : "application/x-www-form-urlencoded", 
        "Authorization" : "Basic " + new Buffer(client_id + ':' + client_secret).toString('base64') 
    }; 
 
    return fetch(url, { 
        method : 'POST', 
        body : params, 
        headers: headers 
    }) 
    .then((response) => { 
        return response.json(); 
    }); 
} 
ユーティリティも示しておきます。

redirect.js
class Redirect{ 
    constructor(url){ 
        this.statusCode = 303; 
        this.headers = {'Location' : url}; 
        this.body = null; 
    } 
} 
response.js
module.exports = Redirect; 
 
class Response{ 
    constructor(context){ 
        this.statusCode = 200; 
        this.headers = {'Access-Control-Allow-Origin' : '*'}; 
        if( context ) 
            this.set_body(context); 
        else 
            this.body = ""; 
    } 
 
    set_error(error){ 
        this.body = JSON.stringify({"err": error}); 
        return this; 
    } 
 
    set_body(content){ 
        this.body = JSON.stringify(content);         
        return this; 
    } 
 
    get_body(){ 
        return JSON.parse(this.body); 
    } 
} 
 
module.exports = Response; 
環境に合わせて、以下を変更してください。

【REST API appsのClient ID】

【REST API appsのSecret】

【RESTfulサーバを立ち上げているURL】

また、

 "return_url": base_url + "/paypal-redirect"

の部分で、ユーザによる同意の結果のリダイレクト先を指定しています。

また、以下の部分で、決済完了後のリダイレクト先を指定しています。

 return resolve( new Redirect(base_url + '/paypal/index.html?' + params.toString() ));

クライアント側のHTMLページも示します。

/paypal/index.html
<!DOCTYPE html> 
<html lang="ja"> 
<head> 
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"> 
  <meta name="format-detection" content="telephone=no"> 
  <meta name="msapplication-tap-highlight" content="no"> 
  <meta name="apple-mobile-web-app-capable" content="yes" /> 
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> 
 
  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> 
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> 
  <!-- Latest compiled and minified CSS --> 
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> 
  <!-- Optional theme --> 
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> 
  <!-- Latest compiled and minified JavaScript --> 
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> 
 
  <title>PayPal 連携 テスト</title> 
 
  <script src="js/vue_utils.js"></script> 
  <script src="https://unpkg.com/vue"></script> 
</head> 
<body> 
    <div id="top" class="container"> 
        <h1>PayPal 連携 テスト</h1> 
        <br> 
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】/paypal-create">Payment</a> 
        <br> 
    </div> 
 
    <script src="js/start.js"></script> 
</body> 
start.js
'use strict'; 
 
const base_url = 【RESTfulサーバを立ち上げたURL】; 
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】; 
 
var vue_options = { 
    el: "#top", 
    data: { 
    }, 
    computed: { 
    }, 
    methods: { 
    }, 
    created: function(){ 
    }, 
    mounted: function(){ 
        proc_load(); 
 
        if( searchs.paymentId ){ 
            history.replaceState(null, null, '.'); 
            alert('決済が完了しました。'); 
        } 
    } 
}; 
var vue = new Vue( vue_options ); 
vue_utils.js
var hashs = {}; 
var searchs = {}; 
 
function proc_load() { 
  hashs = parse_url_vars(location.hash); 
  searchs = parse_url_vars(location.search); 
} 
 
function parse_url_vars(param){ 
  if( param.length < 1 ) 
      return {}; 
 
  var hash = param; 
  if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' ) 
      hash = hash.slice(1); 
  var hashs  = hash.split('&'); 
  var vars = {}; 
  for( var i = 0 ; i < hashs.length ; i++ ){ 
      var array = hashs[i].split('='); 
      vars[array[0]] = array[1]; 
  } 
 
  return vars; 
} 
 
function vue_add_methods(options, funcs){ 
    for(var func in funcs){ 
        options.methods[func] = funcs[func]; 
    } 
} 
function vue_add_computed(options, funcs){ 
    for(var func in funcs){ 
        options.computed[func] = funcs[func]; 
    } 
} 
以下に示すように、決済が完了して元のページに戻ってくると、paymentIdが指定されてくるので、それを受けてダイアログ表示するようにしています。

if( searchs.paymentId ){ 
        history.replaceState(null, null, '.'); 
        alert('決済が完了しました。'); 
    } 
それではRESTfulサーバを立ち上げてみましょう。



image.png


Paymentボタンを押下します。

そうすると、ログイン画面が表示されます。

Personalアカウントでログインします。自動で作られるXXXXX-buyer@YYYYY.yyy がありました。



image.png


今度は、品目と価格の確認画面に変わります。

内容を確認したら「同意して続行」ボタンを押下します。



image.png


めでたく最初のページに戻ってきて、ダイアログが表示できました。



image.png



PayPalアカウントでのログイン機能の追加

ついでに、PayPalアカウントでのログイン機能を追加してみます。

クライアント側に少しコードを追加します。

/paypal/index.html
<!DOCTYPE html> 
<html lang="ja"> 
<head> 
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"> 
  <meta name="format-detection" content="telephone=no"> 
  <meta name="msapplication-tap-highlight" content="no"> 
  <meta name="apple-mobile-web-app-capable" content="yes" /> 
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> 
 
  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> 
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> 
  <!-- Latest compiled and minified CSS --> 
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> 
  <!-- Optional theme --> 
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> 
  <!-- Latest compiled and minified JavaScript --> 
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> 
 
  <title>PayPal 連携 テスト</title> 
 
  <script src="js/vue_utils.js"></script> 
  <script src="https://unpkg.com/vue"></script> 
</head> 
<body> 
    <div id="top" class="container"> 
        <h1>PayPal 連携 テスト</h1> 
        <br> 
        <span id='cwppButton'></span> 
        <br> 
        <br> 
        <a class="btn btn-primary" href="【RESTfulサーバを立ち上げたURL】paypal-create">Payment</a> 
        <br> 
    </div> 
 
    <script src="js/start.js"></script> 
    <script src='https://www.paypalobjects.com/js/external/connect/api.js'></script> 
    <script> 
        paypal.use( ['login'], function (login) { 
            login.render ({ 
            "appid": PAYPAL_CLIENT_ID, 
            "authend":"sandbox", 
            "scopes":"openid profile", 
            "containerid":"cwppButton", 
            "locale":"ja-jp", 
            "buttonType":"CWP", 
            "buttonSize":"lg", 
            "returnurl": base_url + "/paypal/" 
            }); 
        }); 
    </script> 
</body> 
以下の部分が、PayPalアカウントでのログインボタンになります。

<span id='cwppButton'></span> 
各種設定は、以下の部分で行います。

 paypal.use( ['login'], function (login)

containeridの指定の部分で指定したIDのspanの部分に、ボタンを描画してくれます。

ログインのために、PayPalが提供する画面に遷移したのち、ログイン完了後にまた戻ってきます。戻ってくる先は、「returnurl」の部分に指定します。

スクリプトの本体は、以下です。

 <script src='https://www.paypalobjects.com/js/external/connect/api.js'> 
PayPalからログイン完了後に戻ってくると、codeが指定されています。

これは認可コードであって、これをアクセストークンに変換する必要があります。

そのために、いったんRESTfulサーバに処理を移す必要があり、それが、以下の部分です。

if( searchs.code ){ 
エンドポイント「/paypal-token」を呼んでいます。

呼び出しが成功すると、PayPalアカウント名が書かれたダイアログを表示するようにしています。

start.js
'use strict'; 
 
const base_url = 【RESTfulサーバを立ち上げたURL】; 
const PAYPAL_CLIENT_ID = 【REST API appsのClient ID】; 
 
var vue_options = { 
    el: "#top", 
    data: { 
    }, 
    computed: { 
    }, 
    methods: { 
    }, 
    created: function(){ 
    }, 
    mounted: function(){ 
        proc_load(); 
 
        if( searchs.paymentId ){ 
            history.replaceState(null, null, '.'); 
            alert('決済が完了しました。'); 
        } 
        if( searchs.code ){ 
            history.replaceState(null, null, '.'); 
 
            do_post(base_url + '/paypal-token', { code: searchs.code} ) 
            .then(json =>{ 
                console.log(json); 
                alert(json.name + 'さん、こんにちは'); 
            }); 
        } 
    } 
}; 
var vue = new Vue( vue_options ); 
 
function do_post(url, body){ 
    console.log('do_post: ' + url); 
 
    const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } ); 
 
    return fetch(url, { 
        method : 'POST', 
        body : JSON.stringify(body), 
        headers: headers 
    }) 
    .then((response) => { 
        return response.json(); 
    }); 
} 
エンドポイント、/paypal-tokenのサーバ部分は、すでに実装を示してました。

世の中で一般的なOAuth2やOpenID Connectとほぼ同じなので、なじみがあるかと思います。

swagger.yaml
/paypal-token: 
    post: 
      x-swagger-router-controller: routing 
      operationId: paypal-token 
      parameters: 
        - in: body 
          name: body 
          schema: 
            type: object 
      responses: 
        200: 
          description: Success 
          schema: 
            type: object 
それでは、もう一度、RESTfulサーバを再起動して、ブラウザからアクセスしてみます。



image.png


PayPalのボタンが増えているのがわかると思います。

PERSONALのアカウントでログインします。



image.png


PayPalのログインアカウント名が書かれたダイアログが表示されたかと思います。成功です。



image.png



感想

PayPalのドキュメント類は、すごく見にくかったのですが、REST APIに絞って熟読することで、なんとなく動作させることができました。

今後は、これを足掛かりに、他の機能も見ていこうと思います。

以上

コメント

このブログの人気の投稿

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-04-30 23:37:32 RSSフィード2021-04-30 23:00 分まとめ(42件)

投稿時間:2023-02-05 02:09:04 RSSフィード2023-02-05 02:00 分まとめ(9件)