使用 paypal v2 创建和获取付款 node.js

Creating and Capturing payment with paypal v2 node.js

我正在尝试将网站上的 PayPal 服务器端支付与新的 v2 集成,因为 v1 已被弃用。因为我不得不从他们的 git 中包含相当多的代码,所以我将 post 这里的所有代码都用于一个工作示例

在 v1 中,我可以非常轻松地做到这一点:

app.get('/create', function (req, res) {
    //build PayPal payment request
    let payReq = JSON.stringify({
        'intent': 'sale',
        'redirect_urls': {
            'return_url': 'http://localhost:8081/process',
            'cancel_url': 'http://localhost:8081/cancel'
        },
        'payer': {
            'payment_method': 'paypal'
        },
        'transactions': [{
            'amount': {
                'total': '7.47',
                'currency': 'USD'
            },
            'description': 'This is the payment transaction description.'
        }]
    });

    paypal.payment.create(payReq, function (error, payment) {
        if (error) {
            console.error(error);
        } else {
            //capture HATEOAS links
            let links = {};
            payment.links.forEach(function (linkObj) {
                links[linkObj.rel] = {
                    'href': linkObj.href,
                    'method': linkObj.method
                };
            })

            //if redirect url present, redirect user
            if (links.hasOwnProperty('approval_url')) {
                res.redirect(links['approval_url'].href);
            } else {
                console.error('no redirect URI present');
            }
        }
    });
});


app.get('/process', function (req, res) {
    let paymentId = req.query.paymentId;
    let payerId = {'payer_id': req.query.PayerID};

    paypal.payment.execute(paymentId, payerId, function (error, payment) {
        if (error) {
            console.error(error);
        } else {
            if (payment.state == 'approved') {
                const payerCountry = payment.payer.payer_info.country_code;
                const total = payment.transactions[0].amount.total;
                const currency = payment.transactions[0].amount.currency;

                savePayment(payerCountry, total, currency);

                res.send('payment completed successfully ' + cnt++);
            } else {
                res.send('payment not successful');
            }
        }
    });
});

create 端点基本上创建订单,paypal API returns hateos links 并且控制器说浏览器应该重定向到 link.一旦重定向,如果用户批准在 paypal 网站上付款,他将被重定向到

'redirect_urls': {
    'return_url': 'http://localhost:8081/process',
    'cancel_url': 'http://localhost:8081/cancel'
},

process 端点从查询中检索 PAYMENT IDPAYER ID 并捕获订单 - 允许我在回调中做任何我想做的事情(例如在数据库中保存付款)。

现在 v2 看起来一团糟:

已关注开发者 guide 我已创建

Paypal 按钮(复制粘贴):

import React, {useEffect} from "react";
import {PayPalButtons, PayPalScriptProvider, usePayPalScriptReducer} from "@paypal/react-paypal-js";

// This values are the props in the UI
const amount = "2";
const currency = "USD";
const style = {"layout": "vertical"};
    const ButtonWrapper = ({currency, showSpinner}) => {
        // usePayPalScriptReducer can be use only inside children of PayPalScriptProviders
        // This is the main reason to wrap the PayPalButtons in a new component
        const [{options, isPending}, dispatch] = usePayPalScriptReducer();
    
        useEffect(() => {
            dispatch({
                type: "resetOptions",
                value: {
                    ...options,
                    currency: currency,
                },
            });
        }, [currency, showSpinner]);
    
        const createOrder = (data, actions) => {
            console.log("create")
            return fetch('http://localhost:8081/create', {
                method: 'post'
            }).then(function (res) {
                return res.json();
            }).then(function (orderData) {
                console.log(orderData);
                window.location = orderData.redirect;
            });
        }
    
        // Call your server to finalize the transaction
        const onApprove = (data, actions) => {
            console.log("approve")
            return fetch('/process', {
                method: 'post'
            }).then(function (res) {
                return res.json();
            }).then(function (orderData) {
                // Three cases to handle:
                //   (1) Recoverable INSTRUMENT_DECLINED -> call actions.restart()
                //   (2) Other non-recoverable errors -> Show a failure message
                //   (3) Successful transaction -> Show confirmation or thank you
    
                // This example reads a v2/checkout/orders capture response, propagated from the server
                // You could use a different API or structure for your 'orderData'
                var errorDetail = Array.isArray(orderData.details) && orderData.details[0];
    
                if (errorDetail && errorDetail.issue === 'INSTRUMENT_DECLINED') {
                    return actions.restart(); // Recoverable state, per:
                    // https://developer.paypal.com/docs/checkout/integration-features/funding-failure/
                }
    
                if (errorDetail) {
                    var msg = 'Sorry, your transaction could not be processed.';
                    if (errorDetail.description) msg += '\n\n' + errorDetail.description;
                    if (orderData.debug_id) msg += ' (' + orderData.debug_id + ')';
                    return alert(msg); // Show a failure message (try to avoid alerts in production environments)
                }
    
                // Successful capture! For demo purposes:
                console.log('Capture result', orderData, JSON.stringify(orderData, null, 2));
                var transaction = orderData.purchase_units[0].payments.captures[0];
                alert('Transaction ' + transaction.status + ': ' + transaction.id + '\n\nSee console for all available details');
            });
        }
    
    
        return (<>
                {(showSpinner && isPending) && <div className="spinner"/>}
                <PayPalButtons
                    style={style}
                    disabled={false}
                    forceReRender={[amount, currency, style]}
                    fundingSource={undefined}
                    createOrder={(data, actions) => createOrder(data, actions)}
                    onApprove={(data, actions) => onApprove(data, actions)}
                />
            </>
        );
    }


export default function PayPalButton() {
    return (
        <div style={{ maxWidth: "750px", minHeight: "200px" }}>
            <PayPalScriptProvider
                options={{
                    "client-id": "test",
                    components: "buttons",
                    currency: "USD"
                }}
            >
                <ButtonWrapper
                    currency={currency}
                    showSpinner={false}
                />
            </PayPalScriptProvider>
        </div>
    );
}

然后跟随来自 paypal 的 github example

的流程

创建了他们的 HttpClient

const checkoutNodeJssdk = require('@paypal/checkout-server-sdk');

/**
 * Returns PayPal HTTP client instance with environment which has access
 * credentials context. This can be used invoke PayPal API's provided the
 * credentials have the access to do so.
 */
function client() {
    return new checkoutNodeJssdk.core.PayPalHttpClient(environment());
}

/**
 * Setting up and Returns PayPal SDK environment with PayPal Access credentials.
 * For demo purpose, we are using SandboxEnvironment. In production this will be
 * LiveEnvironment.
 */
function environment() {
    let clientId = process.env.PAYPAL_CLIENT_ID || '<clientId>';
    let clientSecret = process.env.PAYPAL_CLIENT_SECRET || '<secret>';

    if (process.env.NODE_ENV === 'production') {
        return new checkoutNodeJssdk.core.LiveEnvironment(clientId, clientSecret);
    }

    return new checkoutNodeJssdk.core.SandboxEnvironment(clientId, clientSecret);
}

async function prettyPrint(jsonData, pre=""){
    let pretty = "";
    function capitalize(string) {
        return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
    }
    for (let key in jsonData){
        if (jsonData.hasOwnProperty(key)){
            if (isNaN(key))
                pretty += pre + capitalize(key) + ": ";
            else
                pretty += pre + (parseInt(key) + 1) + ": ";
            if (typeof jsonData[key] === "object"){
                pretty += "\n";
                pretty += await prettyPrint(jsonData[key], pre + "\t");
            }
            else {
                pretty += jsonData[key] + "\n";
            }

        }
    }
    return pretty;
}



module.exports = {client: client, prettyPrint:prettyPrint};

创建了他们的 CreateOrder:

/**
 * PayPal SDK dependency
 */
const checkoutNodeJssdk = require('@paypal/checkout-server-sdk');

/**
 * PayPal HTTP client dependency
 */
const payPalClient = require('./PayPalClient');

/**
 * Setting up the JSON request body for creating the Order. The Intent in the
 * request body should be set as "CAPTURE" for capture intent flow.
 *
 */
function buildRequestBody() {
    return {
        "intent": "CAPTURE",
        "application_context": {
            "return_url": "http://localhost:8081/process",
            "cancel_url": "https://www.example.com",
            "locale": "en-US",
            "landing_page": "BILLING",
            "user_action": "CONTINUE"
        },
        "purchase_units": [
            {
                "soft_descriptor": "Donation",
                "amount": {
                    "currency_code": "USD",
                    "value": "220.00"
                }
            }
        ]
    };
}

/**
 * This is the sample function which can be sued to create an order. It uses the
 * JSON body returned by buildRequestBody() to create an new Order.
 */
async function createOrder(debug=false) {
    try {
        const request = new checkoutNodeJssdk.orders.OrdersCreateRequest();
        request.headers["prefer"] = "return=representation";
        request.requestBody(buildRequestBody());
        const response = await payPalClient.client().execute(request);
        if (debug){
            console.log("Status Code: " + response.statusCode);
            console.log("Status: " + response.result.status);
            console.log("Order ID: " + response.result.id);
            console.log("Intent: " + response.result.intent);
            console.log("Links: ");
            response.result.links.forEach((item, index) => {
                let rel = item.rel;
                let href = item.href;
                let method = item.method;
                let message = `\t${rel}: ${href}\tCall Type: ${method}`;
                console.log(message);
            });
            console.log(`Gross Amount: ${response.result.purchase_units[0].amount.currency_code} ${response.result.purchase_units[0].amount.value}`);
            // To toggle print the whole body comment/uncomment the below line
            console.log(JSON.stringify(response.result, null, 4));
        }
        return response;
    }
    catch (e) {
        console.log(e)
    }

}

/**
 * This is the driver function which invokes the createOrder function to create
 * an sample order.
 */
if (require.main === module){
    (async() => await createOrder(true))();
}

/**
 * Exports the Create Order function. If needed this can be invoked from the
 * order modules to execute the end to flow like create order, retrieve, capture
 * and refund(Optional)
 */

module.exports = {createOrder:createOrder};

和端点:

const createUsersOrder = async (res) => {
    try {
        let response = await createOrder();
        console.log("Creating Order...");
        let orderId = "";
        if (response.statusCode === 201){
            console.log("Created Successfully");
            orderId = response.result.id;
            console.log("Links:");
            response.result.links.forEach((item, index) => {
                let rel = item.rel;
                let href = item.href;
                let method = item.method;
                let message = `\t${rel}: ${href}\tCall Type: ${method}`;
                console.log(message);
            });

            let links = {};
            response.result.links.forEach(function (linkObj) {
                links[linkObj.rel] = {
                    'href': linkObj.href,
                    'method': linkObj.method
                };
            })

            //if redirect url present, redirect user
            if (links.hasOwnProperty('approve')) {
                var returnObj = {redirect : links['approve'].href}
                console.log("Returning " + returnObj)
                res.send(returnObj);
            } else {
                console.error('no redirect URI present');
            }
        }

        console.log("Copy approve link and paste it in browser. Login with buyer account and follow the instructions.\nOnce approved hit enter...");
        return
    } catch (error) {
        console.log('There was an error: ', error);
    }
};

app.post("/create", function(req,res) {
    createUsersOrder(res);
})

在这里,当点击按钮时调用它,因为调用了“createOrder”方法。我像在 v1 代码中一样创建订单,然后将浏览器重定向到 url。然而,当用户批准 paypal 交易时,因此被重定向到

之一
"application_context": {
    "return_url": "http://localhost:8081/process",
    "cancel_url": "http://localhost:8081/cancel",
    "locale": "en-US",
    "landing_page": "BILLING",
    "user_action": "CONTINUE"
},

return url(/process 成功路线),请求不包含 payment_id,仅包含 PAYER_ID。但是需要 payment_id (或 v2 中的 order_id )来捕获订单。

因为我发现 v2 的博客、教程和指南几乎为零(v1 的博客、教程和指南只有数百万),所以我很困惑从哪里获得订单 ID。我真的需要在创建订单后将其保存在数据库中吗?或者还有其他技巧吗?

此外,该按钮包含 onApprove 方法,但在创建订单后,浏览器被重定向到 paypal,而 paypal 将用户重定向到 http://localhost:8081/process 端点 - 因此 onApprove 方法永远不会被调用并且无用(?)。

v2 的整个流程真的很混乱,我是否遗漏了什么?

感谢帮助

对于您的 v2 代码,请勿使用任何重定向。完全没有。您的按钮应调用服务器上的 2 个端点。这两个端点应该(分别)执行创建和捕获订单的 API 操作,并且在每种情况下 return JSON 结果(捕获路由可以执行任何 server-side像在将 JSON 结果转发给客户端调用者之前将事务结果存储在数据库中这样的操作,因为客户端需要处理任何捕获错误情况并显示成功消息)。您可以在 PayPal integration guide 中找到一个完整的堆栈 node.js 示例,但是将您的 @paypal/react-paypal-js 代码几乎 as-is 用于前端就可以了。