Stripe payment React-Flask
Stripe payment React-Flask
Payment Flow
- User in the React site add items to cart
- User navigate to cart page
- User click on “Pay with card”
- On user input React call the back-end passing cart and user data
- Flask validate cart items throught database (pricing and availability)
- Flask ask Stripe to create a checkout session, then send back session url to React
- React redirect User to stripe checkout page
- User perform payment using card
- Stripe redirect User to success/failed url (the cart React page)
- If payment success React empty cart and notify User
- Stripe notify Flask throught webhook about payment completed with basical order info
- Flask update database and React about buyed items
React (front-end)
React functional with typescript. In the following example it’s implemented the front-end side of stripe checkout payment using redux RTKQ
models/Payment.ts
import { CartFootballer } from './Footballer';
export interface CheckoutResponse {
checkout_url: string;
}
export interface CheckoutRequest {
cartFootballers: CartFootballer[];
email: string;
}
api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import Cookies from 'js-cookie';
import { CheckoutRequest, CheckoutResponse } from 'models/Payment';
export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: process.env.REACT_APP_API_URL,
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = Cookies.get('csrf_access_token');
// If we have a token set in state, let's assume that we should be passing it.
if (token) headers.set('x-csrf-token', token);
return headers;
},
}),
endpoints: (builder) => ({
stripeCheckout: builder.mutation<CheckoutResponse, CheckoutRequest>({
query: (credentials) => ({
url: 'payment/createCheckoutSession',
method: 'POST',
body: credentials,
}),
}),
paypalCheckout: builder.mutation<any, CheckoutRequest>({
query: (credentials) => ({
url: 'payment/paypal/createCheckoutSession',
method: 'POST',
body: credentials,
}),
}),
}),
});
export const { useStripeCheckoutMutation, usePaypalCheckoutMutation } = api;
cart.tsx
- On user input React call the back-end passing cart and user data
const response = await stripeCheckout(data).unwrap();
- React redirect User to stripe checkout page
window.location.replace(response.checkout_url);
- If payment success React empty cart and notify User
useEffect(() => {[...]}
import React, { FC, useEffect } from 'react';
import Item from './Item';
import { useCart } from 'services/cart';
import {useStripeCheckoutMutation,usePaypalCheckoutMutation} from 'redux/api';
import { CheckoutRequest } from 'models/Payment';
import { FUNDING, PayPalButtons } from '@paypal/react-paypal-js';
const App: FC = () => {
const { cart } = useCart();
const [stripeCheckout, { isLoading: stripeIsLoading }] =
useStripeCheckoutMutation();
const [paypalCheckout, { isLoading: paypalIsLoading }] =
usePaypalCheckoutMutation();
const handleCheckout = async () => {
const data: CheckoutRequest = {
cartFootballers: cart,
email: user?.email || '',
};
try {
const response = await stripeCheckout(data).unwrap();
window.location.replace(response.checkout_url);
} catch (error) {
console.log(error.response);
}
};
const handlePaypalCheckoutComplete = async (data: any, actions: any) => {
try {
const response = await actions.order.capture();
console.log(response);
} catch (error) {
console.log(error.response);
}
};
const handlePaypalCreateOrder = async (data: any, actions: any) => {
const requestData: CheckoutRequest = {
cartFootballers: cart,
email: user?.email || '',
};
try {
const response = await paypalCheckout(requestData).unwrap();
console.log(response);
return actions.order.create(response);
} catch (error) {
console.log(error.response);
}
return '';
};
useEffect(() => {
// Check to see if this is a redirect back from Checkout
const query = new URLSearchParams(window.location.search);
if (query.get('success')) {
console.log('Order placed! You will receive an email confirmation.');
// empty cart and redirect to /cart
}
if (query.get('canceled'))
console.log('Order canceled -- continue to shop around');
}, [cart]);
return (
<>
{cart.length === 0 ? (
<p>empty cart</p>
) : (
<>
<div>
{cart.map((cartFootballer, key) => (
<Item
cartFootballer={cartFootballer.footballer}
quantity={cartFootballer.quantity}
key={key}
/>
))}
</div>
<div>
<button onClick={handleCheckout}>Pay with card</button>
<PayPalButtons
createOrder={handlePaypalCreateOrder}
onApprove={handlePaypalCheckoutComplete}
fundingSource={FUNDING.PAYPAL}
style={{ height: 50, shape: 'pill' }}
></PayPalButtons>
</div>
</>
)}
</>
);
};
export default App;
Flask (back-end)
the back-end needs 2 endpoint
- install stripe
pip install stripe
- configure stripe
import stripe
stripe.api_key = API.config["STRIPE_SK"]
create_checkout_session
this endpoist is POST and have to:
- validate user data using database
- check for item availability
- create stripe checkout session
- send to client the checkout page url
create_checkout_session function:
json_data = request.get_json(force=True)
[fbs, quantities] = parse_checkout_request(json_data)
email = json_data["email"]
items = []
info_items = {"items_number": len(fbs)}
for i, fb in enumerate(fbs):
items.append({
"price_data":{
"currency": "eur",
"unit_amount": fb.price,
"product_data": {
"name": fb.name,
},
},
"quantity": quantities[i],
})
info_items["id" + str(i)] = fb.id
info_items["quantity" + str(i)] = quantities[i]
try:
checkout_session = stripe.checkout.Session.create(
customer_email=email,
payment_method_types=["card"],
line_items=items,
mode="payment",
success_url=API.config["BASE_URL_FRONT"] + "/cart?success=true",
cancel_url=API.config["BASE_URL_FRONT"] + "/cart?canceled=true",
metadata=info_items,
)
except:
raise APIException("pagamento fallito")
return {"checkout_url": checkout_session.url}
stripe_checkout_webhook
This endpoint will be called from stripe until he gets the response
It’s very important to retrieve the stripe data with request.data.decode("utf-8")
This endpoist is POST and have to:
- verify that it is called from stripe webhook
- verify that it isn’t a dupplicate call or be idempotent
- respond to stripe with status 200 as early as possible, the best is to respond just after the verification and launch another process to complete the task
- fulfill order updating database and client
stripe_checkout_webhook function:
payload = request.data.decode("utf-8")
sig_header = request.headers["STRIPE_SIGNATURE"]
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, API.config["STRIPE_WEBHOOK_KEY"]
)
except ValueError as e:
raise APIException("Invalid payload")
except stripe.error.SignatureVerificationError as e:
raise APIException("Invalid signature")
if event is None or event["type"] != "checkout.session.completed":
raise APIException("evento non consistente")
# check if event is not dupplicate
[...]
session = event["data"]["object"]
email = session["customer_email"]
items = session["metadata"]
# update db and client
[...]
return {}
Stripe config
- create Stripe account
- get secret key
- create webhook
- get webhook key