I've created a POST request to collect payments from customers via Stripe
let data = {
errorMsg:'',
key: process.env.STRIPE_PUBLIC_KEY
}
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: 'My tools',
},
unit_amount: 10,
},
quantity: 1,
},
],
mode: 'payment',
success_url: 'https://example.com/success',
cancel_url: 'https://example.com/cancel',
});
const sessiondetails = await stripe.checkout.sessions.retrieve(session.id);
console.log("session details", sessiondetails);
res.redirect(303, session.url);
}))
Can someone tell me how can I confirm that the payment is successfully processed before adding credits/allow customers to download digital products or before redirecting the user?
The recommended way to track this is using the checkout.session.completed webhook to handle fulfillment. Either here or retrieving the session as you've done, you can check if the payment_status (API ref) is paid before granting access/credits.
Related
I have integrated stripe api into my ecommerce website. When you checkout you are sent to the stripe api payment link where you type in your information. Of course two things could happen here, either the payment goes through and succeed or the order gets canceled. Everything works except I am trying to remove all cart items only if the payment succeeds and goes through.
Here is my code:
app.post('/api/createCheckoutSession', async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items: req.body.items.map(item => {
const storeItem = storeItems.get(item.id)
return {
price_data: {
currency: 'usd',
product_data: {
name: storeItem.name
},
unit_amount: storeItem.priceInCents
},
quantity: item.quantity
}
}),
success_url: `${process.env.SERVER_URL}`,
cancel_url: `${process.env.SERVER_URL}cart`,
})
res.json({ url: session.url })
} catch (e) {
console.log(e)
res.status(500).json({ error: e.message })
}
});
So if I do the following code I can remove all cartItems from the user, however this happens regardless of if the payment was successful or not:
app.post('/api/createCheckoutSession', async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items: req.body.items.map(item => {
const storeItem = storeItems.get(item.id)
return {
price_data: {
currency: 'usd',
product_data: {
name: storeItem.name
},
unit_amount: storeItem.priceInCents
},
quantity: item.quantity
}
}),
success_url: `${process.env.SERVER_URL}`,
cancel_url: `${process.env.SERVER_URL}cart`,
})
cartItem.remove({ user: req.body.user }, function (err) {
if (err) {
console.log(err)
}
})
res.json({ url: session.url })
} catch (e) {
console.log(e)
res.status(500).json({ error: e.message })
}
});
So looking through Stripe api documentation and googling the only thing that consistently comes up is success_url, which wasn't what I am looking for (at least I do not think it will fix what I am trying to do). So for the original code I tried to console.log(session) and found a payment_status: 'unpaid' and figured I could use this to do what I am trying to by the following:
app.post('/api/createCheckoutSession', async (req, res) => {
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'payment',
line_items: req.body.items.map(item => {
const storeItem = storeItems.get(item.id)
return {
price_data: {
currency: 'usd',
product_data: {
name: storeItem.name
},
unit_amount: storeItem.priceInCents
},
quantity: item.quantity
}
}),
success_url: `${process.env.SERVER_URL}`,
cancel_url: `${process.env.SERVER_URL}cart`,
})
console.log(session)
if (session.payment_status == 'paid') {
cartItem.remove({ user: req.body.user }, function (err) {
if (err) {
console.log(err)
}
})
}
res.json({ url: session.url })
} catch (e) {
console.log(e)
res.status(500).json({ error: e.message })
}
});
With the above code, the if statement does not work as the only time something gets logged to the console is when the session first gets created, so the if statement does not execute after the payment succeeds or gets canceled.
Another solution is for me to create a different success_url where it pops up and I have a react useEffect call the remove cart items function. And then it redirects again somewhere else, but as I alluded to earlier this just seems like a terrible solution when it feels like I am just missing something very simple to get this to work the way I have attempted to.
I'm going to restate what I think your goal is here so I am clear on what the answer is.
After user is sent to checkout you have 2 potential outcomes you want your app to handle:
Successful payment: Get money, get user their goods
Payment canceled: Empty user cart
The problem you are running into is that your code can only await the creation of the user session. At that point you should just redirect to the Checkout URL. At this point your code does not know whether the payment will go through or not because the user is just being redirected to the Checkout UI where they can make their payment.
The state of the user's payment is reported back to your app in 2 different ways.
Customized Success/Cancel URLS - While this doc focuses on capturing the Session ID in the Success URL, you can do the same thing with the Cancel URL. In this case which URL the user is sent to tells your system a Checkout was either successful or was canceled. The Session ID injected into the URL identifies which Checkout session it was.
Checkout Webhook events - This approach provides confirmation of success but requires waiting until the session is expired to confirm cancelation. Still, it is recommended that most integrations make use of Webhooks to monitor account activities.
So to circle back to your code, I would move the code that clears the cart to a function that responds to either the user being redirected to the cancel_url in your app or responds to the checkout.session.expired webhook event
const combineData = function (prod, pot, data) {
const newData = [...prod, ...pot];
const finalCheckout = [];
for (let i = 0; i < newData.length; i++) {
finalCheckout.push({
name: newData[i].plantName || newData[i].potName,
images: [newData[i].images[0]],
amount: newData[i].price * 100,
currency: "inr",
quantity: data[i].quantity,
metadata: { id: String(newData[i]._id) },// **Mongoose ID**
});
}
return finalCheckout;
};
exports.getCheckOutSession = catchAsync(async (req, res, next) => {
const [product, pot] = filterVal(req.body.product);
const userId = req.user._id;
const products = await ProductModel.find({ _id: { $in: product } });
const pots = await PotModel.find({ _id: { $in: pot } });
const newData = combineData(products, pots, req.body.product);
// 2. create the checkout session
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
success_url: `${req.protocol}://${req.get("host")}/?alert=booking`,
cancel_url: `${req.protocol}://${req.get("host")}/products/`,
customer_email: req.user.email,
client_reference_id: userId,
line_items: newData,
});
res.status(200).json({
status: "success",
url: session.url,
});
});
I'm build an Ecommerce website, when user makes a payment I want to send the product id's stored in my mongodb database as metadata in stripe. But i'm getting an error how to solve this.
Error:parameter_unknown - line_items[0][metadata]
Received unknown parameter: line_items[0][metadata]
Stripe is rejecting the mongodb id's has metadata
"metadata": {
"id": "61e40a5a7d83539092e7a92f"
}
NOTE: I'm sending all the successfull data like price, name,images amount,quanity in the above combineData function.
I have tested the above code without metadata key. It works fine my payment is registered and webhook event is registered in stripe.
When i use metadata keyword the above error occurs in stripe.
You need to create product + price first.
After informing stripe of the line items, the session token will be returned to client side, and users will then be redirected to stripe's payment page.
Stripe will be able to know what items (e.g image, product name, product description) to display to the user, through the line items you submitted while creating the session.
If you just pass a MongoDB _id, stripe will not know what items to display since they have no info of the product.
const session = await stripe.checkout.sessions.create({
customer: customerId,
billing_address_collection: 'auto',
payment_method_types: ['card'],
line_items: [
{
price: STRIPE_PRODUCT_PRICE, //price of created product
// For metered billing, do not pass quantity
quantity: 1,
},
],
// success_url: `${YOUR_DOMAIN}?success=true&session_id={CHECKOUT_SESSION_ID}`,
// cancel_url: `${YOUR_DOMAIN}?canceled=true`,
});
Example of how strip product and price id looks like. A single product can have many different prices.
STRIPE_PRODUCT=prod_I7a38cue83jd
STRIPE_PRICE=price_1HXL0wBXeaFPR83jhdue883
These references will be useful to you.
Creating a product: https://stripe.com/docs/api/products/create
Creating a price: https://stripe.com/docs/api/prices/create
I will normally store the product id and price id as stripe_product_id and stripe_product_price in the local MongoDB database so you can use during checkout.
So after you receive the order, you can create a local order first, with the products and quantity, then...
const line_items = ordered_products.map(product => ({ price: product.stripe_product_price, quantity: product.quantity }))
const session = await stripe.checkout.sessions.create({
....
line_items,
...
})
Advanced method - creating product and price on the go.
This is more tedious because you need to do more checks.
You can refer to creating line_items in the link below from stripe docs.
https://stripe.com/docs/api/checkout/sessions/object
I have this:
const stripe = require('stripe')('sk_test', {
stripeAccount: 'acct_...'
});
const paymentIntent = await stripe.paymentIntents.create({
amount: 1900,
currency: 'cad',
customer: 'cus_...',
// confirm: true,
}, {
stripeAccount: 'acct_...',
});
console.log(paymentIntent)
so then I go to run this paymentIntent, and it works, but doesn't actaully charge the customer because it says that it has no payment method on file. So then I take this customers id, and look at my stripe dashboard, and it shows the payment method there, and the method matches with the id. so now I believe I am doing something wrong creating the paymentIntent, but the payment is going through, just not confirmed because it says no payment method attached! So why is this not working?
error: UnhandledPromiseRejectionWarning: Error: You cannot confirm this PaymentIntent because it's missing a payment method. You can either update the PaymentIntent with a payment method and then confirm it again, or confirm it again directly with a payment method.
PaymentIntent requires a Payment Method Object such as;
payment_method_types: [card],
PaymentIntent object
const {
error: backendError,
clientSecret,
paymentIntentId,
transfer_group,
} = await fetch('/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentMethodType: 'card',
currency: 'cad',
customerEmail: billingDetails.email,
description: 'Basket_Order_Id',
}),
}).then((r) => r.json());
and when you created the paymentintent on your backend you should return
app.post('/create-payment-intent', async (req, res) => {
const {paymentMethodType, currency, customerEmail, description,
suppliers} =
req.body;
console.log('paymentIntent Create requ body', req.body);
req.session.suppliers = suppliers;
suppliersArray = suppliers;
const idEmpotency = nanoid();
// Each payment method type has support for different currencies. In order
to
// support many payment method types and several currencies, this server
// endpoint accepts both the payment method type and the currency as
// parameters
//
// Some example payment method types include `card`, `ideal`, and
`alipay`.
const params = {
payment_method_types: [paymentMethodType], 'CARD'
amount: 20000,
currency: currency,
description: description,
receipt_email: customerEmail,
statement_descriptor_suffix: 'Your Bank Descriptor on Customer Account',
transfer_group: idEmpotency,
// confirm: true,
// capture_method: 'manual',
};
try {
const paymentIntent = await stripe.paymentIntents.create(params);
// Send publishable key and PaymentIntent details to client
console.log('paymentIntent', paymentIntent);
res.send({
clientSecret: paymentIntent.client_secret, - SENDING YOUR CLIENTSECRET
paymentIntentId: paymentIntent.id,
transfer_group: paymentIntent.transfer_group,
});
} catch (e) {
return res.status(400).send({
error: {
message: e.message,
},
});
}
});
client_secret and use it on your front-end
const {error: stripeError, paymentIntent} = await stripe.confirmCardPayment(
clientSecret, USE YOUR CLIENT SECRET THAT RETURNED FROM YOUR BACKEND FROM PAYMENT INTENT OBJECT
{
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: 'Michael',
},
},
}
);
Before confirming the client_secret that returned from payment_intent you can not succesfully confirm the payment.
You can use stripe elements to start with their own card payment component.
I recommend you to check here https://stripe.com/docs/payments/quickstart, you will get more idea...
What I want to do:
I use firebase so as soon as someone purchases a Product over stripe I want to write that on the document of the user that bought it.
I want to use a stripe webhook for that. This webhook looks like this:
It essentially makes a https request to firebase functions as soon as the payment went trough. It triggers a https function that should write to the user's document that he has purchased the product. But to do that the webhook needs to contain the firebase user uid of the user that bought the product.
But to pass the user uid to the webhook I need to pass it to the stripe.checkout.sessions.create() function that looks like this:
exports.createCheckoutSession = functions.https.onCall(async(data, context) => {
const YOUR_DOMAIN = 'http://localhost:4242';
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: 'Stubborn Attachments',
images: ['https://i.imgur.com/EHyR2nP.png'],
},
unit_amount: 2000,
},
quantity: 1,
}, ],
mode: 'payment',
success_url: `${YOUR_DOMAIN}/success.html`,
cancel_url: `${YOUR_DOMAIN}/cancel.html`,
});
console.log(session.id)
return session.id
})
(the user calls this cloud function to get a checkout session returned)
But how can I pass the user uid (context.user.uid) in so I can later send it with the webhook.
I hope it is clear what I want to do if not please ask. Thank you for your time.
You can add Firebase UID of user or any other information in metadata of stripe checkout session like this:
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
// line items and other stuff
metadata: {firebaseUID: uid, product_id: "ProductID"},
});
You can read them in the checkout session completed webhook or any success webhook.
I'm using Stripe Subscription running under node.
I want to create a new checkout prefilling the email address. So I tried to do in the client:
// Setup event handler to create a Checkout Session when button is clicked
document
.getElementById("basic-plan-btn")
.addEventListener("click", function(evt) {
createCheckoutSession(basicPlanId).then(function(data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId,
})
.then(handleResult);
});
});
The email here is directly in the code just to test it.
In createCheckoutSession I added the customerEmail:
var createCheckoutSession = function(planId) {
return fetch("https://example.com:4343/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
planId: planId,
customerEmail: 'mario.rossi#gmail.com'
})
}).then(function(result) {
return result.json();
});
};
Then on the server I try to catch and forward the email but how can I do it?
app.post("/create-checkout-session", async (req, res) => {
const domainURL = process.env.DOMAIN;
const { planId } = req.body;
// Create new Checkout Session for the order
// Other optional params include:
// [billing_address_collection] - to display billing address details on the page
// [customer] - if you have an existing Stripe Customer ID
// [customer_email] - lets you prefill the email input in the form
// For full details see https://stripe.com/docs/api/checkout/sessions/create
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
subscription_data: { items: [{ plan: planId }] },
// ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
success_url: `${domainURL}/success.html?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${domainURL}/canceled.html`
});
res.send({
sessionId: session.id
});
});
I also tried to pass the email directly to the server using:
subscription_data: { items: [{ plan: planId, customer_email: 'a.b#gmail.com' }] },
But this doesn't populate the field in the checkout page
How do I fix it?
It is not part of subscription_data; it is its own field titled customer_email.
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
// THIS LINE, HERE:
customer_email: 'a.b#gmail.com',
subscription_data: { items: [{ plan: planId }] },
// ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
success_url: `${domainURL}/success.html?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${domainURL}/canceled.html`
});
Use "customer_email" parameter as shown below and this is the example in Node.js:
const session = await stripe.checkout.sessions.create({
customer_email: 'example#gmail.com', // Here
line_items=[
{
'price_data': {
'currency': 'jpy',
'unit_amount': 1000,
'product_data': {
'name': 'Honey',
'description': 'Good honey',
},
},
'quantity': 1,
},
],
mode: 'payment',
success_url: 'https://example.com/success',
cancel_url: 'https://example.com/cancel',
});
And because you use "customer_email" parameter so the email field is readonly as shown below: