I am building a marketplace with a model similar to fiverr. You pay for a service, and after you pay you can fill out the preferences that you want from the seller. The problem I am facing is that the successlink after payment can be just copy and pasted to progress to the preferences page without payment. How do I ensure this doesn't happen with stripe. Here is my server code:
//checkout stripe session code:
app.post('/create-checkout-session', async (req, res) => {
const {request} = req;
const account_id = req.body.account_id;
const user_id = req.body.user_id;
var successLink = 'http://localhost:3000/dashboard/userreq/'+user_id;
console.log('request: ' + req.body.account_id);
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: 'Story',
},
unit_amount: 1300,
},
quantity: 1,
},
],
payment_intent_data: {
application_fee_amount: 123,
transfer_data: {
destination: account_id,
},
},
mode: 'payment',
success_url: successLink,
cancel_url: 'http://localhost:3000/cancel.html',
});
res.send({
sessionId: session.id,
});});
//webhook to see if payment was successful
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (request, response) => {
const sig = request.headers['stripe-signature'];
let event;
// Verify webhook signature and extract the event.
// See https://stripe.com/docs/webhooks/signatures for more information.
try {
event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
handleCompletedCheckoutSession(session);
}
response.json({received: true});
});
const handleCompletedCheckoutSession = (session) => {
// Fulfill the purchase.
console.log(JSON.stringify(session));
}
app.listen(process.env.PORT || 8080, () => {
console.log("Server started...");
});
You'd want to encode the Checkout Session ID within your success URL: https://stripe.com/docs/payments/checkout/custom-success-page#modify-success-url
Then when your success page is hit, you'd check to see if the session ID is valid by retrieving the Checkout Session from the backend and checking its payment_status: https://stripe.com/docs/api/checkout/sessions/object#checkout_session_object-payment_status
If there's no session ID or the payment_status is not paid, then you wouldn't grant access to whatever it is that you're selling.
Related
Node code:
app.post("/create-payment-intent", async (req, res) => {
try {
const { items } = req.body;
console.log(items)
const paymentIntent = await stripe.paymentIntents.create({
currency: "gbp",
amount: items,
automatic_payment_methods: { enabled: true },
});
// Send publishable key and PaymentIntent details to client
res.send({
clientSecret: paymentIntent.client_secret,
});
React code:
useEffect(() => {
fetch("/create-payment-intent", {
method: "POST",
headers: {
"Content-Type" : "application/json"
},
body: JSON.stringify({items: [{ price: totalAmount }]}),
}).then(async (result) => {
var { clientSecret } = await result.json();
setClientSecret(clientSecret);
});
}, [totalAmount]);
Error: error
I do not understand why I cannot destructure items on the back end when it has been passed.
I have used the Stripe docs in order to attempt this: https://stripe.com/docs/payments/quickstart?locale=en-GB&lang=node
Is it possible to host stripe webhook with node.js deploy.
I've setup my stripe webhook with stripe checkout and It's ran successfully in localhost using stripe cli.
But I'm tried to take webhooks live.
const express = require("express");
const Stripe = require("stripe");
const { Order } = require("../models/Order");
require("dotenv").config();
const stripe = Stripe(`${process.env.STRIPE_SECRET}`);
const router = express.Router();
router.post("/create-checkout-session", async (req, res) => {
const customer = await stripe.customers.create({
metadata: {
userId: req.body.userId,
cart: JSON.stringify(req.body.products),
},
});
const line_items = req.body.products.map((item) => {
return {
price_data: {
currency: "usd",
product_data: {
name: item.title,
description: item.description,
metadata: {
id: item.id,
},
},
unit_amount: item.price * 100,
},
quantity: item.quantity,
};
});
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
shipping_address_collection: {
allowed_countries: ["BD", "US", "CA"],
},
shipping_options: [
{
shipping_rate_data: {
type: "fixed_amount",
fixed_amount: {
amount: 0,
currency: "usd",
},
display_name: "Free shipping",
// Delivers between 5-7 business days
delivery_estimate: {
minimum: {
unit: "business_day",
value: 5,
},
maximum: {
unit: "business_day",
value: 7,
},
},
},
},
{
shipping_rate_data: {
type: "fixed_amount",
fixed_amount: {
amount: 1500,
currency: "usd",
},
display_name: "Next day air",
// Delivers in exactly 1 business day
delivery_estimate: {
minimum: {
unit: "business_day",
value: 1,
},
maximum: {
unit: "business_day",
value: 1,
},
},
},
},
],
line_items,
mode: "payment",
customer: customer.id,
success_url: `https://nobab-3b3c4.web.app/checkout-success`,
cancel_url: `https://nobab-3b3c4.web.app/`,
});
// res.redirect(303, session.url);
res.send({ url: session.url });
});
// Create order function
const createOrder = async (customer, data) => {
const Items = JSON.parse(customer.metadata.cart);
const products = Items.map((item) => {
return {
productId: item.id,
quantity: item.quantity,
};
});
const newOrder = new Order({
userId: customer.metadata.userId,
customerId: data.customer,
paymentIntentId: data.payment_intent,
products,
subtotal: data.amount_subtotal,
total: data.amount_total,
shipping: data.customer_details,
payment_status: data.payment_status,
});
try {
const savedOrder = await newOrder.save();
console.log("Processed Order:", savedOrder);
} catch (err) {
console.log(err);
}
};
// Stripe webhoook
router.post(
"/webhook",
express.json({ type: "application/json" }),
async (req, res) => {
let data;
let eventType;
// Check if webhook signing is configured.
// let webhookSecret = `${process.env.STRIPE_WEB_HOOK}`;
let webhookSecret;
if (webhookSecret) {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
let signature = req.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
req.body,
signature,
webhookSecret
);
} catch (err) {
console.log(`⚠️ Webhook signature verification failed: ${err}`);
return res.sendStatus(400);
}
// Extract the object from the event.
data = event.data.object;
eventType = event.type;
} else {
// Webhook signing is recommended, but if the secret is not configured in `config.js`,
// retrieve the event data directly from the request body.
data = req.body.data.object;
eventType = req.body.type;
}
// Handle the checkout.session.completed event
if (eventType === "checkout.session.completed") {
stripe.customers
.retrieve(data.customer)
.then(async (customer) => {
try {
// CREATE ORDER
createOrder(customer, data);
console.log("Ordered");
res.status(200).json({ message: 'Order created', data: data })
res.status(200).send("Order created")
} catch (err) {
console.log(typeof createOrder);
console.log(err);
}
})
.catch((err) => console.log(err.message));
}
res.status(200).end();
}
);
module.exports = router;
This code working fine and store user order in mongodb database, but i need to run this after my server deployment.
Thanks everyone
for help me
Yes it should be possible to host webhook with Node: Stripe Doc. It's a separated logic with your Checkout Creation endpoint, and you will need to test the webhook endpoint properly.
My Angular 11 app uses Stripe Checkout with an Express server to handle the payment. Everything works fine with Angular & Node. I'd like to use Firebase Functions instead of Node, but when I call my Firebase Function, I get the error:
IntegrationError: stripe.redirectToCheckout: You must provide one of lineItems, items, or sessionId.
Angular code:
checkout(): void {
var stripe = Stripe(environment.stripe.key);
var productName = 'T-shirt!!';
var price = '2000';
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
body: JSON.stringify({
productName: productName,
price: price
})
};
var url = 'http://localhost:4242/create-checkout-session'; // Node server
//var url = 'http://localhost:5000/MY_FIREBASE_PROJECT/us-central1/stripeTest'; // Firebase emulation
fetch(url, options)
.then(function (response) {
return response.json();
})
.then(function (session) {
return stripe.redirectToCheckout({ sessionId: session.id });
})
.then(function (result) {
if (result.error) { // redirect fails due to browser or network error
alert(result.error.message);
}
})
.catch(function (error) {
console.error('Error:', error);
});
}
Node server:
const express = require('express');
const app = express();
const stripe = require('stripe')('MY_SECRET_KEY')
const port = 4242;
app.use(express.json()) // parse request body as JSON
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4200'); // website you wish to allow to connect
// res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); // request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'POST');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
next(); // pass to next layer of middleware
});
app.post('/create-checkout-session', async (req, res) => {
var productName = req.body.productName;
var price = req.body.price;
console.log('body = ', req.body);
console.log('price = ', req.body.price);
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: productName,
},
unit_amount: price,
},
quantity: 1,
},
],
mode: 'payment',
success_url: 'http://localhost:4200/home?action=success',
cancel_url: 'http://localhost:4200/home?action=cancel',
});
res.json({ id: session.id });
});
app.listen(port, () => console.log('Listening on port ' + port + '!'));
Firebase Function:
const stripe = require('stripe')('MY_SECRET_KEY')
const functions = require("firebase-functions");
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.stripeTest = functions.https.onCall((data, context) => {
var productName = data['productName'];
var price = data['price'];
console.log('data: ', data);
console.log('product name = ', productName);
console.log('price = ', price);
const session = stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: productName,
},
unit_amount: price,
},
quantity: 1,
},
],
mode: 'payment',
success_url: 'http://localhost:4200/home?action=success',
cancel_url: 'http://localhost:4200/home?action=cancel',
}, async (err, data) => {});
return { id: session.id };
})
I don't understand the issue because I pass in line_items. Thanks!
Thanks everyone for helping me get here.
When using Firebase Functions, you need to make your call from Angular with httpsCallable instead of fetch...
Angular code:
import { AngularFireFunctions } from '#angular/fire/functions';
constructor(
// ...
private afFun: AngularFireFunctions) {
afFun.useEmulator("localhost", 5000);
}
checkoutFirebase(): void {
var stripe = Stripe(environment.stripe.key);
this.afFun.httpsCallable("stripeTest")({ productName: 'T-shirt', price: '400' })
.subscribe(result => { // the result is your Stripe sessionId
console.log({result});
stripe.redirectToCheckout({
sessionId: result,
}).then(function (result) {
console.log(result.error.message);
});
});
}
Then we need to add async to the onCall in Firebase.
Firebase Function:
exports.stripeTest = functions.https.onCall(async (data, context) => {
var productName = data['productName'];
var price = data['price'];
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: productName,
},
unit_amount: price,
},
quantity: 1,
},
],
mode: 'payment',
success_url: 'http://localhost:4200/home?action=success',
cancel_url: 'http://localhost:4200/home?action=cancel',
});
return session.id;
})
session.id is typeof 'undefined', as the error message hints for.
Which means, that it would have to be passed into the function along with data (there are no server-side sessions available).
Also verify the property names, because at least line_items is unknown to Stripe API.
Rebounding Ryan Loggerythm's updated answer.
After about half an hour or so of fiddling with Stripe's docs solutions (which seem more geared towards getting devs to try out the demo instead of actually implementing the technology) and other guides on Medium, I loaded up your updated answer and lo and behold it worked absolutely flawlessly and all that within 2 paragraphs, 1 for Angular, 1 for Firebase functions, hence the whole point of using Stripe's Checkout product instead of making our own checkout (or turning towards Shopify's): turnkey simplicity.
Meanwhile Stripe's API docs felt like a maze for this simple solution. Maybe Ryan should be the one making Stripe's documentation 😁
I have a MEVN stack application that uses JWT for auth and that can take stripe payments.
Upon payment, I need to retrieve the payment intent object for that charge and send it to the front end to validate payment and serve up a PDF. My Question is, how can I make sure that the customer only had access to the charge created by that particular user by using the Json web token.
My current node.js code for stripe (without JWT)
const express = require("express");
const router = express.Router();
const endpointSecret = process.env.WEBHOOK_SECRET;
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
let Intent;
router.post("/", async (req, res) => {
const session = await stripe.checkout.sessions.create(
{
success_url: "http://localhost:8080/#/success",
cancel_url: "http://localhost:8080/#/cancel",
payment_method_types: ["card"],
line_items: [
{
price: "price_1H0up7Kc91wTjOOikyrKImZs",
quantity: 1,
},
],
mode: "payment",
},
function (err, session) {
if (err) {
console.log(err);
res.status(500).send({ success: false, reason: "session didnt work" });
} else {
console.log(session);
Intent = session.payment_intent;
console.log(Intent);
res.json({ session_id: session.id });
// res.status(200).send({ success: true });
}
}
);
});
router.get("/confirm", async (req, res) => {
const intentObject = await stripe.paymentIntents.retrieve(Intent, function (
err,
paymentIntent
) {
if (err) {
console.log(err);
res
.status(500)
.send({ success: false, reason: "cannot retrieve payment" });
} else {
console.log(paymentIntent);
res.status(200).json({ status: paymentIntent.status });
setTimeout(() => (intent = ""), 10);
}
});
});
module.exports = router;
You can't use Intent the way you're using it there; it won't persist between requests.
You might want to consider using something like this: https://github.com/auth0/express-jwt
I am trying to make a subscription to stripe, and in order for that I need a user created first - done.
After I create the user how can i proceed automatically to make the subscription for given user?
my code so far
const stripe = require('./../constants/stripe');
const postStripeCharge = res => (stripeErr, stripeRes) => {
if (stripeErr) {
res.status(500).send({
error: stripeErr
});
}
else {
res.status(200).send({
success: stripeRes
});
}
}
const paymentApi = app => {
app.get('/', (req, res) => {
res.send({
message: 'Hello Stripe checkout server!',
timestamp: new Date().toISOString()
})
});
app.post('/', (req, res) => {
console.log('request in server', req);
// stripe.charges.create(req.body, postStripeCharge(res));
stripe.customers.create({
description: 'customer for ' + req.body.email,
source: req.body.token
}, postStripeCharge(res))
});
return app;
};
module.exports = paymentApi;
this is the code i want to call after the user is created :
stripe.subscriptions.create({
customer: req.id,
items: [{
plan: 'plan_CyQRCVcrEztYI7'
}],
}
Fixed it: it is not req.body.token but req.body.source, and then it works as it should.