繁体   English   中英

尝试使用 Stripe + React + Express 创建结帐时无法 POST / 错误 session

[英]Cannot POST / error when trying to create a checkout session with Stripe + React + Express

我正在使用 Stripe 的预建结账方法在我的 Reactjs 应用程序上订购和支付产品。 我最近开发了一个基本的购物车,现在我正在尝试创建一个“转到结帐”输入表单,允许用户将购物车中的产品发送到我的快递服务器 POST 路线,快递将用户重定向到条带结帐页。 问题是,当我按下表单输入以发出 HTTP 发布请求时,我收到“Cannot POST /cart”响应,没有任何错误消息。

有趣的是,通过使用 Postman 我能够到达 POST 路由。 此外,我还设置了其他路由,用于从其他 API 获取数据,它们工作正常,但出于某种原因,无论我在做什么,这个 POST 路由都不起作用。

欢迎大家提出意见。

以下是相关文件和其中的代码。

cart-page.js - 这是负责购物车的代码,并且具有在按下时应该发出 HTTP 请求的表单代码('const goToCheckout')

import React from "react";

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

//* Allows Stripe to authentificate our API requests with our key
const stripePublishableKey = nodeEnv ? process.env.REACT_APP_stripe_dev_publishable_key : process.env.REACT_APP_stripe_prod_pubishable_key;
const stripe = require('stripe')(stripePublishableKey);

const CartPage = (props) => {

    const { cart, onAdd, onRemove } = props;

    const productTotal = cart.reduce((a, c) => a + c.unit_amount * c.qty, 0) // default value 0
    const taxTotal = <p>Tax is included in the price.</p>
    const shippingTotal = <p>You can choose your shipping options at checkout.</p>
    const totalCost = productTotal;

    const checkoutData = cart.map(item => (
        { 
            price: item.id, 
            quantity: item.qty,
        }
    ));

    const goToCheckout = async () => {

        // Call your backend to create the Checkout Session
        await fetch('/create-checkout-session', {
            method: "POST",
            headers: {
                    "Content-Type": "application/json"
                },
            body: JSON.stringify({
                items: [
                    checkoutData
                ]
            }),
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(session) {
            return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function(result) {
            // If `redirectToCheckout` fails due to a browser or network
            // error, you should display the localized error message to your
            // customer using `error.message`.
            if (result.error) {
                alert(result.error.message);
            }
        })
        .catch((error) => {
            console.error(error);
        });
    };

    const currencyFormatter = new Intl.NumberFormat('en-gb', {
        style:"currency", 
        currency:"GBP"
    }) 

    return (
        <main>

            <h1>Your Cart</h1>

            {cart.length === 0 && <p>Your Cart is Empty...</p>}

            {cart.map((item) => (
                <section className='cart-item' key={item.product.id}>
                    <h4>{item.product.name}</h4>
                        
                    <section className='cart-item-buttons'>
                        <button onClick={() => onAdd(item)}>+</button>
                        <button onClick={() => onRemove(item)}>-</button>
                    </section>
                        
                    <p>{item.qty} * {currencyFormatter.format(item.unit_amount / 100)}</p>
                </section>
            ))}

            {cart.length !== 0 && (
                <section>
                    <p>Total Product Price: {currencyFormatter.format(productTotal / 100)}</p>
                    <p>Toal Tax: {taxTotal}</p>
                    <p>Shipping Costs: {shippingTotal}</p>
                    <p><strong>Total Costs: {currencyFormatter.format(totalCost / 100)}</strong></p>
                </section>
            )}

            {cart.length > 0 && (
                <section>
                    <p>ADD CHECKOUT BUTTON</p>

                    <form method='POST' action={goToCheckout}>
                        <input type='submit' value='Go To Checkout' />
                    </form>

                </section>
            )}

        </main>
    );
};

export default CartPage;

createCheskoutSession.js - 此文件包含负责“/create-checkout-session”路由的所有代码。 它应该接受请求并使用 Stripe API 创建结帐页面,该页面将填充购物车项目。 据我了解,我的 POST 请求没有达到这一点。 我认为...

require('dotenv').config();

const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';

const YOUR_DOMAIN = nodeEnv ? process.env.REACT_APP_dev_domain : process.env.REACT_APP_prod_domain;

//* Allows Stripe to authentificate our API requests with our key
const stripeSecretKey = nodeEnv ? process.env.REACT_APP_stripe_dev_secret_key : process.env.REACT_APP_stripe_prod_secret_key;
const stripe = require('stripe')(stripeSecretKey);

//* To override the API version, provide the apiVersion option:
//*  Before upgrading your API version in the Dashboard, review both the API changelog and the library changelog.
/*
const stripe = require('stripe')(stripeSecretKey, {
  apiVersion: '2020-08-27',
});
*/

//* After creating a Checkout Session, redirect your customer to the URL returned in the response.
//* Add an endpoint on your server that creates a Checkout Session. A Checkout Session controls what your customer sees 
//* in the Stripe-hosted payment page such as line items, the order amount and currency, and acceptable payment methods.

const createCheckoutSession = async (req, res) => {
    
    const session = await stripe.checkout.sessions.create({

    //* Prefill customer data
    //* Use customer_email to prefill the customer’s email address in the email input field. You can also pass a
    //*  Customer ID to customer field to prefill the email address field with the email stored on the Customer.
    //* customer_email: 'customer@example.com',
        
    //* Pick a submit button // Configure the copy displayed on the Checkout submit button by setting the submit_type. There are four different submit types.
        submit_type: 'donate',
        
        /*
        Collect billing and shipping details
        Use billing_address_collection and shipping_address_collection to collect your customer’s address. 
        shipping_address_collection requires a list of allowed_countries. Checkout displays the list of allowed
        countries in a dropdown on the page.
        */
            
        billing_address_collection: 'auto',
        shipping_address_collection: {
            allowed_countries: ['US', 'CA', 'LV'],
        },

        /*
        Define a product to sell
        Always keep sensitive information about your product inventory, like price and availability, on your server 
        to prevent customer manipulation from the client. Define product information when you create the Checkout
        Session using predefined price IDs or on the fly with price_data.
        */
        /*
            line_items: [
                {
                    price: 'price_1JsxdVBSHV1ZLiWD7n4PcKf9',
                    quantity: 1,
                },
            ],
        */
        //* Provide the exact Price ID (e.g. pr_1234) of the product you want to sell
/*
            line_items: [
                cartItems.map(item => {
                    return {
                        price: item.price,
                        quantity: item.quantity,
                    }
                })
            ],
*/
            line_items: req.body.items,
            
            /*
            req.body.items.map(item => {
                return {
                    price: item.id,
                    quantity: item.qty,
                },
            },
            */

        //* When you pass multiple payment methods, Checkout dynamically displays them to prioritize what’s most 
        //* relevant to the customer. Apple Pay and Google Pay are included automatically when you include card in 
        //* payment_method_types.
        //* Apple Pay and Google Pay are enabled by default and automatically appear in Checkout when a customer 
        //* uses a supported device and has saved at least one card in their digital wallet. 
            payment_method_types: [
                'card',
            ],

        //* Choose the mode
        //* Checkout has three modes: payment, subscription, or setup. Use payment mode for one-time purchases.
        //*  Learn more about subscription and setup modes in the docs.
            mode: 'payment',

        //* Supply success and cancel URLs
        //* Specify URLs for success and cancel pages—make sure they are publicly accessible so Stripe can redirect 
        //* customers to them. You can also handle both the success and canceled states with the same URL.
            success_url: `${YOUR_DOMAIN}/stripe/stripe-success.html`, //! Change
            cancel_url: `${YOUR_DOMAIN}/stripe/stripe-cancel.html`, //! Change
        //* Activate Stripe Tax to monitor your tax obligations, automatically collect tax, and access the reports you need to file returns.
        //* automatic_tax: {enabled: true},
    });

    //* Redirect to Checkout
    //* After creating the session, redirect your customer to the Checkout page’s URL returned in the response.
  
    res.redirect(303, session.url);

};

module.exports = createCheckoutSession;

server.js - 这是负责管理我的快速服务器的大部分代码。 还有另一个文件使用 Router 来定义快速端点/路由。

const express = require('express');
const helmet = require('helmet'); 
const cors = require('cors'); 
const path = require('path'); // Allows to access files through the server in our filesystem

/**
**  ------------- GENERAL SETUP -------------
*/

// Provides access to variables from the .env file by using process.env.REACT_APP_variable_name
    require('dotenv').config();

    const nodeEnv = process.env.REACT_APP_NODE_ENV === 'development';
    const devPort = process.env.REACT_APP_server_dev_port;
    const prodPort = process.env.REACT_APP_server_prod_port;

//* Creates the Express server instance as "app" 
    const app = express();

//* MIDDLEWARE
// Called BETWEEN processing the Request and sending the Response in your application method.
    app.use(cors()); // To allow cross origin conections (Allows our React app to make HTTP requests to Express application)
    app.use(helmet()); // Sets many http headers to make them more secure
    app.use(express.static(path.join(__dirname, 'public'))); // To load static files or client files from here http://localhost:3000/images/kitten.jpg
    // Instead of using body-parser middleware, use the new Express implementation of the same thing
        app.use(express.json()); // To recognize the incoming Request Object (req.body) as a JSON Object
        app.use(express.urlencoded({ extended: false })); // To recognize the incoming Request Object as strings or arrays

/**
** -------------- SERVER ----------------
*/
       
// Determines the PORT and enables LISTENing for requests on the PORT (http://localhost:8000)

    const PORT = nodeEnv ? devPort : prodPort;
       
    app.listen(PORT, () => {
      console.debug(`Server is listening at http://localhost:${PORT}`);
    });
  
/**
** ------- ROUTES / ENDPOINTS ---------
*/

// Go to /test to make sure the basic API functioning is working properly
    app.get('/test', (req, res) => {
        res.status(200).send('The Basic API endpoints are working.')
    });

// Imports all of the routes from ./routes/index.js
    app.use(require('./routes/allRoutes'));

更新 1#

这是我浏览器的请求控制台显示的内容

Request URL: http://localhost:3000/cart
Request Method: POST
Status Code: 404 Not Found
Remote Address: 127.0.0.1:3000
Referrer Policy: strict-origin-when-cross-origin
access-control-allow-origin: *
connection: close
content-length: 144
content-security-policy: default-src 'none'
content-type: text/html; charset=utf-8
date: Fri, 26 Nov 2021 05:25:35 GMT
expect-ct: max-age=0
referrer-policy: no-referrer
strict-transport-security: max-age=15552000; includeSubDomains
Vary: Accept-Encoding
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
X-Powered-By: Express
x-xss-protection: 0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: lv-LV,lv;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
DNT: 1
Host: localhost:3000
Origin: http://localhost:3000
Referer: http://localhost:3000/cart
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1

更新 2#

我有一些进步。 我可以通过更改来调用 goToCheckout() function ...

<form method='POST' action={goToCheckout}>
     <input type='submit' value='Go To Checkout' />
</form>

到...

<form type="button" onSubmit={goToCheckout}>
      <button>
          Go To Checkout
      </button>
</form>

现在唯一的问题是,在我按下结帐按钮后,goToCheckout function 中的代码无法执行,但我被重定向到购物车页面,现在唯一的区别是如果在 URL 之前是“http://localhost: 3000/cart”现在是“http://localhost:3000/cart?”。 我认为这是因为该按钮位于表单中(但这是我能够弄清楚如何调用 goToCheckout() 函数的唯一方法)。 我尝试将 event.preventDefault() 添加到 function 中,但这似乎没有任何作用。

有谁知道为什么获取代码没有正确执行并将用户重定向到条带结帐页面,而是只是让我回到同一个 URL? 没有附加任何参数。

更新 3#

当我从 Postman 访问相同的路线时,我能够获得 Stripe 结帐 URL 以将用户重定向到结帐页面,以便他们可以在测试模式下为产品付款(目前)。

这意味着路线本身按预期工作。

现在我只需要弄清楚当我使用表单调用我的fetch function时如何停止页面刷新,添加一个“?” 在 URL 的末尾签名,然后像 Postman 一样执行提取。

如果有人知道如何在不使用表格的情况下做到这一点,这就是我现在使用的表格,那将是一个很大的帮助。 我尝试使用 a,但无论我如何添加 goToCheckout(); function 到 onClick/action 等函数不会调用。

这是很久以前的事了,但我想我也可以回答我自己的问题。

  1. 必须更改 goToCheckout function 的内部逻辑:
    const goToCheckout = (e) => {
        e.preventDefault();
        fetch(`${customProxy}/create-checkout-session`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ items: checkoutData}),
        })
        .then(res => {
            if (res.ok) return res.json()
            return res.json().then(json => Promise.reject(json))
        })
        .then(({ url }) => {
            window.location = url
        })
        .catch((error) => {
            // console.error(error);
            alert("Create Stripe checkout:" + error);
        });
    };
  1. 为 goToCheckout 按钮更改了 HTML
<button className='go-to-checkout-button' onClick={goToCheckout}>
       Go To Checkout
</button>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM